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 gpu_plan: Option<std::sync::Arc<Vec<(usize, usize, usize)>>>,
55 pub tokenizer: std::sync::Arc<Tokenizer>,
58 pub kv_cache: KvCache,
59 pub sampler_config: SamplerConfig,
60 pub weights: PipelineWeights,
61 pub hidden_size: usize,
62 pub intermediate_size: usize,
63 pub num_heads: usize,
64 pub num_kv_heads: usize,
65 pub head_dim: usize,
66 pub num_layers: usize,
68 pub physical_layers: usize,
70 pub loop_final_norm: bool,
72 pub vocab_size: usize,
73 pub rms_eps: f64,
74 pub rope_base: f32,
75 pub norm_style: NormStyle,
76 pub rotary_dim: usize,
78 pub attention_heads_per_layer: Option<Vec<usize>>,
80 pub vmf_cfg: Option<VmfPhaseCfg>,
82 pub gdn_cfg: Option<GdnCfg>,
84 pub logit_multiplier: Option<f32>,
86 pub cancel: std::sync::Arc<std::sync::atomic::AtomicBool>,
91 graph_failed: std::sync::atomic::AtomicBool,
96 pub kv_history: Vec<u32>,
101 pub kda_cfg: Option<crate::linear_core::KdaCfg>,
103 pub g3n: Option<Box<(crate::g3n::G3nGlobals, Vec<crate::g3n::G3nLayer>)>>,
106 pub dsv4: Option<
110 Box<(
111 crate::dsv4::Dsv4Globals,
112 Vec<crate::dsv4::Dsv4Layer>,
113 crate::dsv4::Dsv4Cfg,
114 crate::dsv4::Dsv4State,
115 )>,
116 >,
117 pub dsv41: Option<
121 Box<(
122 crate::dsv41::Dsv41Globals,
123 Vec<crate::dsv41::Dsv41Layer>,
124 crate::dsv41::Dsv41Cfg,
125 crate::dsv41::Dsv41State,
126 )>,
127 >,
128 pub dsv41_vision: Option<crate::dsv41_vision::VisionModel>,
130 dsv41_prefill: Option<(Vec<Option<Vec<f32>>>, Vec<bool>)>,
132 pub qwen4_exp: Option<
135 Box<(
136 crate::qwen4_exp::Globals,
137 Vec<crate::qwen4_exp::Layer>,
138 crate::qwen4_exp::Cfg,
139 crate::qwen4_exp::State,
140 )>,
141 >,
142 pub dsv4_mtp: Vec<crate::dsv4::Dsv4Mtp>,
146 pub dspark: Option<crate::dsv4::DsparkState>,
148 pub dspark_pending: Vec<(usize, Vec<u32>, bool, usize)>,
151 pub dspark_hist: Vec<usize>,
153 pub dspark_real: Vec<u32>,
157 pub dspark_trunk_picks: Vec<Vec<(usize, Vec<usize>)>>,
161 pub dspark_exp: Vec<(usize, usize, usize, usize)>,
163 pub dspark_draft_ns: u128,
167 pub short_conv_cfg: Option<ShortConvCfg>,
170 pub mtp: Option<MtpModule>,
172 pub speculative: bool,
174 pub ignore_eos: bool,
180 pub draft_full_streak: u32,
186 pub spec_k_adapt: Option<usize>,
194 pub spec_acc_ewma: f32,
196 rng: SplitMix64,
197 sampler_scratch: SamplerScratch,
198 spec_forced: Option<u32>,
204 spec_q: Vec<Vec<f32>>,
205 spec_p: Vec<f32>,
206 spec_res: Vec<f32>,
207 spec_qs: Vec<sampler::Sparse>,
209 spec_ps: sampler::Sparse,
210 spec_ress: sampler::Sparse,
211 mtp_graph_mode: Option<bool>,
218 #[cfg(target_os = "macos")]
221 metal_verify: Option<MetalVerifyPending>,
222 pub(crate) inv_freq: std::sync::Arc<Vec<f32>>,
226 ws: ForwardScratch,
230 pool: Option<std::sync::Arc<Pool>>,
232 pub(crate) model: Option<std::sync::Arc<cortiq_core::CmfModel>>,
236 pub(crate) dyn_force_f32: bool,
238 pub(crate) dyn_skill_layers: Vec<Option<Vec<usize>>>,
243 pub(crate) dyn_active: Option<usize>,
249 pub(crate) dyn_blend_loaded: bool,
253 pub(crate) dyn_phi_layer: Option<usize>,
256 dyn_phi_ema: Vec<f32>,
258 dyn_phi_seen: usize,
259 pub dyn_router: Option<crate::swarm::DynRouter>,
262 o1_cfg: Option<crate::nystrom::O1Cfg>,
265 o1_epoch: u64,
268 o1_flags: Vec<bool>,
270 trace: bool,
273 calib_temp: f32,
276 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
278 graph_kv_id: u64,
279 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
282 graph_want_logits: bool,
283 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
287 graph_head_required: bool,
288 graph_logits: Option<Vec<f32>>,
291 pub embed_multiplier: f32,
293 pub attn_scale: f32,
296 pub swa: Option<(usize, usize)>,
299 pub sliding_layers: Option<Vec<bool>>,
302 pub inv_freq_local: Option<std::sync::Arc<Vec<f32>>>,
305 pub rotary_dim_local: Option<usize>,
306 pub rope_scale: f32,
307 pub rope_scale_local: f32,
308 pub global_attn: Option<(usize, usize)>,
311 pub inv_freq_global: Option<std::sync::Arc<Vec<f32>>>,
314 pub attn_v_norm: bool,
316 pub qk_norm_after_rope: bool,
318 pub final_softcap: Option<f32>,
320 pub head_clusters: Option<std::sync::Arc<Vec<f32>>>,
324 pub attn_softcap: f32,
326 confidence_on: bool,
330 #[cfg(test)]
333 nll_test_fail_at: Option<usize>,
334 #[cfg(test)]
337 nll_test_force_serial: bool,
338}
339
340#[cfg(target_os = "macos")]
341impl Drop for Pipeline {
342 fn drop(&mut self) {
343 let _ = crate::gpu_metal::wait_replay();
345 crate::gpu::kv_mirror_drop(self.graph_kv_id);
346 }
347}
348
349pub struct PipelineWeights {
354 pub embed_tokens: QTensor,
356 pub layers: Vec<LayerWeights>,
358 pub lm_head: QTensor,
360 pub final_norm: Vec<f32>,
362}
363
364pub struct LayerWeights {
366 pub input_norm: Vec<f32>,
367 pub post_norm: Vec<f32>,
370 pub attn_out_norm: Option<Vec<f32>>,
373 pub layer_scale: Option<f32>,
375 pub ffn_out_norm: Option<Vec<f32>>,
378 pub ffn: FfnKind,
379 pub attn: AttnKind,
380}
381
382#[derive(Clone, Copy, PartialEq, Debug, Default)]
385pub enum Act {
386 #[default]
387 Silu,
388 GeluTanh,
389 Situ {
392 beta: f32,
393 linear_beta: f32,
394 },
395}
396
397impl Act {
398 pub fn from_arch(name: &str) -> Self {
399 if name == "gelu_tanh" {
400 Self::GeluTanh
401 } else {
402 Self::Silu
403 }
404 }
405
406 pub fn from_arch_full(arch: &cortiq_core::ModelArch) -> Self {
408 match arch.hidden_act.as_str() {
409 "situ" => Self::Situ {
410 beta: arch.activation_situ_beta.unwrap_or(1.0) as f32,
411 linear_beta: arch.activation_situ_linear_beta.unwrap_or(0.0) as f32,
412 },
413 other => Self::from_arch(other),
414 }
415 }
416
417 #[inline]
418 pub fn apply(self, x: f32) -> f32 {
419 match self {
420 Self::Silu => inference::silu(x),
421 Self::GeluTanh => inference::gelu_tanh(x),
422 Self::Situ { beta, .. } => beta * (x / beta).tanh() * (1.0 / (1.0 + (-x).exp())),
423 }
424 }
425
426 #[inline]
429 pub fn combine(self, g: f32, u: f32) -> f32 {
430 match self {
431 Self::Situ { linear_beta, .. } if linear_beta > 0.0 => {
432 self.apply(g) * (linear_beta * (u / linear_beta).tanh())
433 }
434 _ => self.apply(g) * u,
435 }
436 }
437}
438
439pub struct DenseFfn {
441 pub gate_proj: QTensor,
442 pub up_proj: QTensor,
443 pub down_proj: QTensor,
444 pub act: Act,
446 pub down_t: Option<QTensor>,
452 pub segs: Vec<FfnSeg>,
459}
460
461pub struct FfnSeg {
466 pub gate: QTensor,
467 pub up: QTensor,
468 pub down: QTensor,
469 pub start: usize,
470 pub width: usize,
471}
472
473pub enum FfnKind {
476 Dense(DenseFfn),
477 Moe(MoeFfn),
481 DenseMoe(Box<DenseMoeFfn>),
488}
489
490pub struct DenseMoeFfn {
492 pub dense: DenseFfn,
493 pub moe: MoeFfn,
494 pub post_norm_1: Vec<f32>,
496 pub pre_norm_2: Vec<f32>,
499 pub post_norm_2: Vec<f32>,
501}
502
503pub struct MoeFfn {
504 pub router: QTensor,
506 pub experts: Vec<DenseFfn>,
507 pub top_k: usize,
508 pub norm_topk_prob: bool,
509 pub router_sigmoid: bool,
512 pub expert_bias: Option<Vec<f32>>,
516 pub routed_scaling: f32,
519 pub route_tau: Option<f32>,
525 pub shared: Option<(DenseFfn, Option<QTensor>)>,
528 pub stats: std::cell::RefCell<Vec<u64>>,
532 pub act_sq: std::cell::RefCell<Vec<f64>>,
539 pub act_rows: std::cell::RefCell<Vec<f32>>,
545 pub mask: Option<Vec<bool>>,
550 pub per_expert_scale: Option<Vec<f32>>,
553 pub router_input_norm: bool,
557 pub resonance: Option<Resonance>,
561}
562
563pub struct Resonance {
565 pub mu: Vec<f32>,
567 pub u: Vec<f32>,
569 pub k: usize,
570 pub bias: Vec<f32>,
572}
573
574impl Resonance {
575 pub fn scores(&self, x: &[f32], out: &mut [f32]) {
577 let h = x.len();
578 let ne = out.len();
579 for e in 0..ne {
580 let mu = &self.mu[e * h..(e + 1) * h];
581 let mut d2 = 0.0f32;
582 for j in 0..h {
583 let d = x[j] - mu[j];
584 d2 += d * d;
585 }
586 let mut proj = 0.0f32;
587 for i in 0..self.k {
588 let u = &self.u[(e * self.k + i) * h..(e * self.k + i + 1) * h];
589 let mut p = 0.0f32;
590 for j in 0..h {
591 p += (x[j] - mu[j]) * u[j];
592 }
593 proj += p * p;
594 }
595 out[e] = self.bias.get(e).copied().unwrap_or(0.0) - (d2 - proj);
596 }
597 }
598}
599
600pub enum AttnKind {
603 Full {
605 wq: QTensor,
606 wk: QTensor,
607 wv: QTensor,
608 wo: QTensor,
609 q_norm: Option<Vec<f32>>,
610 k_norm: Option<Vec<f32>>,
611 output_gate: bool,
612 softplus_gate: Option<(QTensor, bool)>,
616 bias: Option<(Vec<f32>, Vec<f32>, Vec<f32>)>,
618 },
619 Linear(VmfPhaseWeights),
621 LinearGdn(GdnWeights),
623 ShortConv(ShortConvWeights),
626 Mla(Box<MlaWeights>),
634 Kda(Box<crate::linear_core::KdaWeights>),
638}
639
640pub struct MlaWeights {
642 pub q_proj: QTensor,
646 pub q_a: Option<QTensor>,
649 pub q_a_norm: Option<Vec<f32>>,
650 pub kv_a: QTensor,
652 pub kv_a_norm: Vec<f32>,
654 pub kv_b: QTensor,
656 pub o_proj: QTensor,
658 pub nh: usize,
659 pub qk_rope: usize,
660 pub qk_nope: usize,
661 pub v_dim: usize,
662 pub lora: usize,
663 pub scale: f32,
665 pub nope: bool,
667}
668
669pub struct MtpModule {
674 pub enorm: Vec<f32>,
675 pub hnorm: Vec<f32>,
676 pub eh_proj: QTensor,
678 pub layer: LayerWeights,
679 pub final_norm: Vec<f32>,
680 pub kv: crate::kv_cache::LayerKvCache,
681}
682
683#[cfg(target_os = "macos")]
690enum MetalRowsItem<'a> {
691 Gdn {
692 run: Vec<crate::gpu_metal::GdnGpuLayer<'a>>,
693 first: usize,
694 },
695 Attn {
696 l: crate::gpu_metal::AttnGpuLayer<'a>,
697 li: usize,
698 q_norm: Option<&'a [f32]>,
699 k_norm: Option<&'a [f32]>,
700 output_gate: bool,
701 },
702}
703
704#[cfg(target_os = "macos")]
705struct MetalVerifyPending {
706 graph: crate::gpu_metal::VerifyGraph,
707 gdn_layers: Vec<usize>,
708 attn_layers: Vec<(usize, usize)>,
709}
710
711#[cfg(target_os = "macos")]
715struct MetalWarmPending {
716 graph: crate::gpu_metal::VerifyGraph,
717 cpu_stored: usize,
718 b: usize,
719}
720
721#[cfg(target_os = "macos")]
722enum MetalRowsRun {
723 Declined,
725 Failed,
728 Completed(MetalVerifyPending),
729}
730
731#[cfg(target_os = "macos")]
732enum MetalPrefillOutcome {
733 Declined,
734 Failed,
735 Completed(Vec<f32>),
736}
737
738#[cfg(target_os = "macos")]
739enum MetalBatchNllOutcome {
740 Declined,
741 Failed(String),
742 Completed(f64, usize),
743}
744
745#[derive(Clone, Copy)]
749enum SpecTrial {
750 Spec {
751 t0: std::time::Instant,
752 gen0: usize,
753 rounds: usize,
754 },
755 Plain {
756 t0: std::time::Instant,
757 gen0: usize,
758 },
759 Decided {
760 spec: bool,
761 recheck_at: usize,
762 },
763}
764
765pub(crate) fn spec_time_level() -> u8 {
769 static L: std::sync::OnceLock<u8> = std::sync::OnceLock::new();
770 *L.get_or_init(|| match std::env::var("CMF_GRAPH_SPEC_TIME") {
771 Ok(v) => v.trim().parse::<u8>().map(|n| n.max(1)).unwrap_or(1),
772 Err(_) => 0,
773 })
774}
775
776struct SpecStampLog {
782 t_last: std::time::Instant,
783 items: Vec<(&'static str, f32)>,
784}
785
786static SPEC_STAMPS: std::sync::Mutex<Option<SpecStampLog>> = std::sync::Mutex::new(None);
787
788pub(crate) fn spec_stamp(name: &'static str) {
789 if spec_time_level() == 0 {
790 return;
791 }
792 if let Ok(mut g) = SPEC_STAMPS.lock() {
793 if let Some(log) = g.as_mut() {
794 let now = std::time::Instant::now();
795 log.items
796 .push((name, (now - log.t_last).as_secs_f32() * 1e3));
797 log.t_last = now;
798 }
799 }
800}
801
802fn spec_stamps_begin() {
803 if spec_time_level() == 0 {
804 return;
805 }
806 if let Ok(mut g) = SPEC_STAMPS.lock() {
807 *g = Some(SpecStampLog {
808 t_last: std::time::Instant::now(),
809 items: Vec::with_capacity(64),
810 });
811 }
812}
813
814fn spec_stamps_take() -> Vec<(&'static str, f32)> {
815 SPEC_STAMPS
816 .lock()
817 .ok()
818 .and_then(|mut g| g.take())
819 .map(|l| l.items)
820 .unwrap_or_default()
821}
822
823fn spec_stamps_format(items: &[(&'static str, f32)]) -> String {
826 let mut agg: Vec<(&'static str, f32, u32)> = Vec::with_capacity(items.len());
827 for &(n, ms) in items {
828 match agg.iter_mut().find(|e| e.0 == n) {
829 Some(e) => {
830 e.1 += ms;
831 e.2 += 1;
832 }
833 None => agg.push((n, ms, 1)),
834 }
835 }
836 let mut s = String::with_capacity(agg.len() * 16);
837 for (n, ms, k) in agg {
838 if k > 1 {
839 s.push_str(&format!("{n} {ms:.1}/{k} "));
840 } else {
841 s.push_str(&format!("{n} {ms:.1} "));
842 }
843 }
844 s
845}
846
847#[derive(Default, Clone, Copy)]
869struct SpecMon {
870 round_ms: f64,
871 tokens: f64,
872 plain_ms: f64,
873 n: u32,
874 fails: u32,
875 metal: bool,
876}
877
878const SPEC_PROXY_TOKENS: f64 = 3.5;
881const SPEC_PLAIN_MIN_MS: f64 = 200.0;
884
885impl SpecMon {
886 fn round(&mut self, dt_ms: f64, produced: usize) {
887 self.n += 1;
888 if self.n == 1 {
889 return; }
891 let a = if self.n == 2 { 1.0 } else { 0.3 };
892 self.round_ms += a * (dt_ms - self.round_ms);
893 self.tokens += a * (produced as f64 - self.tokens);
894 }
895 fn pays(&self) -> bool {
896 if self.plain_ms > 0.0 {
897 self.tokens * self.plain_ms > self.round_ms * 1.03
898 } else {
899 self.metal && self.tokens >= SPEC_PROXY_TOKENS
900 }
901 }
902 fn plain_done(&self, t0: std::time::Instant, gen0: usize, generated: usize) -> bool {
904 let n = generated.saturating_sub(gen0);
905 if n >= 8 {
906 return true;
907 }
908 self.metal && n >= 2 && t0.elapsed().as_secs_f64() * 1e3 >= SPEC_PLAIN_MIN_MS
909 }
910}
911
912pub struct GenerateResult {
914 pub text: String,
915 pub token_ids: Vec<u32>,
916 pub prompt_tokens: usize,
917 pub tokens_generated: usize,
918 pub finish_reason: String,
919 pub mtp_drafted: usize,
921 pub mtp_accepted: usize,
922 pub token_confidence: Vec<f32>,
927 pub traces: Vec<TokenTrace>,
930}
931
932#[derive(Clone, Debug)]
937pub struct TokenTrace {
938 pub t: usize,
940 pub token_id: u32,
942 pub confidence: f32,
944 pub active_skill: Option<String>,
946 pub recon: Option<f32>,
950 pub switched: bool,
953}
954
955#[cfg_attr(not(test), allow(dead_code))]
960fn top1_prob_t(logits: &[f32], id: u32, temp: f32) -> f32 {
961 let t = if temp > 1e-3 { temp } else { 1.0 };
962 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
963 let sum: f32 = logits.iter().map(|&v| ((v - max) / t).exp()).sum();
964 if sum > 0.0 {
965 (((logits[id as usize] - max) / t).exp()) / sum
966 } else {
967 0.0
968 }
969}
970
971fn prefill_batched() -> bool {
974 std::env::var("CMF_PREFILL")
975 .map(|v| v != "seq")
976 .unwrap_or(true)
977}
978
979#[inline]
983fn nll_graph_policy(
984 unmasked: bool,
985 prefer_graph: bool,
986 native_metal: bool,
987) -> (bool, bool) {
988 let graph_quality = unmasked && prefer_graph;
989 let fused_head_quality = graph_quality && native_metal;
990 (graph_quality, fused_head_quality)
991}
992
993#[derive(Clone, Copy)]
997enum PrefillIn<'a> {
998 Ids(&'a [u32]),
999 Hidden(&'a [f32]),
1000}
1001
1002impl Pipeline {
1009 fn can_prefill_batched(&self) -> bool {
1010 #[cfg(test)]
1011 let force_serial = self.nll_test_force_serial;
1012 #[cfg(not(test))]
1013 let force_serial = false;
1014 prefill_batched() && !force_serial && !self.weights.layers.is_empty()
1015 }
1016
1017 fn automatic_gpu_prefix(&self) -> Option<usize> {
1020 let (model, _, _, _) = self.weights.embed_tokens.graph_weight()?;
1021 crate::gpu::automatic_layer_prefix(&model, self.num_layers, self.physical_layers)
1022 }
1023
1024 pub fn prefill_chunk(&self) -> usize {
1028 let env = env_prefill_chunk();
1029 if env.is_some() || ChunkHost::here() != ChunkHost::Other {
1030 return prefill_chunk_rule(env, ChunkHost::here(), false);
1031 }
1032 prefill_chunk_rule(None, ChunkHost::Other, self.chunk_stack_facts().dense_on_discrete())
1033 }
1034
1035 fn chunk_stack_facts(&self) -> ChunkStackFacts {
1036 let plain_dense = !self.weights.layers.is_empty()
1037 && self.g3n.is_none()
1038 && self.dsv4.is_none()
1039 && self.dsv41.is_none()
1040 && self.qwen4_exp.is_none()
1041 && self.weights.layers.iter().all(|lw| {
1042 matches!(lw.attn, AttnKind::Full { .. }) && matches!(lw.ffn, FfnKind::Dense(_))
1043 });
1044 let gpu_on = crate::gpu::enabled();
1045 ChunkStackFacts {
1046 plain_dense,
1047 discrete: gpu_on && crate::gpu::discrete(),
1048 gpu_on,
1049 capacity_split: std::env::var_os("CMF_GPU_LAYERS").is_some()
1052 || (plain_dense && gpu_on && self.automatic_gpu_prefix().is_some()),
1053 multi_gpu: self.gpu_plan.is_some(),
1054 o1: self.o1_active(),
1055 }
1056 }
1057}
1058
1059pub fn prefill_chunk() -> usize {
1068 prefill_chunk_rule(env_prefill_chunk(), ChunkHost::here(), false)
1069}
1070
1071fn env_prefill_chunk() -> Option<usize> {
1072 std::env::var("CMF_PREFILL_CHUNK")
1073 .ok()
1074 .and_then(|v| v.parse::<usize>().ok())
1075}
1076
1077#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1079enum ChunkHost {
1080 Macos,
1081 Aarch64,
1083 Other,
1085}
1086
1087impl ChunkHost {
1088 fn here() -> Self {
1089 if cfg!(target_os = "macos") {
1090 ChunkHost::Macos
1091 } else if cfg!(target_arch = "aarch64") {
1092 ChunkHost::Aarch64
1093 } else {
1094 ChunkHost::Other
1095 }
1096 }
1097}
1098
1099const DISCRETE_DENSE_PREFILL_CHUNK: usize = 512;
1106
1107fn prefill_chunk_rule(env: Option<usize>, host: ChunkHost, dense_on_discrete: bool) -> usize {
1113 if let Some(n) = env {
1114 return n.max(1);
1115 }
1116 match host {
1117 ChunkHost::Macos => 512,
1118 ChunkHost::Aarch64 => 256,
1121 ChunkHost::Other if dense_on_discrete => DISCRETE_DENSE_PREFILL_CHUNK,
1122 ChunkHost::Other => 48,
1123 }
1124}
1125
1126#[derive(Clone, Copy, Debug, Default)]
1128struct ChunkStackFacts {
1129 plain_dense: bool,
1132 discrete: bool,
1134 gpu_on: bool,
1136 capacity_split: bool,
1138 multi_gpu: bool,
1140 o1: bool,
1142}
1143
1144impl ChunkStackFacts {
1145 fn dense_on_discrete(self) -> bool {
1146 self.plain_dense
1147 && self.discrete
1148 && self.gpu_on
1149 && !self.capacity_split
1150 && !self.multi_gpu
1151 && !self.o1
1152 }
1153}
1154
1155#[inline]
1161fn mtp_prefill_pair_count(start: usize, end: usize, input_len: usize) -> usize {
1162 if end <= start || start >= input_len {
1163 return 0;
1164 }
1165 let rows = (end.min(input_len) - start).min(input_len - start);
1166 if end < input_len {
1167 rows
1168 } else {
1169 rows.saturating_sub(1)
1170 }
1171}
1172
1173pub type TokenCallback = Box<dyn FnMut(&str) -> bool + Send>;
1175
1176#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1178pub(crate) struct ReuseLayer {
1179 pub full: bool,
1182 pub host_rows: usize,
1184 pub device_rows: Option<usize>,
1186 pub device_state: bool,
1188}
1189
1190#[derive(Debug, Clone, PartialEq, Eq)]
1192pub(crate) enum ReusePlan {
1193 Ready,
1195 Pull(Vec<(usize, usize, usize)>),
1198 Fresh,
1200}
1201
1202pub(crate) fn kv_reuse_plan(reuse_from: usize, layers: &[ReuseLayer]) -> ReusePlan {
1212 let mut pulls = Vec::new();
1213 for (li, l) in layers.iter().enumerate() {
1214 if !l.full {
1215 if l.device_state {
1216 return ReusePlan::Fresh;
1217 }
1218 continue;
1219 }
1220 if l.host_rows == reuse_from {
1221 continue;
1222 }
1223 if l.host_rows < reuse_from && l.device_rows.is_some_and(|d| d >= reuse_from) {
1224 pulls.push((li, l.host_rows, reuse_from));
1225 continue;
1226 }
1227 return ReusePlan::Fresh;
1228 }
1229 if pulls.is_empty() {
1230 ReusePlan::Ready
1231 } else {
1232 ReusePlan::Pull(pulls)
1233 }
1234}
1235
1236impl Pipeline {
1237 fn clear_sequence_state(&mut self) {
1245 #[cfg(target_os = "macos")]
1248 let _ = crate::gpu_metal::wait_replay();
1249 self.kv_cache.clear();
1250 self.kv_history.clear();
1251 if let Some(b) = &mut self.dsv41 {
1252 b.3.clear();
1253 }
1254 crate::gpu::graph_kv_reset(self.graph_kv_id);
1255 crate::gpu::graph_kv_reset(self.mtp_kv_id());
1260 }
1261
1262 fn prepare_kv_reuse(&mut self, reuse_from: usize) -> bool {
1269 if self.graph_prefill_preferred() {
1270 return true;
1271 }
1272 let kv_id = self.graph_kv_id;
1273 let layers: Vec<ReuseLayer> = (0..self.num_layers)
1274 .map(|li| {
1275 let full = matches!(
1276 self.weights.layers[self.phys_layer(li)].attn,
1277 AttnKind::Full { .. }
1278 );
1279 ReuseLayer {
1280 full,
1281 host_rows: self.kv_cache.layers[li].seq_len,
1282 device_rows: crate::gpu::graph_kv_stored(kv_id, li),
1283 device_state: crate::gpu::graph_state_resident(kv_id, li),
1284 }
1285 })
1286 .collect();
1287 if layers
1291 .iter()
1292 .all(|l| l.device_rows.is_none() && !l.device_state)
1293 {
1294 return true;
1295 }
1296 let plan = kv_reuse_plan(reuse_from, &layers);
1297 let (what, rows, n) = match &plan {
1298 ReusePlan::Ready => ("host ready", 0, 0),
1299 ReusePlan::Fresh => ("fresh", 0, 0),
1300 ReusePlan::Pull(p) => (
1301 "pull",
1302 p.iter().map(|&(_, a, b)| b - a).max().unwrap_or(0),
1303 p.len(),
1304 ),
1305 };
1306 let t0 = std::time::Instant::now();
1307 let ok = self.apply_kv_reuse_plan(reuse_from, plan, &layers);
1308 if std::env::var("CMF_PREFILL_PROF").is_ok() {
1309 eprintln!(
1310 "kv-reuse: {what}{}: {rows} device row(s) × {n} layer(s) to the host in {:.2} ms",
1311 if ok { "" } else { " (failed → fresh)" },
1312 t0.elapsed().as_secs_f64() * 1e3
1313 );
1314 }
1315 ok
1316 }
1317
1318 fn apply_kv_reuse_plan(
1319 &mut self,
1320 reuse_from: usize,
1321 plan: ReusePlan,
1322 layers: &[ReuseLayer],
1323 ) -> bool {
1324 let kv_id = self.graph_kv_id;
1325 match plan {
1326 ReusePlan::Fresh => return false,
1327 ReusePlan::Ready => {}
1328 ReusePlan::Pull(pulls) => {
1329 let (nkv, hd) = {
1332 let c = &self.kv_cache.layers[pulls[0].0];
1333 (c.num_kv_heads, c.head_dim)
1334 };
1335 if pulls.iter().any(|&(li, _, _)| {
1336 let c = &self.kv_cache.layers[li];
1337 (c.num_kv_heads, c.head_dim) != (nkv, hd)
1338 }) {
1339 return false;
1340 }
1341 let Some(rows) = crate::gpu::graph_kv_read_rows(kv_id, &pulls, nkv, hd) else {
1342 return false;
1343 };
1344 for ((li, from, to), (k, v)) in pulls.into_iter().zip(rows) {
1345 let cache = &mut self.kv_cache.layers[li];
1346 let row = nkv * hd;
1347 for p in 0..to - from {
1348 cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
1349 }
1350 if cache.seq_len != to {
1351 return false;
1352 }
1353 }
1354 }
1355 }
1356 for (li, l) in layers.iter().enumerate() {
1360 if l.full
1361 && l.device_rows.is_some_and(|d| d > reuse_from)
1362 && !crate::gpu::graph_kv_set_stored(kv_id, li, reuse_from)
1363 {
1364 return false;
1365 }
1366 }
1367 true
1368 }
1369
1370 fn finish_generation(
1376 &mut self,
1377 mtp: &mut Option<MtpModule>,
1378 router: &mut Option<crate::swarm::DynRouter>,
1379 clear_sequence: bool,
1380 ) {
1381 if router.is_some() {
1385 let _ = self.set_active_skill(None);
1386 }
1387 #[cfg(target_os = "macos")]
1394 let clear_sequence = clear_sequence || !crate::gpu_metal::wait_replay();
1395 if clear_sequence {
1396 self.clear_sequence_state();
1397 if let Some(m) = mtp.as_mut() {
1398 m.kv.clear();
1404 }
1405 if let Some(m) = self.mtp.as_mut() {
1406 m.kv.clear();
1410 }
1411 }
1412 self.graph_want_logits = false;
1413 self.graph_head_required = false;
1414 self.graph_logits = None;
1415 self.graph_failed
1416 .store(false, std::sync::atomic::Ordering::Relaxed);
1417 self.cancel
1418 .store(false, std::sync::atomic::Ordering::Relaxed);
1419 self.dyn_router = router.take().or(self.dyn_router.take());
1420 self.mtp = mtp.take().or(self.mtp.take());
1421 self.mtp_graph_mode = None;
1422 self.spec_forced = None;
1423 }
1424
1425 fn check_forward_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
1429 if self
1430 .graph_failed
1431 .swap(false, std::sync::atomic::Ordering::Relaxed)
1432 {
1433 self.cancel
1434 .store(false, std::sync::atomic::Ordering::Relaxed);
1435 self.clear_sequence_state();
1436 self.graph_logits = None;
1437 self.graph_want_logits = false;
1438 self.graph_head_required = false;
1439 return Err(format!("GPU graph failed during {phase} at position {pos}"));
1440 }
1441 Ok(())
1442 }
1443
1444 #[cfg(target_os = "macos")]
1445 fn fail_metal_graph(&mut self, reason: &str) {
1446 crate::pipeline::METAL_GRAPH_ERRORS
1447 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1448 self.clear_sequence_state();
1449 self.graph_logits = None;
1450 self.graph_failed
1451 .store(true, std::sync::atomic::Ordering::Relaxed);
1452 self.cancel
1453 .store(true, std::sync::atomic::Ordering::Relaxed);
1454 tracing::error!("native Metal TokenGraph failed closed: {reason}");
1455 }
1456
1457 fn nll_begin(&mut self) -> Result<(), String> {
1462 if self
1463 .graph_failed
1464 .swap(false, std::sync::atomic::Ordering::Relaxed)
1465 {
1466 self.cancel
1467 .store(false, std::sync::atomic::Ordering::Relaxed);
1468 self.clear_sequence_state();
1469 self.graph_logits = None;
1470 self.graph_want_logits = false;
1471 self.graph_head_required = false;
1472 return Err("GPU graph failed before NLL scoring".to_string());
1473 }
1474 self.clear_sequence_state();
1475 self.graph_logits = None;
1476 self.graph_want_logits = false;
1477 self.graph_head_required = false;
1478 Ok(())
1479 }
1480
1481 fn nll_end(&mut self) {
1485 self.clear_sequence_state();
1486 self.graph_logits = None;
1487 self.graph_want_logits = false;
1488 self.graph_head_required = false;
1489 self.graph_failed
1490 .store(false, std::sync::atomic::Ordering::Relaxed);
1491 }
1492
1493 fn nll_check_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
1496 #[cfg(test)]
1497 if self.nll_test_fail_at == Some(pos) {
1498 self.nll_test_fail_at = None;
1499 self.graph_failed
1500 .store(true, std::sync::atomic::Ordering::Relaxed);
1501 self.cancel
1502 .store(true, std::sync::atomic::Ordering::Relaxed);
1503 }
1504 if self
1505 .graph_failed
1506 .swap(false, std::sync::atomic::Ordering::Relaxed)
1507 {
1508 self.cancel
1509 .store(false, std::sync::atomic::Ordering::Relaxed);
1510 self.clear_sequence_state();
1511 self.graph_logits = None;
1512 self.graph_want_logits = false;
1513 return Err(format!(
1514 "GPU graph failed during NLL {phase} at position {pos}"
1515 ));
1516 }
1517 Ok(())
1518 }
1519
1520 #[inline]
1524 pub fn phys_layer(&self, virtual_idx: usize) -> usize {
1525 virtual_idx % self.physical_layers
1526 }
1527
1528 #[inline]
1531 pub fn is_loop_end(&self, virtual_idx: usize) -> bool {
1532 self.loop_final_norm && (virtual_idx + 1) % self.physical_layers == 0
1533 }
1534
1535 #[allow(clippy::too_many_arguments)]
1537
1538 #[cfg(target_os = "macos")]
1557 fn graph_prefill_preferred(&self) -> bool {
1558 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
1559 if !crate::gpu::enabled_here()
1560 || !graph_force
1561 || std::env::var("CMF_GPU_BLOCK")
1562 .map(|v| v == "0")
1563 .unwrap_or(false)
1564 || std::env::var("CMF_PREFILL_GRAPH").as_deref() == Ok("0")
1567 {
1568 return false;
1569 }
1570 self.weights
1571 .layers
1572 .iter()
1573 .any(|lw| {
1574 matches!(&lw.attn, AttnKind::LinearGdn(w) if w.in_proj_qkv.metal_graph_parts().is_some())
1575 })
1576 }
1577
1578 #[cfg(not(target_os = "macos"))]
1579 fn graph_prefill_preferred(&self) -> bool {
1580 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill);
1588 if !graph_on || !crate::gpu::enabled_here() {
1589 return false;
1590 }
1591 if self.o1_active() {
1605 return false;
1606 }
1607 if self
1608 .weights
1609 .layers
1610 .iter()
1611 .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(_)))
1612 {
1613 return true;
1614 }
1615 self.weights
1626 .layers
1627 .iter()
1628 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)))
1629 && self.automatic_gpu_prefix().is_none()
1630 }
1631
1632 #[cfg(target_os = "macos")]
1633 fn q1_graph_gpu(
1634 &mut self,
1635 start: usize,
1636 upto: Option<usize>,
1637 position: usize,
1638 h: &mut [f32],
1639 ) -> usize {
1640 let _mt0 = std::time::Instant::now(); use crate::gpu::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, MetalFfn, TokenGraph};
1642 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
1643 if self.attn_softcap > 0.0 || !crate::gpu::enabled_here()
1645 || !graph_force
1646 || std::env::var("CMF_GPU_BLOCK")
1647 .map(|v| v == "0")
1648 .unwrap_or(false)
1649 {
1650 if std::env::var("CMF_GRAPH_DBG").is_ok() {
1651 eprintln!(
1652 "block-graph: front gate (softcap={} enabled_here={} graph_force={})",
1653 self.attn_softcap > 0.0,
1654 crate::gpu::enabled_here(),
1655 graph_force,
1656 );
1657 }
1658 if self.graph_head_required {
1659 self.fail_metal_graph("native graph front gate refused");
1660 }
1661 return start;
1662 }
1663 if self.swa.is_some()
1667 || self.global_attn.is_some()
1668 || self.attention_heads_per_layer.is_some()
1669 || self.attn_v_norm
1670 || self.weights.layers.iter().any(|lw| {
1671 lw.attn_out_norm.is_some()
1672 || lw.ffn_out_norm.is_some()
1673 || lw.layer_scale.is_some()
1674 || matches!(&lw.ffn, FfnKind::Dense(d) if d.act != Act::Silu)
1675 })
1676 {
1677 if std::env::var("CMF_GRAPH_DBG").is_ok() {
1678 eprintln!(
1679 "block-graph: arch ineligible (swa={} gattn={} hpl={} vnorm={} scale_delta={:.2e})",
1680 self.swa.is_some(),
1681 self.global_attn.is_some(),
1682 self.attention_heads_per_layer.is_some(),
1683 self.attn_v_norm,
1684 (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs(),
1685 );
1686 }
1687 if self.graph_head_required {
1688 self.fail_metal_graph("native graph architecture gate refused");
1689 }
1690 return start;
1691 }
1692 let limit = upto
1695 .map(|u| u + 1)
1696 .unwrap_or(self.num_layers)
1697 .min(self.num_layers);
1698
1699 enum Item<'a> {
1700 Gdn {
1701 run: Vec<GdnGpuLayer<'a>>,
1702 first: usize,
1703 },
1704 Attn {
1705 l: AttnGpuLayer<'a>,
1706 li: usize,
1707 q_norm: Option<&'a [f32]>,
1708 k_norm: Option<&'a [f32]>,
1709 output_gate: bool,
1710 bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
1711 full_gpu: bool,
1714 },
1715 }
1716
1717 let attend_mode = std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into());
1724 let attend_contract = attend_mode != "0"
1725 && attend_mode != "off"
1726 && self.head_dim % 4 == 0
1727 && self.head_dim <= 256
1728 && self.rotary_dim >= 2
1729 && self.rotary_dim <= self.head_dim
1730 && (self.rotary_dim / 2) % 32 == 0
1731 && self.num_kv_heads > 0
1732 && self.num_heads % self.num_kv_heads == 0;
1733
1734 let mut plan: Vec<Item> = Vec::new();
1735 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
1736 let block_diag = std::env::var("CMF_GRAPH_DBG").is_ok();
1738 let mut scan = start;
1739 while scan < limit {
1740 let lw = &self.weights.layers[self.phys_layer(scan)];
1741 let ffn = match &lw.ffn {
1742 FfnKind::Dense(d) if d.segs.is_empty() => {
1743 let (Some(g), Some(u), Some(dn)) = (
1744 d.gate_proj.metal_graph_parts(),
1745 d.up_proj.metal_graph_parts(),
1746 d.down_proj.metal_graph_parts(),
1747 ) else {
1748 if block_diag {
1749 eprintln!(
1750 "block-graph: L{scan} FFN trio not graph-mappable — run ends"
1751 );
1752 }
1753 break;
1754 };
1755 MetalFfn::Dense {
1756 gate: g,
1757 up: u,
1758 down: dn,
1759 }
1760 }
1761 FfnKind::Moe(m) => {
1762 let Some(moe) = metal_moe_graph_parts(m, self.hidden_size) else {
1763 if block_diag {
1764 eprintln!(
1765 "block-graph: L{scan} MoE outside the graph contract — run ends"
1766 );
1767 }
1768 break;
1769 };
1770 if let QTensor::Mapped { model, .. } = &m.experts[0].gate_proj {
1771 model_ref.get_or_insert_with(|| model.clone());
1772 }
1773 MetalFfn::Moe(moe)
1774 }
1775 _ => {
1776 if block_diag {
1777 eprintln!("block-graph: L{scan} non-graph FFN — run ends");
1778 }
1779 break;
1780 }
1781 };
1782 match &lw.attn {
1783 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
1784 let parts = (
1785 w.in_proj_qkv.metal_graph_parts(),
1786 w.in_proj_z.metal_graph_parts(),
1787 w.in_proj_a.f32_parts(),
1788 w.in_proj_b.f32_parts(),
1789 w.out_proj.metal_graph_parts(),
1790 );
1791 let (Some(qkv), Some(z), Some(a), Some(b), Some(out)) = parts else {
1792 if block_diag {
1793 eprintln!(
1794 "block-graph: L{scan} GDN parts refused (qkv={} z={} a_f32={} b_f32={} out={})",
1795 w.in_proj_qkv.metal_graph_parts().is_some(),
1796 w.in_proj_z.metal_graph_parts().is_some(),
1797 w.in_proj_a.f32_parts().is_some(),
1798 w.in_proj_b.f32_parts().is_some(),
1799 w.out_proj.metal_graph_parts().is_some(),
1800 );
1801 }
1802 break;
1803 };
1804 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
1805 model_ref.get_or_insert_with(|| model.clone());
1806 }
1807 let gl = GdnGpuLayer {
1808 attn_norm: &lw.input_norm,
1809 post_norm: &lw.post_norm,
1810 qkv,
1811 z,
1812 a,
1813 b,
1814 out,
1815 ffn,
1816 conv1d: &w.conv1d,
1817 a_log: &w.a_log,
1818 dt_bias: &w.dt_bias,
1819 gnorm: &w.norm,
1820 };
1821 match plan.last_mut() {
1822 Some(Item::Gdn { run, .. }) => run.push(gl),
1823 _ => plan.push(Item::Gdn {
1824 run: vec![gl],
1825 first: scan,
1826 }),
1827 }
1828 }
1829 AttnKind::Full {
1830 wq,
1831 wk,
1832 wv,
1833 wo,
1834 q_norm,
1835 k_norm,
1836 output_gate,
1837 softplus_gate: None,
1838 bias,
1839 } if !self.kv_cache.layers[scan].o1_sealed()
1840 || std::env::var("CMF_O1_METAL").as_deref() == Ok("1") =>
1845 {
1846 let parts = (
1847 wq.metal_graph_parts(),
1848 wk.metal_graph_parts(),
1849 wv.metal_graph_parts(),
1850 wo.metal_graph_parts(),
1851 );
1852 let (Some(pq), Some(pk), Some(pv), Some(po)) = parts else {
1853 break;
1854 };
1855 if let QTensor::Mapped { model, .. } = wq {
1856 model_ref.get_or_insert_with(|| model.clone());
1857 }
1858 let cache = &self.kv_cache.layers[scan];
1859 let o1_metal = cache.o1.is_some()
1863 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1")
1864 && cache.o1_views().is_some();
1865 let full_gpu = attend_contract
1866 && cache.mode == crate::kv_cache::KvMode::F32
1867 && (cache.o1.is_none() || o1_metal)
1868 && bias.is_none()
1869 && pq.1 == self.num_heads * self.head_dim * (1 + *output_gate as usize)
1870 && pk.1 == self.num_kv_heads * self.head_dim
1871 && pv.1 == self.num_kv_heads * self.head_dim
1872 && po.2 == self.num_heads * self.head_dim;
1873 plan.push(Item::Attn {
1874 l: AttnGpuLayer {
1875 attn_norm: &lw.input_norm,
1876 post_norm: &lw.post_norm,
1877 wq: pq,
1878 wk: pk,
1879 wv: pv,
1880 wo: po,
1881 ffn,
1882 },
1883 li: scan,
1884 q_norm: q_norm.as_deref(),
1885 k_norm: k_norm.as_deref(),
1886 output_gate: *output_gate,
1887 bias: bias
1888 .as_ref()
1889 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
1890 full_gpu,
1891 });
1892 }
1893 _ => break,
1894 }
1895 scan += 1;
1896 }
1897 let Some(model) = model_ref else {
1898 if std::env::var("CMF_GRAPH_DBG").is_ok() {
1899 eprintln!("q1-graph: no model ref (start {start}, scanned to {scan})");
1900 }
1901 if self.graph_head_required {
1902 self.fail_metal_graph("native graph has no mapped model reference");
1903 }
1904 return start;
1905 };
1906 if plan.is_empty() {
1907 if std::env::var("CMF_GRAPH_DBG").is_ok() {
1908 eprintln!("q1-graph: empty plan at layer {start}");
1909 }
1910 if self.graph_head_required {
1911 self.fail_metal_graph("native graph plan is empty");
1912 }
1913 return start;
1914 }
1915 let has_moe = plan.iter().any(|it| match it {
1916 Item::Gdn { run, .. } => run.iter().any(|l| matches!(l.ffn, MetalFfn::Moe(_))),
1917 Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::Moe(_)),
1918 });
1919 let has_gdn = plan.iter().any(|it| matches!(it, Item::Gdn { .. }));
1920 let dev_attend = attend_contract
1921 && (self.head_dim <= 128
1922 || has_moe
1923 || (self.head_dim <= 256 && has_gdn)
1929 || attend_mode == "force"
1930 || attend_mode == "256");
1931 if !dev_attend {
1932 for it in &mut plan {
1933 if let Item::Attn { li, full_gpu, .. } = it {
1934 let keep_o1 = self.kv_cache.layers[*li].o1.is_some()
1937 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1");
1938 if !keep_o1 {
1939 *full_gpu = false;
1940 }
1941 }
1942 }
1943 }
1944 if std::env::var("CMF_GRAPH_DBG").is_ok() {
1945 use std::sync::atomic::{AtomicBool, Ordering};
1946 static SAID: AtomicBool = AtomicBool::new(false);
1947 if !SAID.swap(true, Ordering::Relaxed) {
1948 let fg = plan
1949 .iter()
1950 .filter(|it| matches!(it, Item::Attn { full_gpu: true, .. }))
1951 .count();
1952 let att = plan
1953 .iter()
1954 .filter(|it| matches!(it, Item::Attn { .. }))
1955 .count();
1956 eprintln!(
1957 "q1-graph: plan of {} items from layer {start} to {scan} | dev_attend={dev_attend} full_gpu {fg}/{att} | hd={} rd={} nkv={} nh={}",
1958 plan.len(),
1959 self.head_dim,
1960 self.rotary_dim,
1961 self.num_kv_heads,
1962 self.num_heads,
1963 );
1964 }
1965 }
1966 let dims = GraphDims {
1967 hidden: self.hidden_size,
1968 eps: self.rms_eps as f32,
1969 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
1970 };
1971 let Some(mut graph) = TokenGraph::new(&model, dims, h) else {
1972 if self.graph_head_required {
1973 self.fail_metal_graph("native TokenGraph allocation refused");
1974 }
1975 return start;
1976 };
1977 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
1978 nv: cfg.num_v_heads,
1979 nk: cfg.num_k_heads,
1980 dk: cfg.key_head_dim,
1981 dv: cfg.value_head_dim,
1982 kk: cfg.conv_kernel,
1983 hidden: self.hidden_size,
1984 inter: self.intermediate_size,
1985 c_dim: cfg.conv_dim(),
1986 eps: cfg.rms_eps as f32,
1987 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
1988 });
1989 let mut valid = 0usize;
1993 let mut end = start;
1994 crate::gpu::stageprof(1, _mt0.elapsed()); if std::env::var("CMF_PLAN_DUMP").is_ok() {
1996 static ONCE: std::sync::Once = std::sync::Once::new();
1997 ONCE.call_once(|| {
1998 for it in &plan {
1999 match it {
2000 Item::Gdn { first, run } => {
2001 eprintln!("plan: Gdn first={first} len={}", run.len())
2002 }
2003 Item::Attn { li, full_gpu, .. } => {
2004 eprintln!("plan: Attn li={li} full_gpu={full_gpu}")
2005 }
2006 }
2007 }
2008 });
2009 }
2010 for item in &plan {
2011 let ok = match item {
2012 Item::Gdn { run, .. } => gcfg
2013 .as_ref()
2014 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
2015 .unwrap_or(false),
2016 Item::Attn { l, .. } => graph.attn_ok(l),
2017 };
2018 if !ok {
2019 if block_diag {
2020 eprintln!(
2021 "block-graph: plan item {} ({}) failed graph preflight",
2022 valid,
2023 match item {
2024 Item::Gdn { run, first } => format!("GDN run L{first}+{}", run.len()),
2025 Item::Attn { li, .. } => format!("Attn L{li}"),
2026 }
2027 );
2028 }
2029 break;
2030 }
2031 valid += 1;
2032 end += match item {
2033 Item::Gdn { run, .. } => run.len(),
2034 Item::Attn { .. } => 1,
2035 };
2036 }
2037 plan.truncate(valid);
2038 if plan.is_empty() {
2039 if self.graph_head_required {
2040 self.fail_metal_graph("native graph preflight produced no valid items");
2041 }
2042 return start;
2043 }
2044
2045 if self.graph_head_required && (upto.is_some() || end != self.num_layers) {
2046 self.fail_metal_graph("fused-head NLL requires a complete 64-layer graph");
2047 return start;
2048 }
2049
2050 let one_pass = |t: (usize, usize, usize)| {
2059 use cortiq_core::TensorDtype as D;
2060 matches!(
2061 model.tensors[t.0].dtype,
2062 D::Q4TiledP | D::Q4Tiled | D::Q4Block | D::Q8Row | D::Q8_2f | D::Q1
2063 )
2064 };
2065 let dense_fast = plan.iter().all(|it| match it {
2066 Item::Attn {
2067 l, li, full_gpu, ..
2068 } => {
2069 *full_gpu
2070 && self.kv_cache.layers[*li].o1.is_none()
2071 && [l.wq, l.wk, l.wv, l.wo].into_iter().all(one_pass)
2072 && match l.ffn {
2073 MetalFfn::Dense { gate, up, down } => {
2074 one_pass(gate) && one_pass(up) && one_pass(down)
2075 }
2076 _ => false,
2077 }
2078 }
2079 Item::Gdn { .. } => false,
2080 });
2081 let ab = crate::gpu_metal::dense_ab_arm().filter(|_| dense_fast);
2082 let _mv_fast = match ab {
2083 Some((bits, _)) => {
2084 graph.set_dense_concurrent_raw(bits & crate::gpu_metal::DENSE_CONC != 0);
2085 crate::gpu_metal::MvFastGuard::set_raw(bits)
2086 }
2087 None => {
2088 graph.set_dense_concurrent(dense_fast);
2089 crate::gpu_metal::MvFastGuard::set_bits(if dense_fast {
2090 crate::gpu_metal::DENSE_MV | crate::gpu_metal::DENSE_FUSE
2091 } else {
2092 0
2093 })
2094 }
2095 };
2096
2097 let inv_freq = self.inv_freq.clone();
2098 let pool = self.pool.clone();
2099 let (nh, nkv, hd, hs, rd, eps) = (
2100 self.num_heads,
2101 self.num_kv_heads,
2102 self.head_dim,
2103 self.hidden_size,
2104 self.rotary_dim,
2105 self.rms_eps,
2106 );
2107 let norm_style = self.norm_style;
2108 let gemma = norm_style == cortiq_core::NormStyle::Gemma;
2109 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
2110 let kv_id = self.graph_kv_id;
2111 let mut pending: Vec<(usize, usize)> = Vec::new();
2114 let mut dev_attn: Vec<usize> = Vec::new();
2117 for item in &plan {
2118 let _xt0 = std::time::Instant::now();
2119 let _xkind: u32 = match item {
2120 Item::Gdn { .. } => 2,
2121 Item::Attn { .. } => 3,
2122 };
2123 if self.loop_final_norm {
2125 let item_start = match item {
2126 Item::Gdn { first, .. } => *first,
2127 Item::Attn { li, .. } => *li,
2128 };
2129 if item_start > start && self.is_loop_end(item_start - 1) {
2130 graph.encode_loop_norm(&self.weights.final_norm);
2131 }
2132 }
2133 match item {
2134 Item::Gdn { run, first } => {
2135 for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
2136 if l.linear_state.len() != want {
2137 l.linear_state = vec![0f32; want];
2138 }
2139 }
2140 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
2141 .iter()
2142 .map(|l| l.linear_state.as_slice())
2143 .collect();
2144 let _ig = std::time::Instant::now();
2145 if !graph.encode_gdn_run(run, &ro, gcfg.as_ref().unwrap()) {
2146 tracing::error!("q1 graph: GDN run refused after validation");
2148 return start;
2149 }
2150 graph.commit_kind = 2;
2153 graph.commit();
2154 crate::gpu::stageprof(0, _ig.elapsed());
2155 pending.push((*first, run.len()));
2156 }
2157 Item::Attn {
2158 l,
2159 li,
2160 q_norm,
2161 k_norm,
2162 output_gate,
2163 bias,
2164 full_gpu,
2165 } => {
2166 let _ia = std::time::Instant::now();
2167 if *full_gpu {
2169 let cache = &self.kv_cache.layers[*li];
2170 let o1p = if cache.o1.is_some() {
2171 match cache.o1_views() {
2172 Some(views) => Some(crate::gpu::O1AttnParams {
2173 views,
2174 epoch: self.o1_epoch,
2175 }),
2176 None => None,
2178 }
2179 } else {
2180 None
2181 };
2182 let o1_layer = cache.o1.is_some();
2183 if o1_layer && o1p.is_none() {
2184 }
2186 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
2187 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
2188 let cpu_stored = if o1_layer { 0 } else { cpu_k[0].len() / hd };
2189 let p = crate::gpu::AttnDeviceParams {
2190 kv_id,
2191 layer: *li,
2192 nh,
2193 nkv,
2194 hd,
2195 rd,
2196 position,
2197 scale: self.attn_scale,
2198 eps: eps as f32,
2199 gemma,
2200 late_qk_norm: self.qk_norm_after_rope,
2201 output_gate: *output_gate,
2202 q_norm: *q_norm,
2203 k_norm: *k_norm,
2204 inv_freq: &inv_freq,
2205 cpu_k,
2206 cpu_v,
2207 cpu_stored,
2208 o1: o1p,
2209 };
2210 let o1_bad = o1_layer && p.o1.is_none();
2211 if !o1_bad && graph.attn_device_ok(l, &p) && graph.encode_attn_device(l, &p)
2212 {
2213 if p.o1.is_none() {
2215 dev_attn.push(*li);
2216 }
2217 graph.commit_kind = 3;
2218 graph.commit();
2219 crate::gpu::stageprof(_xkind, _xt0.elapsed());
2223 continue;
2224 }
2225 }
2227 graph.encode_attn_prefix(l);
2228 if let Err(err) = graph.sync_checked() {
2229 self.fail_metal_graph(&err);
2230 return start;
2231 }
2232 if !pending.is_empty() {
2233 let idxs: Vec<usize> =
2234 pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
2235 let mut outs: Vec<&mut [f32]> = self
2236 .kv_cache
2237 .layers
2238 .iter_mut()
2239 .enumerate()
2240 .filter(|(i, _)| idxs.binary_search(i).is_ok())
2241 .map(|(_, s)| s.linear_state.as_mut_slice())
2242 .collect();
2243 graph.read_states(&mut outs);
2244 }
2245 let mut q_raw = attention::take_buf(l.wq.1);
2246 let mut k = attention::take_buf(l.wk.1);
2247 let mut v = attention::take_buf(l.wv.1);
2248 graph.read_qkv(&mut q_raw, &mut k, &mut v);
2249 let cfg = QwenAttnCfg {
2250 num_heads: nh,
2251 num_kv_heads: nkv,
2252 head_dim: hd,
2253 hidden_size: hs,
2254 position,
2255 inv_freq: &inv_freq,
2256 rotary_dim: rd,
2257 scale: self.attn_scale,
2258 softcap: self.attn_softcap,
2259 window: None,
2260 v_norm: false,
2261 qk_norm_after_rope: self.qk_norm_after_rope,
2262 q_norm: *q_norm,
2263 k_norm: *k_norm,
2264 output_gate: *output_gate,
2265 softplus_gate: None,
2266 rope_scale: 1.0,
2267 bias: *bias,
2268 rms_eps: eps,
2269 norm_style,
2270 pool: pool.as_deref(),
2271 };
2272 let oracle = std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1")
2275 || std::env::var("CMF_ATTN_DUMP").is_ok();
2276 let _ = full_gpu;
2277 let oracle_in = oracle.then(|| (q_raw.clone(), k.clone(), v.clone()));
2278 let mut ao = attention::qwen_attention_core(
2279 q_raw,
2280 k,
2281 v,
2282 &mut self.kv_cache.layers[*li],
2283 &cfg,
2284 );
2285 if let Ok(dir) = std::env::var("CMF_ATTN_DUMP") {
2289 if let Some((qr0, k0, v0)) = oracle_in.clone() {
2290 let (cq, _cg, _ck, _cv) =
2291 attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
2292 let cache = &self.kv_cache.layers[*li];
2293 let n = cache.head_keys(0).len() / hd;
2294 let mut bytes: Vec<u8> = Vec::new();
2295 for v in [nh as u32, nkv as u32, hd as u32, n as u32, position as u32] {
2296 bytes.extend_from_slice(&v.to_le_bytes());
2297 }
2298 for v in &cq {
2299 bytes.extend_from_slice(&v.to_le_bytes());
2300 }
2301 for g in 0..nkv {
2302 for v in cache.head_keys(g) {
2303 bytes.extend_from_slice(&v.to_le_bytes());
2304 }
2305 }
2306 for g in 0..nkv {
2307 for v in cache.head_values(g) {
2308 bytes.extend_from_slice(&v.to_le_bytes());
2309 }
2310 }
2311 let _ =
2312 std::fs::write(format!("{dir}/L{li}_pos{position}.bin"), &bytes);
2313 }
2314 }
2315 if let Some((qr0, k0, v0)) =
2316 oracle_in.filter(|_| std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1"))
2317 {
2318 let (cq, _cg, ck, cv) =
2319 attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
2320 let mut h_now = vec![0f32; hs];
2321 graph.read_h(&mut h_now);
2322 let cache = &self.kv_cache.layers[*li];
2323 let n_after = cache.head_keys(0).len() / hd;
2324 let stored = n_after.saturating_sub(1);
2328 let cpu_k: Vec<&[f32]> = (0..nkv)
2329 .map(|g| &cache.head_keys(g)[..stored * hd])
2330 .collect();
2331 let cpu_v: Vec<&[f32]> = (0..nkv)
2332 .map(|g| &cache.head_values(g)[..stored * hd])
2333 .collect();
2334 let p = crate::gpu::AttnDeviceParams {
2335 kv_id,
2336 layer: *li,
2337 nh,
2338 nkv,
2339 hd,
2340 rd,
2341 position,
2342 scale: self.attn_scale,
2343 eps: eps as f32,
2344 gemma,
2345 late_qk_norm: self.qk_norm_after_rope,
2346 output_gate: *output_gate,
2347 q_norm: *q_norm,
2348 k_norm: *k_norm,
2349 inv_freq: &inv_freq,
2350 cpu_k,
2351 cpu_v,
2352 cpu_stored: stored,
2353 o1: None,
2354 };
2355 if let Some((dq, dk, dv, dao)) = graph.debug_attn_device(l, &p, &h_now) {
2356 let md = |a: &[f32], b: &[f32]| {
2357 a.iter().zip(b).fold(0f32, |m, (x, y)| m.max((x - y).abs()))
2358 };
2359 let nn = |a: &[f32]| a.iter().map(|x| x * x).sum::<f32>().sqrt();
2360 eprintln!(
2361 "attn-oracle L{li} pos {position}: |q| {:.2} max|dq| {:.4} | |k| {:.2} max|dk| {:.4} | |v| {:.2} max|dv| {:.4} | |ao| {:.2} max|dao| {:.4}",
2362 nn(&cq),
2363 md(&cq, &dq),
2364 nn(&ck),
2365 md(&ck, &dk),
2366 nn(&cv),
2367 md(&cv, &dv),
2368 nn(&ao),
2369 md(&ao, &dao)
2370 );
2371 } else {
2372 eprintln!("attn-oracle L{li}: device probe declined");
2373 }
2374 }
2375 graph.encode_attn_suffix(l, &ao);
2376 graph.commit();
2379 attention::recycle_buf(&mut ao);
2380 }
2381 }
2382
2383 crate::gpu::stageprof(_xkind, _xt0.elapsed());
2384 }
2385 let mut lm_rows = None;
2390 if self.graph_want_logits
2391 && upto.is_none()
2392 && end == self.num_layers
2393 && std::env::var("CMF_GPU_LMHEAD")
2394 .map(|v| v != "0")
2395 .unwrap_or(true)
2396 {
2397 if let Some(lm) = self.weights.lm_head.metal_graph_parts() {
2398 if graph.lm_head_ok(lm) {
2399 graph.encode_lm_head(&self.weights.final_norm, lm);
2400 lm_rows = Some(lm.1);
2401 }
2402 }
2403 }
2404 if self.graph_head_required && lm_rows.is_none() {
2405 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2406 self.fail_metal_graph("fused graph head was requested but not encodable");
2407 return start;
2408 }
2409 let _sy0 = std::time::Instant::now();
2410 if let Err(err) = graph.sync_checked() {
2411 self.fail_metal_graph(&err);
2412 return start;
2413 }
2414 let _rs0 = std::time::Instant::now();
2415 if !pending.is_empty() {
2416 let idxs: Vec<usize> = pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
2417 let mut outs: Vec<&mut [f32]> = self
2418 .kv_cache
2419 .layers
2420 .iter_mut()
2421 .enumerate()
2422 .filter(|(i, _)| idxs.binary_search(i).is_ok())
2423 .map(|(_, s)| s.linear_state.as_mut_slice())
2424 .collect();
2425 graph.read_states(&mut outs);
2426 }
2427 if std::env::var("CMF_GRAPH_HOSTPROF").as_deref() == Ok("1") {
2428 use std::sync::atomic::{AtomicU64, Ordering};
2429 static SY: AtomicU64 = AtomicU64::new(0);
2430 static RS: AtomicU64 = AtomicU64::new(0);
2431 static N: AtomicU64 = AtomicU64::new(0);
2432 SY.fetch_add((_rs0 - _sy0).as_nanos() as u64, Ordering::Relaxed);
2433 RS.fetch_add(_rs0.elapsed().as_nanos() as u64, Ordering::Relaxed);
2434 let n = N.fetch_add(1, Ordering::Relaxed) + 1;
2435 if n % 100 == 0 {
2436 eprintln!(
2437 "postprof: sync-wait {:.1} ms/ток | read_states {:.1} ms/ток ({n})",
2438 SY.load(Ordering::Relaxed) as f64 / n as f64 / 1e6,
2439 RS.load(Ordering::Relaxed) as f64 / n as f64 / 1e6
2440 );
2441 }
2442 }
2443 if let Some(rows) = lm_rows {
2444 crate::gpu::hostprof_encode_done(_mt0);
2445 let mut lg = attention::take_buf(rows.min(self.vocab_size));
2446 graph.read_logits(&mut lg);
2447 crate::gpu::hostprof_total(_mt0);
2448 lg.resize(self.vocab_size, 0.0);
2449 if let Some(c) = self.final_softcap {
2450 for l in lg.iter_mut() {
2451 *l = c * (*l / c).tanh();
2452 }
2453 }
2454 self.graph_logits = Some(lg);
2455 }
2456 graph.read_h(h);
2457 if self.graph_head_required && self.graph_logits.is_none() {
2458 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2459 self.fail_metal_graph("fused graph head completed without logits readback");
2460 return start;
2461 }
2462 METAL_GRAPH_TOK_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2463 METAL_GRAPH_LAYERS.fetch_add(
2464 end.saturating_sub(start) as u64,
2465 std::sync::atomic::Ordering::Relaxed,
2466 );
2467 if self.graph_head_required {
2468 METAL_GRAPH_HEAD_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2469 }
2470 for li in dev_attn {
2474 let mut krow = attention::take_buf(nkv * hd);
2475 let mut vrow = attention::take_buf(nkv * hd);
2476 if crate::gpu::kv_mirror_read_last(kv_id, li, nkv, hd, &mut krow, &mut vrow) {
2477 let cache = &mut self.kv_cache.layers[li];
2478 cache.append(&krow, &vrow, &[]);
2479 let n = cache.seq_len;
2480 let mut imp = attention::take_buf(n);
2481 crate::gpu::kv_mirror_take_imp(kv_id, li, &mut imp);
2482 cache.accumulate_imp(&imp);
2483 attention::recycle_buf(&mut imp);
2484 }
2485 attention::recycle_buf(&mut krow);
2486 attention::recycle_buf(&mut vrow);
2487 }
2488 if let Some((_, arm)) = ab {
2489 crate::gpu_metal::dense_ab_record(arm, _mt0.elapsed());
2490 }
2491 end
2492 }
2493
2494 pub fn new(
2495 tokenizer: Tokenizer,
2496 weights: PipelineWeights,
2497 hidden_size: usize,
2498 intermediate_size: usize,
2499 num_heads: usize,
2500 num_kv_heads: usize,
2501 head_dim: usize,
2502 num_layers: usize,
2503 physical_layers: usize,
2504 loop_final_norm: bool,
2505 vocab_size: usize,
2506 rms_eps: f64,
2507 rope_base: f32,
2508 norm_style: NormStyle,
2509 max_seq_len: usize,
2510 sampler_config: SamplerConfig,
2511 ) -> Self {
2512 let rng = match sampler_config.seed {
2513 Some(s) => SplitMix64::new(s),
2514 None => SplitMix64::from_entropy(),
2515 };
2516 let inv_freq = std::sync::Arc::new(attention::rope_inv_freq(head_dim, rope_base));
2517 let pool = Pool::from_env();
2518 if let Some(p) = &pool {
2519 tracing::info!("worker pool: {} threads", p.n_workers());
2520 if let Some(model) = weights
2522 .lm_head
2523 .model_arc()
2524 .or_else(|| weights.embed_tokens.model_arc())
2525 {
2526 let regions: Vec<&[u8]> =
2527 model.tensors.iter().map(|t| model.entry_bytes(t)).collect();
2528 p.bind_numa(®ions);
2529 }
2530 }
2531 Self {
2532 gpu_plan: None,
2533 tokenizer: std::sync::Arc::new(tokenizer),
2534 kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
2535 sampler_config,
2536 weights,
2537 hidden_size,
2538 intermediate_size,
2539 num_heads,
2540 num_kv_heads,
2541 head_dim,
2542 num_layers,
2543 physical_layers,
2544 loop_final_norm,
2545 vocab_size,
2546 rms_eps,
2547 rope_base,
2548 norm_style,
2549 rotary_dim: head_dim,
2550 attention_heads_per_layer: None,
2551 vmf_cfg: None,
2552 gdn_cfg: None,
2553 kda_cfg: None,
2554 g3n: None,
2555 dsv4: None,
2556 dsv41: None,
2557 dsv41_vision: None,
2558 dsv41_prefill: None,
2559 qwen4_exp: None,
2560 dsv4_mtp: Vec::new(),
2561 dspark: None,
2562 dspark_pending: Vec::new(),
2563 dspark_hist: Vec::new(),
2564 dspark_real: Vec::new(),
2565 dspark_trunk_picks: Vec::new(),
2566 dspark_exp: Vec::new(),
2567 dspark_draft_ns: 0,
2568 logit_multiplier: None,
2569 cancel: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
2570 graph_failed: std::sync::atomic::AtomicBool::new(false),
2571 kv_history: Vec::new(),
2572 short_conv_cfg: None,
2573 mtp: None,
2574 speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
2575 ignore_eos: false,
2576 draft_full_streak: 0,
2577 spec_k_adapt: None,
2578 spec_acc_ewma: 0.7,
2579 rng,
2580 sampler_scratch: SamplerScratch::default(),
2581 spec_forced: None,
2582 spec_q: Vec::new(),
2583 spec_p: Vec::new(),
2584 spec_res: Vec::new(),
2585 spec_qs: Vec::new(),
2586 spec_ps: Vec::new(),
2587 spec_ress: Vec::new(),
2588 mtp_graph_mode: None,
2589 #[cfg(target_os = "macos")]
2590 metal_verify: None,
2591 inv_freq,
2592 ws: ForwardScratch::new(hidden_size),
2593 pool,
2594 model: None,
2595 dyn_force_f32: false,
2596 dyn_skill_layers: Vec::new(),
2597 dyn_active: None,
2598 dyn_blend_loaded: false,
2599 dyn_phi_layer: None,
2600 dyn_phi_ema: Vec::new(),
2601 dyn_phi_seen: 0,
2602 dyn_router: None,
2603 o1_cfg: None,
2604 o1_epoch: 0,
2605 o1_flags: Vec::new(),
2606 trace: false,
2607 calib_temp: 1.0,
2608 confidence_on: true,
2609 embed_multiplier: 1.0,
2610 attn_scale: 1.0 / (head_dim as f32).sqrt(),
2611 swa: None,
2612 sliding_layers: None,
2613 inv_freq_local: None,
2614 rotary_dim_local: None,
2615 rope_scale: 1.0,
2616 rope_scale_local: 1.0,
2617 global_attn: None,
2618 inv_freq_global: None,
2619 attn_v_norm: false,
2620 qk_norm_after_rope: false,
2621 final_softcap: None,
2622 head_clusters: None,
2623 attn_softcap: 0.0,
2624 graph_want_logits: false,
2625 graph_head_required: false,
2626 graph_logits: None,
2627 graph_kv_id: {
2628 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
2629 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
2630 },
2631 #[cfg(test)]
2632 nll_test_fail_at: None,
2633 #[cfg(test)]
2634 nll_test_force_serial: false,
2635 }
2636 }
2637
2638 pub fn set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) {
2646 if let Some(c) = &cfg {
2647 if crate::nystrom::o1_deferred_boundary(c.w, c.sink).is_none() {
2648 tracing::error!(
2649 "o1 disabled: w + sink + slack + 1 overflows usize (w={}, sink={})",
2650 c.w,
2651 c.sink
2652 );
2653 self.o1_flags.clear();
2654 self.o1_cfg = None;
2655 return;
2656 }
2657 }
2658 self.o1_flags = match &cfg {
2659 Some(c) => {
2660 let mut flags = c.layer_flags(self.num_layers);
2661 for (li, f) in flags.iter_mut().enumerate() {
2662 if *f
2663 && !matches!(
2664 self.weights.layers[self.phys_layer(li)].attn,
2665 AttnKind::Full { .. }
2666 )
2667 {
2668 *f = false;
2669 }
2670 }
2671 flags
2672 }
2673 None => Vec::new(),
2674 };
2675 if let Some(c) = &cfg {
2676 let n = self.o1_flags.iter().filter(|&&f| f).count();
2677 tracing::info!(
2678 "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
2679 self.num_layers,
2680 c.m,
2681 c.w,
2682 c.sink,
2683 c.rect
2684 );
2685 }
2686 self.o1_cfg = cfg;
2687 }
2688
2689 pub fn o1_active(&self) -> bool {
2691 self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
2692 }
2693
2694 pub fn generation_batch_k(&self) -> usize {
2706 if let Some(k) = std::env::var("CMF_BATCH_K")
2707 .ok()
2708 .and_then(|v| v.parse::<usize>().ok())
2709 {
2710 return k;
2711 }
2712 #[cfg(not(target_os = "macos"))]
2713 if self.graph_prefill_preferred() && !self.o1_active() {
2714 return 32;
2715 }
2716 0
2717 }
2718
2719 pub fn generation_graph_prefill(&self) -> bool {
2720 let graph = self.graph_prefill_preferred();
2721 #[cfg(not(target_os = "macos"))]
2732 if graph
2733 && self.generation_batch_k() > 0
2734 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err()
2735 {
2736 return false;
2737 }
2738 graph
2739 }
2740
2741 pub fn o1_device_stats(&self) -> (usize, u64) {
2746 crate::gpu::o1_device_stats(self.graph_kv_id)
2747 }
2748
2749 pub fn o1_begin(&mut self) {
2754 self.o1_begin_with_prefix(None);
2755 }
2756
2757 pub fn o1_begin_with_prefix(&mut self, requested_prefix: Option<usize>) {
2761 if let Some(c) = &self.o1_cfg {
2762 let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
2763 let boundary = requested_prefix.map(|p| {
2764 p.max(
2765 crate::nystrom::o1_deferred_boundary(w, sink)
2766 .expect("o1 config boundary validated in set_o1"),
2767 )
2768 });
2769 for (li, &f) in self.o1_flags.iter().enumerate() {
2770 if f {
2771 self.kv_cache.layers[li].o1_begin_with_boundary(m, w, sink, rect, boundary);
2772 }
2773 }
2774 }
2775 }
2776
2777 fn o1_effective_boundary(&self, requested_prefix: usize) -> Option<usize> {
2779 self.o1_cfg.as_ref().and_then(|c| {
2780 crate::nystrom::o1_deferred_boundary(c.w, c.sink)
2781 .map(|floor| requested_prefix.max(floor))
2782 })
2783 }
2784
2785 fn o1_note_transition(&mut self) {
2786 let mut transitioned = false;
2790 for (li, &flagged) in self.o1_flags.iter().enumerate() {
2791 if flagged {
2792 transitioned |= self.kv_cache.layers[li].take_o1_transition();
2793 }
2794 }
2795 if transitioned {
2796 self.o1_epoch = self.o1_epoch.wrapping_add(1);
2797 }
2798 }
2799
2800 fn o1_pending(&self) -> bool {
2801 self.o1_flags.iter().enumerate().any(|(li, &f)| {
2802 f && self.kv_cache.layers[li].seq_len > 0
2803 && self.kv_cache.layers[li].o1_pending_boundary().is_some()
2804 })
2805 }
2806
2807 fn o1_fail(&mut self, err: String) {
2808 tracing::error!("o1 deferred seal failed; terminating sequence: {err}");
2809 self.clear_sequence_state();
2810 self.graph_failed
2811 .store(true, std::sync::atomic::Ordering::Relaxed);
2812 self.cancel
2813 .store(true, std::sync::atomic::Ordering::Relaxed);
2814 }
2815
2816 pub fn o1_seal_checked(&mut self) -> Result<bool, String> {
2821 if self.o1_cfg.is_none() {
2822 return Ok(false);
2823 }
2824 let mut participating = false;
2825 for li in 0..self.num_layers {
2826 if !self.o1_flags.get(li).copied().unwrap_or(false) {
2827 continue;
2828 }
2829 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
2830 return Err(err);
2831 }
2832 if self.kv_cache.layers[li].seq_len == 0 {
2833 continue;
2834 }
2835 participating = true;
2836 let num_heads = self.layer_num_heads(li);
2837 self.kv_cache.layers[li].o1_seal_checked(num_heads)?;
2838 }
2839 self.o1_note_transition();
2840 for li in 0..self.num_layers {
2841 if self.o1_flags.get(li).copied().unwrap_or(false) {
2842 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
2843 return Err(err);
2844 }
2845 }
2846 }
2847 Ok(participating
2848 && (0..self.num_layers).all(|li| {
2849 !self.o1_flags.get(li).copied().unwrap_or(false)
2850 || self.kv_cache.layers[li].seq_len == 0
2851 || self.kv_cache.layers[li].o1_sealed()
2852 }))
2853 }
2854
2855 fn o1_progress(&mut self) {
2858 if !self.o1_active() {
2859 return;
2860 }
2861 for li in 0..self.num_layers {
2862 if self.o1_flags.get(li).copied().unwrap_or(false) {
2863 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
2864 self.o1_fail(err);
2865 return;
2866 }
2867 }
2868 }
2869 self.o1_note_transition();
2873 if !self.o1_pending() {
2874 return;
2875 }
2876 if let Err(err) = self.o1_seal_checked() {
2877 self.o1_fail(err);
2878 }
2879 }
2880
2881 fn check_o1_progress_failure(&mut self, phase: &str) -> Result<(), String> {
2886 if self
2887 .graph_failed
2888 .swap(false, std::sync::atomic::Ordering::Relaxed)
2889 {
2890 self.cancel
2891 .store(false, std::sync::atomic::Ordering::Relaxed);
2892 self.clear_sequence_state();
2893 return Err(format!("{phase}: deferred O(1) transition failed"));
2894 }
2895 Ok(())
2896 }
2897
2898 pub fn o1_seal(&mut self) {
2902 if let Err(err) = self.o1_seal_checked() {
2903 self.o1_fail(err);
2904 }
2905 }
2906
2907 pub fn set_trace(&mut self, on: bool) {
2909 self.trace = on;
2910 }
2911
2912 pub fn set_sampler_config(&mut self, config: SamplerConfig) {
2915 self.rng = match config.seed {
2916 Some(seed) => SplitMix64::new(seed),
2917 None => SplitMix64::from_entropy(),
2918 };
2919 self.sampler_config = config;
2920 }
2921
2922 pub fn set_confidence(&mut self, on: bool) {
2927 self.confidence_on = on;
2928 }
2929
2930 pub fn set_calib_temp(&mut self, t: f32) {
2933 self.calib_temp = if t > 1e-3 { t } else { 1.0 };
2934 }
2935
2936 pub fn calib_temp(&self) -> f32 {
2938 self.calib_temp
2939 }
2940
2941 pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
2944 self.rotary_dim = rotary_dim.min(self.head_dim);
2945 self.inv_freq = std::sync::Arc::new(attention::rope_inv_freq(self.rotary_dim, base));
2946 }
2947
2948 fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
2949 QwenAttnCfg {
2950 num_heads: self.num_heads,
2951 num_kv_heads: self.num_kv_heads,
2952 head_dim: self.head_dim,
2953 hidden_size: self.hidden_size,
2954 position,
2955 inv_freq: &self.inv_freq,
2956 rotary_dim: self.rotary_dim,
2957 scale: self.attn_scale,
2958 softcap: self.attn_softcap,
2959 window: None,
2960 v_norm: false,
2961 qk_norm_after_rope: self.qk_norm_after_rope,
2962 q_norm: None,
2963 k_norm: None,
2964 output_gate: false,
2965 softplus_gate: None,
2966 rope_scale: self.rope_scale,
2967 bias: None,
2968 rms_eps: self.rms_eps,
2969 norm_style: self.norm_style,
2970 pool: self.pool.as_deref(),
2971 }
2972 }
2973
2974 pub fn generate(
2976 &mut self,
2977 prompt: &str,
2978 max_tokens: usize,
2979 task_mask: Option<&TaskMask>,
2980 on_token: Option<TokenCallback>,
2981 ) -> Result<GenerateResult, String> {
2982 let input_ids = self.tokenizer.with_bos(self.tokenizer.encode(prompt));
2983 self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
2984 }
2985
2986 pub fn generate_from_vl(
2989 &mut self,
2990 input: &crate::dsv41_vision::PreparedVlInputs,
2991 max_tokens: usize,
2992 task_mask: Option<&TaskMask>,
2993 on_token: Option<TokenCallback>,
2994 ) -> Result<GenerateResult, String> {
2995 let Some(dsv41) = &self.dsv41 else {
2996 return Err("V4.1 multimodal input requires a DeepSeek-V4.1 pipeline".into());
2997 };
2998 if input.token_ids.is_empty() {
2999 return Err("empty V4.1 multimodal prompt".into());
3000 }
3001 if input.token_types.len() != input.token_ids.len() {
3002 return Err(format!(
3003 "V4.1 token type count {} != token count {}",
3004 input.token_types.len(),
3005 input.token_ids.len()
3006 ));
3007 }
3008 let dim = dsv41.2.dim;
3009 let mut embeddings = vec![None; input.token_ids.len()];
3010 let mut participates = vec![true; input.token_ids.len()];
3011 if !input.images.is_empty() {
3012 let vision = self
3013 .dsv41_vision
3014 .as_ref()
3015 .ok_or_else(|| "V4.1 image prompt has no loaded vision tower".to_string())?;
3016 for image in &input.images {
3017 let end = image.start.saturating_add(image.types.len());
3018 if end > input.token_ids.len() {
3019 return Err(format!(
3020 "V4.1 image span {}..{} exceeds prompt length {}",
3021 image.start,
3022 end,
3023 input.token_ids.len()
3024 ));
3025 }
3026 let mut span = vec![0.0f32; image.types.len() * dim];
3027 vision.fill_image_span(image, &mut span, self.pool.as_deref())?;
3028 for (offset, &kind) in image.types.iter().enumerate() {
3029 let pos = image.start + offset;
3030 if input.token_types[pos] != kind {
3031 return Err(format!(
3032 "V4.1 image type mismatch at position {pos}: {} != {kind}",
3033 input.token_types[pos]
3034 ));
3035 }
3036 embeddings[pos] = Some(span[offset * dim..(offset + 1) * dim].to_vec());
3037 participates[pos] = false;
3038 }
3039 }
3040 }
3041 for (pos, &kind) in input.token_types.iter().enumerate() {
3042 if kind == crate::dsv41_vision::TEXT && embeddings[pos].is_some() {
3043 return Err(format!("V4.1 text position {pos} has an image embedding"));
3044 }
3045 if kind != crate::dsv41_vision::TEXT && embeddings[pos].is_none() {
3046 return Err(format!("V4.1 image position {pos} has no image embedding"));
3047 }
3048 }
3049 self.dsv41_prefill = Some((embeddings, participates));
3050 let result = self.generate_from_ids(&input.token_ids, max_tokens, task_mask, on_token);
3051 self.dsv41_prefill = None;
3052 result
3053 }
3054
3055 fn drop_open_mask<'m>(&self, m: Option<&'m TaskMask>) -> Option<&'m TaskMask> {
3057 m.filter(|m| !m.fully_open(self.intermediate_size, self.num_heads))
3058 }
3059
3060 pub fn generate_from_ids(
3068 &mut self,
3069 input_ids: &[u32],
3070 max_tokens: usize,
3071 task_mask: Option<&TaskMask>,
3072 mut on_token: Option<TokenCallback>,
3073 ) -> Result<GenerateResult, String> {
3074 if std::env::var("CMF_TRACE_H").is_ok() {
3075 eprintln!("input_ids: {input_ids:?}");
3076 }
3077 if input_ids.is_empty() {
3078 return Err("empty prompt: nothing to generate from".to_string());
3079 }
3080 self.graph_failed
3084 .store(false, std::sync::atomic::Ordering::Relaxed);
3085 let task_mask = self.drop_open_mask(task_mask);
3090
3091 let mut reuse_from = {
3099 let on = !std::env::var("CMF_KV_REUSE").is_ok_and(|v| v == "0");
3100 let h = &self.kv_history;
3101 if on
3102 && task_mask.is_none()
3103 && self.mtp.is_none()
3104 && self.o1_cfg.is_none()
3105 && self.dsv41.is_none()
3106 && !h.is_empty()
3107 && h.len() < input_ids.len()
3108 && input_ids[..h.len()] == h[..]
3109 {
3110 h.len()
3111 } else {
3112 0
3113 }
3114 };
3115 if reuse_from > 0 && !self.prepare_kv_reuse(reuse_from) {
3118 reuse_from = 0;
3119 }
3120 if reuse_from == 0 {
3121 self.clear_sequence_state();
3123 } else if std::env::var("CMF_PREFILL_PROF").is_ok() {
3124 eprintln!(
3125 "kv-reuse: {} of {} prompt positions already cached",
3126 reuse_from,
3127 input_ids.len()
3128 );
3129 }
3130 crate::gpu::graph_race_begin_generation();
3131 let o1_prefill = if self.o1_active() && task_mask.is_none() {
3135 std::env::var("CMF_O1_PREFILL")
3136 .ok()
3137 .and_then(|v| v.parse::<usize>().ok())
3138 .filter(|&p| p > 0)
3139 } else {
3140 None
3141 };
3142 if task_mask.is_none() {
3143 self.o1_begin_with_prefix(o1_prefill);
3144 }
3145
3146 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
3152 #[cfg(target_os = "macos")]
3191 let metal_graph = crate::gpu::q1_force()
3192 && crate::gpu::enabled_here()
3193 && std::env::var("CMF_GPU_BLOCK")
3194 .map(|v| v != "0")
3195 .unwrap_or(true);
3196 #[cfg(not(target_os = "macos"))]
3197 let metal_graph = false;
3198 let spec_sample_env = std::env::var("CMF_GRAPH_SPEC_SAMPLE").ok();
3199 let spec_cheap_round = self.sampler_config.temperature < 1e-6
3203 || sampler::sparse_ok(&self.sampler_config);
3204 let spec_sampling_ok = self.sampler_config.temperature < 1e-6
3205 || match spec_sample_env.as_deref() {
3206 Some("1") => true,
3207 Some(_) => false,
3208 None => metal_graph && spec_cheap_round,
3209 };
3210 let (mut dense_n, mut dense_q4tp) = (0usize, 0usize);
3226 for lw in &self.weights.layers {
3227 if let FfnKind::Dense(d) = &lw.ffn {
3228 dense_n += 1;
3229 if matches!(d.gate_proj.graph_weight(), Some((_, _, 6, _)))
3230 && matches!(d.up_proj.graph_weight(), Some((_, _, 6, _)))
3231 && matches!(d.down_proj.graph_weight(), Some((_, _, 6, _)))
3232 {
3233 dense_q4tp += 1;
3234 }
3235 }
3236 }
3237 let spec_default_ok = dense_n == 0 || dense_q4tp * 10 >= dense_n * 9;
3238 let penalized = !metal_graph
3250 && (self.sampler_config.repetition_penalty != 1.0
3251 || self.sampler_config.presence_penalty != 0.0
3252 || !self.sampler_config.suppress_tokens.is_empty());
3253 #[cfg(feature = "gpu")]
3258 let metal_wgpu = graph_on && crate::gpu_wgpu::wgpu_backend_is_metal();
3259 #[cfg(not(feature = "gpu"))]
3260 let metal_wgpu = false;
3261 let spec_env = std::env::var("CMF_GRAPH_SPEC").ok();
3262 let spec_wanted = match spec_env.as_deref() {
3263 Some("0") => false,
3264 Some(_) => {
3265 if metal_wgpu {
3266 tracing::warn!(
3267 "CMF_GRAPH_SPEC forced on wgpu/Metal: the batched verify graph is not \
3268 verified on this backend (garbage measured on Qwen3.5-0.8B)"
3269 );
3270 }
3271 true
3272 }
3273 None => spec_default_ok && !penalized && !metal_wgpu,
3274 };
3275 let graph_spec = self.speculative
3279 && (graph_on || metal_graph)
3280 && self.mtp.is_some()
3281 && task_mask.is_none()
3282 && !self.o1_active()
3283 && spec_sampling_ok
3284 && spec_wanted;
3285 #[cfg(target_os = "macos")]
3289 if metal_graph {
3290 static SAID: std::sync::Once = std::sync::Once::new();
3291 SAID.call_once(|| {
3292 let spec = if graph_spec {
3293 let k = std::env::var("CMF_GRAPH_SPEC_K")
3294 .ok()
3295 .and_then(|v| v.parse::<usize>().ok())
3296 .filter(|&v| (1..=8).contains(&v))
3297 .unwrap_or(7);
3298 let arm = if self.sampler_config.temperature < 1e-6 {
3299 "greedy"
3300 } else {
3301 "sampling"
3302 };
3303 format!(
3304 "spec k={k} {arm} (batched verify, draft shortlist {}, trial: proxy)",
3305 Self::draft_vocab_rows(usize::MAX)
3306 )
3307 } else if !self.speculative {
3308 "spec off (CMF_MTP=0)".to_string()
3309 } else if self.mtp.is_none() {
3310 "spec off (no MTP head)".to_string()
3311 } else if !spec_sampling_ok {
3312 if spec_cheap_round {
3313 "spec off (CMF_GRAPH_SPEC_SAMPLE=0)".to_string()
3314 } else {
3315 "spec off (sampling without a top-k: the dense chain \
3316 costs more than it saves)"
3317 .to_string()
3318 }
3319 } else if !spec_wanted {
3320 "spec off (CMF_GRAPH_SPEC=0 or non-q4tp FFNs)".to_string()
3321 } else if task_mask.is_some() {
3322 "spec off (task mask)".to_string()
3323 } else {
3324 "spec off (O(1) attention)".to_string()
3325 };
3326 let on = |var: &str| {
3327 if std::env::var(var).as_deref() == Ok("0") {
3328 "off"
3329 } else {
3330 "on"
3331 }
3332 };
3333 tracing::info!(
3334 "metal native: {spec}, state4 {}, async replay {}, prefill graph {}, \
3335 MTP graph {}, attend {}, probe {}",
3336 if crate::gpu_metal::state4_on() { "on" } else { "off" },
3337 if crate::gpu_metal::async_replay_on() { "on" } else { "off" },
3338 on("CMF_METAL_PREFILL"),
3339 on("CMF_MTP_GRAPH"),
3340 std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into()),
3341 if crate::gpu::probe_enabled() { "bypassed (q1 force)" } else { "off" },
3342 );
3343 });
3344 }
3345 let pair_pays = self.gdn_cfg.is_none() || std::env::var("CMF_MTP").as_deref() == Ok("1");
3352 let spec_active = self.speculative
3353 && self.mtp.is_some()
3354 && task_mask.is_none()
3355 && !self.o1_active()
3356 && ((!graph_on && pair_pays && self.sampler_config.temperature < 1e-6) || graph_spec);
3357 let mut mtp = if spec_active { self.mtp.take() } else { None };
3360 if std::env::var("CMF_MTP_CHAIN_PROBE").is_ok() {
3361 eprintln!(
3362 "mtp-probe gate: spec_active={spec_active} mtp={} speculative={} graph_on={graph_on} temp_ok={}",
3363 mtp.is_some(),
3364 self.speculative,
3365 self.sampler_config.temperature < 1e-6,
3366 );
3367 }
3368 if let Some(m) = &mut mtp {
3369 m.kv.clear();
3370 crate::gpu::graph_kv_reset(self.mtp_kv_id());
3372 self.mtp_graph_mode = None;
3373 }
3374 let mut router = if mtp.is_none() {
3378 self.dyn_router.take()
3379 } else {
3380 None
3381 };
3382 if let Some(r) = &mut router {
3383 r.reset(); self.dyn_phi_seen = 0; let _ = self.set_active_skill(None);
3386 }
3387
3388 let mut all_ids = input_ids.to_vec();
3389 let mut generated = 0usize;
3390 let mut finish_reason = "max_tokens".to_string();
3391 let mut drafted = 0usize;
3392 let mut accepted = 0usize;
3393 let mut dsv4_spec_bad = 0usize;
3400 let mut dsv4_spec_retry_at = 0usize;
3401 let mut confidence: Vec<f32> = Vec::new();
3402 let trace_on = self.trace;
3403 let calib_temp = self.calib_temp;
3404 let mut traces: Vec<TokenTrace> = Vec::new();
3405
3406 let mut hidden = vec![0.0f32; self.hidden_size];
3412 let mut pos = reuse_from;
3413 let fuse_lm = mtp.is_none()
3422 && router.is_none()
3423 && std::env::var("CMF_GPU_LMHEAD").as_deref() != Ok("0");
3424 self.graph_logits = None;
3425 self.graph_want_logits = false;
3426 let _tpf = std::time::Instant::now();
3427 let batch_k = self.generation_batch_k();
3428 while self.qwen4_exp.is_some()
3439 && mtp.is_none()
3440 && pos < input_ids.len()
3441 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
3442 {
3443 let token_id = input_ids[pos];
3444 let want_logits = pos + 1 == input_ids.len();
3445 let mut lg = Vec::new();
3446 if let Some(b) = &mut self.qwen4_exp {
3447 crate::qwen4_exp::forward_token(
3448 &b.0,
3449 &b.1,
3450 &b.2,
3451 &mut b.3,
3452 token_id,
3453 pos,
3454 &self.inv_freq,
3455 self.pool.as_deref(),
3456 &mut lg,
3457 want_logits,
3458 );
3459 }
3460 if want_logits {
3461 self.graph_logits = Some(lg);
3462 }
3463 pos += 1;
3464 hidden.fill(0.0);
3465 }
3466 while self.dsv4.is_some()
3467 && mtp.is_none()
3468 && pos < input_ids.len()
3469 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
3470 {
3471 let end = (pos + prefill_chunk()).min(input_ids.len());
3472 let ids: Vec<u32> = input_ids[pos..end].to_vec();
3473 let mut lg = Vec::new();
3474 if let Some(b) = &mut self.dsv4 {
3475 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
3476 crate::dsv4::forward_chunk(
3477 g,
3478 layers,
3479 &cfg,
3480 st,
3481 &ids,
3482 pos,
3483 &self.inv_freq,
3484 self.pool.as_deref(),
3485 &mut lg,
3486 end == input_ids.len(),
3487 );
3488 }
3489 if end == input_ids.len() {
3490 self.graph_logits = Some(lg);
3491 }
3492 pos = end;
3493 hidden = vec![0.0; self.hidden_size];
3494 }
3495 let dsv41_prefill = self.dsv41_prefill.take();
3496 while self.dsv41.is_some()
3497 && mtp.is_none()
3498 && pos < input_ids.len()
3499 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
3500 {
3501 let end = (pos + prefill_chunk()).min(input_ids.len());
3502 let ids: Vec<u32> = input_ids[pos..end].to_vec();
3503 let mut lg = Vec::new();
3504 if let Some(b) = &mut self.dsv41 {
3505 let (g, layers, cfg, st) = (&b.0, &b.1, &b.2, &mut b.3);
3506 if let Some((embeddings, participates)) = dsv41_prefill.as_ref() {
3507 crate::dsv41::forward_chunk_masked_with_embeddings(
3508 g,
3509 layers,
3510 cfg,
3511 st,
3512 &ids,
3513 pos,
3514 &embeddings[pos..end],
3515 &participates[pos..end],
3516 self.pool.as_deref(),
3517 &mut lg,
3518 );
3519 } else {
3520 crate::dsv41::forward_chunk(
3521 g,
3522 layers,
3523 cfg,
3524 st,
3525 &ids,
3526 pos,
3527 self.pool.as_deref(),
3528 &mut lg,
3529 );
3530 }
3531 }
3532 if end == input_ids.len() {
3533 self.graph_logits = Some(lg);
3534 }
3535 pos = end;
3536 hidden = vec![0.0; self.hidden_size];
3537 }
3538 let dyn_prefill = router.is_some();
3543 let o1_prefill_limit = o1_prefill
3551 .and_then(|requested| self.o1_effective_boundary(requested))
3552 .map(|boundary| boundary.min(input_ids.len()));
3553 let mut o1_sealed = false;
3554 if let Some(limit) = o1_prefill_limit {
3555 if self.can_prefill_batched() && limit > 2 {
3558 let chunk = self.prefill_chunk();
3559 let hs = self.hidden_size;
3560 while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
3561 let end = (pos + chunk).min(limit);
3562 let hb = self.prefill_batch(&input_ids[pos..end], pos);
3563 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
3564 pos = end;
3565 }
3566 } else {
3567 while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
3568 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, None);
3569 pos += 1;
3570 }
3571 }
3572 if pos >= limit {
3573 o1_sealed = match self.o1_seal_checked() {
3574 Ok(sealed) => sealed,
3575 Err(err) => {
3576 self.finish_generation(&mut mtp, &mut router, true);
3577 return Err(err);
3578 }
3579 };
3580 tracing::info!(
3581 "o1 bounded prompt prefix: requested={} effective={} processed={} of {} token(s)",
3582 o1_prefill.unwrap_or(0),
3583 self.o1_effective_boundary(o1_prefill.unwrap_or(0))
3584 .unwrap_or(limit),
3585 limit,
3586 input_ids.len()
3587 );
3588 }
3589 }
3590 let graph_prefill = self.graph_prefill_preferred();
3596 #[cfg(target_os = "macos")]
3604 if task_mask.is_none()
3605 && !dyn_prefill
3606 && (crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in())
3607 && crate::gpu::enabled_here()
3608 && self.gdn_cfg.is_some()
3609 && self.g3n.is_none()
3610 && input_ids.len() > 8
3611 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err()
3612 && std::env::var("CMF_METAL_PREFILL").as_deref() != Ok("0")
3613 {
3614 let chunk: usize = std::env::var("CMF_METAL_PREFILL_CHUNK")
3615 .ok()
3616 .and_then(|v| v.parse().ok())
3617 .filter(|&v| (16..=512).contains(&v))
3618 .unwrap_or(256);
3619 let hs = self.hidden_size;
3620 let _tp = std::time::Instant::now();
3621 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
3622 let end = (pos + chunk).min(input_ids.len());
3623 let hb = match self.prefill_batch_metal(&input_ids[pos..end], pos) {
3624 MetalPrefillOutcome::Completed(hb) => hb,
3625 MetalPrefillOutcome::Declined => break,
3626 MetalPrefillOutcome::Failed => {
3627 self.finish_generation(&mut mtp, &mut router, true);
3628 return Err("ordinary Metal prefill failed after admission".into());
3629 }
3630 };
3631 if let Some(m) = &mut mtp {
3632 let n_pairs = if end < input_ids.len() {
3633 end - pos
3634 } else {
3635 end - pos - 1
3636 };
3637 if n_pairs > 0 {
3638 let pairs: Vec<(&[f32], u32)> = (0..n_pairs)
3639 .map(|j| (&hb[j * hs..(j + 1) * hs], input_ids[pos + j + 1]))
3640 .collect();
3641 if !self.mtp_warm_batch_metal(m, &pairs, pos) {
3642 for (j, (h, t)) in pairs.iter().enumerate() {
3643 let h = h.to_vec();
3644 let _ = self.mtp_step(m, &h, *t, pos + j);
3645 }
3646 }
3647 }
3648 }
3649 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
3650 pos = end;
3651 }
3652 if std::env::var("CMF_PREFILL_PROF").is_ok() {
3653 eprintln!(
3654 "metal-prefill: {} of {} tokens in {:.1} ms",
3655 pos,
3656 input_ids.len(),
3657 _tp.elapsed().as_secs_f64() * 1e3
3658 );
3659 }
3660 }
3661 if task_mask.is_none()
3662 && !dyn_prefill
3663 && !graph_prefill
3664 && self.can_prefill_batched()
3665 && self.g3n.is_none()
3666 && o1_prefill.is_none()
3667 && input_ids.len() > 2
3668 {
3669 let chunk = self.prefill_chunk();
3675 let hs = self.hidden_size;
3676 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
3677 let end = (pos + chunk).min(input_ids.len());
3678 let hb = self.prefill_batch(&input_ids[pos..end], pos);
3679 if let Some(m) = &mut mtp {
3680 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
3681 .ok()
3682 .and_then(|v| v.parse().ok())
3683 .unwrap_or(0);
3684 for p in pos..end {
3685 if p + 1 < input_ids.len() {
3686 if probe >= 1 && p + 2 < input_ids.len() {
3687 let (d1, mut hx) = self.mtp_step_h(
3691 m,
3692 &hb[(p - pos) * hs..(p - pos + 1) * hs],
3693 input_ids[p + 1],
3694 p,
3695 );
3696 let mut ok = d1 == input_ids[p + 2];
3697 Self::chain_probe_note(0, ok);
3698 let mut d_prev = d1;
3699 let mut extra = 0usize;
3700 for j in 1..probe {
3701 if p + 2 + j >= input_ids.len() {
3702 break;
3703 }
3704 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, p + 1 + j);
3705 extra += 1;
3706 ok = ok && dj == input_ids[p + 2 + j];
3707 Self::chain_probe_note(j, ok);
3708 d_prev = dj;
3709 hx = hj;
3710 }
3711 m.kv.truncate_last(extra);
3712 } else {
3713 let _ = self.mtp_step(
3714 m,
3715 &hb[(p - pos) * hs..(p - pos + 1) * hs],
3716 input_ids[p + 1],
3717 p,
3718 );
3719 }
3720 }
3721 }
3722 }
3723 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
3724 pos = end;
3725 }
3726 }
3727 let pair_off = std::env::var("CMF_PAIR").is_ok_and(|v| v == "0");
3728 if task_mask.is_none()
3729 && !dyn_prefill
3730 && !graph_prefill
3731 && !pair_off
3732 && self.pair_supported()
3733 && o1_prefill.is_none()
3734 {
3735 while pos + 1 < input_ids.len()
3736 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
3737 {
3738 let e1 = self.embed_single(input_ids[pos]);
3739 let e2 = self.embed_single(input_ids[pos + 1]);
3740 let (h1, h2) = self.forward_pair(&e1, &e2, pos);
3741 self.commit_linear_scratch();
3743 if let Some(m) = &mut mtp {
3744 let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
3745 if pos + 2 < input_ids.len() {
3746 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
3747 .ok()
3748 .and_then(|v| v.parse().ok())
3749 .unwrap_or(0);
3750 if probe >= 1 && pos + 3 < input_ids.len() {
3751 let (d1, mut hx) = self.mtp_step_h(m, &h2, input_ids[pos + 2], pos + 1);
3755 let mut ok = d1 == input_ids[pos + 3];
3756 Self::chain_probe_note(0, ok);
3757 let mut d_prev = d1;
3758 let mut extra = 0usize;
3759 for j in 1..probe {
3760 if pos + 3 + j >= input_ids.len() {
3761 break;
3762 }
3763 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 2 + j);
3764 extra += 1;
3765 ok = ok && dj == input_ids[pos + 3 + j];
3766 Self::chain_probe_note(j, ok);
3767 d_prev = dj;
3768 hx = hj;
3769 }
3770 m.kv.truncate_last(extra);
3771 } else {
3772 let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
3773 }
3774 }
3775 }
3776 hidden = h2;
3777 pos += 2;
3778 }
3779 }
3780 let o1_batch_ready = o1_sealed
3793 && o1_prefill.is_some()
3794 && mtp.is_none()
3795 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
3796 && (0..self.num_layers).all(|li| {
3797 let cache = &self.kv_cache.layers[self.phys_layer(li)];
3798 cache.o1.is_none() || cache.o1_views().is_some()
3799 });
3800 let mtp_batch_prefill = mtp.is_some()
3805 && graph_prefill
3806 && task_mask.is_none()
3807 && !dyn_prefill
3808 && !self.o1_active()
3809 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err();
3810 if batch_k > 0
3811 && (graph_prefill || o1_batch_ready)
3812 && task_mask.is_none()
3813 && (!self.o1_active() || o1_batch_ready)
3814 && (mtp.is_none() || mtp_batch_prefill)
3815 && !dyn_prefill
3816 && pos + 1 < input_ids.len()
3817 {
3818 let hs = self.hidden_size;
3819 let chunk = batch_k;
3820 while pos < input_ids.len() {
3821 let end = (pos + chunk).min(input_ids.len());
3822 let bk = end - pos;
3823 let mut hiddens = vec![0f32; bk * hs];
3824 for (j, &id) in input_ids[pos..end].iter().enumerate() {
3825 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
3826 }
3827 let positions: Vec<usize> = (pos..end).collect();
3828 let t_chunk = std::time::Instant::now();
3829 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
3830 let ok_b = outcome == crate::gpu::BatchGraphOutcome::Completed;
3831 if std::env::var("CMF_GRAPH_PROF").is_ok() {
3832 let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
3833 eprintln!(
3834 "batch-chunk: phase=prompt mode={} k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
3835 if o1_batch_ready {
3836 "o1"
3837 } else if mtp_batch_prefill {
3838 "ordinary_mtp"
3839 } else {
3840 "ordinary"
3841 },
3842 bk as f64 / (ms / 1000.0)
3843 );
3844 }
3845 {
3846 use std::sync::atomic::{AtomicBool, Ordering};
3847 static SAID: AtomicBool = AtomicBool::new(false);
3848 if !SAID.swap(true, Ordering::Relaxed) {
3849 if ok_b {
3850 tracing::info!(
3851 "batched prefill: ACTIVE mode={} (k={bk})",
3852 if o1_batch_ready {
3853 "o1"
3854 } else if mtp_batch_prefill {
3855 "ordinary_mtp"
3856 } else {
3857 "ordinary"
3858 }
3859 );
3860 } else {
3861 tracing::warn!("batched prefill {:?} — per-position graph", outcome);
3862 }
3863 }
3864 }
3865 if ok_b {
3866 if mtp_batch_prefill {
3867 let n_pairs = mtp_prefill_pair_count(pos, end, input_ids.len());
3868 if n_pairs > 0 {
3869 let rows: Vec<Vec<f32>> = (0..n_pairs)
3875 .map(|j| hiddens[j * hs..(j + 1) * hs].to_vec())
3876 .collect();
3877 let pairs: Vec<(&[f32], u32)> = rows
3878 .iter()
3879 .enumerate()
3880 .map(|(j, row)| (row.as_slice(), input_ids[pos + j + 1]))
3881 .collect();
3882 if std::env::var("CMF_GRAPH_PROF").is_ok() {
3883 eprintln!(
3884 "mtp-warm: phase=prompt mode=ordinary_mtp first_pos={} pairs={} last_pos={}",
3885 pos,
3886 n_pairs,
3887 pos + n_pairs - 1,
3888 );
3889 }
3890 let warm_error = if let Some(m) = mtp.as_mut() {
3891 self.mtp_warm_prefill_pairs(m, &pairs, pos).err()
3892 } else {
3893 None
3894 };
3895 if let Some(err) = warm_error {
3896 self.finish_generation(&mut mtp, &mut router, true);
3901 return Err(err.to_string());
3902 }
3903 }
3904 }
3905 hidden.copy_from_slice(&hiddens[(bk - 1) * hs..]);
3906 pos = end;
3907 } else if outcome == crate::gpu::BatchGraphOutcome::Failed {
3908 self.finish_generation(&mut mtp, &mut router, true);
3913 return Err(if o1_batch_ready {
3914 "sealed O(1) batch graph failed after admission".to_string()
3915 } else {
3916 "ordinary recurrent batch graph failed after admission".to_string()
3917 });
3918 } else {
3919 break; }
3921 }
3922 }
3923 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
3924 self.graph_want_logits = fuse_lm && pos + 1 == input_ids.len();
3925 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
3926 if let Some(m) = &mut mtp {
3927 if pos + 1 < input_ids.len() {
3928 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
3934 .ok()
3935 .and_then(|v| v.parse().ok())
3936 .unwrap_or(0);
3937 if probe >= 1 && pos + 2 < input_ids.len() {
3938 let (d1, mut hx) = self.mtp_step_h(m, &hidden, input_ids[pos + 1], pos);
3939 let mut ok = d1 == input_ids[pos + 2];
3940 Self::chain_probe_note(0, ok);
3941 let mut d_prev = d1;
3942 let mut extra = 0usize;
3943 for j in 1..probe {
3944 if pos + 2 + j >= input_ids.len() {
3945 break;
3946 }
3947 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 1 + j);
3948 extra += 1;
3949 ok = ok && dj == input_ids[pos + 2 + j];
3950 Self::chain_probe_note(j, ok);
3951 d_prev = dj;
3952 hx = hj;
3953 }
3954 m.kv.truncate_last(extra);
3957 } else {
3958 let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
3959 }
3960 }
3961 }
3962 pos += 1;
3963 }
3964 if std::env::var("CMF_PREFILL_PROF").is_ok() {
3965 eprintln!(
3966 "prefill: {} tokens in {:.1} ms (batch_k={batch_k})",
3967 input_ids.len(),
3968 _tpf.elapsed().as_secs_f64() * 1000.0
3969 );
3970 }
3971 if self
3972 .graph_failed
3973 .swap(false, std::sync::atomic::Ordering::Relaxed)
3974 {
3975 self.finish_generation(&mut mtp, &mut router, true);
3980 return Err("GPU token graph failed during prefill".to_string());
3981 }
3982 if self
3985 .cancel
3986 .swap(false, std::sync::atomic::Ordering::Relaxed)
3987 {
3988 self.finish_generation(&mut mtp, &mut router, true);
3992 return Ok(GenerateResult {
3993 text: String::new(),
3994 token_ids: Vec::new(),
3995 prompt_tokens: input_ids.len(),
3996 tokens_generated: 0,
3997 finish_reason: "cancelled".to_string(),
3998 mtp_drafted: 0,
3999 mtp_accepted: 0,
4000 token_confidence: Vec::new(),
4001 traces: Vec::new(),
4002 });
4003 }
4004
4005 if !o1_sealed {
4008 match self.o1_seal_checked() {
4009 Ok(_) => {}
4010 Err(err) => {
4011 self.finish_generation(&mut mtp, &mut router, true);
4012 return Err(err);
4013 }
4014 }
4015 }
4016
4017 macro_rules! commit {
4019 ($id:expr) => {{
4020 all_ids.push($id);
4021 generated += 1;
4022 self.note_draft_id($id);
4023 if self.tokenizer.is_eos($id) && !self.ignore_eos {
4024 finish_reason = "stop".to_string();
4025 false
4026 } else {
4027 let token_text = self.tokenizer.decode_token($id);
4028 let mut go = true;
4029 if let Some(ref mut cb) = on_token {
4030 if !cb(&token_text) {
4031 finish_reason = "cancelled".to_string();
4032 go = false;
4033 }
4034 }
4035 go
4036 }
4037 }};
4038 }
4039
4040 let mut spec_trial = SpecTrial::Spec {
4051 t0: std::time::Instant::now(),
4052 gen0: generated,
4053 rounds: 0,
4054 };
4055 let mut spec_mon = SpecMon {
4061 metal: graph_spec && crate::gpu::q1_force() && spec_cheap_round,
4062 ..SpecMon::default()
4063 };
4064 let mut spec_watchdog_off = false;
4065 let mut spec_walls: Vec<f32> = Vec::new();
4068 let mut spec_round_end: Option<std::time::Instant> = None;
4071 let mut next_pos = input_ids.len();
4073 'decode: while generated < max_tokens {
4074 if self
4075 .graph_failed
4076 .swap(false, std::sync::atomic::Ordering::Relaxed)
4077 {
4078 self.finish_generation(&mut mtp, &mut router, true);
4083 return Err("GPU token graph failed during decode".to_string());
4084 }
4085 if self
4086 .cancel
4087 .swap(false, std::sync::atomic::Ordering::Relaxed)
4088 {
4089 finish_reason = "cancelled".to_string();
4090 break 'decode;
4091 }
4092 let forced = self.spec_forced.take();
4097 let mut logits = match (forced, self.graph_logits.take()) {
4098 (Some(_), _) => Vec::new(),
4099 (None, Some(lg)) => lg,
4100 (None, None) => {
4101 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::Head);
4102 inference::rms_norm_into(
4103 &hidden,
4104 &self.weights.final_norm,
4105 self.rms_eps,
4106 self.norm_style,
4107 &mut self.ws.n1,
4108 );
4109 self.lm_head_forward(&self.ws.n1)
4110 }
4111 };
4112 if generated
4115 == std::env::var("CMF_LOGIT_DUMP_STEP")
4116 .ok()
4117 .and_then(|v| v.parse().ok())
4118 .unwrap_or(0)
4119 {
4120 if let Ok(path) = std::env::var("CMF_LOGIT_DUMP") {
4121 let mut bytes: Vec<u8> = Vec::with_capacity((hidden.len() + logits.len()) * 4);
4122 for v in hidden.iter().chain(logits.iter()) {
4123 bytes.extend_from_slice(&v.to_le_bytes());
4124 }
4125 if let Err(e) = std::fs::write(&path, &bytes) {
4126 eprintln!("logit dump: failed to write {path}: {e}");
4127 self.finish_generation(&mut mtp, &mut router, true);
4128 return Err(format!("logit dump write failed: {e}"));
4129 }
4130 }
4131 }
4132 let t_next = match forced {
4133 Some(c) => c,
4134 None => {
4135 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::Sampler);
4136 sampler::sample_with_scratch_pool(
4137 &logits,
4138 &self.sampler_config,
4139 &all_ids,
4140 &mut self.rng,
4141 &mut self.sampler_scratch,
4142 self.pool.as_deref(),
4143 )
4144 }
4145 };
4146 if self.confidence_on {
4147 confidence.push(if logits.is_empty() {
4148 0.0
4149 } else {
4150 sampler::top1_prob_pool(
4151 self.pool.as_deref(),
4152 &mut self.sampler_scratch,
4153 &logits,
4154 t_next,
4155 calib_temp,
4156 )
4157 });
4158 }
4159 if !logits.is_empty() {
4160 attention::recycle_buf(&mut logits);
4161 }
4162 if trace_on {
4163 let skill = router.as_ref().and_then(|r| r.active_id());
4167 traces.push(TokenTrace {
4168 t: generated,
4169 token_id: t_next,
4170 confidence: confidence.last().copied().unwrap_or(0.0),
4171 active_skill: skill,
4172 recon: None,
4173 switched: false,
4174 });
4175 }
4176 if !commit!(t_next) {
4177 break 'decode;
4178 }
4179 if generated >= max_tokens {
4180 break 'decode;
4181 }
4182
4183 if self.dsv41.is_none() && self.kv_cache.needs_eviction() {
4184 static SAID: std::sync::Once = std::sync::Once::new();
4190 SAID.call_once(|| {
4191 tracing::warn!(
4192 "KV cache full at {} positions — evicting half; quality \
4193 will degrade. Raise CMF_MAX_SEQ.",
4194 self.kv_cache.max_seq_len,
4195 );
4196 });
4197 let keep = (self.kv_cache.max_seq_len / 2).max(1);
4198 self.kv_cache.evict(keep);
4199 }
4200
4201 if graph_spec {
4204 match spec_trial {
4205 SpecTrial::Plain { t0, gen0 } if spec_mon.plain_done(t0, gen0, generated) => {
4206 spec_mon.plain_ms =
4207 t0.elapsed().as_secs_f64() * 1e3 / (generated - gen0) as f64;
4208 let keep = spec_mon.pays();
4209 tracing::info!(
4210 "speculation trial: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
4211 spec_mon.tokens,
4212 spec_mon.round_ms,
4213 spec_mon.plain_ms,
4214 if keep { "speculating" } else { "plain" }
4215 );
4216 spec_mon.fails = 0;
4217 spec_trial = SpecTrial::Decided {
4218 spec: keep,
4219 recheck_at: if keep { usize::MAX } else { generated + 128 },
4220 };
4221 }
4222 SpecTrial::Decided { recheck_at, .. } if generated >= recheck_at => {
4223 spec_mon.n = 0;
4224 spec_trial = SpecTrial::Spec {
4225 t0: std::time::Instant::now(),
4226 gen0: generated,
4227 rounds: 0,
4228 };
4229 }
4230 _ => {}
4231 }
4232 spec_watchdog_off = matches!(
4233 spec_trial,
4234 SpecTrial::Plain { .. } | SpecTrial::Decided { spec: false, .. }
4235 );
4236 }
4237 match &mut mtp {
4238 #[cfg(feature = "gpu")]
4240 Some(m)
4241 if graph_spec
4242 && !spec_watchdog_off
4243 && generated + 1 < max_tokens
4244 && next_pos > 0 =>
4245 {
4246 let t_round = std::time::Instant::now();
4247 if spec_time_level() >= 2 {
4248 if let Some(t) = spec_round_end.take() {
4249 eprintln!(
4250 "spec-gap {:.2} ms (host between rounds)",
4251 t.elapsed().as_secs_f64() * 1e3
4252 );
4253 }
4254 }
4255 spec_stamps_begin();
4256 #[cfg(target_os = "macos")]
4261 let allocs0 = crate::gpu_metal::IO_BUF_ALLOCS
4262 .load(std::sync::atomic::Ordering::Relaxed);
4263 #[cfg(not(target_os = "macos"))]
4264 let allocs0 = 0u64;
4265 if let Some((extra, n_pos, new_h)) = self.graph_spec_step(
4266 m,
4267 &hidden,
4268 t_next,
4269 next_pos,
4270 &mut drafted,
4271 &mut accepted,
4272 &mut all_ids,
4273 max_tokens - generated,
4274 ) {
4275 next_pos = n_pos;
4276 hidden = new_h;
4277 let level = spec_time_level();
4278 if level > 0 {
4279 let wall = t_round.elapsed().as_secs_f32() * 1e3;
4280 let stamps = spec_stamps_take();
4281 let median = if spec_walls.len() >= 3 {
4284 let mut s = spec_walls.clone();
4285 s.sort_by(|a, b| a.partial_cmp(b).unwrap());
4286 Some(s[s.len() / 2])
4287 } else {
4288 None
4289 };
4290 let outlier = median.is_some_and(|m| wall > 1.4 * m);
4291 #[cfg(target_os = "macos")]
4292 let allocs = crate::gpu_metal::IO_BUF_ALLOCS
4293 .load(std::sync::atomic::Ordering::Relaxed)
4294 - allocs0;
4295 #[cfg(not(target_os = "macos"))]
4296 let allocs = allocs0;
4297 eprintln!(
4298 "spec-round wall {wall:.1} ms → {} tokens{}{}",
4299 extra.len() + 1,
4300 if allocs > 0 {
4301 format!(" [{allocs} new device buffers]")
4302 } else {
4303 String::new()
4304 },
4305 match (outlier, median) {
4306 (true, Some(m)) => format!(" OUTLIER (median {m:.1})"),
4307 _ => String::new(),
4308 }
4309 );
4310 if level >= 2 || outlier {
4311 let sum: f32 = stamps.iter().map(|s| s.1).sum();
4312 eprintln!(
4313 "spec-stamps: {}| untracked {:.1}",
4314 spec_stamps_format(&stamps),
4315 wall - sum
4316 );
4317 }
4318 if spec_mon.n >= 1 {
4319 spec_walls.push(wall);
4320 }
4321 }
4322 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, extra.len() + 1);
4326 spec_trial = Self::spec_trial_round(
4329 spec_trial,
4330 &mut spec_mon,
4331 generated + extra.len() + 1,
4332 );
4333 let mut stopped = false;
4334 for &id in &extra {
4335 if self.confidence_on {
4336 confidence.push(0.0);
4337 }
4338 if !commit!(id) {
4339 stopped = true;
4340 break;
4341 }
4342 }
4343 if stopped {
4344 break 'decode;
4345 }
4346 if spec_time_level() >= 2 {
4347 spec_round_end = Some(std::time::Instant::now());
4348 }
4349 continue 'decode;
4350 }
4351 if self
4352 .graph_failed
4353 .swap(false, std::sync::atomic::Ordering::Relaxed)
4354 {
4355 self.finish_generation(&mut mtp, &mut router, true);
4361 return Err("GPU MTP graph failed during speculative decode".to_string());
4362 }
4363 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, 1);
4374 spec_mon.tokens = 0.0;
4375 spec_mon.fails = 3;
4376 spec_trial = Self::spec_trial_round(spec_trial, &mut spec_mon, generated + 1);
4377 hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
4378 next_pos += 1;
4379 continue 'decode;
4380 }
4381 Some(m) if !graph_spec && generated + 1 < max_tokens => {
4383 let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
4384 drafted += 1;
4385 let emb1 = self.embed_single(t_next);
4386 let emb2 = self.embed_single(draft);
4387 let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
4388
4389 inference::rms_norm_into(
4390 &h1,
4391 &self.weights.final_norm,
4392 self.rms_eps,
4393 self.norm_style,
4394 &mut self.ws.n1,
4395 );
4396 let mut logits1 = self.lm_head_forward(&self.ws.n1);
4397 let t_after = sampler::sample_with_scratch_pool(
4398 &logits1,
4399 &self.sampler_config,
4400 &all_ids,
4401 &mut self.rng,
4402 &mut self.sampler_scratch,
4403 self.pool.as_deref(),
4404 );
4405 if self.confidence_on {
4406 confidence.push(sampler::top1_prob_pool(
4407 self.pool.as_deref(),
4408 &mut self.sampler_scratch,
4409 &logits1,
4410 t_after,
4411 calib_temp,
4412 ));
4413 }
4414 attention::recycle_buf(&mut logits1);
4415 if trace_on {
4416 traces.push(TokenTrace {
4419 t: generated,
4420 token_id: t_after,
4421 confidence: confidence.last().copied().unwrap_or(0.0),
4422 active_skill: None,
4423 recon: None,
4424 switched: false,
4425 });
4426 }
4427 let stop = !commit!(t_after);
4428
4429 if t_after == draft {
4430 accepted += 1;
4431 self.commit_linear_scratch();
4432 let _ = self.mtp_step(m, &h1, t_after, next_pos);
4433 hidden = h2;
4434 next_pos += 2;
4435 } else {
4436 for layer in &mut self.kv_cache.layers {
4438 layer.truncate_last(1);
4439 }
4440 if !stop {
4441 let _ = self.mtp_step(m, &h1, t_after, next_pos);
4442 hidden = self.forward_layers(
4443 &self.embed_single(t_after),
4444 next_pos + 1,
4445 None,
4446 );
4447 }
4448 next_pos += 2;
4449 }
4450 if stop {
4451 break 'decode;
4452 }
4453 }
4454 _ => {
4456 #[cfg(feature = "gpu")]
4461 if Self::dsv4_spec_on() && self.dsv4.is_some() {
4462 static SAID: std::sync::Once = std::sync::Once::new();
4463 SAID.call_once(|| {
4464 eprintln!(
4465 "dsv4-spec гейт: mtp={} mask={} router={} trace={} temp={} rep={} ",
4466 !self.dsv4_mtp.is_empty(),
4467 task_mask.is_none(),
4468 router.is_none(),
4469 !trace_on,
4470 self.sampler_config.temperature < 1e-6,
4471 self.sampler_config.repetition_penalty == 1.0,
4472 );
4473 });
4474 }
4475 #[cfg(feature = "gpu")]
4476 if Self::dsv4_spec_on()
4477 && self.dsv4.is_some()
4478 && !self.dsv4_mtp.is_empty()
4479 && task_mask.is_none()
4480 && router.is_none()
4481 && !trace_on
4482 && self.sampler_config.temperature < 1e-6
4483 && self.sampler_config.repetition_penalty == 1.0
4484 && generated + 1 < max_tokens
4485 && all_ids.len() >= 2
4486 && generated >= dsv4_spec_retry_at
4487 {
4488 let tip_token = all_ids[all_ids.len() - 2];
4489 let drafted0 = drafted;
4490 let round = self.dsv4_spec_step(
4491 tip_token,
4492 t_next,
4493 next_pos,
4494 max_tokens.saturating_sub(generated),
4495 &mut drafted,
4496 &mut accepted,
4497 );
4498 if drafted > drafted0 {
4499 let useful = round.as_ref().is_some_and(|(extra, _)| !extra.is_empty());
4500 if useful {
4501 dsv4_spec_bad = 0;
4502 } else {
4503 dsv4_spec_bad += 1;
4504 if dsv4_spec_bad >= 2 {
4505 dsv4_spec_bad = 0;
4506 dsv4_spec_retry_at = generated.saturating_add(32);
4507 tracing::info!(
4508 "dsv4: draft не окупился дважды — точный walk на 32 токена"
4509 );
4510 }
4511 }
4512 }
4513 if let Some((extra, n_pos)) = round {
4514 next_pos = n_pos;
4515 let mut stopped = false;
4516 for &id in &extra {
4517 if self.confidence_on {
4518 confidence.push(0.0);
4519 }
4520 if !commit!(id) {
4521 stopped = true;
4522 break;
4523 }
4524 }
4525 if stopped {
4526 break 'decode;
4527 }
4528 continue 'decode;
4529 }
4530 }
4531 self.graph_want_logits = fuse_lm;
4532 let mut t_fwd = t_next;
4538 let pure_greedy = self.sampler_config.temperature < 1e-6
4539 && self.sampler_config.repetition_penalty == 1.0
4540 && self.sampler_config.suppress_tokens.is_empty();
4541 let burst_k = std::env::var("CMF_MULTISTEP")
4546 .ok()
4547 .and_then(|v| v.parse::<usize>().ok())
4548 .unwrap_or(0);
4549 if pure_greedy
4550 && burst_k >= 1
4551 && fuse_lm
4552 && task_mask.is_none()
4553 && router.is_none()
4554 && !trace_on
4555 && !self.confidence_on
4556 {
4557 let mut stopped = false;
4558 loop {
4559 let room = max_tokens.saturating_sub(generated);
4560 if room <= 2 {
4561 break;
4562 }
4563 let k = burst_k.min(room - 1);
4564 if k < 1 {
4565 break;
4566 }
4567 let Some(ids) = self.try_multi_burst(t_fwd, next_pos, k) else {
4568 if self
4569 .graph_failed
4570 .swap(false, std::sync::atomic::Ordering::Relaxed)
4571 {
4572 self.finish_generation(&mut mtp, &mut router, true);
4573 return Err(
4574 "GPU token graph failed during greedy burst".to_string()
4575 );
4576 }
4577 break;
4578 };
4579 next_pos += k;
4580 for &id in &ids {
4581 if !commit!(id) {
4582 stopped = true;
4583 break;
4584 }
4585 }
4586 if stopped {
4587 break;
4588 }
4589 t_fwd = *ids.last().unwrap();
4590 }
4591 if stopped {
4592 break 'decode;
4593 }
4594 }
4595 #[cfg(target_os = "macos")]
4605 if graph_spec
4606 && spec_watchdog_off
4607 && next_pos > 0
4608 && self.mtp_graph_mode == Some(true)
4609 && crate::gpu::q1_force()
4610 {
4611 if let Some(m) = mtp.as_mut() {
4612 let _ = self.mtp_step_metal(m, &hidden, t_fwd, next_pos - 1, false);
4613 }
4614 }
4615 hidden = self.forward_layers(&self.embed_single(t_fwd), next_pos, task_mask);
4616 next_pos += 1;
4617 if let Some(r) = &mut router {
4620 let phi = self.dyn_phi_ema.clone();
4621 let decision = r.step(&phi, generated);
4622 if let Some(new_active) = decision {
4623 let _ = self.set_active_skill(new_active);
4624 }
4625 if trace_on {
4628 if let Some(last) = traces.last_mut() {
4629 let e = r.last_best_e();
4630 last.recon = e.is_finite().then_some(e);
4631 last.switched = decision.is_some();
4632 }
4633 }
4634 }
4635 }
4636 }
4637 }
4638
4639 let cancelled = finish_reason == "cancelled";
4640 self.finish_generation(&mut mtp, &mut router, cancelled);
4641
4642 let output_ids = &all_ids[input_ids.len()..];
4643 let forwarded = input_ids.len() + output_ids.len().saturating_sub(1);
4647 if cancelled {
4648 self.kv_history.clear();
4649 } else {
4650 self.kv_history = all_ids[..forwarded.min(all_ids.len())].to_vec();
4651 }
4652 confidence.truncate(output_ids.len()); traces.truncate(output_ids.len());
4654 Ok(GenerateResult {
4655 text: self.tokenizer.decode(output_ids),
4656 token_ids: output_ids.to_vec(),
4657 prompt_tokens: input_ids.len(),
4658 tokens_generated: generated,
4659 finish_reason,
4660 mtp_drafted: drafted,
4661 mtp_accepted: accepted,
4662 token_confidence: confidence,
4663 traces,
4664 })
4665 }
4666
4667 fn mtp_step(
4671 &mut self,
4672 m: &mut MtpModule,
4673 hidden: &[f32],
4674 next_token: u32,
4675 position: usize,
4676 ) -> u32 {
4677 self.mtp_step_h(m, hidden, next_token, position).0
4678 }
4679
4680 fn chain_probe_note(depth: usize, prefix_ok: bool) {
4684 use std::sync::Mutex;
4685 static T: Mutex<Vec<(u64, u64)>> = Mutex::new(Vec::new());
4686 let mut t = T.lock().unwrap();
4687 if t.len() <= depth {
4688 t.resize(depth + 1, (0, 0));
4689 }
4690 t[depth].0 += 1;
4691 t[depth].1 += prefix_ok as u64;
4692 if depth == 0 && t[0].0 % 128 == 0 {
4693 let line: Vec<String> = t
4694 .iter()
4695 .enumerate()
4696 .map(|(d, (n, k))| {
4697 format!(
4698 "d{}={:.0}%({n})",
4699 d + 1,
4700 100.0 * *k as f64 / (*n).max(1) as f64
4701 )
4702 })
4703 .collect();
4704 eprintln!("mtp-chain: {}", line.join(" "));
4705 }
4706 }
4707
4708 fn mtp_step_hl(
4716 &mut self,
4717 m: &mut MtpModule,
4718 hidden: &[f32],
4719 next_token: u32,
4720 position: usize,
4721 ) -> (Vec<f32>, Vec<f32>) {
4722 #[cfg(target_os = "macos")]
4727 if self.mtp_graph_mode != Some(false) && crate::gpu::q1_force() {
4728 if let Some(r) = self.mtp_step_metal(m, hidden, next_token, position, true) {
4729 self.mtp_graph_mode = Some(true);
4730 return r;
4731 }
4732 if self.mtp_graph_mode == Some(true) {
4733 tracing::error!("mtp Metal graph failed after admission");
4734 self.clear_sequence_state();
4735 self.graph_failed
4736 .store(true, std::sync::atomic::Ordering::Relaxed);
4737 self.cancel
4738 .store(true, std::sync::atomic::Ordering::Relaxed);
4739 return (Vec::new(), Vec::new());
4740 }
4741 self.mtp_graph_mode = Some(false);
4742 }
4743 #[cfg(feature = "gpu")]
4744 if self.mtp_graph_mode != Some(false) {
4745 if !self.mtp_graph_ok(m) {
4746 if self.mtp_graph_mode == Some(true) {
4747 tracing::error!("mtp graph became unavailable after admission");
4752 self.clear_sequence_state();
4753 self.graph_failed
4754 .store(true, std::sync::atomic::Ordering::Relaxed);
4755 self.cancel
4756 .store(true, std::sync::atomic::Ordering::Relaxed);
4757 return (Vec::new(), Vec::new());
4758 }
4759 self.mtp_graph_mode = Some(false);
4760 } else {
4761 if let Some(r) = self.mtp_step_graph(m, hidden, next_token, position) {
4762 self.mtp_graph_mode = Some(true);
4763 return r;
4764 }
4765 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
4766 return (Vec::new(), Vec::new());
4773 }
4774 tracing::error!("mtp graph failed or declined after admission");
4778 self.clear_sequence_state();
4779 self.graph_failed
4780 .store(true, std::sync::atomic::Ordering::Relaxed);
4781 self.cancel
4782 .store(true, std::sync::atomic::Ordering::Relaxed);
4783 return (Vec::new(), Vec::new());
4784 }
4785 }
4786 let e = self.embed_single(next_token);
4790 let mut cat = vec![0.0f32; 2 * self.hidden_size];
4791 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
4792 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
4793 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
4794 let mut x = vec![0.0f32; self.hidden_size];
4795 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
4796
4797 let lw = &m.layer;
4799 inference::rms_norm_into(
4800 &x,
4801 &lw.input_norm,
4802 self.rms_eps,
4803 self.norm_style,
4804 &mut self.ws.n1,
4805 );
4806 let attn = match &lw.attn {
4807 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
4809 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
4810 AttnKind::Full {
4811 wq,
4812 wk,
4813 wv,
4814 wo,
4815 q_norm,
4816 k_norm,
4817 output_gate,
4818 softplus_gate,
4819 bias,
4820 } => {
4821 let mut cfg = self.attn_cfg(position);
4822 cfg.q_norm = q_norm.as_deref();
4823 cfg.k_norm = k_norm.as_deref();
4824 cfg.output_gate = *output_gate;
4825 cfg.softplus_gate = softplus_gate
4826 .as_ref()
4827 .map(|(gate, per_head)| (gate, *per_head));
4828 cfg.bias = bias
4829 .as_ref()
4830 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
4831 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
4832 }
4833 AttnKind::Linear(_) | AttnKind::LinearGdn(_) | AttnKind::ShortConv(_) => {
4834 unreachable!("MTP block is full attention")
4835 }
4836 };
4837 for (i, &a) in attn.iter().enumerate() {
4838 x[i] += a;
4839 }
4840 inference::rms_norm_into(
4841 &x,
4842 &lw.post_norm,
4843 self.rms_eps,
4844 self.norm_style,
4845 &mut self.ws.p1,
4846 );
4847 let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref(), None);
4848 for (i, &f) in ffn.iter().enumerate() {
4849 x[i] += f;
4850 }
4851
4852 inference::rms_norm_into(
4853 &x,
4854 &m.final_norm,
4855 self.rms_eps,
4856 self.norm_style,
4857 &mut self.ws.n1,
4858 );
4859 let lg = self.lm_head_forward(&self.ws.n1);
4860 (lg, x)
4861 }
4862
4863 fn mtp_step_h(
4865 &mut self,
4866 m: &mut MtpModule,
4867 hidden: &[f32],
4868 next_token: u32,
4869 position: usize,
4870 ) -> (u32, Vec<f32>) {
4871 let (mut lg, x) = self.mtp_step_hl(m, hidden, next_token, position);
4872 let draft = sampler::argmax(&lg);
4873 attention::recycle_buf(&mut lg);
4874 (draft, x)
4875 }
4876
4877 fn spec_trial_round(trial: SpecTrial, mon: &mut SpecMon, generated: usize) -> SpecTrial {
4883 match trial {
4884 SpecTrial::Spec { t0, gen0, rounds } => {
4885 let rounds = rounds + 1;
4886 if rounds >= 5 {
4887 if mon.plain_ms > 0.0 {
4888 let keep = mon.pays();
4889 mon.fails = 0;
4890 tracing::info!(
4891 "speculation re-check: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
4892 mon.tokens,
4893 mon.round_ms,
4894 mon.plain_ms,
4895 if keep { "speculating" } else { "plain" }
4896 );
4897 SpecTrial::Decided {
4898 spec: keep,
4899 recheck_at: if keep { usize::MAX } else { generated + 128 },
4900 }
4901 } else if mon.pays() {
4902 mon.fails = 0;
4907 tracing::info!(
4908 "speculation trial: {:.2} tok/round in {:.1} ms — speculating (plain not timed)",
4909 mon.tokens,
4910 mon.round_ms,
4911 );
4912 SpecTrial::Decided {
4913 spec: true,
4914 recheck_at: usize::MAX,
4915 }
4916 } else {
4917 SpecTrial::Plain {
4918 t0: std::time::Instant::now(),
4919 gen0: generated,
4920 }
4921 }
4922 } else {
4923 SpecTrial::Spec { t0, gen0, rounds }
4924 }
4925 }
4926 SpecTrial::Decided { spec: true, .. } => {
4927 if mon.pays() {
4928 mon.fails = 0;
4929 trial
4930 } else {
4931 mon.fails += 1;
4932 if mon.fails >= 4 {
4933 if mon.plain_ms <= 0.0 {
4934 tracing::info!(
4938 "speculation doubtful: {:.2} tok/round in {:.1} ms — timing plain",
4939 mon.tokens,
4940 mon.round_ms,
4941 );
4942 return SpecTrial::Plain {
4943 t0: std::time::Instant::now(),
4944 gen0: generated,
4945 };
4946 }
4947 tracing::info!(
4948 "speculation stopped: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok",
4949 mon.tokens,
4950 mon.round_ms,
4951 mon.plain_ms
4952 );
4953 SpecTrial::Decided {
4954 spec: false,
4955 recheck_at: generated + 128,
4956 }
4957 } else {
4958 trial
4959 }
4960 }
4961 }
4962 other => other,
4963 }
4964 }
4965
4966 fn mtp_kv_id(&self) -> u64 {
4969 self.graph_kv_id | (1u64 << 40)
4970 }
4971
4972 const MTP_LAYER_BASE: usize = 0;
4977
4978 #[cfg(feature = "gpu")]
4985 fn rewind_mtp_graph_mirror(&self, stored: usize) -> bool {
4986 self.mtp_graph_mode != Some(true)
4987 || crate::gpu::graph_kv_set_stored(self.mtp_kv_id(), Self::MTP_LAYER_BASE, stored)
4988 }
4989
4990 #[cfg(feature = "gpu")]
4996 fn rewind_trunk_graph_mirrors(&self, stored: usize) -> bool {
4997 let mut ok = true;
4998 let mut expected = false;
4999 for li in 0..self.num_layers {
5000 if matches!(
5001 self.weights.layers[self.phys_layer(li)].attn,
5002 AttnKind::Full { .. }
5003 ) {
5004 expected = true;
5005 ok &= crate::gpu::graph_kv_set_stored(self.graph_kv_id, li, stored);
5006 }
5007 }
5008 !expected || ok
5009 }
5010
5011 fn graph_gdn_layer_count(&self) -> usize {
5015 (0..self.num_layers)
5016 .filter(|&li| {
5017 matches!(
5018 &self.weights.layers[self.phys_layer(li)].attn,
5019 AttnKind::LinearGdn(_)
5020 )
5021 })
5022 .count()
5023 }
5024
5025 fn mtp_block_input(&mut self, m: &MtpModule, hidden: &[f32], next_token: u32) -> Vec<f32> {
5028 let e = self.embed_single(next_token);
5029 let mut cat = vec![0.0f32; 2 * self.hidden_size];
5030 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
5031 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
5032 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
5033 let mut x = vec![0.0f32; self.hidden_size];
5034 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
5035 x
5036 }
5037
5038 #[cfg(feature = "gpu")]
5041 fn mtp_block_graph_ok(&self, m: &MtpModule) -> bool {
5042 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0") {
5043 return false;
5044 }
5045 if !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
5046 || !crate::gpu::enabled_here()
5047 || self.attn_softcap > 0.0
5048 || self.attention_heads_per_layer.is_some()
5049 {
5050 return false;
5051 }
5052 matches!(
5053 &m.layer.attn,
5054 AttnKind::Full {
5055 softplus_gate: None,
5056 ..
5057 }
5058 ) && matches!(&m.layer.ffn, FfnKind::Dense(_))
5059 }
5060
5061 #[cfg(feature = "gpu")]
5065 fn mtp_graph_ok(&self, m: &MtpModule) -> bool {
5066 if !self.mtp_block_graph_ok(m) {
5067 return false;
5068 }
5069 let AttnKind::Full { wq, wk, wv, wo, .. } = &m.layer.attn else {
5070 return false;
5071 };
5072 let FfnKind::Dense(d) = &m.layer.ffn else {
5073 return false;
5074 };
5075 d.segs.is_empty()
5076 && wq.graph_weight().is_some()
5077 && wk.graph_weight().is_some()
5078 && wv.graph_weight().is_some()
5079 && wo.graph_weight().is_some()
5080 && d.gate_proj.graph_weight().is_some()
5081 && d.up_proj.graph_weight().is_some()
5082 && d.down_proj.graph_weight().is_some()
5083 && self.weights.lm_head.graph_weight().is_some()
5084 }
5085
5086 #[cfg(feature = "gpu")]
5092 fn mtp_step_graph(
5093 &mut self,
5094 m: &mut MtpModule,
5095 hidden: &[f32],
5096 next_token: u32,
5097 position: usize,
5098 ) -> Option<(Vec<f32>, Vec<f32>)> {
5099 if !self.mtp_graph_ok(m) {
5100 return None;
5101 }
5102 let lw = &m.layer;
5103 let AttnKind::Full {
5104 wq,
5105 wk,
5106 wv,
5107 wo,
5108 q_norm,
5109 k_norm,
5110 output_gate,
5111 softplus_gate,
5112 bias,
5113 } = &lw.attn
5114 else {
5115 return None;
5116 };
5117 if softplus_gate.is_some() {
5118 return None;
5119 }
5120 let FfnKind::Dense(d) = &lw.ffn else {
5121 return None;
5122 };
5123 if !d.segs.is_empty() {
5124 return None; }
5126 let mut x = self.mtp_block_input(m, hidden, next_token);
5129 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
5130 let (_, i, kind, rs) = t.graph_weight()?;
5131 Some(crate::gpu::GraphW {
5132 idx: i,
5133 kind,
5134 row_scale: rs,
5135 data: &[],
5136 prism: crate::gpu::GraphPrismOp::None,
5137 affine: false,
5138 })
5139 }
5140 let (model, _, _, _) = wq.graph_weight()?;
5141 let model = model.clone();
5142 let (lm_gw, lm_rows) = {
5143 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
5144 let rows = if kind == 6 {
5148 self.draft_head_rows(self.weights.lm_head.rows())
5149 } else {
5150 self.weights.lm_head.rows()
5151 };
5152 (
5153 crate::gpu::GraphW {
5154 idx: i,
5155 kind,
5156 row_scale: rs,
5157 data: &[],
5158 prism: crate::gpu::GraphPrismOp::None,
5159 affine: false,
5160 },
5161 rows,
5162 )
5163 };
5164 let layer = crate::gpu::GraphLayer {
5165 input_norm: &lw.input_norm,
5166 attn: crate::gpu::GraphAttn::Full {
5167 wq: gw(wq)?,
5168 wk: gw(wk)?,
5169 wv: gw(wv)?,
5170 wo: gw(wo)?,
5171 q_norm: q_norm.as_deref(),
5172 k_norm: k_norm.as_deref(),
5173 late_qk_norm: self.qk_norm_after_rope,
5174 bias: bias
5175 .as_ref()
5176 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
5177 output_gate: *output_gate,
5178 cpu_k: m.kv.k_heads(),
5179 cpu_v: m.kv.v_heads(),
5180 },
5181 post_norm: &lw.post_norm,
5182 ffn: crate::gpu::GraphFfn::Dense {
5183 gate: gw(&d.gate_proj)?,
5184 up: gw(&d.up_proj)?,
5185 down: gw(&d.down_proj)?,
5186 },
5187 };
5188 let nh = self.num_heads;
5189 let (nkv, hd, rd) = self.layer_geom(0);
5190 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
5191 let mut logits = Vec::new();
5192 let ok = crate::gpu::forward_token_graph(
5193 &model,
5194 self.mtp_kv_id(),
5195 std::slice::from_ref(&layer),
5196 &[None],
5197 self.o1_epoch,
5198 &self.inv_freq,
5199 &mut x,
5200 nh,
5201 nkv,
5202 hd,
5203 self.attn_scale,
5204 rd,
5205 self.hidden_size,
5206 self.intermediate_size,
5207 position,
5208 self.kv_cache.max_seq_len,
5209 gemma,
5210 self.rms_eps as f32,
5211 Some((&lm_gw, lm_rows)),
5212 &m.final_norm,
5213 &mut logits,
5214 &[],
5215 1,
5216 None,
5217 None,
5218 None,
5219 Self::MTP_LAYER_BASE,
5220 true,
5221 );
5222 match ok {
5223 crate::gpu::TokenGraphOutcome::Completed => {}
5224 crate::gpu::TokenGraphOutcome::Declined => return None,
5225 crate::gpu::TokenGraphOutcome::Failed => {
5226 self.clear_sequence_state();
5230 self.graph_failed
5231 .store(true, std::sync::atomic::Ordering::Relaxed);
5232 self.cancel
5233 .store(true, std::sync::atomic::Ordering::Relaxed);
5234 return None;
5235 }
5236 }
5237 logits.resize(self.vocab_size, 0.0);
5238 Some((logits, x))
5239 }
5240
5241 #[cfg(feature = "gpu")]
5249 fn mtp_warm_graph(
5250 &mut self,
5251 m: &mut MtpModule,
5252 pairs: &[(&[f32], u32)],
5253 first_pos: usize,
5254 ) -> crate::gpu::BatchGraphOutcome {
5255 if pairs.is_empty() {
5256 return crate::gpu::BatchGraphOutcome::Completed;
5257 }
5258 if !self.mtp_block_graph_ok(m) {
5259 return crate::gpu::BatchGraphOutcome::Declined;
5260 }
5261 let hs = self.hidden_size;
5262 let mut hiddens = Vec::with_capacity(pairs.len() * hs);
5265 for (h, t) in pairs {
5266 hiddens.extend_from_slice(&self.mtp_block_input(m, h, *t));
5267 }
5268 let lw = &m.layer;
5269 let AttnKind::Full {
5270 wq,
5271 wk,
5272 wv,
5273 wo,
5274 q_norm,
5275 k_norm,
5276 output_gate,
5277 bias,
5278 ..
5279 } = &lw.attn
5280 else {
5281 return crate::gpu::BatchGraphOutcome::Declined;
5282 };
5283 let FfnKind::Dense(d) = &lw.ffn else {
5284 return crate::gpu::BatchGraphOutcome::Declined;
5285 };
5286 if !d.segs.is_empty() {
5287 return crate::gpu::BatchGraphOutcome::Declined; }
5289 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
5290 let (_, i, kind, rs) = t.graph_weight()?;
5291 Some(crate::gpu::GraphW {
5292 idx: i,
5293 kind,
5294 row_scale: rs,
5295 data: &[],
5296 prism: crate::gpu::GraphPrismOp::None,
5297 affine: false,
5298 })
5299 }
5300 let Some((model, _, _, _)) = wq.graph_weight() else {
5301 return crate::gpu::BatchGraphOutcome::Declined;
5302 };
5303 let model = model.clone();
5304 let (Some(gwq), Some(gwk), Some(gwv), Some(gwo), Some(gg), Some(gu), Some(gd)) = (
5305 gw(wq),
5306 gw(wk),
5307 gw(wv),
5308 gw(wo),
5309 gw(&d.gate_proj),
5310 gw(&d.up_proj),
5311 gw(&d.down_proj),
5312 ) else {
5313 return crate::gpu::BatchGraphOutcome::Declined;
5314 };
5315 let layer = crate::gpu::GraphLayer {
5316 input_norm: &lw.input_norm,
5317 attn: crate::gpu::GraphAttn::Full {
5318 wq: gwq,
5319 wk: gwk,
5320 wv: gwv,
5321 wo: gwo,
5322 q_norm: q_norm.as_deref(),
5323 k_norm: k_norm.as_deref(),
5324 late_qk_norm: self.qk_norm_after_rope,
5325 bias: bias
5326 .as_ref()
5327 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
5328 output_gate: *output_gate,
5329 cpu_k: m.kv.k_heads(),
5330 cpu_v: m.kv.v_heads(),
5331 },
5332 post_norm: &lw.post_norm,
5333 ffn: crate::gpu::GraphFfn::Dense {
5334 gate: gg,
5335 up: gu,
5336 down: gd,
5337 },
5338 };
5339 let positions: Vec<usize> = (first_pos..first_pos + pairs.len()).collect();
5340 let nh = self.num_heads;
5341 let (nkv, hd, rd) = self.layer_geom(0);
5342 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
5343 crate::gpu::forward_batch_graph(
5344 &model,
5345 self.mtp_kv_id(),
5346 std::slice::from_ref(&layer),
5347 &self.inv_freq,
5348 &mut hiddens,
5349 nh,
5350 nkv,
5351 hd,
5352 rd,
5353 hs,
5354 self.intermediate_size,
5355 &positions,
5356 self.kv_cache.max_seq_len,
5357 gemma,
5358 self.rms_eps as f32,
5359 self.attn_scale,
5360 pairs.len(),
5361 &[],
5362 0,
5363 None,
5364 )
5365 }
5366
5367 #[cfg(feature = "gpu")]
5374 fn mtp_warm_graph_fallback(
5375 &mut self,
5376 m: &mut MtpModule,
5377 pairs: &[(&[f32], u32)],
5378 first_pos: usize,
5379 ) -> bool {
5380 if pairs.is_empty() {
5381 return true;
5382 }
5383 let graphable = self.mtp_block_graph_ok(m);
5384 if !graphable {
5385 if self.mtp_graph_mode == Some(true) {
5389 return false;
5390 }
5391 self.mtp_graph_mode = Some(false);
5392 for (j, (h, t)) in pairs.iter().enumerate() {
5393 self.mtp_warm(m, h, *t, first_pos + j);
5394 }
5395 return true;
5396 }
5397
5398 for (j, (h, t)) in pairs.iter().enumerate() {
5403 if self.mtp_step_graph(m, h, *t, first_pos + j).is_none() {
5404 return false;
5405 }
5406 }
5407 self.mtp_graph_mode = Some(true);
5408 true
5409 }
5410
5411 #[cfg(feature = "gpu")]
5416 fn mtp_warm_prefill_pairs(
5417 &mut self,
5418 m: &mut MtpModule,
5419 pairs: &[(&[f32], u32)],
5420 first_pos: usize,
5421 ) -> Result<(), &'static str> {
5422 if self.mtp_graph_mode == Some(false) || !self.mtp_graph_ok(m) {
5427 if self.mtp_graph_mode == Some(true) {
5428 return Err("MTP token graph became unavailable after admission");
5429 }
5430 self.mtp_graph_mode = Some(false);
5431 for (j, (h, t)) in pairs.iter().enumerate() {
5432 self.mtp_warm(m, h, *t, first_pos + j);
5433 }
5434 return Ok(());
5435 }
5436 match self.mtp_warm_graph(m, pairs, first_pos) {
5437 crate::gpu::BatchGraphOutcome::Completed => {
5438 if !pairs.is_empty() {
5439 self.mtp_graph_mode = Some(true);
5440 }
5441 Ok(())
5442 }
5443 crate::gpu::BatchGraphOutcome::Declined => {
5444 if self.mtp_warm_graph_fallback(m, pairs, first_pos) {
5445 Ok(())
5446 } else {
5447 Err("MTP warm-up fallback failed after device admission")
5448 }
5449 }
5450 crate::gpu::BatchGraphOutcome::Failed => {
5451 Err("MTP warm batch graph failed after admission")
5452 }
5453 }
5454 }
5455
5456 #[cfg(not(feature = "gpu"))]
5457 fn mtp_warm_prefill_pairs(
5458 &mut self,
5459 m: &mut MtpModule,
5460 pairs: &[(&[f32], u32)],
5461 first_pos: usize,
5462 ) -> Result<(), &'static str> {
5463 for (j, (h, t)) in pairs.iter().enumerate() {
5464 self.mtp_warm(m, h, *t, first_pos + j);
5465 }
5466 Ok(())
5467 }
5468
5469 fn mtp_warm(&mut self, m: &mut MtpModule, hidden: &[f32], next_token: u32, position: usize) {
5473 let e = self.embed_single(next_token);
5474 let mut cat = vec![0.0f32; 2 * self.hidden_size];
5475 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
5476 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
5477 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
5478 let mut x = vec![0.0f32; self.hidden_size];
5479 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
5480 inference::rms_norm_into(
5481 &x,
5482 &m.layer.input_norm,
5483 self.rms_eps,
5484 self.norm_style,
5485 &mut self.ws.n1,
5486 );
5487 let attn = match &m.layer.attn {
5488 AttnKind::Full {
5489 wq,
5490 wk,
5491 wv,
5492 wo,
5493 q_norm,
5494 k_norm,
5495 output_gate,
5496 softplus_gate,
5497 bias,
5498 } => {
5499 let mut cfg = self.attn_cfg(position);
5500 cfg.q_norm = q_norm.as_deref();
5501 cfg.k_norm = k_norm.as_deref();
5502 cfg.output_gate = *output_gate;
5503 cfg.softplus_gate = softplus_gate.as_ref().map(|(g, p)| (g, *p));
5504 cfg.bias = bias
5505 .as_ref()
5506 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
5507 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
5508 }
5509 _ => return,
5510 };
5511 let _ = attn;
5512 }
5513
5514 #[cfg(feature = "gpu")]
5521 #[allow(clippy::too_many_arguments)]
5522 fn graph_spec_step(
5523 &mut self,
5524 m: &mut MtpModule,
5525 hidden: &[f32],
5526 t_next: u32,
5527 next_pos: usize,
5528 drafted: &mut usize,
5529 accepted: &mut usize,
5530 all_ids: &mut Vec<u32>,
5534 room: usize,
5539 ) -> Option<(Vec<u32>, usize, Vec<f32>)> {
5540 #[cfg(target_os = "macos")]
5551 let metal_native = crate::gpu::q1_force();
5552 #[cfg(not(target_os = "macos"))]
5553 let metal_native = false;
5554 #[cfg(feature = "gpu")]
5555 let k_default = if metal_native {
5556 7
5559 } else if crate::gpu_wgpu::verify_i8_on() {
5560 5
5561 } else {
5562 4
5563 };
5564 #[cfg(not(feature = "gpu"))]
5565 let k_default = 4;
5566 let k_env: Option<usize> = std::env::var("CMF_GRAPH_SPEC_K")
5567 .ok()
5568 .and_then(|v| v.parse().ok())
5569 .filter(|&v| (1..=8).contains(&v));
5570 let (k_start, k_max) = if metal_native { (7, 7) } else { (3, k_default.max(5)) };
5575 let k_full: usize = k_env.unwrap_or_else(|| self.spec_k_adapt.unwrap_or(k_start));
5576 let k_spec = k_full.min(room).max(1);
5577 let k_capped = k_spec < k_full;
5580 if next_pos == 0 {
5581 return None;
5582 }
5583 let t_round = std::time::Instant::now();
5584 let subs = || crate::gpu_wgpu::SUBMITS.load(std::sync::atomic::Ordering::Relaxed);
5600 let sub0 = subs();
5601 let cfg = self.sampler_config.clone();
5606 let penalized = !(cfg.repetition_penalty == 1.0
5607 && cfg.presence_penalty == 0.0
5608 && cfg.suppress_tokens.is_empty());
5609 let greedy_pen = cfg.temperature < 1e-6 && penalized;
5614 let sampling = cfg.temperature >= 1e-6;
5615 let sparse = sampling && sampler::sparse_ok(&cfg);
5621 let base_len = all_ids.len();
5622 if sampling && !sparse && self.spec_q.len() < k_spec {
5623 self.spec_q.resize_with(k_spec, Vec::new);
5624 }
5625 if sparse && self.spec_qs.len() < k_spec {
5626 self.spec_qs.resize_with(k_spec, Vec::new);
5627 }
5628 let mut drafts = Vec::with_capacity(k_spec);
5633 let mut hx = hidden.to_vec();
5634 let spec_dbg = std::env::var("CMF_SPEC_DBG").is_ok();
5637 spec_stamp("pro");
5638 #[cfg(target_os = "macos")]
5644 if metal_native && !sampling && !greedy_pen && self.mtp_graph_mode != Some(false) {
5645 match self.mtp_draft_chain_metal(m, hidden, t_next, next_pos - 1, k_spec) {
5646 Ok(ids) => {
5647 self.mtp_graph_mode = Some(true);
5648 drafts = ids;
5649 }
5650 Err(true) => {
5651 tracing::error!("mtp Metal draft chain failed after commit");
5652 self.clear_sequence_state();
5653 self.graph_failed
5654 .store(true, std::sync::atomic::Ordering::Relaxed);
5655 self.cancel
5656 .store(true, std::sync::atomic::Ordering::Relaxed);
5657 return None;
5658 }
5659 Err(false) => {}
5660 }
5661 }
5662 for j in drafts.len()..k_spec {
5663 let tok_in = if j == 0 { t_next } else { drafts[j - 1] };
5664 let mut dbg_ref: Option<(Vec<f32>, Vec<f32>)> = None;
5665 if spec_dbg {
5666 let saved = self.mtp_graph_mode;
5667 self.mtp_graph_mode = Some(false);
5668 let r = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
5669 self.mtp_graph_mode = saved;
5670 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
5671 return None;
5672 }
5673 m.kv.truncate_last(1);
5674 dbg_ref = Some(r);
5675 }
5676 let (mut lg, hj) = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
5677 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
5678 return None;
5679 }
5680 if let Some((lg_cpu, h_cpu)) = dbg_ref {
5681 let n = |v: &[f32]| v.iter().map(|x| x * x).sum::<f32>().sqrt();
5682 let dl = lg
5683 .iter()
5684 .zip(&lg_cpu)
5685 .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
5686 let dh = hj
5687 .iter()
5688 .zip(&h_cpu)
5689 .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
5690 eprintln!(
5691 "spec-dbg j={j} pos {} tok_in {tok_in}: per-op draft {} graph draft {} | max|dlogit| {dl:.3} | |h_cpu| {:.2} |h_graph| {:.2} max|dh| {dh:.3} | kv rows {}",
5692 next_pos - 1 + j,
5693 sampler::argmax(&lg_cpu),
5694 sampler::argmax(&lg),
5695 n(&h_cpu),
5696 n(&hj),
5697 m.kv.seq_len
5698 );
5699 }
5700 let dj = if sparse {
5701 let mut q = std::mem::take(&mut self.spec_qs[j]);
5702 let ok = sampler::sparse_distribution_into(
5703 &lg,
5704 &cfg,
5705 all_ids,
5706 &mut self.sampler_scratch,
5707 self.pool.as_deref(),
5708 &mut q,
5709 );
5710 let d = if ok {
5711 sampler::draw_sparse(&q, &mut self.rng)
5712 } else {
5713 let t = sampler::argmax(&lg);
5715 q.clear();
5716 q.push((t, 1.0));
5717 t
5718 };
5719 self.spec_qs[j] = q;
5720 all_ids.push(d);
5721 d
5722 } else if sampling {
5723 let mut q = std::mem::take(&mut self.spec_q[j]);
5724 sampler::distribution_into(
5725 &lg,
5726 &cfg,
5727 all_ids,
5728 &mut self.sampler_scratch,
5729 self.pool.as_deref(),
5730 &mut q,
5731 );
5732 let d = sampler::draw(&q, &mut self.rng);
5733 self.spec_q[j] = q;
5734 all_ids.push(d); d
5736 } else if greedy_pen {
5737 let d = sampler::argmax_penalized(
5738 &lg,
5739 &cfg,
5740 all_ids,
5741 &mut self.sampler_scratch,
5742 self.pool.as_deref(),
5743 );
5744 all_ids.push(d);
5745 d
5746 } else {
5747 sampler::argmax(&lg)
5748 };
5749 attention::recycle_buf(&mut lg);
5750 drafts.push(dj);
5751 hx = hj;
5752 spec_stamp("d.pick");
5753 }
5754 all_ids.truncate(base_len);
5755 *drafted += k_spec;
5756 let t_draft = t_round.elapsed();
5757 let sub_draft = subs();
5758 let b = k_spec + 1;
5761 let mut hiddens = vec![0.0f32; b * self.hidden_size];
5762 for (i, &t) in std::iter::once(&t_next).chain(drafts.iter()).enumerate() {
5763 let e = self.embed_single(t);
5764 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&e);
5765 }
5766 let positions: Vec<usize> = (next_pos..next_pos + b).collect();
5767 spec_stamp("v.emb");
5768 let (lm_gw, lm_rows) = {
5769 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
5770 (
5771 crate::gpu::GraphW {
5772 idx: i,
5773 kind,
5774 row_scale: rs,
5775 data: &[],
5776 prism: crate::gpu::GraphPrismOp::None,
5777 affine: false,
5778 },
5779 self.weights.lm_head.rows(),
5780 )
5781 };
5782 let mut logits = Vec::new();
5783 let final_norm = self.weights.final_norm.clone();
5784 #[cfg(target_os = "macos")]
5793 let greedy_dev = metal_native
5794 && !sampling
5795 && !greedy_pen
5796 && !self.confidence_on
5797 && self.final_softcap.is_none()
5798 && self.vocab_size == lm_rows
5804 && std::env::var_os("CMF_METAL_VERIFY_CHECK").is_none()
5805 && std::env::var_os("CMF_LOGIT_DUMP").is_none()
5806 && std::env::var("CMF_METAL_DEV_ARGMAX").as_deref() != Ok("0");
5807 #[cfg(not(target_os = "macos"))]
5808 let greedy_dev = false;
5809 let mut dev_ids: Vec<u32> = Vec::new();
5810 #[cfg(target_os = "macos")]
5811 let verify_outcome = if metal_native {
5812 let lm = self.weights.lm_head.q1_parts()?;
5813 let n_score = self.vocab_size.min(lm_rows);
5814 self.try_batch_graph_metal(
5815 &mut hiddens,
5816 &positions,
5817 b,
5818 Some((lm, &final_norm, &mut logits)),
5819 if greedy_dev {
5820 Some((n_score, &mut dev_ids))
5821 } else {
5822 None
5823 },
5824 )
5825 } else {
5826 self.try_batch_graph_wgpu(
5827 &mut hiddens,
5828 &positions,
5829 b,
5830 Some(crate::gpu::SpecTail {
5831 lm: lm_gw,
5832 lm_rows,
5833 final_norm: &final_norm,
5834 logits_out: &mut logits,
5835 }),
5836 )
5837 };
5838 #[cfg(not(target_os = "macos"))]
5839 let verify_outcome = self.try_batch_graph_wgpu(
5840 &mut hiddens,
5841 &positions,
5842 b,
5843 Some(crate::gpu::SpecTail {
5844 lm: lm_gw,
5845 lm_rows,
5846 final_norm: &final_norm,
5847 logits_out: &mut logits,
5848 }),
5849 );
5850 match verify_outcome {
5851 crate::gpu::BatchGraphOutcome::Completed => {}
5852 crate::gpu::BatchGraphOutcome::Declined => {
5853 m.kv.truncate_last(k_spec);
5857 if !metal_native && !self.rewind_mtp_graph_mirror(next_pos) {
5858 self.clear_sequence_state();
5859 self.graph_failed
5860 .store(true, std::sync::atomic::Ordering::Relaxed);
5861 self.cancel
5862 .store(true, std::sync::atomic::Ordering::Relaxed);
5863 tracing::error!("MTP graph mirror rewind failed after verify decline");
5864 }
5865 return None;
5866 }
5867 crate::gpu::BatchGraphOutcome::Failed => {
5868 self.clear_sequence_state();
5872 self.graph_failed
5873 .store(true, std::sync::atomic::Ordering::Relaxed);
5874 self.cancel
5875 .store(true, std::sync::atomic::Ordering::Relaxed);
5876 tracing::error!("MTP verify batch graph failed after admission");
5877 return None;
5878 }
5879 }
5880 #[cfg(target_os = "macos")]
5886 if metal_native && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("1") {
5887 let snap: Vec<Vec<f32>> = self
5888 .kv_cache
5889 .layers
5890 .iter()
5891 .map(|l| l.linear_state.clone())
5892 .collect();
5893 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
5894 let toks: Vec<u32> = std::iter::once(t_next)
5895 .chain(drafts.iter().copied())
5896 .collect();
5897 let want_save = self.graph_want_logits;
5898 self.graph_want_logits = false;
5899 for (i, &t) in toks.iter().enumerate() {
5900 let hi = self.forward_layers(&self.embed_single(t), next_pos + i, None);
5901 let _ = self.graph_logits.take();
5902 if std::env::var("CMF_SPEC_PLAIN_HIDDEN").as_deref() == Ok("1") {
5906 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&hi);
5907 }
5908 let ref_lg = self.logits_from_hidden(&hi);
5909 let row = &logits[i * lm_rows..(i + 1) * lm_rows];
5910 let ra = sampler::argmax(&ref_lg);
5911 let va = sampler::argmax(row);
5912 let mut md = 0f32;
5913 let mut rms = 0f64;
5914 for j in 0..lm_rows.min(ref_lg.len()) {
5915 let d = (ref_lg[j] - row[j]).abs();
5916 md = md.max(d);
5917 rms += (d as f64) * (d as f64);
5918 }
5919 let mut hd = 0f32;
5920 for j in 0..self.hidden_size {
5921 hd = hd.max((hi[j] - hiddens[i * self.hidden_size + j]).abs());
5922 }
5923 eprintln!(
5924 "verify-check row {i} tok {t} pos {}: ref argmax {ra} verify argmax {va} {} | max|dlogit| {md:.3} rms {:.4} | max|dhidden| {hd:.4}",
5925 next_pos + i,
5926 if ra == va { "OK" } else { "MISMATCH" },
5927 (rms / lm_rows as f64).sqrt()
5928 );
5929 }
5930 self.graph_want_logits = want_save;
5931 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
5934 if l.linear_state.len() == st.len() {
5935 l.linear_state.copy_from_slice(&st);
5936 } else {
5937 l.linear_state = st;
5938 }
5939 }
5940 for (li, (l, n0)) in self.kv_cache.layers.iter_mut().zip(attn_lens).enumerate() {
5941 let extra = l.seq_len.saturating_sub(n0);
5942 if extra > 0 {
5943 l.truncate_last(extra);
5944 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, n0);
5945 }
5946 }
5947 }
5948 let t_verify = t_round.elapsed();
5949 let sub_verify = subs();
5950 let mut a = 0usize;
5955 let mut forced: Option<u32> = None;
5956 let ids: Vec<u32> = if sparse {
5957 let mut p = std::mem::take(&mut self.spec_ps);
5958 let mut res = std::mem::take(&mut self.spec_ress);
5959 while a < k_spec {
5960 let ok = sampler::sparse_distribution_into(
5961 &logits[a * lm_rows..(a + 1) * lm_rows],
5962 &cfg,
5963 all_ids,
5964 &mut self.sampler_scratch,
5965 self.pool.as_deref(),
5966 &mut p,
5967 );
5968 if !ok {
5969 let t = sampler::argmax(&logits[a * lm_rows..(a + 1) * lm_rows]);
5970 p.clear();
5971 p.push((t, 1.0));
5972 }
5973 match sampler::spec_accept_or_correct_sparse(
5974 &p,
5975 &self.spec_qs[a],
5976 drafts[a],
5977 &mut self.rng,
5978 &mut res,
5979 ) {
5980 None => {
5981 all_ids.push(drafts[a]);
5982 a += 1;
5983 }
5984 Some(c) => {
5985 forced = Some(c);
5986 break;
5987 }
5988 }
5989 }
5990 all_ids.truncate(base_len);
5991 self.spec_ps = p;
5992 self.spec_ress = res;
5993 drafts.clone()
5994 } else if sampling {
5995 let mut p = std::mem::take(&mut self.spec_p);
5996 let mut res = std::mem::take(&mut self.spec_res);
5997 while a < k_spec {
5998 sampler::distribution_into(
5999 &logits[a * lm_rows..(a + 1) * lm_rows],
6000 &cfg,
6001 all_ids,
6002 &mut self.sampler_scratch,
6003 self.pool.as_deref(),
6004 &mut p,
6005 );
6006 match sampler::spec_accept_or_correct(
6007 &p,
6008 &self.spec_q[a],
6009 drafts[a],
6010 &mut self.rng,
6011 &mut res,
6012 self.pool.as_deref(),
6013 ) {
6014 None => {
6015 all_ids.push(drafts[a]);
6016 a += 1;
6017 }
6018 Some(c) => {
6019 forced = Some(c);
6020 break;
6021 }
6022 }
6023 }
6024 all_ids.truncate(base_len);
6025 self.spec_p = p;
6026 self.spec_res = res;
6027 drafts.clone()
6029 } else if greedy_pen {
6030 let mut ids: Vec<u32> = Vec::with_capacity(b);
6034 for i in 0..b {
6035 let t = sampler::argmax_penalized(
6036 &logits[i * lm_rows..(i + 1) * lm_rows],
6037 &cfg,
6038 all_ids,
6039 &mut self.sampler_scratch,
6040 self.pool.as_deref(),
6041 );
6042 ids.push(t);
6043 if i < k_spec && t == drafts[i] {
6044 all_ids.push(t);
6045 } else {
6046 break;
6047 }
6048 }
6049 all_ids.truncate(base_len);
6050 while a < k_spec && a < ids.len() && ids[a] == drafts[a] {
6051 a += 1;
6052 }
6053 ids
6056 } else if greedy_dev && dev_ids.len() == b {
6057 let ids = std::mem::take(&mut dev_ids);
6058 while a < k_spec && ids[a] == drafts[a] {
6059 a += 1;
6060 }
6061 ids
6062 } else {
6063 if logits.len() < b * lm_rows {
6064 self.clear_sequence_state();
6067 self.graph_failed
6068 .store(true, std::sync::atomic::Ordering::Relaxed);
6069 self.cancel
6070 .store(true, std::sync::atomic::Ordering::Relaxed);
6071 tracing::error!("Metal verify returned neither logits nor argmax ids");
6072 return None;
6073 }
6074 let ids: Vec<u32> = (0..b)
6075 .map(|i| sampler::argmax(&logits[i * lm_rows..(i + 1) * lm_rows]))
6076 .collect();
6077 while a < k_spec && ids[a] == drafts[a] {
6078 a += 1;
6079 }
6080 ids
6081 };
6082 spec_stamp("acc");
6083 if spec_dbg {
6084 eprintln!(
6085 "spec-dbg round: t_next {t_next} drafts {:?} verified {:?} accepted {a}",
6086 drafts, ids
6087 );
6088 }
6089 #[cfg(target_os = "macos")]
6093 let commit_ref: Option<(Vec<Vec<f32>>, Vec<(usize, Vec<f32>, Vec<f32>)>)> = if metal_native
6094 && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("2")
6095 {
6096 let snap: Vec<Vec<f32>> = self
6097 .kv_cache
6098 .layers
6099 .iter()
6100 .map(|l| l.linear_state.clone())
6101 .collect();
6102 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
6103 let toks: Vec<u32> = std::iter::once(t_next)
6104 .chain(drafts.iter().copied())
6105 .collect();
6106 let want_save = self.graph_want_logits;
6107 self.graph_want_logits = false;
6108 for (i, &t) in toks.iter().take(a + 1).enumerate() {
6109 let _ = self.forward_layers(&self.embed_single(t), next_pos + i, None);
6110 let _ = self.graph_logits.take();
6111 }
6112 self.graph_want_logits = want_save;
6113 let plain_states: Vec<Vec<f32>> = self
6114 .kv_cache
6115 .layers
6116 .iter()
6117 .map(|l| l.linear_state.clone())
6118 .collect();
6119 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
6120 let mut rows = Vec::new();
6121 for (li, (l, n0)) in self
6122 .kv_cache
6123 .layers
6124 .iter_mut()
6125 .zip(attn_lens.iter())
6126 .enumerate()
6127 {
6128 let extra = l.seq_len.saturating_sub(*n0);
6129 if extra > 0 {
6130 let mut kk = Vec::new();
6131 let mut vv = Vec::new();
6132 for g in 0..nkv {
6133 kk.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
6134 vv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
6135 }
6136 rows.push((li, kk, vv));
6137 l.truncate_last(extra);
6138 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, *n0);
6139 }
6140 }
6141 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
6142 if l.linear_state.len() == st.len() {
6143 l.linear_state.copy_from_slice(&st);
6144 } else {
6145 l.linear_state = st;
6146 }
6147 }
6148 Some((plain_states, rows))
6149 } else {
6150 None
6151 };
6152 let warm_off = std::env::var("CMF_SPEC_WARM").is_ok_and(|v| v == "0");
6153 #[cfg(target_os = "macos")]
6162 let mut warm_pending: Option<MetalWarmPending> = None;
6163 #[cfg(target_os = "macos")]
6164 if metal_native {
6165 m.kv.truncate_last(k_spec.saturating_sub(1));
6166 if self.mtp_graph_mode == Some(true) {
6167 crate::gpu_metal::kv_mirror_set_stored(
6170 self.mtp_kv_id(),
6171 Self::MTP_LAYER_BASE,
6172 m.kv.seq_len,
6173 );
6174 if !warm_off && a > 0 {
6175 let pairs: Vec<(&[f32], u32)> = (0..a)
6176 .map(|j| {
6177 (
6178 &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size],
6179 ids[j],
6180 )
6181 })
6182 .collect();
6183 warm_pending = self.mtp_warm_batch_submit(m, &pairs, next_pos);
6184 }
6185 }
6186 spec_stamp("c.wsub");
6187 }
6188 #[cfg(target_os = "macos")]
6190 if metal_native {
6191 if !self.metal_verify_commit(a) {
6194 self.clear_sequence_state();
6195 self.graph_failed
6196 .store(true, std::sync::atomic::Ordering::Relaxed);
6197 self.cancel
6198 .store(true, std::sync::atomic::Ordering::Relaxed);
6199 tracing::error!("Metal verify state/KV handoff failed after admission");
6200 return None;
6201 }
6202 if let Some((plain_states, rows)) = commit_ref {
6203 crate::gpu_metal::queue_fence();
6204 let _ = crate::gpu_metal::wait_replay();
6207 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
6208 let mut worst_s = 0f32;
6209 let mut worst_li = 0usize;
6210 for (li, (l, ps)) in self.kv_cache.layers.iter().zip(&plain_states).enumerate() {
6211 if l.linear_state.len() != ps.len() || ps.is_empty() {
6212 continue;
6213 }
6214 let d = l
6215 .linear_state
6216 .iter()
6217 .zip(ps)
6218 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
6219 let n = ps.iter().fold(0f32, |m, y| m.max(y.abs()));
6220 let rel = d / n.max(1e-6);
6221 if rel > worst_s {
6222 worst_s = rel;
6223 worst_li = li;
6224 }
6225 }
6226 let mut worst_k = 0f32;
6227 for (li, kk, vv) in &rows {
6228 let l = &self.kv_cache.layers[*li];
6229 let n0 = l.seq_len - (kk.len() / (nkv * hd));
6230 let mut ck = Vec::new();
6231 let mut cv = Vec::new();
6232 for g in 0..nkv {
6233 ck.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
6234 cv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
6235 }
6236 if ck.len() == kk.len() {
6237 let dk = ck
6238 .iter()
6239 .zip(kk)
6240 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
6241 let dv = cv
6242 .iter()
6243 .zip(vv)
6244 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
6245 worst_k = worst_k.max(dk).max(dv);
6246 } else {
6247 eprintln!(
6248 "commit-check L{li}: kv row count mismatch {} vs {}",
6249 ck.len(),
6250 kk.len()
6251 );
6252 }
6253 }
6254 eprintln!(
6255 "commit-check a={a}: worst GDN state rel-max diff {worst_s:.2e} (L{worst_li}) | worst K/V row abs diff {worst_k:.4}"
6256 );
6257 }
6258 }
6259 if !metal_native && a + 1 < b {
6260 let expected_gdn_layers = self.graph_gdn_layer_count();
6261 if expected_gdn_layers > 0
6262 && !crate::gpu::gdn_spec_restore(self.graph_kv_id, a, next_pos, expected_gdn_layers)
6263 {
6264 self.clear_sequence_state();
6265 self.graph_failed
6266 .store(true, std::sync::atomic::Ordering::Relaxed);
6267 self.cancel
6268 .store(true, std::sync::atomic::Ordering::Relaxed);
6269 tracing::error!("GDN speculative restore failed after verify");
6270 return None;
6271 }
6272 }
6273 if !metal_native && !self.rewind_trunk_graph_mirrors(next_pos + a + 1) {
6274 self.clear_sequence_state();
6279 self.graph_failed
6280 .store(true, std::sync::atomic::Ordering::Relaxed);
6281 self.cancel
6282 .store(true, std::sync::atomic::Ordering::Relaxed);
6283 tracing::error!("trunk graph KV rewind failed after speculative verify");
6284 return None;
6285 }
6286 *accepted += a;
6287 if !metal_native {
6298 m.kv.truncate_last(k_spec.saturating_sub(1));
6300 }
6301 spec_stamp("c.trunc");
6302 if !metal_native
6303 && self.mtp_graph_mode == Some(true)
6304 && !self.rewind_mtp_graph_mirror(next_pos)
6305 {
6306 self.clear_sequence_state();
6310 self.graph_failed
6311 .store(true, std::sync::atomic::Ordering::Relaxed);
6312 self.cancel
6313 .store(true, std::sync::atomic::Ordering::Relaxed);
6314 tracing::error!("MTP graph mirror rewind failed after verify commit");
6315 return None;
6316 }
6317 if !warm_off && a > 0 {
6318 let mut warmed = false;
6321 #[cfg(target_os = "macos")]
6322 if metal_native && self.mtp_graph_mode == Some(true) {
6323 warmed = match warm_pending.take() {
6327 Some(p) => self.mtp_warm_batch_finish(m, p),
6328 None => false,
6329 };
6330 if !warmed {
6331 warmed = true;
6332 for j in 0..a {
6333 let row =
6334 hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec();
6335 if self
6336 .mtp_step_metal(m, &row, ids[j], next_pos + j, false)
6337 .is_none()
6338 {
6339 warmed = false;
6340 break;
6341 }
6342 }
6343 }
6344 }
6345 if !warmed && self.mtp_graph_mode != Some(false) && !metal_native {
6346 let rows: Vec<Vec<f32>> = (0..a)
6347 .map(|j| hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec())
6348 .collect();
6349 let pairs: Vec<(&[f32], u32)> = rows
6350 .iter()
6351 .zip(ids.iter())
6352 .map(|(r, &t)| (r.as_slice(), t))
6353 .collect();
6354 match self.mtp_warm_prefill_pairs(m, &pairs, next_pos) {
6355 Ok(()) => warmed = true,
6356 Err(err) => {
6357 tracing::error!("{err}");
6363 self.clear_sequence_state();
6364 self.graph_failed
6365 .store(true, std::sync::atomic::Ordering::Relaxed);
6366 self.cancel
6367 .store(true, std::sync::atomic::Ordering::Relaxed);
6368 return None;
6369 }
6370 }
6371 }
6372 if !warmed {
6373 for j in 0..a {
6374 let row = &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size];
6375 let row = row.to_vec();
6376 self.mtp_warm(m, &row, ids[j], next_pos + j);
6377 }
6378 }
6379 }
6380 spec_stamp("c.warm");
6384 if let Some(c) = forced {
6385 self.spec_forced = Some(c);
6386 self.graph_logits = None;
6387 } else if greedy_dev && logits.is_empty() {
6388 self.spec_forced = Some(ids[a]);
6391 self.graph_logits = None;
6392 } else {
6393 let mut row = logits[a * lm_rows..(a + 1) * lm_rows].to_vec();
6394 row.resize(self.vocab_size, 0.0);
6395 if let Some(c) = self.final_softcap {
6396 for l in row.iter_mut() {
6397 *l = c * (*l / c).tanh();
6398 }
6399 }
6400 self.graph_logits = Some(row);
6401 }
6402 let new_hidden = hiddens[a * self.hidden_size..(a + 1) * self.hidden_size].to_vec();
6403 spec_stamp("c.row");
6404 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
6410 let end = subs();
6411 eprintln!(
6412 "spec-round: draft {:.1} ms/{} sub | verify {:.1} ms/{} sub | \
6413 commit {:.1} ms/{} sub (accepted {a} of {k_spec}, full-head streak {})",
6414 t_draft.as_secs_f64() * 1e3,
6415 sub_draft - sub0,
6416 (t_verify - t_draft).as_secs_f64() * 1e3,
6417 sub_verify - sub_draft,
6418 (t_round.elapsed() - t_verify).as_secs_f64() * 1e3,
6419 end - sub_verify,
6420 self.draft_full_streak,
6421 );
6422 }
6423 if k_env.is_none() && !metal_native && !k_capped {
6428 let f = a as f32 / k_spec.max(1) as f32;
6432 self.spec_acc_ewma += 0.2 * (f - self.spec_acc_ewma);
6433 let mut k_next = k_spec;
6434 if self.spec_acc_ewma >= 0.75 && k_spec < k_max {
6435 k_next = k_spec + 1;
6436 } else if self.spec_acc_ewma < 0.4 && k_spec > 2 {
6437 k_next = k_spec - 1;
6438 }
6439 if k_next != k_spec {
6440 self.spec_acc_ewma = 0.6;
6441 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
6442 eprintln!("spec-k: {k_spec} → {k_next}");
6443 }
6444 }
6445 self.spec_k_adapt = Some(k_next);
6446 }
6447 spec_stamp("end");
6448 Some((drafts[..a].to_vec(), next_pos + a + 1, new_hidden))
6449 }
6450
6451 pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
6460 if !self.pair_supported() {
6461 return (0.0, 0.0);
6462 }
6463 let graph_env = std::env::var_os("CMF_GPU_WGPU_GRAPH");
6470 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
6471 let emb1 = self.embed_single(1);
6472 let emb2 = self.embed_single(2);
6473 let pos = self.kv_cache.seq_len();
6474
6475 let t0 = std::time::Instant::now();
6476 for _ in 0..iters {
6477 let _ = self.forward_layers(&emb1, pos, None);
6478 let _ = self.forward_layers(&emb2, pos + 1, None);
6479 for l in &mut self.kv_cache.layers {
6480 l.truncate_last(2);
6481 }
6482 }
6483 let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
6484
6485 let t1 = std::time::Instant::now();
6486 for _ in 0..iters {
6487 let _ = self.forward_pair(&emb1, &emb2, pos);
6488 for l in &mut self.kv_cache.layers {
6489 l.truncate_last(2);
6490 }
6491 }
6492 let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
6493 match graph_env {
6494 Some(value) => unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", value) },
6495 None => unsafe { std::env::remove_var("CMF_GPU_WGPU_GRAPH") },
6496 }
6497 (singles_ms, pair_ms)
6498 }
6499
6500 fn pair_supported(&self) -> bool {
6508 !self.weights.layers.is_empty()
6515 && self.g3n.is_none()
6516 && !self
6517 .weights
6518 .layers
6519 .iter()
6520 .any(|lw| matches!(&lw.attn, AttnKind::Mla(_) | AttnKind::Kda(_)))
6521 }
6522
6523 fn forward_pair(
6524 &mut self,
6525 emb1: &[f32],
6526 emb2: &[f32],
6527 position: usize,
6528 ) -> (Vec<f32>, Vec<f32>) {
6529 let mut h1 = emb1.to_vec();
6530 let mut h2 = emb2.to_vec();
6531 let (_nkv, _hd, hs, _rd, eps) = (
6532 self.num_kv_heads,
6533 self.head_dim,
6534 self.hidden_size,
6535 self.rotary_dim,
6536 self.rms_eps,
6537 );
6538 let pool = self.pool.clone();
6539
6540 for li in 0..self.num_layers {
6541 let lw = &self.weights.layers[self.phys_layer(li)];
6542 inference::rms_norm_into(
6545 &h1,
6546 &lw.input_norm,
6547 self.rms_eps,
6548 self.norm_style,
6549 &mut self.ws.n1,
6550 );
6551 inference::rms_norm_into(
6552 &h2,
6553 &lw.input_norm,
6554 self.rms_eps,
6555 self.norm_style,
6556 &mut self.ws.n2,
6557 );
6558
6559 let (a1, a2) = match &lw.attn {
6560 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
6561 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
6562 AttnKind::Linear(w) => {
6563 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
6564 let layer = &mut self.kv_cache.layers[li];
6565 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
6566 vmf_phase_pair(
6567 &self.ws.n1,
6568 &self.ws.n2,
6569 w,
6570 &cfg,
6571 state,
6572 scratch,
6573 self.pool.as_deref(),
6574 )
6575 }
6576 AttnKind::LinearGdn(w) => {
6577 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
6578 let layer = &mut self.kv_cache.layers[li];
6579 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
6580 gdn_pair(
6581 &self.ws.n1,
6582 &self.ws.n2,
6583 w,
6584 &cfg,
6585 state,
6586 scratch,
6587 self.pool.as_deref(),
6588 )
6589 }
6590 AttnKind::ShortConv(w) => {
6591 let cfg = self
6592 .short_conv_cfg
6593 .expect("short-conv layer without short_conv_cfg");
6594 let layer = &mut self.kv_cache.layers[li];
6595 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
6596 short_conv_pair(
6597 &self.ws.n1,
6598 &self.ws.n2,
6599 w,
6600 &cfg,
6601 state,
6602 scratch,
6603 self.pool.as_deref(),
6604 )
6605 }
6606 AttnKind::Full {
6607 wq,
6608 wk,
6609 wv,
6610 wo,
6611 q_norm,
6612 k_norm,
6613 output_gate,
6614 softplus_gate,
6615 bias,
6616 } => {
6617 let inv_freq_l = self.layer_inv_freq(li);
6618 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
6619 let cfg = QwenAttnCfg {
6620 num_heads: self.layer_num_heads(li),
6621 num_kv_heads: nkv_l,
6622 head_dim: hd_l,
6623 hidden_size: hs,
6624 position,
6625 inv_freq: &inv_freq_l,
6626 rotary_dim: rd_l,
6627 scale: self.attn_scale,
6628 softcap: self.attn_softcap,
6629 window: self.layer_window(li),
6630 v_norm: self.attn_v_norm,
6631 qk_norm_after_rope: self.qk_norm_after_rope,
6632 q_norm: q_norm.as_deref(),
6633 k_norm: k_norm.as_deref(),
6634 output_gate: *output_gate,
6635 softplus_gate: softplus_gate
6636 .as_ref()
6637 .map(|(gate, per_head)| (gate, *per_head)),
6638 rope_scale: self.layer_rope_scale(li),
6639 bias: bias
6640 .as_ref()
6641 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
6642 rms_eps: eps,
6643 norm_style: self.norm_style,
6644 pool: pool.as_deref(),
6645 };
6646 attention::qwen_attention_pair(
6647 &self.ws.n1,
6648 &self.ws.n2,
6649 wq,
6650 wk,
6651 wv,
6652 wo,
6653 &mut self.kv_cache.layers[li],
6654 &cfg,
6655 )
6656 }
6657 };
6658 let (a1, a2) = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
6659 Some(w) => (
6660 inference::rms_norm(&a1, w, self.rms_eps, self.norm_style),
6661 inference::rms_norm(&a2, w, self.rms_eps, self.norm_style),
6662 ),
6663 None => (a1, a2),
6664 };
6665 for i in 0..self.hidden_size {
6666 h1[i] += a1[i];
6667 h2[i] += a2[i];
6668 }
6669 let (mut a1, mut a2) = (a1, a2);
6670 attention::recycle_buf(&mut a1);
6671 attention::recycle_buf(&mut a2);
6672
6673 let lw = &self.weights.layers[self.phys_layer(li)];
6674 inference::rms_norm_into(
6675 &h1,
6676 &lw.post_norm,
6677 self.rms_eps,
6678 self.norm_style,
6679 &mut self.ws.p1,
6680 );
6681 inference::rms_norm_into(
6682 &h2,
6683 &lw.post_norm,
6684 self.rms_eps,
6685 self.norm_style,
6686 &mut self.ws.p2,
6687 );
6688 let (f1, f2) = match &lw.ffn {
6689 FfnKind::DenseMoe(dm) => (
6692 dense_moe_ffn(
6693 dm,
6694 &self.ws.p1,
6695 &h1,
6696 self.rms_eps,
6697 self.norm_style,
6698 self.pool.as_deref(),
6699 ),
6700 dense_moe_ffn(
6701 dm,
6702 &self.ws.p2,
6703 &h2,
6704 self.rms_eps,
6705 self.norm_style,
6706 self.pool.as_deref(),
6707 ),
6708 ),
6709 _ => ffn_forward_pair(
6710 &lw.ffn,
6711 &self.ws.p1,
6712 &self.ws.p2,
6713 self.pool.as_deref(),
6714 None,
6715 ),
6716 };
6717 let (f1, f2) = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
6718 Some(w) => (
6719 inference::rms_norm(&f1, w, self.rms_eps, self.norm_style),
6720 inference::rms_norm(&f2, w, self.rms_eps, self.norm_style),
6721 ),
6722 None => (f1, f2),
6723 };
6724 for i in 0..self.hidden_size {
6725 h1[i] += f1[i];
6726 h2[i] += f2[i];
6727 }
6728 let (mut f1, mut f2) = (f1, f2);
6729 attention::recycle_buf(&mut f1);
6730 attention::recycle_buf(&mut f2);
6731 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
6732 for i in 0..self.hidden_size {
6733 h1[i] *= sc;
6734 h2[i] *= sc;
6735 }
6736 }
6737 if self.is_loop_end(li) && li + 1 < self.num_layers {
6739 h1 = inference::rms_norm(
6740 &h1,
6741 &self.weights.final_norm,
6742 self.rms_eps,
6743 self.norm_style,
6744 );
6745 h2 = inference::rms_norm(
6746 &h2,
6747 &self.weights.final_norm,
6748 self.rms_eps,
6749 self.norm_style,
6750 );
6751 }
6752 }
6753 if self.o1_active() {
6759 self.commit_linear_scratch();
6760 }
6761 self.o1_progress();
6762 (h1, h2)
6763 }
6764
6765 fn commit_linear_scratch(&mut self) {
6767 for layer in &mut self.kv_cache.layers {
6768 if !layer.linear_scratch.is_empty() {
6769 std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
6770 layer.linear_scratch.clear();
6771 }
6772 }
6773 }
6774
6775 pub fn forward_ids(
6778 &mut self,
6779 ids: &[u32],
6780 task_mask: Option<&TaskMask>,
6781 ) -> Result<Vec<f32>, String> {
6782 if ids.is_empty() {
6783 return Err("empty id sequence".to_string());
6784 }
6785 self.clear_sequence_state();
6786 self.check_forward_graph("forward_ids setup", 0)?;
6787 if task_mask.is_none() {
6788 self.o1_begin();
6789 }
6790 let mut hidden = vec![0.0f32; self.hidden_size];
6791 let mut pos = 0usize;
6792 if let Some(b) = &mut self.dsv41 {
6793 let pool = self.pool.clone();
6794 let mut logits = Vec::new();
6795 crate::dsv41::forward_chunk(
6796 &b.0,
6797 &b.1,
6798 &b.2,
6799 &mut b.3,
6800 ids,
6801 0,
6802 pool.as_deref(),
6803 &mut logits,
6804 );
6805 if let Err(err) = self.o1_seal_checked() {
6806 self.clear_sequence_state();
6807 return Err(err);
6808 }
6809 return Ok(logits);
6810 }
6811 if self.can_prefill_batched() && !self.graph_prefill_preferred() && ids.len() > 2 {
6819 let chunk = self.prefill_chunk();
6823 let hs = self.hidden_size;
6824 while pos < ids.len() {
6825 let end = (pos + chunk).min(ids.len());
6826 let hb = self.prefill_batch_masked(&ids[pos..end], pos, task_mask);
6827 self.check_forward_graph("forward_ids batched prefill", end - 1)?;
6828 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
6829 pos = end;
6830 }
6831 }
6832 if task_mask.is_none()
6841 && !self.graph_prefill_preferred()
6842 && !std::env::var("CMF_PAIR").is_ok_and(|v| v == "0")
6843 && self.pair_supported()
6844 {
6845 while pos + 1 < ids.len() {
6846 let e1 = self.embed_single(ids[pos]);
6847 let e2 = self.embed_single(ids[pos + 1]);
6848 let (_, h2) = self.forward_pair(&e1, &e2, pos);
6849 self.check_forward_graph("forward_ids pair", pos + 1)?;
6850 self.commit_linear_scratch();
6851 hidden = h2;
6852 pos += 2;
6853 }
6854 }
6855 while pos < ids.len() {
6856 hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
6857 self.check_forward_graph("forward_ids", pos)?;
6858 pos += 1;
6859 }
6860 if let Err(err) = self.o1_seal_checked() {
6864 self.clear_sequence_state();
6865 return Err(err);
6866 }
6867 let normed = inference::rms_norm(
6868 &hidden,
6869 &self.weights.final_norm,
6870 self.rms_eps,
6871 self.norm_style,
6872 );
6873 Ok(self.lm_head_forward(&normed))
6874 }
6875
6876 #[doc(hidden)]
6880 pub fn dsv41_serial_logits(&mut self, ids: &[u32]) -> Result<Vec<Vec<f32>>, String> {
6881 #[cfg(target_os = "macos")]
6882 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
6883 if ids.is_empty() {
6884 return Err("empty id sequence".to_string());
6885 }
6886 self.clear_sequence_state();
6887 self.dsv41
6888 .as_ref()
6889 .ok_or_else(|| "dsv41 serial logits require a DeepSeek-V4.1 model".to_string())?;
6890 self.o1_begin();
6891 let rows = {
6892 let pool = self.pool.clone();
6893 let b = self
6894 .dsv41
6895 .as_mut()
6896 .expect("dsv41 checked above; state cannot change during forward");
6897 let mut rows = Vec::with_capacity(ids.len());
6898 for (position, &id) in ids.iter().enumerate() {
6899 let mut logits = Vec::new();
6900 crate::dsv41::forward_token(
6901 &b.0,
6902 &b.1,
6903 &b.2,
6904 &mut b.3,
6905 id,
6906 position,
6907 pool.as_deref(),
6908 &mut logits,
6909 );
6910 rows.push(logits);
6911 }
6912 rows
6913 };
6914 self.o1_seal();
6915 Ok(rows)
6916 }
6917
6918 pub fn ppl_ids(&mut self, ids: &[u32]) -> Result<f64, String> {
6925 let (nll, cnt) = self.nll_ids_from(ids, 0)?;
6926 Ok((nll / cnt.max(1) as f64).exp())
6927 }
6928
6929 pub fn probe_ffn_mass(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
6934 self.clear_sequence_state();
6935 FFN_PROBE.with(|p| {
6936 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
6937 });
6938 crate::gpu::cpu_scope(|| {
6939 for (pos, &id) in ids.iter().enumerate() {
6940 let emb = self.embed_single(id);
6941 let _ = self.forward_layers(&emb, pos, None);
6942 }
6943 });
6944 self.clear_sequence_state();
6945 FFN_PROBE
6946 .with(|p| p.borrow_mut().take())
6947 .unwrap_or_default()
6948 }
6949
6950 pub fn probe_ffn_mass_batch(&mut self, ids: &[u32]) -> Result<Vec<Vec<f64>>, String> {
6954 if let Err(err) = self.nll_begin() {
6955 let _ = FFN_PROBE.with(|p| p.borrow_mut().take());
6959 self.nll_end();
6960 return Err(err);
6961 }
6962 FFN_PROBE.with(|p| {
6963 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
6964 });
6965 let result: Result<(), String> = (|| {
6966 for chunk in ids.chunks(256) {
6967 if chunk.len() < 2 {
6968 continue;
6969 }
6970 self.nll_ids_masked(chunk, 0, None)?;
6971 }
6972 Ok(())
6973 })();
6974 self.nll_end();
6975 let probe = FFN_PROBE
6976 .with(|p| p.borrow_mut().take())
6977 .unwrap_or_default();
6978 match result {
6979 Ok(()) => Ok(probe),
6980 Err(err) => {
6981 drop(probe);
6982 Err(err)
6983 }
6984 }
6985 }
6986
6987 pub fn ppl_ids_masked(&mut self, ids: &[u32], mask: &TaskMask) -> Result<f64, String> {
6991 self.nll_begin()?;
6992 let result: Result<f64, String> = (|| {
6993 let mut nll = 0f64;
6994 let mut cnt = 0usize;
6995 let mut hidden = vec![0f32; self.hidden_size];
6996 for (pos, &id) in ids.iter().enumerate() {
6997 if pos > 0 {
6998 inference::rms_norm_into(
6999 &hidden,
7000 &self.weights.final_norm,
7001 self.rms_eps,
7002 self.norm_style,
7003 &mut self.ws.n1,
7004 );
7005 let mut logits = self.lm_head_forward(&self.ws.n1);
7006 let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
7007 let sum: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum();
7008 let p = ((logits[id as usize] - max) as f64).exp() / sum.max(1e-300);
7009 nll -= p.max(1e-300).ln();
7010 cnt += 1;
7011 attention::recycle_buf(&mut logits);
7012 }
7013 let emb = self.embed_single(id);
7014 hidden = self.forward_layers(&emb, pos, Some(mask));
7015 self.nll_check_graph("masked serial forward", pos)?;
7016 let _ = self.graph_logits.take();
7020 }
7021 Ok((nll / cnt.max(1) as f64).exp())
7022 })();
7023 self.nll_end();
7024 result
7025 }
7026
7027 pub fn nll_ids_masked(
7046 &mut self,
7047 ids: &[u32],
7048 start: usize,
7049 task_mask: Option<&TaskMask>,
7050 ) -> Result<(f64, usize), String> {
7051 let task_mask = self.drop_open_mask(task_mask);
7052 self.nll_ids_inner(ids, start, task_mask)
7053 }
7054
7055 pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> Result<(f64, usize), String> {
7056 self.nll_ids_inner(ids, start, None)
7057 }
7058
7059 fn nll_ids_inner(
7060 &mut self,
7061 ids: &[u32],
7062 start: usize,
7063 task_mask: Option<&TaskMask>,
7064 ) -> Result<(f64, usize), String> {
7065 self.nll_begin()?;
7066 let result: Result<(f64, usize), String> = (|| {
7067 let mut nll = 0f64;
7068 let mut cnt = 0usize;
7069 let (graph_quality, fused_head_quality) = nll_graph_policy(
7082 task_mask.is_none(),
7083 self.graph_prefill_preferred(),
7084 crate::gpu::q1_force(),
7085 );
7086 self.graph_head_required = fused_head_quality;
7087 self.graph_want_logits = fused_head_quality;
7088 #[cfg(target_os = "macos")]
7089 if graph_quality && std::env::var("CMF_METAL_BATCH_NLL").as_deref() != Ok("0") {
7090 match self.nll_batch_metal(ids, start) {
7091 MetalBatchNllOutcome::Completed(nll, count) => {
7092 return Ok((nll, count));
7093 }
7094 MetalBatchNllOutcome::Declined => {}
7095 MetalBatchNllOutcome::Failed(err) => return Err(err),
7096 }
7097 }
7098 if self.can_prefill_batched() && !graph_quality {
7099 const CHUNK: usize = 128;
7105 const LM_SUB: usize = 32;
7106 let n = ids.len().saturating_sub(1);
7107 let hs = self.hidden_size;
7108 let rows = self.weights.lm_head.rows();
7109 let mut pos = 0usize;
7110 while pos < n {
7111 let end = (pos + CHUNK).min(n);
7112 let bsz = end - pos;
7113 let hb = self.prefill_batch_masked(&ids[pos..end], pos, task_mask);
7114 self.nll_check_graph("batched prefill", pos)?;
7115 let mut k0 = 0usize;
7116 while k0 < bsz {
7117 let k1 = (k0 + LM_SUB).min(bsz);
7118 let sb = k1 - k0;
7119 if pos + k1 <= start {
7122 k0 = k1;
7123 continue;
7124 }
7125 let mut normed = vec![0.0f32; sb * hs];
7126 for k in 0..sb {
7127 let r = inference::rms_norm(
7128 &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
7129 &self.weights.final_norm,
7130 self.rms_eps,
7131 self.norm_style,
7132 );
7133 normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
7134 }
7135 let mut logits = vec![0.0f32; sb * rows];
7136 self.weights
7137 .lm_head
7138 .matmat(&normed, sb, &mut logits, self.pool.as_deref());
7139 for k in 0..sb {
7140 if pos + k0 + k < start {
7141 continue;
7142 }
7143 self.nll_check_graph("batched score row", pos + k0 + k)?;
7144 let lg = &mut logits[k * rows..k * rows + self.vocab_size.min(rows)];
7145 if let Some(mu) = self.logit_multiplier {
7146 for v in lg.iter_mut() {
7147 *v *= mu;
7148 }
7149 }
7150 if let Some(c) = self.final_softcap {
7154 for v in lg.iter_mut() {
7155 *v = c * (*v / c).tanh();
7156 }
7157 }
7158 if let Some(cm) = self.head_clusters.clone() {
7161 self.hierarchical_head_logprobs(
7162 &normed[k * hs..(k + 1) * hs],
7163 &cm,
7164 lg,
7165 );
7166 }
7167 let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
7168 let target = ids[pos + k0 + k + 1] as usize;
7169 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
7170 let lse: f64 = lg
7171 .iter()
7172 .map(|&v| ((v - max) as f64).exp())
7173 .sum::<f64>()
7174 .ln()
7175 + max as f64;
7176 nll += lse - lg[target] as f64;
7177 cnt += 1;
7178 if std::env::var("CMF_PPL_TRACE").is_ok() {
7179 let top = lg
7180 .iter()
7181 .enumerate()
7182 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
7183 .map(|(i, _)| i)
7184 .unwrap_or(0);
7185 eprintln!(
7186 "BTRACE pos {} target {} nll {:.4} top {} lg_t {:.3} lg_top {:.3}",
7187 pos + k0 + k,
7188 target,
7189 lse - lg[target] as f64,
7190 top,
7191 lg[target],
7192 lg[top]
7193 );
7194 }
7195 }
7196 k0 = k1;
7197 }
7198 pos = end;
7199 }
7200 return Ok((nll, cnt));
7201 }
7202 for pos in 0..ids.len().saturating_sub(1) {
7203 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
7204 self.nll_check_graph("serial forward", pos)?;
7205 let out_of_band = self.graph_logits.take();
7213 if self.graph_head_required && out_of_band.is_none() {
7214 METAL_GRAPH_HEAD_MISS.fetch_add(
7215 1,
7216 std::sync::atomic::Ordering::Relaxed,
7217 );
7218 return Err(format!(
7219 "fused Metal graph head did not complete at NLL position {pos}"
7220 ));
7221 }
7222 if pos < start {
7223 continue;
7224 }
7225 let logits = match out_of_band {
7226 Some(lg) => lg,
7227 None => {
7228 let normed = inference::rms_norm(
7229 &hidden,
7230 &self.weights.final_norm,
7231 self.rms_eps,
7232 self.norm_style,
7233 );
7234 self.lm_head_forward(&normed)
7238 }
7239 };
7240 let target = ids[pos + 1] as usize;
7241 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
7242 let lse: f64 = logits
7243 .iter()
7244 .map(|&v| ((v - max) as f64).exp())
7245 .sum::<f64>()
7246 .ln()
7247 + max as f64;
7248 let tok_nll = lse - logits[target] as f64;
7249 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
7250 let top = logits
7251 .iter()
7252 .enumerate()
7253 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
7254 .map(|(i, _)| i)
7255 .unwrap_or(0);
7256 eprintln!(
7257 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
7258 logits[target], logits[top]
7259 );
7260 }
7261 nll += tok_nll;
7262 cnt += 1;
7263 }
7264 Ok((nll, cnt))
7265 })();
7266 self.nll_end();
7267 result
7268 }
7269
7270 fn nll_from_hidden(&mut self, hidden: &[f32], target: u32, pos: usize) -> f64 {
7275 let normed = inference::rms_norm(
7276 hidden,
7277 &self.weights.final_norm,
7278 self.rms_eps,
7279 self.norm_style,
7280 );
7281 let mut logits = self.lm_head_forward(&normed);
7284 let target = target as usize;
7285 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
7286 let lse: f64 = logits
7287 .iter()
7288 .map(|&v| ((v - max) as f64).exp())
7289 .sum::<f64>()
7290 .ln()
7291 + max as f64;
7292 let tok_nll = lse - logits[target] as f64;
7293 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
7294 let top = logits
7295 .iter()
7296 .enumerate()
7297 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
7298 .map(|(i, _)| i)
7299 .unwrap_or(0);
7300 eprintln!(
7301 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
7302 logits[target], logits[top]
7303 );
7304 }
7305 attention::recycle_buf(&mut logits);
7306 tok_nll
7307 }
7308
7309 pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> Result<(f64, usize), String> {
7327 self.nll_begin()?;
7332 let requested_prefix = (prefill > 0).then_some(prefill);
7333 self.o1_begin_with_prefix(requested_prefix);
7334 let n = ids.len().saturating_sub(1);
7335 let requested_start = prefill.min(n);
7336 let exact_end = if self.o1_active() {
7341 match requested_prefix {
7342 Some(requested) => self.o1_effective_boundary(requested),
7343 None => self
7344 .o1_cfg
7345 .as_ref()
7346 .and_then(|c| crate::nystrom::o1_deferred_boundary(c.w, c.sink)),
7347 }
7348 .unwrap_or(requested_start)
7349 .min(n)
7350 } else {
7351 requested_start
7352 };
7353 let mut nll = 0f64;
7354 let mut cnt = 0usize;
7355
7356 let mut pos = 0usize;
7360 if self.can_prefill_batched() {
7361 const CHUNK: usize = 128;
7362 while pos < exact_end {
7363 let end = (pos + CHUNK).min(exact_end);
7364 let hiddens = self.prefill_batch(&ids[pos..end], pos);
7365 if self
7366 .graph_failed
7367 .swap(false, std::sync::atomic::Ordering::Relaxed)
7368 {
7369 self.cancel
7370 .store(false, std::sync::atomic::Ordering::Relaxed);
7371 self.nll_end();
7372 return Err("GPU graph failed during O(1) NLL prefix".into());
7373 }
7374 for row in 0..end - pos {
7375 let score_pos = pos + row;
7376 if score_pos >= requested_start && score_pos < n {
7377 nll += self.nll_from_hidden(
7378 &hiddens[row * self.hidden_size..(row + 1) * self.hidden_size],
7379 ids[score_pos + 1],
7380 score_pos,
7381 );
7382 cnt += 1;
7383 }
7384 }
7385 pos = end;
7386 }
7387 } else {
7388 while pos < exact_end {
7389 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
7390 if self
7391 .graph_failed
7392 .swap(false, std::sync::atomic::Ordering::Relaxed)
7393 {
7394 self.cancel
7395 .store(false, std::sync::atomic::Ordering::Relaxed);
7396 self.nll_end();
7397 return Err("GPU graph failed during O(1) NLL prefix".into());
7398 }
7399 if pos >= requested_start && pos < n {
7400 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
7401 cnt += 1;
7402 }
7403 pos += 1;
7404 }
7405 }
7406 self.o1_seal_checked().map_err(|err| {
7407 self.nll_end();
7408 err
7409 })?;
7410
7411 let batch_k = std::env::var("CMF_BATCH_K")
7420 .ok()
7421 .and_then(|v| v.parse::<usize>().ok())
7422 .unwrap_or(0);
7423 let batch_admitted = batch_k > 0
7424 && self.can_prefill_batched()
7425 && self.o1_active()
7426 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
7427 && (0..self.num_layers).all(|li| {
7428 let cache = &self.kv_cache.layers[self.phys_layer(li)];
7429 cache.o1.is_none() || cache.o1_views().is_some()
7430 });
7431 if std::env::var("CMF_GRAPH_PROF").is_ok() {
7432 eprintln!(
7433 "nll-batch: phase=post-seal admission={} requested_k={} scored_rows={}",
7434 batch_admitted,
7435 batch_k,
7436 n.saturating_sub(exact_end),
7437 );
7438 }
7439 let mut batch_completed = false;
7440 if batch_admitted && exact_end < n {
7441 let hs = self.hidden_size;
7442 let mut batch_pos = exact_end;
7443 while batch_pos < n {
7444 let end = (batch_pos + batch_k).min(n);
7445 let bk = end - batch_pos;
7446 let mut hiddens = vec![0.0f32; bk * hs];
7447 for (row, &id) in ids[batch_pos..end].iter().enumerate() {
7448 hiddens[row * hs..(row + 1) * hs].copy_from_slice(&self.embed_single(id));
7449 }
7450 let positions: Vec<usize> = (batch_pos..end).collect();
7451 let t_batch = std::time::Instant::now();
7452 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
7453 if std::env::var("CMF_GRAPH_PROF").is_ok() {
7454 let ms = t_batch.elapsed().as_secs_f64() * 1000.0;
7455 eprintln!(
7456 "nll-batch: phase=post-seal mode=o1 k={bk} pos={}..{} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
7457 batch_pos,
7458 end.saturating_sub(1),
7459 bk as f64 / (ms / 1000.0),
7460 );
7461 }
7462 if let Err(err) = self.nll_check_graph("batch graph", batch_pos) {
7463 self.nll_end();
7464 return Err(err);
7465 }
7466 match outcome {
7467 crate::gpu::BatchGraphOutcome::Completed => {
7468 batch_completed = true;
7469 for row in 0..bk {
7470 nll += self.nll_from_hidden(
7471 &hiddens[row * hs..(row + 1) * hs],
7472 ids[batch_pos + row + 1],
7473 batch_pos + row,
7474 );
7475 cnt += 1;
7476 }
7477 batch_pos = end;
7478 }
7479 crate::gpu::BatchGraphOutcome::Declined => {
7480 if batch_completed {
7481 self.nll_end();
7482 return Err(format!(
7483 "O(1) NLL batch declined after completed chunk at position {batch_pos}"
7484 ));
7485 }
7486 break;
7487 }
7488 crate::gpu::BatchGraphOutcome::Failed => {
7489 self.nll_end();
7490 return Err(format!(
7491 "O(1) NLL batch graph failed after admission at position {batch_pos}"
7492 ));
7493 }
7494 }
7495 }
7496 if batch_completed && cnt == n.saturating_sub(requested_start) {
7497 self.nll_end();
7498 return Ok((nll, cnt));
7499 }
7500 }
7501
7502 for pos in exact_end..n {
7507 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
7508 if self
7509 .graph_failed
7510 .swap(false, std::sync::atomic::Ordering::Relaxed)
7511 {
7512 self.cancel
7513 .store(false, std::sync::atomic::Ordering::Relaxed);
7514 self.nll_end();
7515 return Err(format!(
7516 "GPU graph failed during O(1) NLL serial scoring at position {pos}"
7517 ));
7518 }
7519 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
7520 cnt += 1;
7521 }
7522 self.nll_end();
7523 Ok((nll, cnt))
7524 }
7525
7526 pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
7534 self.clear_sequence_state();
7535 let n = ids.len().saturating_sub(1);
7536 let mut correct = Vec::with_capacity(n);
7537 let mut pmax = Vec::with_capacity(n);
7538 for pos in 0..n {
7539 let emb = self.embed_single(ids[pos]);
7540 let hidden = self.forward_layers(&emb, pos, None);
7541 let normed = inference::rms_norm(
7542 &hidden,
7543 &self.weights.final_norm,
7544 self.rms_eps,
7545 self.norm_style,
7546 );
7547 let logits = self.lm_head_forward(&normed);
7551 let target = ids[pos + 1] as usize;
7552 let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
7553 for (i, &v) in logits.iter().enumerate() {
7554 if v > mval {
7555 mval = v;
7556 amax = i;
7557 }
7558 }
7559 correct.push(amax == target);
7560 let row: Vec<f32> = temps
7561 .iter()
7562 .map(|&t| {
7563 let tt = t.max(1e-3);
7564 let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
7565 1.0 / s.max(1e-12) })
7567 .collect();
7568 pmax.push(row);
7569 }
7570 self.clear_sequence_state();
7571 (correct, pmax)
7572 }
7573
7574 pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> Result<(f64, usize), String> {
7581 if self.dyn_router.is_none() {
7582 return Ok((self.ppl_ids(ids)?, 0));
7583 }
7584 self.nll_begin()?;
7585 let saved_active = self.dyn_active;
7586 let mut router = self
7587 .dyn_router
7588 .take()
7589 .ok_or_else(|| "dynamic router disappeared before PPL scoring".to_string())?;
7590 router.reset();
7591 self.dyn_phi_seen = 0;
7592 let _ = self.set_active_skill(None);
7593
7594 let result: Result<(f64, usize), String> = (|| {
7595 let mut nll = 0f64;
7596 let mut cnt = 0usize;
7597 for pos in 0..ids.len().saturating_sub(1) {
7598 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
7599 self.nll_check_graph("dynamic serial forward", pos)?;
7600 let out_of_band = self.graph_logits.take();
7601 let mut logits = match out_of_band {
7602 Some(lg) => lg,
7603 None => {
7604 let normed = inference::rms_norm(
7605 &hidden,
7606 &self.weights.final_norm,
7607 self.rms_eps,
7608 self.norm_style,
7609 );
7610 self.lm_head_forward(&normed)
7614 }
7615 };
7616 let target = ids[pos + 1] as usize;
7617 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
7618 let lse: f64 = logits
7619 .iter()
7620 .map(|&v| ((v - max) as f64).exp())
7621 .sum::<f64>()
7622 .ln()
7623 + max as f64;
7624 let tok_nll = lse - logits[target] as f64;
7625 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
7626 let top = logits
7627 .iter()
7628 .enumerate()
7629 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
7630 .map(|(i, _)| i)
7631 .unwrap_or(0);
7632 eprintln!(
7633 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
7634 logits[target], logits[top]
7635 );
7636 }
7637 nll += tok_nll;
7638 cnt += 1;
7639 attention::recycle_buf(&mut logits);
7640 let phi = self.dyn_phi_ema.clone();
7642 if let Some(new_active) = router.step(&phi, pos) {
7643 let _ = self.set_active_skill(new_active);
7644 }
7645 }
7646 Ok(((nll / cnt.max(1) as f64).exp(), router.switches.len()))
7647 })();
7648
7649 let _ = self.set_active_skill(saved_active);
7652 self.dyn_router = Some(router);
7653 self.nll_end();
7654 result
7655 }
7656
7657 pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
7659 self.clear_sequence_state();
7660 let mut acc = vec![0f32; self.hidden_size];
7661 for (pos, &id) in ids.iter().enumerate() {
7662 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
7663 for (a, v) in acc.iter_mut().zip(&h) {
7664 *a += v;
7665 }
7666 }
7667 let n = ids.len().max(1) as f32;
7668 for a in acc.iter_mut() {
7669 *a /= n;
7670 }
7671 self.clear_sequence_state();
7672 acc
7673 }
7674
7675 fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
7681 self.prefill_batch_masked(ids, start_pos, None)
7682 }
7683
7684 fn prefill_batch_masked(
7690 &mut self,
7691 ids: &[u32],
7692 start_pos: usize,
7693 task_mask: Option<&TaskMask>,
7694 ) -> Vec<f32> {
7695 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, usize::MAX)
7696 }
7697
7698 fn prefill_batch_span(
7704 &mut self,
7705 input: PrefillIn<'_>,
7706 start_pos: usize,
7707 task_mask: Option<&TaskMask>,
7708 from: usize,
7709 upto_excl: usize,
7710 ) -> Vec<f32> {
7711 let hs = self.hidden_size;
7712 let b = match input {
7713 PrefillIn::Ids(ids) => ids.len(),
7714 PrefillIn::Hidden(hb) => hb.len() / hs,
7715 };
7716 let upto_excl = upto_excl.min(self.num_layers);
7717 let mut h: Vec<f32>;
7721 let mut h_ready;
7722 match input {
7723 PrefillIn::Ids(_) => {
7724 h = vec![0.0; b * hs];
7725 h_ready = false;
7726 }
7727 PrefillIn::Hidden(hb) => {
7728 h = hb.to_vec();
7729 h_ready = true;
7730 }
7731 }
7732 let fill_h = |h: &mut Vec<f32>, me: &Self| {
7733 if let PrefillIn::Ids(ids) = input {
7734 for (bi, &id) in ids.iter().enumerate() {
7735 let e = me.embed_single(id);
7736 h[bi * hs..(bi + 1) * hs].copy_from_slice(&e);
7737 }
7738 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
7739 if let Ok(t) = tp.parse::<usize>() {
7740 if t >= start_pos && t < start_pos + ids.len() {
7741 let bi = t - start_pos;
7742 let row = &h[bi * hs..(bi + 1) * hs];
7743 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
7744 eprintln!(
7745 "BATCH pos {t} embed: id {} |h| = {n:.6} h0 {:.6} h1 {:.6} | b={} start={start_pos} ids[..8]={:?}",
7746 ids[bi],
7747 row[0],
7748 row[1],
7749 ids.len(),
7750 &ids[..ids.len().min(8)]
7751 );
7752 }
7753 }
7754 }
7755 }
7756 };
7757 let (_nkv, _hd, _rd, eps) = (
7758 self.num_kv_heads,
7759 self.head_dim,
7760 self.rotary_dim,
7761 self.rms_eps,
7762 );
7763 let pool = self.pool.clone();
7764 let norm_style = self.norm_style;
7765 let automatic_gpu_prefix = self.automatic_gpu_prefix();
7766
7767 #[cfg(target_os = "macos")]
7768 let mut chunk_skip_until = 0usize;
7769 for li in from..upto_excl {
7770 let _capacity_tail = automatic_gpu_prefix
7771 .filter(|&prefix| li >= prefix)
7772 .map(|_| crate::gpu::enter_cpu_scope());
7773 crate::gpu::set_layer(li as i64); #[cfg(target_os = "macos")]
7780 if task_mask.is_none() {
7781 if li < chunk_skip_until {
7782 continue;
7783 }
7784 if !h_ready && li == 0 && self.weights.embed_tokens.q8_row_parts().is_none() {
7790 fill_h(&mut h, self);
7791 h_ready = true;
7792 }
7793 let ids_for_embed = match input {
7794 PrefillIn::Ids(ids) => (!h_ready && li == 0).then_some(ids),
7795 PrefillIn::Hidden(_) => None,
7796 };
7797 let end = self.chunk_run_gpu(li, &mut h, b, start_pos, ids_for_embed, upto_excl);
7798 if end > li {
7799 h_ready = true;
7800 chunk_skip_until = end;
7801 if self.is_loop_end(end - 1) && end < self.num_layers {
7804 for bi in 0..b {
7805 let normed = inference::rms_norm(
7806 &h[bi * hs..(bi + 1) * hs],
7807 &self.weights.final_norm,
7808 eps,
7809 norm_style,
7810 );
7811 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
7812 }
7813 }
7814 continue;
7815 }
7816 }
7817 if !h_ready {
7818 fill_h(&mut h, self);
7819 h_ready = true;
7820 }
7821 let lw = &self.weights.layers[self.phys_layer(li)];
7822 match &lw.attn {
7824 AttnKind::Kda(w) => {
7825 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
7827 let mut normed = vec![0.0f32; b * hs];
7828 for bi in 0..b {
7829 inference::rms_norm_into(
7830 &h[bi * hs..(bi + 1) * hs],
7831 &lw.input_norm,
7832 eps,
7833 norm_style,
7834 &mut normed[bi * hs..(bi + 1) * hs],
7835 );
7836 }
7837 let attn = crate::linear_core::kda_forward_batch(
7838 &normed,
7839 b,
7840 w,
7841 &cfg,
7842 &mut self.kv_cache.layers[li].linear_state,
7843 pool.as_deref(),
7844 );
7845 for (dst, &a) in h.iter_mut().zip(&attn) {
7846 *dst += a;
7847 }
7848 }
7849 AttnKind::LinearGdn(w) => {
7850 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
7852 let mut normed = vec![0.0f32; b * hs];
7853 for bi in 0..b {
7854 let r = inference::rms_norm(
7855 &h[bi * hs..(bi + 1) * hs],
7856 &lw.input_norm,
7857 eps,
7858 norm_style,
7859 );
7860 normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
7861 }
7862 let attn = crate::linear_core::gdn_forward_batch(
7863 &normed,
7864 b,
7865 w,
7866 &cfg,
7867 &mut self.kv_cache.layers[li].linear_state,
7868 pool.as_deref(),
7869 );
7870 for (dst, &a) in h.iter_mut().zip(&attn) {
7871 *dst += a;
7872 }
7873 }
7874 AttnKind::ShortConv(w) => {
7875 let cfg = self
7878 .short_conv_cfg
7879 .expect("short-conv layer without short_conv_cfg");
7880 let mut normed = vec![0.0f32; b * hs];
7881 for bi in 0..b {
7882 inference::rms_norm_into(
7883 &h[bi * hs..(bi + 1) * hs],
7884 &lw.input_norm,
7885 eps,
7886 norm_style,
7887 &mut normed[bi * hs..(bi + 1) * hs],
7888 );
7889 }
7890 let attn = short_conv_forward_batch(
7891 &normed,
7892 b,
7893 w,
7894 &cfg,
7895 &mut self.kv_cache.layers[li].linear_state,
7896 pool.as_deref(),
7897 );
7898 for (dst, &a) in h.iter_mut().zip(&attn) {
7899 *dst += a;
7900 }
7901 }
7902 AttnKind::Mla(w) => {
7903 let inv_freq_l = self.layer_inv_freq(li);
7906 let rs = self.layer_rope_scale(li);
7907 let mut normed = vec![0.0f32; hs];
7908 for bi in 0..b {
7909 inference::rms_norm_into(
7910 &h[bi * hs..(bi + 1) * hs],
7911 &lw.input_norm,
7912 eps,
7913 norm_style,
7914 &mut normed,
7915 );
7916 let ao = mla_attention(
7917 w,
7918 &normed,
7919 &mut self.kv_cache.layers[li],
7920 start_pos + bi,
7921 &inv_freq_l,
7922 rs,
7923 eps,
7924 pool.as_deref(),
7925 );
7926 for (dst, &a) in h[bi * hs..(bi + 1) * hs].iter_mut().zip(&ao) {
7927 *dst += a;
7928 }
7929 }
7930 }
7931 AttnKind::Full {
7932 wq,
7933 wk,
7934 wv,
7935 wo,
7936 q_norm,
7937 k_norm,
7938 output_gate,
7939 softplus_gate,
7940 bias,
7941 } => {
7942 let mut normed = vec![0.0f32; b * hs];
7946 for bi in 0..b {
7947 inference::rms_norm_into(
7948 &h[bi * hs..(bi + 1) * hs],
7949 &lw.input_norm,
7950 eps,
7951 norm_style,
7952 &mut normed[bi * hs..(bi + 1) * hs],
7953 );
7954 }
7955 let inv_freq_l = self.layer_inv_freq(li);
7956 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
7957 let cfg = QwenAttnCfg {
7958 num_heads: self.layer_num_heads(li),
7959 num_kv_heads: nkv_l,
7960 head_dim: hd_l,
7961 hidden_size: hs,
7962 position: start_pos,
7963 inv_freq: &inv_freq_l,
7964 rotary_dim: rd_l,
7965 scale: self.attn_scale,
7966 softcap: self.attn_softcap,
7967 window: self.layer_window(li),
7968 v_norm: self.attn_v_norm,
7969 qk_norm_after_rope: self.qk_norm_after_rope,
7970 q_norm: q_norm.as_deref(),
7971 k_norm: k_norm.as_deref(),
7972 output_gate: *output_gate,
7973 softplus_gate: softplus_gate
7974 .as_ref()
7975 .map(|(gate, per_head)| (gate, *per_head)),
7976 rope_scale: self.layer_rope_scale(li),
7977 bias: bias
7978 .as_ref()
7979 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
7980 rms_eps: eps,
7981 norm_style,
7982 pool: pool.as_deref(),
7983 };
7984 let mut attn = attention::qwen_attention_batch(
7985 &normed,
7986 b,
7987 wq,
7988 wk,
7989 wv,
7990 wo,
7991 &mut self.kv_cache.layers[li],
7992 &cfg,
7993 );
7994 if let Some(w) = &lw.attn_out_norm {
7995 for bi in 0..b {
7996 inference::rms_norm_into(
7997 &attn[bi * hs..(bi + 1) * hs],
7998 w,
7999 eps,
8000 norm_style,
8001 &mut normed[bi * hs..(bi + 1) * hs],
8002 );
8003 }
8004 attn.copy_from_slice(&normed);
8005 }
8006 for (dst, &a) in h.iter_mut().zip(&attn) {
8007 *dst += a;
8008 }
8009 }
8010 AttnKind::Linear(w) => {
8011 for bi in 0..b {
8012 let normed = inference::rms_norm(
8013 &h[bi * hs..(bi + 1) * hs],
8014 &lw.input_norm,
8015 eps,
8016 norm_style,
8017 );
8018 vmf_phase_forward(
8019 &normed,
8020 w,
8021 &self.vmf_cfg.expect("linear layer without vmf_cfg"),
8022 &mut self.kv_cache.layers[li].linear_state,
8023 pool.as_deref(),
8024 )
8025 .iter()
8026 .enumerate()
8027 .for_each(|(i, &a)| h[bi * hs + i] += a);
8028 }
8029 }
8030 }
8031
8032 let lw = &self.weights.layers[self.phys_layer(li)];
8034 let mut post = vec![0.0f32; b * hs];
8035 for bi in 0..b {
8036 let r =
8037 inference::rms_norm(&h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
8038 post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
8039 }
8040 let mask_row = task_mask
8043 .filter(|m| m.ffn_active_count(li) < self.intermediate_size)
8044 .and_then(|m| m.ffn_masks.get(li))
8045 .map(|v| v.as_slice());
8046 let mut ffn = match &lw.ffn {
8047 FfnKind::Dense(d) if !d.segs.is_empty() => {
8048 tube_ffn(d, &post, b, pool.as_deref(), mask_row)
8049 }
8050 FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref(), mask_row),
8051 FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None),
8052 FfnKind::DenseMoe(dm) => {
8055 let mut out = vec![0.0f32; b * hs];
8056 for bi in 0..b {
8057 let r = dense_moe_ffn(
8058 dm,
8059 &post[bi * hs..(bi + 1) * hs],
8060 &h[bi * hs..(bi + 1) * hs],
8061 eps,
8062 norm_style,
8063 pool.as_deref(),
8064 );
8065 out[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
8066 }
8067 out
8068 }
8069 };
8070 if let Some(w) = &lw.ffn_out_norm {
8071 for bi in 0..b {
8072 inference::rms_norm_into(
8073 &ffn[bi * hs..(bi + 1) * hs],
8074 w,
8075 eps,
8076 norm_style,
8077 &mut post[bi * hs..(bi + 1) * hs],
8078 );
8079 }
8080 ffn.copy_from_slice(&post);
8081 }
8082 for (dst, &f) in h.iter_mut().zip(&ffn) {
8083 *dst += f;
8084 }
8085 if let Some(sc) = lw.layer_scale {
8086 for v in h.iter_mut() {
8087 *v *= sc;
8088 }
8089 }
8090 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
8091 if let Ok(t) = tp.parse::<usize>() {
8092 if t >= start_pos && t < start_pos + b {
8093 let bi = t - start_pos;
8094 let row = &h[bi * hs..(bi + 1) * hs];
8095 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
8096 eprintln!(
8097 "BATCH pos {t} after layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
8098 row[0], row[1]
8099 );
8100 }
8101 }
8102 }
8103 if std::env::var("CMF_DEBUG_LAYERS").is_ok() {
8107 let row = &h[(b - 1) * hs..b * hs];
8108 let rms =
8109 (row.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / hs as f64).sqrt();
8110 let mx = row.iter().fold(0f32, |m, &v| m.max(v.abs()));
8111 eprintln!(
8112 "layer {li:>3} {:>10} ffn={:<5} rms={rms:>12.4} max={mx:>12.4}",
8113 match &self.weights.layers[self.phys_layer(li)].attn {
8114 AttnKind::LinearGdn(_) => "gdn",
8115 AttnKind::Linear(_) => "vmf",
8116 AttnKind::ShortConv(_) => "conv",
8117 _ => "attn",
8118 },
8119 match &lw.ffn {
8120 FfnKind::Moe(_) => "moe",
8121 FfnKind::Dense(_) => "dense",
8122 FfnKind::DenseMoe(_) => "dense+moe",
8123 },
8124 );
8125 }
8126 if self.is_loop_end(li) && li + 1 < self.num_layers {
8128 for bi in 0..b {
8129 let normed = inference::rms_norm(
8130 &h[bi * hs..(bi + 1) * hs],
8131 &self.weights.final_norm,
8132 eps,
8133 norm_style,
8134 );
8135 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
8136 }
8137 }
8138 if std::env::var("CMF_TRACE_H").is_ok() {
8139 let n = h[..hs].iter().map(|v| v.abs()).sum::<f32>() / hs as f32;
8140 let mx = h[..hs].iter().fold(0.0f32, |a, &v| a.max(v.abs()));
8141 eprintln!(
8142 "layer {li}: mean|h|={n:.4} max|h|={mx:.2} scale={:?}",
8143 lw.layer_scale
8144 );
8145 }
8146 }
8147 crate::gpu::set_layer(-1); self.o1_progress();
8153 h
8154 }
8155
8156 fn embed_single(&self, id: u32) -> Vec<f32> {
8158 let mut out = vec![0.0f32; self.hidden_size];
8159 if (id as usize) < self.weights.embed_tokens.rows() {
8160 self.weights.embed_tokens.row_f32(id as usize, &mut out);
8161 }
8162 if self.embed_multiplier != 1.0 {
8163 for v in out.iter_mut() {
8164 *v *= self.embed_multiplier;
8165 }
8166 }
8167 if self.dsv4.is_some() || self.dsv41.is_some() || self.qwen4_exp.is_some() {
8171 let mut v = vec![0.0f32; self.hidden_size.max(1)];
8172 v[0] = id as f32;
8173 return v;
8174 }
8175 if let Some(b) = &self.g3n {
8178 return b.0.extend_embedding(id, &out, self.pool.as_deref());
8179 }
8180 out
8181 }
8182
8183 #[cfg(target_os = "macos")]
8189 fn chunk_run_gpu(
8190 &mut self,
8191 li0: usize,
8192 h: &mut [f32],
8193 b: usize,
8194 pos0: usize,
8195 embed_ids: Option<&[u32]>,
8196 cap: usize,
8197 ) -> usize {
8198 if !crate::gpu::enabled_here()
8202 || std::env::var("CMF_GPU_CHUNK")
8203 .map(|v| v == "0")
8204 .unwrap_or(false)
8205 || b < 32
8206 || self.swa.is_some()
8207 || self.global_attn.is_some()
8208 || self.o1_active()
8211 || self.attn_v_norm
8212 || (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9
8213 {
8214 return li0;
8215 }
8216 let Some(model) = self.model.clone() else {
8217 return li0;
8218 };
8219 let inv_freq = self.inv_freq.clone();
8220 let (nh, nkv, hd, hs) = (
8221 self.num_heads,
8222 self.num_kv_heads,
8223 self.head_dim,
8224 self.hidden_size,
8225 );
8226 let loop_end = if self.loop_final_norm {
8230 ((li0 / self.physical_layers) + 1) * self.physical_layers
8231 } else {
8232 self.num_layers
8233 };
8234 let mut layers: Vec<crate::gpu_metal::ChunkLayer> = Vec::new();
8235 let mut stored_at: Vec<usize> = Vec::new();
8236 for li in li0..self.num_layers.min(loop_end).min(cap) {
8237 let lw = &self.weights.layers[self.phys_layer(li)];
8238 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
8239 break;
8240 }
8241 let AttnKind::Full {
8242 wq,
8243 wk,
8244 wv,
8245 wo,
8246 q_norm,
8247 k_norm,
8248 output_gate: false,
8249 softplus_gate: None,
8250 bias,
8251 } = &lw.attn
8252 else {
8253 break;
8254 };
8255 let FfnKind::Dense(d) = &lw.ffn else { break };
8256 if d.act != Act::Silu || !d.segs.is_empty() {
8257 break;
8258 }
8259 fn cw(t: &QTensor) -> Option<(usize, usize, usize, &[f32])> {
8264 t.q8_row_parts()
8265 .or_else(|| t.q4t_parts().map(|(i, r, c)| (i, r, c, &[][..])))
8266 .or_else(|| t.q4tp_parts().map(|(i, r, c)| (i, r, c, &[][..])))
8267 }
8268 let parts = (
8269 cw(wq),
8270 cw(wk),
8271 cw(wv),
8272 cw(wo),
8273 cw(&d.gate_proj),
8274 cw(&d.up_proj),
8275 cw(&d.down_proj),
8276 );
8277 let (Some(pq), Some(pk), Some(pv), Some(po), Some(pg), Some(pu), Some(pd)) = parts
8278 else {
8279 break;
8280 };
8281 let layer = &self.kv_cache.layers[li];
8282 if layer.mode != crate::kv_cache::KvMode::F32 || layer.o1.is_some() {
8283 break;
8284 }
8285 stored_at.push(layer.head_len(0));
8286 layers.push(crate::gpu_metal::ChunkLayer {
8287 model: &model,
8288 kv_id: self.graph_kv_id,
8289 layer: li,
8290 wq: pq,
8291 wk: pk,
8292 wv: pv,
8293 wo: po,
8294 gate: pg,
8295 up: pu,
8296 down: pd,
8297 input_norm: &lw.input_norm,
8298 post_norm: &lw.post_norm,
8299 bias: bias
8300 .as_ref()
8301 .map(|(a, bb, cc)| (a.as_slice(), bb.as_slice(), cc.as_slice())),
8302 q_norm: q_norm.as_deref(),
8303 k_norm: k_norm.as_deref(),
8304 inv_freq: &inv_freq,
8305 rd: self.rotary_dim,
8306 nh,
8307 nkv,
8308 hd,
8309 hs,
8310 inter: d.gate_proj.rows(),
8311 gemma: matches!(self.norm_style, cortiq_core::NormStyle::Gemma),
8312 late_qk_norm: self.qk_norm_after_rope,
8313 eps: self.rms_eps as f32,
8314 });
8315 }
8316 if layers.is_empty() {
8317 return li0;
8318 }
8319 let row = nkv * hd;
8320 let mut store: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = stored_at
8321 .iter()
8322 .map(|&st| (vec![0f32; b * row], vec![0f32; b * row], vec![0f32; st + b]))
8323 .collect();
8324 let mut io: Vec<crate::gpu_metal::ChunkIo> = Vec::with_capacity(layers.len());
8325 for (i, (ok, ov, oi)) in store.iter_mut().enumerate() {
8326 let li = layers[i].layer;
8327 let layer = &self.kv_cache.layers[li];
8328 io.push(crate::gpu_metal::ChunkIo {
8329 cpu_stored: stored_at[i],
8330 cpu_k: (0..nkv).map(|g| layer.head_keys(g)).collect(),
8331 cpu_v: (0..nkv).map(|g| layer.head_values(g)).collect(),
8332 out_k: ok,
8333 out_v: ov,
8334 imp: oi,
8335 });
8336 }
8337 let n_run = layers.len();
8338 let last = layers.last().map(|l| l.layer + 1).unwrap_or(li0);
8339 let ep = embed_ids.and_then(|ids| {
8342 self.weights
8343 .embed_tokens
8344 .q8_row_parts()
8345 .map(|(idx, rows, _c, rs)| crate::gpu_metal::ChunkEmbed {
8346 idx,
8347 rows,
8348 row_scale: rs,
8349 ids,
8350 mult: self.embed_multiplier,
8351 })
8352 });
8353 if embed_ids.is_some() && ep.is_none() {
8354 return li0;
8355 }
8356 if !crate::gpu_metal::chunk_run_gpu(&layers, &mut io, h, b, pos0, ep.as_ref()) {
8357 return li0;
8358 }
8359 drop(io);
8360 drop(layers);
8361 for (i, (ok, ov, oi)) in store.iter().enumerate().take(n_run) {
8364 let li = li0 + i;
8365 let layer = &mut self.kv_cache.layers[li];
8366 for bi in 0..b {
8367 layer.append(
8368 &ok[bi * row..(bi + 1) * row],
8369 &ov[bi * row..(bi + 1) * row],
8370 &[],
8371 );
8372 }
8373 layer.accumulate_imp(oi);
8374 }
8375 last
8376 }
8377
8378 fn layer_is_local(&self, li: usize) -> bool {
8381 if let Some(layers) = &self.sliding_layers {
8382 return layers.get(li).copied().unwrap_or(false);
8383 }
8384 match self.swa {
8385 Some((_, pattern)) => (li + 1) % pattern.max(1) != 0,
8386 None => false,
8387 }
8388 }
8389
8390 fn layer_inv_freq(&self, li: usize) -> std::sync::Arc<Vec<f32>> {
8393 if self.layer_is_local(li) {
8394 if let Some(f) = &self.inv_freq_local {
8395 return f.clone();
8396 }
8397 } else if let Some(f) = &self.inv_freq_global {
8398 return f.clone();
8399 }
8400 self.inv_freq.clone()
8401 }
8402
8403 fn layer_window(&self, li: usize) -> Option<usize> {
8405 self.swa
8406 .and_then(|(w, _)| self.layer_is_local(li).then_some(w))
8407 }
8408
8409 fn layer_num_heads(&self, li: usize) -> usize {
8410 self.attention_heads_per_layer
8411 .as_ref()
8412 .and_then(|v| v.get(li).copied())
8413 .unwrap_or(self.num_heads)
8414 }
8415
8416 fn layer_rope_scale(&self, li: usize) -> f32 {
8417 if self.layer_is_local(li) {
8418 self.rope_scale_local
8419 } else {
8420 self.rope_scale
8421 }
8422 }
8423
8424 fn layer_geom(&self, li: usize) -> (usize, usize, usize) {
8427 if !self.layer_is_local(li) {
8428 if let Some((ghd, gkv)) = self.global_attn {
8429 return (gkv, ghd, ghd);
8430 }
8431 }
8432 (
8433 self.num_kv_heads,
8434 self.head_dim,
8435 if self.layer_is_local(li) {
8436 self.rotary_dim_local.unwrap_or(self.rotary_dim)
8437 } else {
8438 self.rotary_dim
8439 },
8440 )
8441 }
8442
8443 fn forward_layers(
8445 &mut self,
8446 hidden: &[f32],
8447 position: usize,
8448 task_mask: Option<&TaskMask>,
8449 ) -> Vec<f32> {
8450 let out = self.forward_layers_upto(hidden, position, task_mask, None);
8451 self.o1_progress();
8452 out
8453 }
8454
8455 pub fn embed_id(&self, id: u32) -> Vec<f32> {
8463 self.embed_single(id)
8464 }
8465
8466 pub fn split_supported(&self) -> Result<(), String> {
8470 if self.dsv4.is_some() {
8471 return Err(
8472 "network split: DeepSeek-V4 runs its own fused stack (not splittable yet)".into(),
8473 );
8474 }
8475 if self.dsv41.is_some() {
8476 return Err(
8477 "network split: DeepSeek-V4.1 owns the shared CED/CSA2 state (not splittable)"
8478 .into(),
8479 );
8480 }
8481 if self.qwen4_exp.is_some() {
8482 return Err(
8483 "network split: Qwen3.8-Flash-Next hyper/QSA stack is not splittable yet".into(),
8484 );
8485 }
8486 if self.g3n.is_some() {
8487 return Err(
8488 "network split: Gemma-3n runs its own AltUp stack (not splittable yet)".into(),
8489 );
8490 }
8491 Ok(())
8492 }
8493
8494 pub fn forward_span(
8499 &mut self,
8500 hidden: &[f32],
8501 position: usize,
8502 from: usize,
8503 upto: usize,
8504 task_mask: Option<&TaskMask>,
8505 ) -> Result<Vec<f32>, String> {
8506 self.split_supported()?;
8507 if from > upto || upto >= self.num_layers {
8508 return Err(format!(
8509 "forward_span: layer range {from}..={upto} outside 0..{}",
8510 self.num_layers
8511 ));
8512 }
8513 if hidden.len() != self.hidden_size {
8514 return Err(format!(
8515 "forward_span: hidden len {} ≠ hidden_size {}",
8516 hidden.len(),
8517 self.hidden_size
8518 ));
8519 }
8520 let out = self.forward_layers_span(hidden, position, task_mask, from, Some(upto));
8521 self.o1_progress();
8522 if self
8523 .graph_failed
8524 .swap(false, std::sync::atomic::Ordering::Relaxed)
8525 {
8526 self.cancel
8527 .store(false, std::sync::atomic::Ordering::Relaxed);
8528 self.clear_sequence_state();
8529 return Err("forward_span: deferred O(1) transition failed".into());
8530 }
8531 Ok(out)
8532 }
8533
8534 pub fn logits_from_hidden(&mut self, hidden: &[f32]) -> Vec<f32> {
8537 let normed = inference::rms_norm(
8538 hidden,
8539 &self.weights.final_norm,
8540 self.rms_eps,
8541 self.norm_style,
8542 );
8543 self.lm_head_forward(&normed)
8544 }
8545
8546 pub fn sample_next(&mut self, logits: &[f32], past_tokens: &[u32]) -> u32 {
8548 sampler::sample_with_scratch(
8549 logits,
8550 &self.sampler_config,
8551 past_tokens,
8552 &mut self.rng,
8553 &mut self.sampler_scratch,
8554 )
8555 }
8556
8557 pub fn reset_session(&mut self) {
8559 self.clear_sequence_state();
8560 }
8561
8562 pub fn prefill_span_ids(
8568 &mut self,
8569 ids: &[u32],
8570 start_pos: usize,
8571 upto: usize,
8572 task_mask: Option<&TaskMask>,
8573 ) -> Result<Vec<f32>, String> {
8574 self.split_supported()?;
8575 if upto >= self.num_layers {
8576 return Err(format!(
8577 "prefill_span_ids: upto {upto} outside 0..{}",
8578 self.num_layers
8579 ));
8580 }
8581 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
8585 let out =
8586 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, upto + 1);
8587 self.check_o1_progress_failure("prefill_span_ids")?;
8588 Ok(out)
8589 } else {
8590 let hs = self.hidden_size;
8591 let mut out = Vec::with_capacity(ids.len() * hs);
8592 for (i, &id) in ids.iter().enumerate() {
8593 let emb = self.embed_id(id);
8594 out.extend_from_slice(&self.forward_span(
8595 &emb,
8596 start_pos + i,
8597 0,
8598 upto,
8599 task_mask,
8600 )?);
8601 }
8602 Ok(out)
8603 }
8604 }
8605
8606 pub fn prefill_span_hidden(
8609 &mut self,
8610 hidden: &[f32],
8611 start_pos: usize,
8612 from: usize,
8613 upto: usize,
8614 task_mask: Option<&TaskMask>,
8615 ) -> Result<Vec<f32>, String> {
8616 self.split_supported()?;
8617 let hs = self.hidden_size;
8618 if hidden.is_empty() || hidden.len() % hs != 0 {
8619 return Err(format!(
8620 "prefill_span_hidden: {} floats is not a multiple of hidden {hs}",
8621 hidden.len()
8622 ));
8623 }
8624 if from > upto || upto >= self.num_layers {
8625 return Err(format!(
8626 "prefill_span_hidden: layer range {from}..={upto} outside 0..{}",
8627 self.num_layers
8628 ));
8629 }
8630 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
8631 let out = self.prefill_batch_span(
8632 PrefillIn::Hidden(hidden),
8633 start_pos,
8634 task_mask,
8635 from,
8636 upto + 1,
8637 );
8638 self.check_o1_progress_failure("prefill_span_hidden")?;
8639 Ok(out)
8640 } else {
8641 let b = hidden.len() / hs;
8642 let mut out = Vec::with_capacity(hidden.len());
8643 for i in 0..b {
8644 let h = self.forward_span(
8645 &hidden[i * hs..(i + 1) * hs],
8646 start_pos + i,
8647 from,
8648 upto,
8649 task_mask,
8650 )?;
8651 out.extend_from_slice(&h);
8652 }
8653 Ok(out)
8654 }
8655 }
8656
8657 fn try_token_graph_wgpu(
8661 &self,
8662 hidden: &[f32],
8663 position: usize,
8664 logits_out: &mut Vec<f32>,
8665 layers_run: &mut usize,
8666 ) -> Option<Result<Vec<f32>, ()>> {
8667 self.try_token_graph_wgpu_steps(
8668 hidden,
8669 position,
8670 logits_out,
8671 1,
8672 None,
8673 Some(layers_run),
8674 0,
8675 self.num_layers,
8676 )
8677 }
8678
8679 fn try_token_graph_wgpu_span(
8683 &self,
8684 hidden: &[f32],
8685 position: usize,
8686 logits_out: &mut Vec<f32>,
8687 from: usize,
8688 upto_excl: usize,
8689 layers_run: &mut usize,
8690 ) -> Option<Result<Vec<f32>, ()>> {
8691 self.try_token_graph_wgpu_steps(
8692 hidden,
8693 position,
8694 logits_out,
8695 1,
8696 None,
8697 Some(layers_run),
8698 from,
8699 upto_excl,
8700 )
8701 }
8702
8703 fn try_multi_burst(&self, t_next: u32, position: usize, k: usize) -> Option<Vec<u32>> {
8707 if self.o1_active() || self.attn_softcap > 0.0 {
8708 return None;
8709 }
8710 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
8711 if !graph_on || crate::gpu::graph_unsupported() {
8712 return None;
8719 }
8720 let emb = self.embed_single(t_next);
8721 let mut lg = Vec::new();
8722 let mut ids = Vec::new();
8723 match self.try_token_graph_wgpu_steps(
8724 &emb,
8725 position,
8726 &mut lg,
8727 k,
8728 Some(&mut ids),
8729 None,
8730 0,
8731 self.num_layers,
8732 ) {
8733 Some(Ok(_)) => {}
8734 Some(Err(())) => {
8735 self.graph_failed
8740 .store(true, std::sync::atomic::Ordering::Relaxed);
8741 return None;
8742 }
8743 None => return None,
8744 }
8745 (ids.len() == k).then_some(ids)
8746 }
8747
8748 fn try_token_graph_wgpu_steps(
8752 &self,
8753 hidden: &[f32],
8754 position: usize,
8755 logits_out: &mut Vec<f32>,
8756 steps: usize,
8757 ids_out: Option<&mut Vec<u32>>,
8758 layers_run: Option<&mut usize>,
8759 from: usize,
8760 upto_excl: usize,
8761 ) -> Option<Result<Vec<f32>, ()>> {
8762 let o1_gpu = std::env::var("CMF_O1_GPU").as_deref() == Ok("1");
8765 if (self.o1_active() && !o1_gpu) || self.attn_softcap > 0.0 {
8766 return None;
8770 }
8771 let o1_views: Vec<Option<Vec<crate::nystrom::O1DeviceView<'_>>>> = (from..upto_excl)
8776 .map(|li| {
8777 if !o1_gpu {
8778 return None;
8779 }
8780 self.kv_cache.layers[self.phys_layer(li)].o1_views()
8781 })
8782 .collect();
8783 if self.o1_active() && o1_gpu {
8784 let want: usize = (from..upto_excl)
8787 .filter(|li| self.kv_cache.layers[self.phys_layer(*li)].o1.is_some())
8788 .count();
8789 let have = o1_views.iter().filter(|v| v.is_some()).count();
8790 if want == 0 || have != want {
8791 use std::sync::atomic::{AtomicUsize, Ordering};
8801 static LAST: AtomicUsize = AtomicUsize::new(usize::MAX);
8802 let code = have * 1000 + want;
8803 if LAST.swap(code, Ordering::Relaxed) != code {
8804 tracing::warn!(
8805 "o1 graph: {have} of {want} layers sealed — per-op until all seal"
8806 );
8807 }
8808 return None;
8809 }
8810 }
8811 let nh = self.num_heads;
8812 let (nkv, hd, rd) = self.layer_geom(0);
8813 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
8814 let mut layers = Vec::with_capacity(upto_excl - from);
8815 let mut model = None;
8816 let dbg = std::env::var("CMF_GRAPH_DEBUG").is_ok();
8817 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
8818 if let Some((m, i, kind, rs)) = t
8819 .graph_weight()
8820 .or_else(|| t.graph_weight_descriptor())
8821 {
8822 let name = &m.tensors[i].name;
8823 let prism = if crate::prism::is_inverse_embedding(m, name) {
8824 crate::gpu::GraphPrismOp::InverseEmbedding
8825 } else if crate::prism::is_forward_weight(m, name) {
8826 crate::gpu::GraphPrismOp::Forward
8827 } else {
8828 crate::gpu::GraphPrismOp::None
8829 };
8830 return Some(crate::gpu::GraphW {
8831 idx: i,
8832 kind,
8833 row_scale: rs,
8834 data: &[],
8835 prism,
8836 affine: crate::prism::is_affine_target(m, name),
8837 });
8838 }
8839 match t.as_f32() {
8841 Some(d) => Some(crate::gpu::GraphW {
8842 idx: 0,
8843 kind: 4,
8844 row_scale: &[],
8845 data: d,
8846 prism: crate::gpu::GraphPrismOp::None,
8847 affine: false,
8848 }),
8849 None => {
8850 if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
8851 eprintln!("batch graph: weight has no graph/f32 representation");
8852 }
8853 None
8854 }
8855 }
8856 }
8857 for li in from..upto_excl {
8858 let lw = &self.weights.layers[self.phys_layer(li)];
8859 if dbg {
8860 let ak = match &lw.attn {
8861 AttnKind::Mla(_) => "Mla".into(),
8862 AttnKind::Full {
8863 output_gate, bias, ..
8864 } => format!("Full gate={output_gate} bias={}", bias.is_some()),
8865 AttnKind::LinearGdn(_) => "LinearGdn".into(),
8866 AttnKind::Kda(_) => "Kda".into(),
8867 AttnKind::Linear(_) => "Linear".into(),
8868 AttnKind::ShortConv(_) => "ShortConv".into(),
8869 };
8870 let fk = match &lw.ffn {
8871 FfnKind::Dense(_) => "Dense",
8872 FfnKind::Moe(_) => "Moe",
8873 FfnKind::DenseMoe(_) => "DenseMoe",
8874 };
8875 eprintln!("graph L{li}: attn={ak} ffn={fk}");
8876 }
8877 let gffn = match &lw.ffn {
8878 FfnKind::DenseMoe(_) => return None, FfnKind::Dense(d) if !d.segs.is_empty() => return None,
8882 FfnKind::Dense(d) => crate::gpu::GraphFfn::Dense {
8883 gate: gw(&d.gate_proj)?,
8884 up: gw(&d.up_proj)?,
8885 down: gw(&d.down_proj)?,
8886 },
8887 FfnKind::Moe(m) => {
8888 if m.route_tau.is_some() || m.mask.is_some() {
8896 return None;
8897 }
8898 let shared = m.shared.as_ref();
8899 let has_shared = shared.is_some();
8900 let shared_gated = matches!(shared, Some((_, Some(_))));
8901 let sgate = match shared {
8902 Some((_, Some(sg))) => gw(sg)?,
8903 _ => gw(&m.router)?,
8907 };
8908 let router = gw(&m.router)?;
8909 if router.prism != crate::gpu::GraphPrismOp::None
8915 || sgate.prism != crate::gpu::GraphPrismOp::None
8916 || router.affine
8917 || sgate.affine
8918 {
8919 tracing::warn!(
8920 "resident MoE declined: Prism/affine router or shared gate transform is not implemented"
8921 );
8922 return None;
8923 }
8924 let inter = m.experts.first()?.gate_proj.rows();
8925 let mut experts = Vec::with_capacity(m.experts.len() + 1);
8926 let mut q4tp: Option<bool> = None;
8929 let mut gu_q2: Option<bool> = None;
8932 for e in m.experts.iter().chain(shared.map(|(se, _)| se)) {
8933 if !matches!(e.act, Act::Silu)
8934 || e.gate_proj.rows() != inter
8935 || e.up_proj.rows() != inter
8936 {
8937 return None;
8938 }
8939 for expert_weight in [&e.gate_proj, &e.up_proj, &e.down_proj] {
8944 let Some((em, ei, _, _)) = expert_weight
8945 .graph_weight()
8946 .or_else(|| expert_weight.graph_weight_descriptor())
8947 else {
8948 return None;
8949 };
8950 let name = &em.tensors[ei].name;
8951 if crate::prism::is_forward_weight(em, name)
8952 || crate::prism::is_inverse_embedding(em, name)
8953 || crate::prism::is_affine_target(em, name)
8954 {
8955 tracing::warn!(
8956 "resident MoE declined: expert Prism/affine transform is not implemented"
8957 );
8958 return None;
8959 }
8960 }
8961 let (mm, gi, ui, di, is_p, is_q2) = match e.gate_proj.mapped_q4t() {
8962 Some((mm, gi)) => (
8963 mm,
8964 gi,
8965 e.up_proj.mapped_q4t()?.1,
8966 e.down_proj.mapped_q4t()?.1,
8967 false,
8968 false,
8969 ),
8970 None => match e.gate_proj.mapped_q2tp() {
8971 Some((mm, gi)) => (
8972 mm,
8973 gi,
8974 e.up_proj.mapped_q2tp()?.1,
8975 e.down_proj.mapped_q4tp()?.1,
8976 true,
8977 true,
8978 ),
8979 None => {
8980 let (mm, gi) = e.gate_proj.mapped_q4tp()?;
8981 (
8982 mm,
8983 gi,
8984 e.up_proj.mapped_q4tp()?.1,
8985 e.down_proj.mapped_q4tp()?.1,
8986 true,
8987 false,
8988 )
8989 }
8990 },
8991 };
8992 if *q4tp.get_or_insert(is_p) != is_p || *gu_q2.get_or_insert(is_q2) != is_q2
8993 {
8994 tracing::warn!(
9000 "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."
9001 );
9002 return None;
9003 }
9004 model.get_or_insert_with(|| mm.clone());
9005 experts.push((gi, ui, di));
9006 }
9007 crate::gpu::GraphFfn::Moe {
9008 router,
9009 shared_gate: sgate,
9010 experts,
9011 n_exp: m.experts.len(),
9012 top_k: std::env::var("CMF_TOPK_PROBE")
9018 .ok()
9019 .and_then(|v| v.parse::<usize>().ok())
9020 .filter(|k| *k > 0 && *k <= m.top_k)
9021 .unwrap_or(m.top_k),
9022 inter,
9023 norm_topk: m.norm_topk_prob,
9024 q4tp: q4tp?,
9025 gu_q2: gu_q2.unwrap_or(false),
9026 sigmoid: m.router_sigmoid,
9027 bias: m.expert_bias.as_deref(),
9028 has_shared,
9029 shared_gated,
9030 route_scale: m.routed_scaling,
9031 }
9032 }
9033 };
9034 let attn = match &lw.attn {
9035 AttnKind::Full {
9036 wq,
9037 wk,
9038 wv,
9039 wo,
9040 q_norm,
9041 k_norm,
9042 output_gate,
9043 softplus_gate,
9044 bias,
9045 } => {
9046 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
9047 return None;
9048 }
9049 let (m, _, _, _) = wq
9050 .graph_weight()
9051 .or_else(|| wq.graph_weight_descriptor())?;
9052 model = Some(m.clone());
9053 crate::gpu::GraphAttn::Full {
9054 wq: gw(wq)?,
9055 wk: gw(wk)?,
9056 wv: gw(wv)?,
9057 wo: gw(wo)?,
9058 q_norm: q_norm.as_deref(),
9059 k_norm: k_norm.as_deref(),
9060 late_qk_norm: self.qk_norm_after_rope,
9061 bias: bias
9062 .as_ref()
9063 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
9064 output_gate: *output_gate,
9065 cpu_k: self.kv_cache.layers[li].k_heads(),
9066 cpu_v: self.kv_cache.layers[li].v_heads(),
9067 }
9068 }
9069 AttnKind::LinearGdn(w) => {
9070 let cfg = self.gdn_cfg?;
9071 let (m, _, _, _) = w
9072 .in_proj_qkv
9073 .graph_weight()
9074 .or_else(|| w.in_proj_qkv.graph_weight_descriptor())?;
9075 model = Some(m.clone());
9076 crate::gpu::GraphAttn::Gdn {
9077 qkv: gw(&w.in_proj_qkv)?,
9078 z: gw(&w.in_proj_z)?,
9079 a: gw(&w.in_proj_a)?,
9080 b: gw(&w.in_proj_b)?,
9081 out: gw(&w.out_proj)?,
9082 conv1d: &w.conv1d,
9083 a_log: &w.a_log,
9084 dt_bias: &w.dt_bias,
9085 norm: &w.norm,
9086 nv: cfg.num_v_heads,
9087 nk: cfg.num_k_heads,
9088 dk: cfg.key_head_dim,
9089 dv: cfg.value_head_dim,
9090 kk: cfg.conv_kernel,
9091 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
9092 }
9093 }
9094 AttnKind::ShortConv(w) => {
9095 let cfg = self.short_conv_cfg?;
9096 let (m, _, _, _) = w
9097 .in_proj
9098 .graph_weight()
9099 .or_else(|| w.in_proj.graph_weight_descriptor())?;
9100 model = Some(m.clone());
9101 crate::gpu::GraphAttn::ShortConv {
9102 inp: gw(&w.in_proj)?,
9103 out: gw(&w.out_proj)?,
9104 taps: &w.conv,
9105 kernel: cfg.kernel,
9106 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
9107 }
9108 }
9109 _ => return None,
9110 };
9111 layers.push(crate::gpu::GraphLayer {
9112 input_norm: &lw.input_norm,
9113 attn,
9114 post_norm: &lw.post_norm,
9115 ffn: gffn,
9116 });
9117 }
9118 let model = model?;
9119 let lm_gw = if upto_excl == self.num_layers
9125 && self.graph_want_logits
9126 && std::env::var("CMF_GPU_LMHEAD")
9127 .map(|v| v != "0")
9128 .unwrap_or(true)
9129 {
9130 self.weights
9131 .lm_head
9132 .graph_weight()
9133 .or_else(|| self.weights.lm_head.graph_weight_descriptor())
9134 .map(|(m, i, kind, rs)| {
9135 let name = &m.tensors[i].name;
9136 let prism = if crate::prism::is_inverse_embedding(m, name) {
9137 crate::gpu::GraphPrismOp::InverseEmbedding
9138 } else if crate::prism::is_forward_weight(m, name) {
9139 crate::gpu::GraphPrismOp::Forward
9140 } else {
9141 crate::gpu::GraphPrismOp::None
9142 };
9143 (
9144 crate::gpu::GraphW {
9145 idx: i,
9146 kind,
9147 row_scale: rs,
9148 data: &[],
9149 prism,
9150 affine: crate::prism::is_affine_target(m, name),
9151 },
9152 self.weights.lm_head.rows(),
9153 )
9154 })
9155 } else {
9156 None
9157 };
9158 let lm = lm_gw.as_ref().map(|(gw, rows)| (gw, *rows));
9159 let emb_gw = if steps > 1 {
9161 self.weights
9162 .embed_tokens
9163 .graph_weight()
9164 .or_else(|| self.weights.embed_tokens.graph_weight_descriptor())
9165 .map(|(m, i, kind, rs)| {
9166 let name = &m.tensors[i].name;
9167 let prism = if crate::prism::is_inverse_embedding(m, name) {
9168 crate::gpu::GraphPrismOp::InverseEmbedding
9169 } else if crate::prism::is_forward_weight(m, name) {
9170 crate::gpu::GraphPrismOp::Forward
9171 } else {
9172 crate::gpu::GraphPrismOp::None
9173 };
9174 (
9175 crate::gpu::GraphW {
9176 idx: i,
9177 kind,
9178 row_scale: rs,
9179 data: &[],
9180 prism,
9181 affine: crate::prism::is_affine_target(m, name),
9182 },
9183 self.weights.embed_tokens.rows(),
9184 self.embed_multiplier,
9185 )
9186 })
9187 } else {
9188 None
9189 };
9190
9191 let loop_norm_at: Vec<usize> = if self.loop_final_norm {
9197 (from..upto_excl.min(self.num_layers - 1))
9198 .filter(|&li| (li + 1) % self.physical_layers == 0)
9199 .map(|li| li - from)
9200 .collect()
9201 } else {
9202 Vec::new()
9203 };
9204 let mut h = hidden.to_vec();
9205 let dump_hidden = std::env::var_os("CMF_LOGIT_DUMP").is_some();
9211 let outcome = crate::gpu::forward_token_graph(
9212 &model,
9213 self.graph_kv_id,
9214 &layers,
9215 &o1_views,
9216 self.o1_epoch,
9217 &self.inv_freq,
9218 &mut h,
9219 nh,
9220 nkv,
9221 hd,
9222 self.attn_scale,
9223 rd,
9224 self.hidden_size,
9225 self.intermediate_size,
9226 position,
9227 self.kv_cache.max_seq_len,
9228 gemma,
9229 self.rms_eps as f32,
9230 lm,
9231 &self.weights.final_norm,
9232 logits_out,
9233 &loop_norm_at,
9234 steps,
9235 emb_gw.as_ref().map(|(gw, rows, m)| (gw, *rows, *m)),
9236 ids_out,
9237 layers_run,
9238 from,
9239 dump_hidden,
9240 );
9241 match outcome {
9242 crate::gpu::TokenGraphOutcome::Completed => Some(Ok(h)),
9243 crate::gpu::TokenGraphOutcome::Failed => Some(Err(())),
9244 crate::gpu::TokenGraphOutcome::Declined => None,
9245 }
9246 }
9247
9248 #[cfg(target_os = "macos")]
9257 #[allow(clippy::type_complexity)]
9258 fn metal_rows_plan(
9259 &self,
9260 ) -> Option<(
9261 Vec<MetalRowsItem<'_>>,
9262 std::sync::Arc<cortiq_core::CmfModel>,
9263 Option<crate::gpu_metal::GdnGpuCfg>,
9264 )> {
9265 use crate::gpu_metal::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, MetalFfn};
9266 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
9267 if !graph_force
9268 || !crate::gpu::enabled_here()
9269 || std::env::var("CMF_GPU_BLOCK")
9270 .map(|v| v == "0")
9271 .unwrap_or(false)
9272 || self.attn_softcap > 0.0
9273 || self.o1_active()
9274 || self.swa.is_some()
9275 || self.global_attn.is_some()
9276 || self.attention_heads_per_layer.is_some()
9277 || self.attn_v_norm
9278 || self.loop_final_norm
9279 {
9280 return None;
9281 }
9282 let attend_contract = self.head_dim % 4 == 0
9283 && self.head_dim <= 256
9284 && self.rotary_dim >= 2
9285 && self.rotary_dim <= self.head_dim
9286 && (self.rotary_dim / 2) % 32 == 0
9287 && self.num_kv_heads > 0
9288 && self.num_heads % self.num_kv_heads == 0;
9289 if !attend_contract {
9290 return None;
9291 }
9292 let mut plan: Vec<MetalRowsItem> = Vec::new();
9293 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
9294 for li in 0..self.num_layers {
9295 let lw = &self.weights.layers[self.phys_layer(li)];
9296 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
9297 return None;
9298 }
9299 let ffn = match &lw.ffn {
9300 FfnKind::Dense(d) if d.act == Act::Silu && d.segs.is_empty() => {
9301 let (Some(g), Some(u), Some(dn)) = (
9302 d.gate_proj.metal_graph_parts(),
9303 d.up_proj.metal_graph_parts(),
9304 d.down_proj.metal_graph_parts(),
9305 ) else {
9306 return None;
9307 };
9308 MetalFfn::Dense {
9309 gate: g,
9310 up: u,
9311 down: dn,
9312 }
9313 }
9314 _ => return None,
9315 };
9316 match &lw.attn {
9317 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
9318 let (Some(qkv), Some(z), Some(a), Some(bb), Some(out)) = (
9319 w.in_proj_qkv.metal_graph_parts(),
9320 w.in_proj_z.metal_graph_parts(),
9321 w.in_proj_a.f32_parts(),
9322 w.in_proj_b.f32_parts(),
9323 w.out_proj.metal_graph_parts(),
9324 ) else {
9325 return None;
9326 };
9327 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
9328 model_ref.get_or_insert_with(|| model.clone());
9329 }
9330 let gl = GdnGpuLayer {
9331 attn_norm: &lw.input_norm,
9332 post_norm: &lw.post_norm,
9333 qkv,
9334 z,
9335 a,
9336 b: bb,
9337 out,
9338 ffn,
9339 conv1d: &w.conv1d,
9340 a_log: &w.a_log,
9341 dt_bias: &w.dt_bias,
9342 gnorm: &w.norm,
9343 };
9344 match plan.last_mut() {
9345 Some(MetalRowsItem::Gdn { run, .. }) => run.push(gl),
9346 _ => plan.push(MetalRowsItem::Gdn {
9347 run: vec![gl],
9348 first: li,
9349 }),
9350 }
9351 }
9352 AttnKind::Full {
9353 wq,
9354 wk,
9355 wv,
9356 wo,
9357 q_norm,
9358 k_norm,
9359 output_gate,
9360 softplus_gate: None,
9361 bias: None,
9362 } => {
9363 let (Some(pq), Some(pk), Some(pv), Some(po)) =
9364 (
9365 wq.metal_graph_parts(),
9366 wk.metal_graph_parts(),
9367 wv.metal_graph_parts(),
9368 wo.metal_graph_parts(),
9369 )
9370 else {
9371 return None;
9372 };
9373 if let QTensor::Mapped { model, .. } = wq {
9374 model_ref.get_or_insert_with(|| model.clone());
9375 }
9376 let cache = &self.kv_cache.layers[li];
9377 if cache.mode != crate::kv_cache::KvMode::F32 || cache.o1.is_some() {
9378 return None;
9379 }
9380 plan.push(MetalRowsItem::Attn {
9381 l: AttnGpuLayer {
9382 attn_norm: &lw.input_norm,
9383 post_norm: &lw.post_norm,
9384 wq: pq,
9385 wk: pk,
9386 wv: pv,
9387 wo: po,
9388 ffn,
9389 },
9390 li,
9391 q_norm: q_norm.as_deref(),
9392 k_norm: k_norm.as_deref(),
9393 output_gate: *output_gate,
9394 });
9395 }
9396 _ => return None,
9397 }
9398 }
9399 let model = model_ref?;
9400 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
9401 nv: cfg.num_v_heads,
9402 nk: cfg.num_k_heads,
9403 dk: cfg.key_head_dim,
9404 dv: cfg.value_head_dim,
9405 kk: cfg.conv_kernel,
9406 hidden: self.hidden_size,
9407 inter: self.intermediate_size,
9408 c_dim: cfg.conv_dim(),
9409 eps: cfg.rms_eps as f32,
9410 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
9411 });
9412 Some((plan, model, gcfg))
9413 }
9414
9415 #[cfg(target_os = "macos")]
9417 #[allow(clippy::too_many_arguments)]
9418 fn metal_attn_params<'a>(
9419 li: usize,
9420 cache: &'a crate::kv_cache::LayerKvCache,
9421 q_norm: Option<&'a [f32]>,
9422 k_norm: Option<&'a [f32]>,
9423 output_gate: bool,
9424 inv_freq: &'a [f32],
9425 geom: (usize, usize, usize, usize),
9426 pos0: usize,
9427 kv_id: u64,
9428 scale: f32,
9429 eps: f32,
9430 gemma: bool,
9431 late_qk_norm: bool,
9432 ) -> (crate::gpu_metal::AttnDeviceParams<'a>, usize) {
9433 let (nh, nkv, hd, rd) = geom;
9434 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
9435 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
9436 let cpu_stored = cpu_k[0].len() / hd;
9437 (
9438 crate::gpu_metal::AttnDeviceParams {
9439 kv_id,
9440 layer: li,
9441 nh,
9442 nkv,
9443 hd,
9444 rd,
9445 position: pos0,
9446 scale,
9447 eps,
9448 gemma,
9449 late_qk_norm,
9450 output_gate,
9451 q_norm,
9452 k_norm,
9453 inv_freq,
9454 cpu_k,
9455 cpu_v,
9456 cpu_stored,
9457 o1: None,
9458 },
9459 cpu_stored,
9460 )
9461 }
9462
9463 #[cfg(target_os = "macos")]
9468 #[allow(clippy::type_complexity)]
9469 fn metal_rows_run(
9470 &mut self,
9471 hiddens: &mut [f32],
9472 pos0: usize,
9473 b: usize,
9474 prefill: bool,
9475 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
9476 mut argmax_out: Option<(usize, &mut Vec<u32>)>,
9480 ) -> MetalRowsRun {
9481 use crate::gpu_metal::{GraphDims, VerifyGraph};
9482 if !crate::gpu_metal::wait_replay() {
9488 tracing::error!("Metal rows graph: the pending async replay failed");
9489 return MetalRowsRun::Failed;
9490 }
9491 spec_stamp("v.wait");
9492 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
9493 for l in &mut self.kv_cache.layers {
9494 if l.linear_state.len() != want && want > 0 {
9495 l.linear_state = vec![0f32; want];
9496 }
9497 }
9498 let Some((plan, model, gcfg)) = self.metal_rows_plan() else {
9499 return MetalRowsRun::Declined;
9500 };
9501 spec_stamp("v.plan");
9502 let dims = GraphDims {
9503 hidden: self.hidden_size,
9504 eps: self.rms_eps as f32,
9505 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
9506 };
9507 let Some(mut graph) = (if prefill {
9508 VerifyGraph::new_prefill(&model, dims, hiddens, b)
9509 } else {
9510 VerifyGraph::new(&model, dims, hiddens, b)
9511 }) else {
9512 return MetalRowsRun::Declined;
9513 };
9514 let geom = (
9515 self.num_heads,
9516 self.num_kv_heads,
9517 self.head_dim,
9518 self.rotary_dim,
9519 );
9520 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
9521 let eps = self.rms_eps as f32;
9522 let kv_id = self.graph_kv_id;
9523 let inv_freq = self.inv_freq.clone();
9524 for item in &plan {
9525 let ok = match item {
9526 MetalRowsItem::Gdn { run, .. } => gcfg
9527 .as_ref()
9528 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
9529 .unwrap_or(false),
9530 MetalRowsItem::Attn {
9531 l,
9532 li,
9533 q_norm,
9534 k_norm,
9535 output_gate,
9536 } => {
9537 let (p, _) = Self::metal_attn_params(
9538 *li,
9539 &self.kv_cache.layers[*li],
9540 *q_norm,
9541 *k_norm,
9542 *output_gate,
9543 &inv_freq,
9544 geom,
9545 pos0,
9546 kv_id,
9547 self.attn_scale,
9548 eps,
9549 gemma,
9550 self.qk_norm_after_rope,
9551 );
9552 graph.attn_ok(l, &p)
9553 }
9554 };
9555 if !ok {
9556 use std::sync::atomic::{AtomicBool, Ordering};
9557 static SAID: AtomicBool = AtomicBool::new(false);
9558 if !SAID.swap(true, Ordering::Relaxed) {
9559 tracing::warn!("metal rows graph: a layer failed preflight — declining");
9560 }
9561 return MetalRowsRun::Declined;
9562 }
9563 }
9564 let lm = match &spec {
9565 Some((lm, _, _)) => {
9566 if !graph.lm_head_ok(*lm) {
9567 return MetalRowsRun::Declined;
9568 }
9569 Some(*lm)
9570 }
9571 None => None,
9572 };
9573 let mut gdn_layers = Vec::new();
9574 let mut attn_layers = Vec::new();
9575 for item in &plan {
9576 match item {
9577 MetalRowsItem::Gdn { run, first } => {
9578 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
9579 .iter()
9580 .map(|l| l.linear_state.as_slice())
9581 .collect();
9582 if !graph.encode_gdn_run_b(run, &ro, gcfg.as_ref().unwrap()) {
9583 return MetalRowsRun::Declined;
9584 }
9585 gdn_layers.extend(*first..*first + run.len());
9586 }
9587 MetalRowsItem::Attn {
9588 l,
9589 li,
9590 q_norm,
9591 k_norm,
9592 output_gate,
9593 } => {
9594 let (p, cpu_stored) = Self::metal_attn_params(
9595 *li,
9596 &self.kv_cache.layers[*li],
9597 *q_norm,
9598 *k_norm,
9599 *output_gate,
9600 &inv_freq,
9601 geom,
9602 pos0,
9603 kv_id,
9604 self.attn_scale,
9605 eps,
9606 gemma,
9607 self.qk_norm_after_rope,
9608 );
9609 if !graph.encode_attn_b(l, &p) {
9610 return MetalRowsRun::Declined;
9611 }
9612 attn_layers.push((*li, cpu_stored));
9613 }
9614 }
9615 }
9616 if let (Some(lm), Some((_, final_norm, _))) = (lm, spec.as_ref()) {
9617 if !graph.encode_lm_head_b(final_norm, lm) {
9618 return MetalRowsRun::Declined;
9619 }
9620 if let Some((n, _)) = argmax_out.as_ref() {
9625 if !graph.encode_argmax_b(*n) {
9626 argmax_out = None;
9627 }
9628 }
9629 }
9630 spec_stamp("v.enc");
9631 if !graph.sync() {
9632 return MetalRowsRun::Failed;
9633 }
9634 spec_stamp("v.gpu");
9635 match (spec, argmax_out) {
9636 (Some(_), Some((_, ids))) => {
9637 ids.resize(b, 0);
9638 if !graph.read_argmax(ids) {
9639 return MetalRowsRun::Failed;
9640 }
9641 spec_stamp("v.am");
9642 }
9643 (Some((lm, _, logits)), None) => {
9644 logits.resize(b * lm.1, 0.0);
9645 if !graph.read_logits(logits) {
9646 return MetalRowsRun::Failed;
9647 }
9648 spec_stamp("v.lg");
9649 }
9650 (None, _) => {}
9651 }
9652 if !graph.read_hidden(hiddens) {
9653 return MetalRowsRun::Failed;
9654 }
9655 spec_stamp("v.hid");
9656 MetalRowsRun::Completed(MetalVerifyPending {
9657 graph,
9658 gdn_layers,
9659 attn_layers,
9660 })
9661 }
9662
9663 #[cfg(target_os = "macos")]
9669 fn try_batch_graph_metal(
9670 &mut self,
9671 hiddens: &mut [f32],
9672 positions: &[usize],
9673 b: usize,
9674 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
9675 argmax_out: Option<(usize, &mut Vec<u32>)>,
9676 ) -> crate::gpu::BatchGraphOutcome {
9677 let _t0 = std::time::Instant::now();
9678 if positions.len() != b
9679 || positions.windows(2).any(|w| w[1] != w[0] + 1)
9680 || hiddens.len() != b * self.hidden_size
9681 {
9682 return crate::gpu::BatchGraphOutcome::Declined;
9683 }
9684 let pending = match self.metal_rows_run(hiddens, positions[0], b, false, spec, argmax_out) {
9685 MetalRowsRun::Declined => return crate::gpu::BatchGraphOutcome::Declined,
9686 MetalRowsRun::Failed => return crate::gpu::BatchGraphOutcome::Failed,
9687 MetalRowsRun::Completed(pending) => pending,
9688 };
9689 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
9690 eprintln!(
9691 "metal-verify: {:.1} ms | b={b}",
9692 _t0.elapsed().as_secs_f64() * 1e3
9693 );
9694 }
9695 self.metal_verify = Some(pending);
9696 crate::gpu::BatchGraphOutcome::Completed
9697 }
9698
9699 #[cfg(target_os = "macos")]
9704 fn prefill_rows_metal(
9705 &mut self,
9706 ids: &[u32],
9707 start_pos: usize,
9708 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
9709 ) -> MetalPrefillOutcome {
9710 let b = ids.len();
9711 if b == 0 || b > 512 {
9712 return MetalPrefillOutcome::Declined;
9713 }
9714 METAL_PREFILL_CHUNKS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
9715 let with_head = spec.is_some();
9716 let hs = self.hidden_size;
9717 let mut hiddens = vec![0f32; b * hs];
9718 for (j, &id) in ids.iter().enumerate() {
9719 let e = self.embed_single(id);
9720 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&e);
9721 }
9722 let mut pending = match self.metal_rows_run(&mut hiddens, start_pos, b, true, spec, None) {
9723 MetalRowsRun::Declined => return MetalPrefillOutcome::Declined,
9724 MetalRowsRun::Failed => {
9725 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
9726 return MetalPrefillOutcome::Failed;
9727 }
9728 MetalRowsRun::Completed(pending) => pending,
9729 };
9730 let idxs = pending.gdn_layers.clone();
9732 let mut outs: Vec<&mut [f32]> = self
9733 .kv_cache
9734 .layers
9735 .iter_mut()
9736 .enumerate()
9737 .filter(|(i, _)| idxs.binary_search(i).is_ok())
9738 .map(|(_, l)| l.linear_state.as_mut_slice())
9739 .collect();
9740 if !pending.graph.finish_states(&mut outs) {
9741 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
9742 return MetalPrefillOutcome::Failed;
9743 }
9744 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
9745 let mut rows = Vec::with_capacity(pending.attn_layers.len());
9749 for (li, cpu_stored) in &pending.attn_layers {
9750 let mut kbuf = vec![0f32; b * nkv * hd];
9751 let mut vbuf = vec![0f32; b * nkv * hd];
9752 if !crate::gpu_metal::kv_mirror_read_rows(
9753 self.graph_kv_id,
9754 *li,
9755 nkv,
9756 hd,
9757 *cpu_stored,
9758 b,
9759 &mut kbuf,
9760 &mut vbuf,
9761 ) {
9762 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
9763 return MetalPrefillOutcome::Failed;
9764 }
9765 rows.push((*li, *cpu_stored, kbuf, vbuf));
9766 }
9767 for (li, cpu_stored, kbuf, vbuf) in rows {
9768 let cache = &mut self.kv_cache.layers[li];
9769 for r in 0..b {
9770 cache.append(
9771 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
9772 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
9773 &[],
9774 );
9775 }
9776 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + b);
9777 }
9778 METAL_PREFILL_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
9779 if with_head {
9780 METAL_PREFILL_HEAD_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
9781 }
9782 MetalPrefillOutcome::Completed(hiddens)
9783 }
9784
9785 #[cfg(target_os = "macos")]
9786 fn prefill_batch_metal(&mut self, ids: &[u32], start_pos: usize) -> MetalPrefillOutcome {
9787 self.prefill_rows_metal(ids, start_pos, None)
9788 }
9789
9790 #[cfg(target_os = "macos")]
9795 fn nll_batch_metal(&mut self, ids: &[u32], start: usize) -> MetalBatchNllOutcome {
9796 if ids.len() < 2 || self.o1_active() || self.head_clusters.is_some() {
9797 return MetalBatchNllOutcome::Declined;
9798 }
9799 let Some(lm) = self.weights.lm_head.metal_graph_parts() else {
9800 return MetalBatchNllOutcome::Declined;
9801 };
9802 let chunk = std::env::var("CMF_METAL_PREFILL_CHUNK")
9803 .ok()
9804 .and_then(|v| v.parse::<usize>().ok())
9805 .filter(|&v| (1..=512).contains(&v))
9806 .unwrap_or(32);
9807 let final_norm = self.weights.final_norm.clone();
9808 let mut nll = 0.0f64;
9809 let mut count = 0usize;
9810 let mut pos = 0usize;
9811 let mut completed = 0usize;
9812 while pos < ids.len() {
9813 let end = (pos + chunk).min(ids.len());
9814 let mut logits = Vec::new();
9815 let outcome = self.prefill_rows_metal(
9816 &ids[pos..end],
9817 pos,
9818 Some((lm, &final_norm, &mut logits)),
9819 );
9820 match outcome {
9821 MetalPrefillOutcome::Declined => {
9822 return if completed == 0 {
9823 MetalBatchNllOutcome::Declined
9824 } else {
9825 MetalBatchNllOutcome::Failed(format!(
9826 "ordinary Metal NLL batch declined after {completed} chunks"
9827 ))
9828 };
9829 }
9830 MetalPrefillOutcome::Failed => {
9831 return MetalBatchNllOutcome::Failed(
9832 "ordinary Metal NLL batch failed after admission".to_string(),
9833 );
9834 }
9835 MetalPrefillOutcome::Completed(_) => {}
9836 }
9837 completed += 1;
9838 let vocab = self.vocab_size.min(lm.1);
9839 if logits.len() != (end - pos) * lm.1 || vocab == 0 {
9840 return MetalBatchNllOutcome::Failed(
9841 "ordinary Metal NLL head returned an invalid shape".to_string(),
9842 );
9843 }
9844 for row in 0..(end - pos) {
9845 let absolute = pos + row;
9846 if absolute < start || absolute + 1 >= ids.len() {
9847 continue;
9848 }
9849 let lg = &mut logits[row * lm.1..row * lm.1 + vocab];
9850 if let Some(mu) = self.logit_multiplier {
9851 for v in lg.iter_mut() {
9852 *v *= mu;
9853 }
9854 }
9855 if let Some(c) = self.final_softcap {
9856 for v in lg.iter_mut() {
9857 *v = c * (*v / c).tanh();
9858 }
9859 }
9860 let target = ids[absolute + 1] as usize;
9861 if target >= vocab {
9862 return MetalBatchNllOutcome::Failed(format!(
9863 "target token {target} exceeds Metal head rows {vocab}"
9864 ));
9865 }
9866 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
9867 let lse: f64 = lg
9868 .iter()
9869 .map(|&v| ((v - max) as f64).exp())
9870 .sum::<f64>()
9871 .ln()
9872 + max as f64;
9873 nll += lse - lg[target] as f64;
9874 count += 1;
9875 }
9876 pos = end;
9877 }
9878 MetalBatchNllOutcome::Completed(nll, count)
9879 }
9880
9881 #[cfg(target_os = "macos")]
9885 fn metal_verify_commit(&mut self, a: usize) -> bool {
9886 let Some(mut pending) = self.metal_verify.take() else {
9887 return false;
9888 };
9889 let n = a + 1;
9890 let idxs = pending.gdn_layers.clone();
9892 let mut outs: Vec<&mut [f32]> = self
9893 .kv_cache
9894 .layers
9895 .iter_mut()
9896 .enumerate()
9897 .filter(|(i, _)| idxs.binary_search(i).is_ok())
9898 .map(|(_, l)| l.linear_state.as_mut_slice())
9899 .collect();
9900 if !pending.graph.commit(n, &mut outs) {
9901 return false;
9902 }
9903 spec_stamp("c.replay");
9904 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
9905 let mut rows = Vec::with_capacity(pending.attn_layers.len());
9909 for (li, cpu_stored) in &pending.attn_layers {
9910 let mut kbuf = vec![0f32; n * nkv * hd];
9911 let mut vbuf = vec![0f32; n * nkv * hd];
9912 if !crate::gpu_metal::kv_mirror_read_rows(
9913 self.graph_kv_id,
9914 *li,
9915 nkv,
9916 hd,
9917 *cpu_stored,
9918 n,
9919 &mut kbuf,
9920 &mut vbuf,
9921 ) {
9922 return false;
9923 }
9924 rows.push((*li, *cpu_stored, kbuf, vbuf));
9925 }
9926 for (li, cpu_stored, kbuf, vbuf) in rows {
9927 let cache = &mut self.kv_cache.layers[li];
9928 for r in 0..n {
9929 cache.append(
9930 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
9931 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
9932 &[],
9933 );
9934 }
9935 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + n);
9936 }
9937 spec_stamp("c.kv");
9938 true
9939 }
9940
9941 #[cfg(target_os = "macos")]
9948 fn mtp_warm_batch_submit(
9949 &mut self,
9950 m: &mut MtpModule,
9951 pairs: &[(&[f32], u32)],
9952 first_pos: usize,
9953 ) -> Option<MetalWarmPending> {
9954 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, VerifyGraph};
9955 let b = pairs.len();
9956 if b == 0 || b > 512 || m.kv.mode != crate::kv_cache::KvMode::F32 || m.kv.o1.is_some() {
9957 return None;
9958 }
9959 let AttnKind::Full {
9960 wq,
9961 wk,
9962 wv,
9963 wo,
9964 q_norm,
9965 k_norm,
9966 output_gate,
9967 softplus_gate: None,
9968 bias: None,
9969 } = &m.layer.attn
9970 else {
9971 return None;
9972 };
9973 let FfnKind::Dense(d) = &m.layer.ffn else {
9974 return None;
9975 };
9976 if !d.segs.is_empty() {
9977 return None;
9978 }
9979 let (Some(pq), Some(pk), Some(pv), Some(po)) =
9980 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
9981 else {
9982 return None;
9983 };
9984 let (Some(g), Some(u), Some(dn)) = (
9985 d.gate_proj.q1_parts(),
9986 d.up_proj.q1_parts(),
9987 d.down_proj.q1_parts(),
9988 ) else {
9989 return None;
9990 };
9991 let Some(eh) = m.eh_proj.q1_parts() else {
9992 return None;
9993 };
9994 let QTensor::Mapped { model, .. } = wq else {
9995 return None;
9996 };
9997 let model = model.clone();
9998 let hs = self.hidden_size;
9999 let mut cat = vec![0f32; b * 2 * hs];
10001 for (j, (h, tok)) in pairs.iter().enumerate() {
10002 let e = self.embed_single(*tok);
10003 let (ce, ch) = cat[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
10004 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, ce);
10005 inference::rms_norm_into(h, &m.hnorm, self.rms_eps, self.norm_style, ch);
10006 }
10007 let dims = GraphDims {
10008 hidden: hs,
10009 eps: self.rms_eps as f32,
10010 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
10011 };
10012 spec_stamp("w.cat");
10013 let Some(mut graph) = VerifyGraph::new_via_proj(&model, dims, eh, &cat, b) else {
10014 return None;
10015 };
10016 spec_stamp("w.new");
10017 let l = AttnGpuLayer {
10018 attn_norm: &m.layer.input_norm,
10019 post_norm: &m.layer.post_norm,
10020 wq: pq,
10021 wk: pk,
10022 wv: pv,
10023 wo: po,
10024 ffn: MetalFfn::Dense {
10025 gate: g,
10026 up: u,
10027 down: dn,
10028 },
10029 };
10030 let (nh, nkv, hd, rd) = (
10031 self.num_heads,
10032 self.num_kv_heads,
10033 self.head_dim,
10034 self.rotary_dim,
10035 );
10036 let inv_freq = self.inv_freq.clone();
10037 let cpu_stored;
10038 {
10039 let cache = &m.kv;
10040 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
10041 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
10042 cpu_stored = cpu_k[0].len() / hd;
10043 if cpu_stored > first_pos {
10048 spec_stamp("w.decl");
10049 return None;
10050 }
10051 let p = AttnDeviceParams {
10052 kv_id: self.mtp_kv_id(),
10053 layer: Self::MTP_LAYER_BASE,
10054 nh,
10055 nkv,
10056 hd,
10057 rd,
10058 position: first_pos,
10059 scale: self.attn_scale,
10060 eps: self.rms_eps as f32,
10061 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
10062 late_qk_norm: self.qk_norm_after_rope,
10063 output_gate: *output_gate,
10064 q_norm: q_norm.as_deref(),
10065 k_norm: k_norm.as_deref(),
10066 inv_freq: &inv_freq,
10067 cpu_k,
10068 cpu_v,
10069 cpu_stored,
10070 o1: None,
10071 };
10072 if !graph.attn_ok(&l, &p) || !graph.encode_attn_b(&l, &p) {
10073 return None;
10074 }
10075 }
10076 spec_stamp("w.enc");
10077 if !graph.submit() {
10078 return None;
10079 }
10080 spec_stamp("w.sub");
10081 Some(MetalWarmPending {
10082 graph,
10083 cpu_stored,
10084 b,
10085 })
10086 }
10087
10088 #[cfg(target_os = "macos")]
10091 fn mtp_warm_batch_metal(
10092 &mut self,
10093 m: &mut MtpModule,
10094 pairs: &[(&[f32], u32)],
10095 first_pos: usize,
10096 ) -> bool {
10097 match self.mtp_warm_batch_submit(m, pairs, first_pos) {
10098 Some(p) => self.mtp_warm_batch_finish(m, p),
10099 None => false,
10100 }
10101 }
10102
10103 #[cfg(target_os = "macos")]
10108 fn mtp_warm_batch_finish(&mut self, m: &mut MtpModule, pending: MetalWarmPending) -> bool {
10109 let MetalWarmPending {
10110 mut graph,
10111 cpu_stored,
10112 b,
10113 } = pending;
10114 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
10115 if !graph.sync() {
10116 return false;
10117 }
10118 spec_stamp("w.gpu");
10119 let mut kbuf = vec![0f32; b * nkv * hd];
10120 let mut vbuf = vec![0f32; b * nkv * hd];
10121 if !crate::gpu_metal::kv_mirror_read_rows(
10122 self.mtp_kv_id(),
10123 Self::MTP_LAYER_BASE,
10124 nkv,
10125 hd,
10126 cpu_stored,
10127 b,
10128 &mut kbuf,
10129 &mut vbuf,
10130 ) {
10131 return false;
10132 }
10133 for r in 0..b {
10134 m.kv.append(
10135 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
10136 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
10137 &[],
10138 );
10139 }
10140 crate::gpu_metal::kv_mirror_set_stored(
10141 self.mtp_kv_id(),
10142 Self::MTP_LAYER_BASE,
10143 cpu_stored + b,
10144 );
10145 spec_stamp("w.kv");
10146 true
10147 }
10148
10149 pub(crate) fn note_draft_id(&mut self, id: u32) {
10156 let cut = Self::draft_vocab_rows(usize::MAX).max(131_072);
10157 if (id as usize) >= cut {
10158 self.draft_full_streak = 16;
10159 } else {
10160 self.draft_full_streak = self.draft_full_streak.saturating_sub(1);
10161 }
10162 }
10163
10164 fn draft_head_rows(&self, head_rows: usize) -> usize {
10167 if self.draft_full_streak > 0 {
10168 head_rows
10169 } else {
10170 Self::draft_vocab_rows(head_rows)
10171 }
10172 }
10173
10174 fn draft_vocab_rows(head_rows: usize) -> usize {
10177 static N: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
10178 let n = *N.get_or_init(|| {
10179 std::env::var("CMF_DRAFT_VOCAB")
10180 .ok()
10181 .and_then(|v| v.parse().ok())
10182 .unwrap_or(65536)
10183 });
10184 if n == 0 { head_rows } else { n.min(head_rows) }
10185 }
10186
10187 #[cfg(target_os = "macos")]
10192 fn mtp_step_metal(
10193 &mut self,
10194 m: &mut MtpModule,
10195 hidden: &[f32],
10196 next_token: u32,
10197 position: usize,
10198 want_logits: bool,
10199 ) -> Option<(Vec<f32>, Vec<f32>)> {
10200 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
10201 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
10202 || !crate::gpu::q1_force()
10203 || !crate::gpu::enabled_here()
10204 || self.attn_softcap > 0.0
10205 || self.attention_heads_per_layer.is_some()
10206 || m.kv.mode != crate::kv_cache::KvMode::F32
10207 || m.kv.o1.is_some()
10208 {
10209 return None;
10210 }
10211 let AttnKind::Full {
10212 wq,
10213 wk,
10214 wv,
10215 wo,
10216 q_norm,
10217 k_norm,
10218 output_gate,
10219 softplus_gate: None,
10220 bias: None,
10221 } = &m.layer.attn
10222 else {
10223 return None;
10224 };
10225 let FfnKind::Dense(d) = &m.layer.ffn else {
10226 return None;
10227 };
10228 if d.act != Act::Silu || !d.segs.is_empty() {
10229 return None;
10230 }
10231 let (pq, pk, pv, po) = (
10232 wq.q1_parts()?,
10233 wk.q1_parts()?,
10234 wv.q1_parts()?,
10235 wo.q1_parts()?,
10236 );
10237 let (g, u, dn) = (
10238 d.gate_proj.q1_parts()?,
10239 d.up_proj.q1_parts()?,
10240 d.down_proj.q1_parts()?,
10241 );
10242 let QTensor::Mapped { model, .. } = wq else {
10243 return None;
10244 };
10245 let model = model.clone();
10246 let lm = if want_logits {
10247 Some(self.weights.lm_head.q1_parts()?)
10248 } else {
10249 None
10250 };
10251 let dims = GraphDims {
10252 hidden: self.hidden_size,
10253 eps: self.rms_eps as f32,
10254 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
10255 };
10256 let hs = self.hidden_size;
10259 let mut x = vec![0f32; hs];
10260 let mut graph = TokenGraph::new(&model, dims, &x)?;
10261 let mut folded = false;
10262 if let Some(eh) = m.eh_proj.q1_parts() {
10263 let e = self.embed_single(next_token);
10264 let mut cat = vec![0.0f32; 2 * hs];
10265 let (cat_e, cat_h) = cat.split_at_mut(hs);
10266 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
10267 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
10268 folded = graph.encode_input_proj(eh, &cat);
10269 }
10270 if !folded {
10271 x = self.mtp_block_input(m, hidden, next_token);
10272 graph = TokenGraph::new(&model, dims, &x)?;
10273 }
10274 spec_stamp("d.in");
10275 let l = AttnGpuLayer {
10276 attn_norm: &m.layer.input_norm,
10277 post_norm: &m.layer.post_norm,
10278 wq: pq,
10279 wk: pk,
10280 wv: pv,
10281 wo: po,
10282 ffn: MetalFfn::Dense {
10283 gate: g,
10284 up: u,
10285 down: dn,
10286 },
10287 };
10288 let (nh, nkv, hd, rd) = (
10289 self.num_heads,
10290 self.num_kv_heads,
10291 self.head_dim,
10292 self.rotary_dim,
10293 );
10294 let inv_freq = self.inv_freq.clone();
10295 {
10296 let cache = &m.kv;
10297 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
10298 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
10299 let cpu_stored = cpu_k[0].len() / hd;
10300 let p = AttnDeviceParams {
10301 kv_id: self.mtp_kv_id(),
10302 layer: Self::MTP_LAYER_BASE,
10303 nh,
10304 nkv,
10305 hd,
10306 rd,
10307 position,
10308 scale: self.attn_scale,
10309 eps: self.rms_eps as f32,
10310 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
10311 late_qk_norm: self.qk_norm_after_rope,
10312 output_gate: *output_gate,
10313 q_norm: q_norm.as_deref(),
10314 k_norm: k_norm.as_deref(),
10315 inv_freq: &inv_freq,
10316 cpu_k,
10317 cpu_v,
10318 cpu_stored,
10319 o1: None,
10320 };
10321 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
10322 return None;
10323 }
10324 }
10325 let draft_rows = if let Some(lm) = lm {
10331 self.draft_head_rows(lm.1)
10332 } else {
10333 0
10334 };
10335 if let Some(lm) = lm {
10336 if !graph.lm_head_ok(lm) {
10337 return None;
10338 }
10339 if draft_rows < lm.1 {
10340 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
10341 return None;
10342 }
10343 } else {
10344 graph.encode_lm_head(&m.final_norm, lm);
10345 }
10346 }
10347 spec_stamp("d.enc");
10348 if graph.sync_checked().is_err() {
10349 return None;
10350 }
10351 spec_stamp("d.gpu");
10352 let mut logits = Vec::new();
10353 if let Some(lm) = lm {
10354 let n_read = draft_rows.min(lm.1).min(self.vocab_size);
10355 logits = attention::take_buf(n_read);
10356 graph.read_logits(&mut logits);
10357 logits.resize(self.vocab_size, f32::NEG_INFINITY);
10359 }
10360 graph.finish(&mut x);
10361 let mut krow = attention::take_buf(nkv * hd);
10362 let mut vrow = attention::take_buf(nkv * hd);
10363 if crate::gpu_metal::kv_mirror_read_last(
10364 self.mtp_kv_id(),
10365 Self::MTP_LAYER_BASE,
10366 nkv,
10367 hd,
10368 &mut krow,
10369 &mut vrow,
10370 ) {
10371 m.kv.append(&krow, &vrow, &[]);
10372 }
10373 attention::recycle_buf(&mut krow);
10374 attention::recycle_buf(&mut vrow);
10375 spec_stamp("d.rd");
10376 Some((logits, x))
10377 }
10378
10379 fn mtp_chain_on() -> bool {
10392 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
10393 *ON.get_or_init(|| std::env::var("CMF_MTP_CHAIN").as_deref() != Ok("0"))
10394 }
10395
10396 #[cfg(target_os = "macos")]
10408 fn mtp_draft_chain_metal(
10409 &mut self,
10410 m: &mut MtpModule,
10411 hidden: &[f32],
10412 t_next: u32,
10413 position: usize,
10414 k: usize,
10415 ) -> Result<Vec<u32>, bool> {
10416 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
10417 if k == 0
10418 || k > 64
10419 || !Self::mtp_chain_on()
10420 || std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
10421 || !crate::gpu::q1_force()
10422 || !crate::gpu::enabled_here()
10423 || self.attn_softcap > 0.0
10424 || self.attention_heads_per_layer.is_some()
10425 || m.kv.mode != crate::kv_cache::KvMode::F32
10426 || m.kv.o1.is_some()
10427 || self.dsv4.is_some()
10429 || self.dsv41.is_some()
10430 || self.qwen4_exp.is_some()
10431 || self.g3n.is_some()
10432 {
10433 return Err(false);
10434 }
10435 let AttnKind::Full {
10436 wq,
10437 wk,
10438 wv,
10439 wo,
10440 q_norm,
10441 k_norm,
10442 output_gate,
10443 softplus_gate: None,
10444 bias: None,
10445 } = &m.layer.attn
10446 else {
10447 return Err(false);
10448 };
10449 let FfnKind::Dense(d) = &m.layer.ffn else {
10450 return Err(false);
10451 };
10452 if d.act != Act::Silu || !d.segs.is_empty() {
10453 return Err(false);
10454 }
10455 let (Some(pq), Some(pk), Some(pv), Some(po)) =
10456 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
10457 else {
10458 return Err(false);
10459 };
10460 let (Some(g), Some(u), Some(dn)) = (
10461 d.gate_proj.q1_parts(),
10462 d.up_proj.q1_parts(),
10463 d.down_proj.q1_parts(),
10464 ) else {
10465 return Err(false);
10466 };
10467 let (Some(eh), Some(lm)) = (m.eh_proj.q1_parts(), self.weights.lm_head.q1_parts()) else {
10468 return Err(false);
10469 };
10470 let QTensor::Mapped { model, .. } = wq else {
10471 return Err(false);
10472 };
10473 let model = model.clone();
10474 let QTensor::Mapped {
10477 model: em,
10478 idx: eidx,
10479 dtype: cortiq_core::TensorDtype::Q4TiledP,
10480 ..
10481 } = &self.weights.embed_tokens
10482 else {
10483 return Err(false);
10484 };
10485 if !std::sync::Arc::ptr_eq(em, &model)
10486 || crate::prism::is_inverse_embedding(&model, &model.tensors[*eidx].name)
10487 {
10488 return Err(false);
10489 }
10490 let embed = (
10491 *eidx,
10492 self.weights.embed_tokens.rows(),
10493 self.weights.embed_tokens.cols(),
10494 );
10495 if embed.2 != self.hidden_size || hidden.len() != self.hidden_size {
10496 return Err(false);
10497 }
10498 let dims = GraphDims {
10499 hidden: self.hidden_size,
10500 eps: self.rms_eps as f32,
10501 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
10502 };
10503 let Some(mut graph) = TokenGraph::new(&model, dims, hidden) else {
10504 return Err(false);
10505 };
10506 if !graph.chain_embed_ok(embed) || !graph.lm_head_ok(lm) {
10507 return Err(false);
10508 }
10509 let l = AttnGpuLayer {
10510 attn_norm: &m.layer.input_norm,
10511 post_norm: &m.layer.post_norm,
10512 wq: pq,
10513 wk: pk,
10514 wv: pv,
10515 wo: po,
10516 ffn: MetalFfn::Dense {
10517 gate: g,
10518 up: u,
10519 down: dn,
10520 },
10521 };
10522 let (nh, nkv, hd, rd) = (
10523 self.num_heads,
10524 self.num_kv_heads,
10525 self.head_dim,
10526 self.rotary_dim,
10527 );
10528 let inv_freq = self.inv_freq.clone();
10529 let draft_rows = self.draft_head_rows(lm.1);
10530 let n_arg = draft_rows.min(lm.1).min(self.vocab_size);
10531 if n_arg == 0 {
10532 return Err(false);
10533 }
10534 let split = std::env::var("CMF_MTP_CHAIN_SPLIT").as_deref() == Ok("1");
10541 let t_chain = std::time::Instant::now();
10542 graph.chain_ids_init(t_next, k);
10543 let cpu_stored;
10544 {
10545 let cache = &m.kv;
10546 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
10547 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
10548 cpu_stored = cpu_k[0].len() / hd;
10549 for j in 0..k {
10550 if !graph.encode_chain_input(
10551 embed,
10552 j as u32,
10553 &m.enorm,
10554 &m.hnorm,
10555 self.embed_multiplier,
10556 eh,
10557 ) {
10558 return Err(false);
10559 }
10560 let p = AttnDeviceParams {
10564 kv_id: self.mtp_kv_id(),
10565 layer: Self::MTP_LAYER_BASE,
10566 nh,
10567 nkv,
10568 hd,
10569 rd,
10570 position: position + j,
10571 scale: self.attn_scale,
10572 eps: self.rms_eps as f32,
10573 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
10574 late_qk_norm: self.qk_norm_after_rope,
10575 output_gate: *output_gate,
10576 q_norm: q_norm.as_deref(),
10577 k_norm: k_norm.as_deref(),
10578 inv_freq: &inv_freq,
10579 cpu_k: cpu_k.clone(),
10580 cpu_v: cpu_v.clone(),
10581 cpu_stored: cpu_stored + j,
10582 o1: None,
10583 };
10584 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
10585 return Err(false);
10586 }
10587 if draft_rows < lm.1 {
10588 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
10589 return Err(false);
10590 }
10591 } else {
10592 graph.encode_lm_head(&m.final_norm, lm);
10593 }
10594 if !graph.encode_argmax(n_arg, j as u32 + 1) {
10595 return Err(false);
10596 }
10597 if split {
10598 graph.commit();
10601 }
10602 }
10603 }
10604 let t_enc = t_chain.elapsed();
10605 if graph.sync_checked().is_err() {
10606 return Err(true);
10607 }
10608 if std::env::var_os("CMF_GRAPH_SPEC_TIME").is_some() {
10609 eprintln!(
10610 "mtp-chain: encode {:.1} ms | wait {:.1} ms (k={k}, head rows {draft_rows}{})",
10611 t_enc.as_secs_f64() * 1e3,
10612 (t_chain.elapsed() - t_enc).as_secs_f64() * 1e3,
10613 if split { ", split" } else { "" }
10614 );
10615 }
10616 let mut ids = vec![0u32; k];
10617 if !graph.chain_ids_read(&mut ids) {
10618 return Err(true);
10619 }
10620 let mut kbuf = vec![0f32; k * nkv * hd];
10621 let mut vbuf = vec![0f32; k * nkv * hd];
10622 if !crate::gpu_metal::kv_mirror_read_rows(
10623 self.mtp_kv_id(),
10624 Self::MTP_LAYER_BASE,
10625 nkv,
10626 hd,
10627 cpu_stored,
10628 k,
10629 &mut kbuf,
10630 &mut vbuf,
10631 ) {
10632 return Err(true);
10633 }
10634 for r in 0..k {
10635 m.kv.append(
10636 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
10637 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
10638 &[],
10639 );
10640 }
10641 Ok(ids)
10642 }
10643
10644 fn try_batch_graph_wgpu(
10645 &self,
10646 hiddens: &mut [f32],
10647 positions: &[usize],
10648 k: usize,
10649 spec: Option<crate::gpu::SpecTail<'_>>,
10650 ) -> crate::gpu::BatchGraphOutcome {
10651 let _tb = std::time::Instant::now();
10652 let batch_debug = std::env::var_os("CMF_BATCH_DEBUG").is_some();
10653 if self.attn_softcap > 0.0 {
10654 return crate::gpu::BatchGraphOutcome::Declined; }
10656 let nh = self.num_heads;
10657 let (nkv, hd, rd) = self.layer_geom(0);
10658 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
10659 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
10660 if let Some((m, i, kind, rs)) = t
10661 .graph_weight()
10662 .or_else(|| t.graph_weight_descriptor())
10663 {
10664 let name = &m.tensors[i].name;
10665 let prism = if crate::prism::is_inverse_embedding(m, name) {
10666 crate::gpu::GraphPrismOp::InverseEmbedding
10667 } else if crate::prism::is_forward_weight(m, name) {
10668 crate::gpu::GraphPrismOp::Forward
10669 } else {
10670 crate::gpu::GraphPrismOp::None
10671 };
10672 return Some(crate::gpu::GraphW {
10673 idx: i,
10674 kind,
10675 row_scale: rs,
10676 data: &[],
10677 prism,
10678 affine: crate::prism::is_affine_target(m, name),
10679 });
10680 }
10681 if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
10682 eprintln!(
10683 "batch graph: tensor has no graph descriptor/f32 fallback rows={} cols={}",
10684 t.rows(),
10685 t.cols()
10686 );
10687 }
10688 t.as_f32().map(|d| crate::gpu::GraphW {
10689 idx: 0,
10690 kind: 4,
10691 row_scale: &[],
10692 data: d,
10693 prism: crate::gpu::GraphPrismOp::None,
10694 affine: false,
10695 })
10696 }
10697 let built: Option<(
10698 Vec<crate::gpu::GraphLayer<'_>>,
10699 std::sync::Arc<cortiq_core::CmfModel>,
10700 )> = (|| {
10701 let mut layers = Vec::with_capacity(self.num_layers);
10702 let mut model = None;
10703 for li in 0..self.num_layers {
10704 let lw = &self.weights.layers[self.phys_layer(li)];
10705 let gffn = match &lw.ffn {
10712 FfnKind::Dense(d) if !d.segs.is_empty() => {
10713 if batch_debug {
10714 eprintln!("batch graph: dense segmented FFN at layer {li}");
10715 }
10716 return None;
10717 }
10718 FfnKind::Dense(d) => crate::gpu::GraphFfn::Dense {
10719 gate: gw(&d.gate_proj)?,
10720 up: gw(&d.up_proj)?,
10721 down: gw(&d.down_proj)?,
10722 },
10723 FfnKind::Moe(m) => {
10724 if m.route_tau.is_some() || m.mask.is_some() {
10731 return None;
10732 }
10733 let (se, sg) = m.shared.as_ref()?;
10736 let shared_gated = sg.is_some();
10737 let sgate = match sg {
10738 Some(sg) => gw(sg)?,
10739 None => gw(&m.router)?,
10742 };
10743 let router = gw(&m.router)?;
10744 if router.prism != crate::gpu::GraphPrismOp::None
10750 || router.affine
10751 || sgate.prism != crate::gpu::GraphPrismOp::None
10752 || sgate.affine
10753 {
10754 return None;
10755 }
10756 let inter = m.experts.first()?.gate_proj.rows();
10757 let mut experts = Vec::with_capacity(m.experts.len() + 1);
10758 let mut q4tp: Option<bool> = None;
10759 let mut gu_q2: Option<bool> = None;
10760 for e in m.experts.iter().chain(std::iter::once(se)) {
10761 if !matches!(e.act, Act::Silu)
10762 || e.gate_proj.rows() != inter
10763 || e.up_proj.rows() != inter
10764 {
10765 return None;
10766 }
10767 let (mm, gi, ui, di, is_p, is_q2) = match e.gate_proj.mapped_q4t() {
10771 Some((mm, gi)) => (
10772 mm,
10773 gi,
10774 e.up_proj.mapped_q4t()?.1,
10775 e.down_proj.mapped_q4t()?.1,
10776 false,
10777 false,
10778 ),
10779 None => match e.gate_proj.mapped_q2tp() {
10780 Some((mm, gi)) => (
10781 mm,
10782 gi,
10783 e.up_proj.mapped_q2tp()?.1,
10784 e.down_proj.mapped_q4tp()?.1,
10785 true,
10786 true,
10787 ),
10788 None => {
10789 let (mm, gi) = e.gate_proj.mapped_q4tp()?;
10790 (
10791 mm,
10792 gi,
10793 e.up_proj.mapped_q4tp()?.1,
10794 e.down_proj.mapped_q4tp()?.1,
10795 true,
10796 false,
10797 )
10798 }
10799 },
10800 };
10801 if *q4tp.get_or_insert(is_p) != is_p
10802 || *gu_q2.get_or_insert(is_q2) != is_q2
10803 {
10804 return None;
10805 }
10806 if [gi, ui, di].into_iter().any(|idx| {
10807 mm.tensors
10808 .get(idx)
10809 .is_some_and(|t| {
10810 crate::prism::is_forward_weight(mm, &t.name)
10811 || crate::prism::is_affine_target(mm, &t.name)
10812 })
10813 }) {
10814 return None;
10815 }
10816 model.get_or_insert_with(|| mm.clone());
10817 experts.push((gi, ui, di));
10818 }
10819 crate::gpu::GraphFfn::Moe {
10820 router,
10821 shared_gate: sgate,
10822 experts,
10823 n_exp: m.experts.len(),
10824 top_k: m.top_k,
10825 inter,
10826 norm_topk: m.norm_topk_prob,
10827 q4tp: q4tp?,
10828 gu_q2: gu_q2.unwrap_or(false),
10829 sigmoid: m.router_sigmoid,
10830 bias: m.expert_bias.as_deref(),
10831 has_shared: true,
10832 shared_gated,
10833 route_scale: m.routed_scaling,
10834 }
10835 }
10836 _ => return None,
10837 };
10838 let attn = match &lw.attn {
10839 AttnKind::Full {
10840 wq,
10841 wk,
10842 wv,
10843 wo,
10844 q_norm,
10845 k_norm,
10846 output_gate,
10847 softplus_gate,
10848 bias,
10849 } => {
10850 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
10851 if batch_debug {
10852 eprintln!(
10853 "batch graph: unsupported Full attention gate at layer {li} softplus={} heads={}",
10854 softplus_gate.is_some(),
10855 self.attention_heads_per_layer.is_some()
10856 );
10857 }
10858 return None;
10859 }
10860 let (m, _, _, _) = wq
10861 .graph_weight()
10862 .or_else(|| wq.graph_weight_descriptor())?;
10863 model = Some(m.clone());
10864 crate::gpu::GraphAttn::Full {
10865 wq: gw(wq)?,
10866 wk: gw(wk)?,
10867 wv: gw(wv)?,
10868 wo: gw(wo)?,
10869 q_norm: q_norm.as_deref(),
10870 k_norm: k_norm.as_deref(),
10871 late_qk_norm: self.qk_norm_after_rope,
10872 bias: bias
10873 .as_ref()
10874 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
10875 output_gate: *output_gate,
10876 cpu_k: self.kv_cache.layers[li].k_heads(),
10877 cpu_v: self.kv_cache.layers[li].v_heads(),
10878 }
10879 }
10880 AttnKind::LinearGdn(w) => {
10881 let Some(cfg) = self.gdn_cfg else {
10882 if batch_debug {
10883 eprintln!("batch graph: no GDN config at layer {li}");
10884 }
10885 return None;
10886 };
10887 let (m, _, _, _) = w
10888 .in_proj_qkv
10889 .graph_weight()
10890 .or_else(|| w.in_proj_qkv.graph_weight_descriptor())?;
10891 model = Some(m.clone());
10892 crate::gpu::GraphAttn::Gdn {
10893 qkv: gw(&w.in_proj_qkv)?,
10894 z: gw(&w.in_proj_z)?,
10895 a: gw(&w.in_proj_a)?,
10896 b: gw(&w.in_proj_b)?,
10897 out: gw(&w.out_proj)?,
10898 conv1d: &w.conv1d,
10899 a_log: &w.a_log,
10900 dt_bias: &w.dt_bias,
10901 norm: &w.norm,
10902 nv: cfg.num_v_heads,
10903 nk: cfg.num_k_heads,
10904 dk: cfg.key_head_dim,
10905 dv: cfg.value_head_dim,
10906 kk: cfg.conv_kernel,
10907 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
10908 }
10909 }
10910 _ => return None,
10911 };
10912 layers.push(crate::gpu::GraphLayer {
10913 input_norm: &lw.input_norm,
10914 attn,
10915 post_norm: &lw.post_norm,
10916 ffn: gffn,
10917 });
10918 }
10919 Some((layers, model?))
10920 })();
10921 let Some((layers, model)) = built else {
10922 {
10923 use std::sync::atomic::{AtomicBool, Ordering};
10924 static SAID: AtomicBool = AtomicBool::new(false);
10925 if !SAID.swap(true, Ordering::Relaxed) {
10926 tracing::warn!("batch graph: BUILDER refused (layer weights/kinds)");
10927 }
10928 }
10929 return crate::gpu::BatchGraphOutcome::Declined;
10930 };
10931 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
10932 eprintln!("batch-build: {:.1} ms", _tb.elapsed().as_secs_f64() * 1e3);
10933 }
10934 crate::gpu::forward_batch_graph(
10935 &model,
10936 self.graph_kv_id,
10937 &layers,
10938 &self.inv_freq,
10939 hiddens,
10940 nh,
10941 nkv,
10942 hd,
10943 rd,
10944 self.hidden_size,
10945 self.intermediate_size,
10946 positions,
10947 self.kv_cache.max_seq_len,
10948 gemma,
10949 self.rms_eps as f32,
10950 self.attn_scale,
10951 k,
10952 &(0..self.num_layers)
10953 .map(|li| self.kv_cache.layers[self.phys_layer(li)].o1_views())
10954 .collect::<Vec<_>>(),
10955 self.o1_epoch,
10956 spec,
10957 )
10958 }
10959
10960 fn draft_probe() -> bool {
10964 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
10965 *ON.get_or_init(|| std::env::var("CMF_DSV4_DRAFT_PROBE").is_ok_and(|v| v != "0"))
10966 }
10967
10968 #[cfg(feature = "gpu")]
10980 fn dsv4_spec_on() -> bool {
10981 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
10982 *ON.get_or_init(|| {
10983 if let Ok(v) = std::env::var("CMF_DSV4_SPEC_RUN") {
10987 return v != "0";
10988 }
10989 std::env::var("CMF_DSV4_SPEC")
10996 .map(|v| v != "0")
10997 .unwrap_or_else(|_| {
10998 crate::gpu_wgpu::DRAFT_RESERVE.load(std::sync::atomic::Ordering::Relaxed) > 0
10999 })
11000 })
11001 }
11002
11003 #[cfg(feature = "gpu")]
11010 fn dsv4_spec_step(
11011 &mut self,
11012 tip_token: u32,
11013 t_next: u32,
11014 next_pos: usize,
11015 max_extra: usize,
11016 drafted: &mut usize,
11017 accepted_ctr: &mut usize,
11018 ) -> Option<(Vec<u32>, usize)> {
11019 let t_all = std::time::Instant::now();
11020 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
11021 thread_local! {
11022 static LAST: std::cell::Cell<Option<std::time::Instant>> =
11023 const { std::cell::Cell::new(None) };
11024 }
11025 LAST.with(|l| {
11026 if let Some(prev) = l.get() {
11027 eprintln!(
11028 "между раундами {:.1} мс",
11029 prev.elapsed().as_secs_f64() * 1e3
11030 );
11031 }
11032 l.set(Some(std::time::Instant::now()));
11033 });
11034 }
11035 if std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
11036 eprintln!("spec_step: вход pos={next_pos}");
11037 }
11038 let n_layers = self.dsv4.as_ref().map(|b| b.1.len())?;
11039 let cfg = self.dsv4.as_ref().map(|b| b.2)?;
11040 if self.dspark.is_none() {
11042 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
11043 if t.is_empty() {
11044 return None;
11045 }
11046 crate::dsv4::dspark_arm(&t, cfg.dim);
11047 self.dspark = Some(crate::dsv4::DsparkState::new(
11048 self.dsv4_mtp.len(),
11049 &cfg,
11050 t.len(),
11051 ));
11052 }
11053 let targets = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
11054 let pack = crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg);
11055 if pack.is_none() && std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
11056 eprintln!("spec_step: пак не построился (targets {targets:?})");
11057 }
11058 let pack = pack?;
11059 let block = crate::dsv4::dspark_block();
11060 let b_box = self.dsv4.as_mut()?;
11061 let (g, layers, st) = (&b_box.0, &b_box.1, &mut b_box.3);
11062 let ds = self.dspark.as_mut()?;
11063 let dbg = std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok();
11066 if !crate::dsv4::dspark_take(&mut ds.main_hidden) && !ds.have_hidden {
11067 if dbg {
11068 eprintln!("spec_step: нет захвата");
11069 }
11070 return None;
11071 }
11072 ds.have_hidden = true;
11073 let tip_pos = next_pos.checked_sub(1)?;
11074 let draft_started = std::time::Instant::now();
11075 let mut conf = Vec::new();
11076 let props = crate::dsv4::dspark_draft_gpu(
11077 g,
11078 &self.dsv4_mtp,
11079 &cfg,
11080 ds,
11081 pack,
11082 st.kv_id,
11083 tip_token,
11084 tip_pos,
11085 self.pool.as_deref(),
11086 &mut conf,
11087 );
11088 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
11089 *drafted += block;
11090 if props.is_empty() || props[0] != t_next {
11091 if dbg {
11092 eprintln!(
11093 "spec_step: черновик {} (props0={:?} t_next={t_next})",
11094 if props.is_empty() {
11095 "пуст"
11096 } else {
11097 "мимо"
11098 },
11099 props.first()
11100 );
11101 }
11102 return None;
11103 }
11104 let mut k_verify = crate::dsv4::dspark_verify_k()
11111 .min(props.len())
11112 .min(max_extra.saturating_add(1));
11113 let conf_min = {
11119 static M: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
11120 *M.get_or_init(|| {
11121 std::env::var("CMF_DSPARK_CONF_MIN")
11122 .ok()
11123 .and_then(|v| v.parse().ok())
11124 .unwrap_or(0.0)
11125 })
11126 };
11127 if conf_min > 0.0 && conf.len() >= props.len() {
11128 let mut keep = 1usize;
11129 while keep < k_verify && conf.get(keep).copied().unwrap_or(0.0) >= conf_min {
11130 keep += 1;
11131 }
11132 k_verify = k_verify.min(keep.max(2));
11133 }
11134 if k_verify < 2 {
11135 return None;
11136 }
11137 let mut fed = Vec::with_capacity(k_verify);
11138 fed.push(t_next);
11139 fed.extend_from_slice(&props[1..k_verify]);
11140 let mut argmax = Vec::new();
11141 let mut logits_all = Vec::new();
11142 let mut walked = Vec::new();
11143 let txn = crate::dsv4::dsv4_verify_chunk(
11144 g,
11145 layers,
11146 &cfg,
11147 st,
11148 &fed,
11149 next_pos,
11150 &self.inv_freq,
11151 self.pool.as_deref(),
11152 &targets,
11153 &mut argmax,
11154 &mut logits_all,
11155 &mut walked,
11156 );
11157 if txn.is_none() && dbg {
11158 eprintln!("spec_step: verify отказал");
11159 }
11160 let txn = txn?;
11161 let spec_gpu_end = txn.gpu_end;
11162 let b = fed.len();
11163 let mut accepted = 1usize;
11164 while accepted < b && fed[accepted] == argmax[accepted - 1] {
11165 accepted += 1;
11166 }
11167 if std::env::var("CMF_DSV4_SPEC_FORCE_REJECT").is_ok_and(|v| v != "0") {
11172 accepted = 1;
11173 }
11174 if std::env::var("CMF_DSV4_SPEC_TRACE").is_ok() {
11175 eprintln!("spec@{next_pos}: fed={fed:?} argmax={argmax:?} accepted={accepted}");
11176 }
11177 let t_fin = std::time::Instant::now();
11178 if !crate::dsv4::dsv4_spec_finish(
11179 g,
11180 layers,
11181 &cfg,
11182 st,
11183 txn,
11184 accepted,
11185 &fed,
11186 &self.inv_freq,
11187 self.pool.as_deref(),
11188 ) {
11189 tracing::warn!("dsv4: спекулятивный откат не удался — состояние подозрительно");
11190 return None;
11191 }
11192 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
11193 eprintln!(
11194 "finish(k={accepted}): {:.1} мс",
11195 t_fin.elapsed().as_secs_f64() * 1e3
11196 );
11197 }
11198 *accepted_ctr += accepted - 1;
11199 let (hc, dim) = (cfg.hc_mult, cfg.dim);
11204 let dev_caps: Vec<usize> = targets
11209 .iter()
11210 .copied()
11211 .filter(|&t| t < spec_gpu_end)
11212 .collect();
11213 let mut caps_all = vec![0.0f32; dev_caps.len() * b * hc * dim];
11214 if !crate::gpu_wgpu::dsv4_spec_cap_read_all(b, dev_caps.len(), hc * dim, &mut caps_all) {
11215 return None;
11216 }
11217 for t in 0..accepted {
11218 let tip = t + 1 == accepted;
11219 for (slot, &tl) in targets.iter().enumerate() {
11220 if let Some(di) = dev_caps.iter().position(|&d| d == tl) {
11221 let lo = (di * b + t) * hc * dim;
11222 crate::dsv4::dspark_capture(
11223 &caps_all[lo..lo + hc * dim],
11224 &cfg,
11225 slot,
11226 &mut ds.main_hidden,
11227 );
11228 } else if tip
11229 && crate::dsv4::dspark_peek_slot(slot, dim, {
11230 let lo = slot * dim;
11231 &mut ds.main_hidden[lo..lo + dim]
11232 })
11233 {
11234 } else {
11239 crate::dsv4::dspark_capture(
11243 &walked[t * hc * dim..(t + 1) * hc * dim],
11244 &cfg,
11245 slot,
11246 &mut ds.main_hidden,
11247 );
11248 }
11249 }
11250 crate::dsv4::dspark_ring_append(
11251 g,
11252 &self.dsv4_mtp,
11253 &cfg,
11254 ds,
11255 next_pos + t,
11256 self.pool.as_deref(),
11257 );
11258 }
11259 let row = logits_all[(accepted - 1) * cfg.vocab..accepted * cfg.vocab].to_vec();
11260 self.graph_logits = Some(row);
11261 if std::env::var("CMF_DSV4_TRUNK_PICK_DUMP").is_ok() {
11266 crate::dsv4::trunk_freq_note(&crate::dsv4::pick_tally_take());
11267 crate::dsv4::pick_tally_arm();
11268 }
11269 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
11270 eprintln!(
11271 "spec_step total {:.1} мс (k={accepted})",
11272 t_all.elapsed().as_secs_f64() * 1e3
11273 );
11274 }
11275 Some((fed[1..accepted].to_vec(), next_pos + accepted))
11276 }
11277
11278 fn dspark_probe(&mut self, position: usize, token_id: u32) {
11279 if self.dsv4_mtp.is_empty() || !Self::draft_probe() {
11280 return;
11281 }
11282 let trunk_now = crate::dsv4::pick_tally_take();
11284 crate::dsv4::trunk_freq_note(&trunk_now);
11285 if !trunk_now.is_empty() {
11286 self.dspark_trunk_picks.push(trunk_now);
11287 let keep = crate::dsv4::dspark_block();
11288 if self.dspark_trunk_picks.len() > keep {
11289 self.dspark_trunk_picks.remove(0);
11290 }
11291 }
11292 for p in std::mem::take(&mut self.dspark_pending) {
11295 let Some(i) = position.checked_sub(p.0 + 1) else {
11296 continue;
11297 };
11298 let mut p = p;
11299 if i < p.1.len() {
11300 if p.2 && p.1[i] == token_id {
11301 p.3 = i + 1;
11302 } else {
11303 p.2 = false;
11304 }
11305 if i + 1 < p.1.len() {
11306 self.dspark_pending.push(p);
11307 continue;
11308 }
11309 }
11310 self.dspark_hist.push(p.3);
11311 self.dspark_real.push(token_id);
11312 }
11313 let Some(b) = &mut self.dsv4 else { return };
11314 let (g, layers, cfg) = (&b.0, &b.1, b.2);
11315 let n_layers = layers.len();
11316 if self.dspark.is_none() {
11317 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
11318 if t.is_empty() {
11319 return;
11320 }
11321 eprintln!(
11322 "DSpark: захват со слоёв {t:?}, блок {}",
11323 crate::dsv4::dspark_block()
11324 );
11325 crate::dsv4::dspark_arm(&t, cfg.dim);
11326 self.dspark = Some(crate::dsv4::DsparkState::new(
11327 self.dsv4_mtp.len(),
11328 &cfg,
11329 t.len(),
11330 ));
11331 }
11332 let ds = self.dspark.as_mut().unwrap();
11333 if !crate::dsv4::dspark_take(&mut ds.main_hidden) {
11334 return; }
11336 let mut conf = Vec::new();
11337 crate::dsv4::pick_tally_arm();
11338 let draft_started = std::time::Instant::now();
11343 #[cfg(feature = "gpu")]
11344 let gpu_draft = crate::dsv4::dspark_gpu_on();
11345 #[cfg(not(feature = "gpu"))]
11346 let gpu_draft = false;
11347 let props = if gpu_draft {
11348 #[cfg(feature = "gpu")]
11349 {
11350 let kv_id = b.3.kv_id;
11351 match crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg) {
11352 Some(pk) => crate::dsv4::dspark_draft_gpu(
11353 g,
11354 &self.dsv4_mtp,
11355 &cfg,
11356 ds,
11357 pk,
11358 kv_id,
11359 token_id,
11360 position,
11361 self.pool.as_deref(),
11362 &mut conf,
11363 ),
11364 None => Vec::new(),
11365 }
11366 }
11367 #[cfg(not(feature = "gpu"))]
11368 Vec::new()
11369 } else {
11370 crate::gpu::cpu_scope(|| {
11371 crate::dsv4::dspark_draft(
11372 g,
11373 &self.dsv4_mtp,
11374 &cfg,
11375 ds,
11376 token_id,
11377 position,
11378 self.pool.as_deref(),
11379 &mut conf,
11380 )
11381 })
11382 };
11383 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
11384 let draft_picks = crate::dsv4::pick_tally_take();
11385 crate::dsv4::dspark_freq_note(&draft_picks);
11386 crate::dsv4::pick_tally_arm();
11389 if !props.is_empty() {
11390 let (tu, tt) = {
11394 let flat: Vec<(usize, Vec<usize>)> = self
11395 .dspark_trunk_picks
11396 .iter()
11397 .flat_map(|v| v.iter().cloned())
11398 .collect();
11399 let mut per: std::collections::HashMap<usize, Vec<usize>> =
11401 std::collections::HashMap::new();
11402 for (li, picks) in flat {
11403 per.entry(li).or_default().extend(picks);
11404 }
11405 let n = per.len().max(1);
11406 let mut u = 0usize;
11407 let mut t = 0usize;
11408 for (_, v) in per {
11409 t += v.len();
11410 u += v.iter().collect::<std::collections::HashSet<_>>().len();
11411 }
11412 (u / n, t / n)
11413 };
11414 let (du, dt) = crate::dsv4::tally_unique(&draft_picks);
11415 self.dspark_exp.push((tu, tt, du, dt));
11416 self.dspark_pending.push((position, props, true, 0));
11417 }
11418 if self.dspark_hist.len() >= 8 && self.dspark_hist.len() % 8 == 0 {
11419 let n = self.dspark_hist.len() as f32;
11420 let mean: f32 = self.dspark_hist.iter().sum::<usize>() as f32 / n;
11421 let block = crate::dsv4::dspark_block();
11422 let mut at = vec![0usize; block + 1];
11423 for &k in &self.dspark_hist {
11424 at[k] += 1;
11425 }
11426 let mut surv = Vec::with_capacity(block);
11428 for i in 1..=block {
11429 let k = at[i..].iter().sum::<usize>() as f32 / n;
11430 surv.push(format!("{k:.2}"));
11431 }
11432 let distinct = self
11433 .dspark_real
11434 .iter()
11435 .collect::<std::collections::HashSet<_>>()
11436 .len();
11437 let (tu, tt, du, dt) = self.dspark_exp.iter().fold((0, 0, 0, 0), |a, b| {
11438 (a.0 + b.0, a.1 + b.1, a.2 + b.2, a.3 + b.3)
11439 });
11440 let m = self.dspark_exp.len().max(1);
11441 eprintln!(
11442 "DSpark: черновиков {}, принято в среднем {mean:.2} из {block} \
11443 (токенов за проход {:.2}), распределение {at:?}, выживание [{}]",
11444 self.dspark_hist.len(),
11445 mean + 1.0,
11446 surv.join(" ")
11447 );
11448 eprintln!(
11449 "DSpark: разных токенов {distinct} из {} (вырожденность), \
11450 эксперты ствол {}/{} на слой за {block} токенов, \
11451 черновик {}/{} за блок, draft {:.2} мс/блок",
11452 self.dspark_real.len(),
11453 tu / m,
11454 tt / m,
11455 du / m,
11456 dt / m,
11457 self.dspark_draft_ns as f64 / self.dspark_exp.len().max(1) as f64 / 1e6
11458 );
11459 }
11460 }
11461
11462 fn forward_layers_upto(
11463 &mut self,
11464 hidden: &[f32],
11465 position: usize,
11466 task_mask: Option<&TaskMask>,
11467 upto: Option<usize>,
11468 ) -> Vec<f32> {
11469 if let Some(plan) = self.gpu_plan.clone() {
11475 if upto.is_none() && plan.len() > 1 {
11476 let mut h = hidden.to_vec();
11477 for &(dev, from, upto_incl) in plan.iter() {
11478 h = crate::gpu::with_device(dev, || {
11479 self.forward_layers_span(&h, position, task_mask, from, Some(upto_incl))
11480 });
11481 }
11482 return h;
11483 }
11484 }
11485 self.forward_layers_span(hidden, position, task_mask, 0, upto)
11486 }
11487
11488 pub fn set_gpu_plan(&mut self, devices: Option<&[usize]>) -> Result<(), String> {
11493 self.set_gpu_plan_at(devices, None)
11494 }
11495
11496 pub fn set_gpu_plan_at(
11500 &mut self,
11501 devices: Option<&[usize]>,
11502 at: Option<usize>,
11503 ) -> Result<(), String> {
11504 let Some(devs) = devices.filter(|d| d.len() > 1) else {
11505 self.gpu_plan = None;
11506 return Ok(());
11507 };
11508 self.split_supported()?;
11509 let n = self.num_layers;
11510 if devs.len() > n {
11511 return Err(format!("{} devices for {n} layers", devs.len()));
11512 }
11513 if let Some(k) = at {
11514 if k == 0 || k >= n {
11515 return Err(format!("split at {k}: the model has {n} layers"));
11516 }
11517 if devs.len() == 2 {
11518 self.gpu_plan = Some(std::sync::Arc::new(vec![
11519 (devs[0], 0, k - 1),
11520 (devs[1], k, n - 1),
11521 ]));
11522 return Ok(());
11523 }
11524 return Err(format!(
11525 "an explicit split point takes exactly 2 devices, got {}",
11526 devs.len()
11527 ));
11528 }
11529 let per = n.div_ceil(devs.len());
11530 let mut plan = Vec::with_capacity(devs.len());
11531 let mut from = 0usize;
11532 for &d in devs {
11533 if from >= n {
11534 break;
11535 }
11536 let upto = (from + per - 1).min(n - 1);
11537 plan.push((d, from, upto));
11538 from = upto + 1;
11539 }
11540 self.gpu_plan = Some(std::sync::Arc::new(plan));
11541 Ok(())
11542 }
11543
11544 pub fn gpu_plan(&self) -> Option<Vec<(usize, usize, usize)>> {
11546 self.gpu_plan.as_ref().map(|p| p.as_ref().clone())
11547 }
11548
11549 fn forward_layers_span(
11555 &mut self,
11556 hidden: &[f32],
11557 position: usize,
11558 task_mask: Option<&TaskMask>,
11559 from: usize,
11560 upto: Option<usize>,
11561 ) -> Vec<f32> {
11562 debug_assert!(
11563 from == 0
11564 || (self.dsv4.is_none()
11565 && self.dsv41.is_none()
11566 && self.qwen4_exp.is_none()
11567 && self.g3n.is_none())
11568 );
11569 #[cfg(target_os = "macos")]
11575 if !crate::gpu_metal::wait_replay() {
11576 self.fail_metal_graph("the pending async replay failed before a plain forward");
11577 return vec![0.0; self.hidden_size];
11578 }
11579 if let Some(b) = &mut self.qwen4_exp {
11580 let _ = (task_mask, upto);
11581 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
11582 let mut logits = Vec::new();
11583 crate::qwen4_exp::forward_token(
11584 &b.0,
11585 &b.1,
11586 &b.2,
11587 &mut b.3,
11588 token_id,
11589 position,
11590 &self.inv_freq,
11591 self.pool.as_deref(),
11592 &mut logits,
11593 true,
11594 );
11595 self.graph_logits = Some(logits);
11596 return vec![0.0; self.hidden_size];
11597 }
11598 if let Some(b) = &mut self.dsv4 {
11604 let _ = (task_mask, upto);
11605 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
11606 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
11607 st.pos = position;
11608 let mut logits = Vec::new();
11609 crate::dsv4::forward_token(
11610 g,
11611 layers,
11612 &cfg,
11613 st,
11614 token_id,
11615 &self.inv_freq,
11616 self.pool.as_deref(),
11617 &mut logits,
11618 );
11619 self.graph_logits = Some(logits);
11620 self.dspark_probe(position, token_id);
11621 return vec![0.0; self.hidden_size];
11624 }
11625 if let Some(b) = &mut self.dsv41 {
11627 let _ = (task_mask, upto);
11628 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
11629 let mut logits = Vec::new();
11630 crate::dsv41::forward_token(
11631 &b.0,
11632 &b.1,
11633 &b.2,
11634 &mut b.3,
11635 token_id,
11636 position,
11637 self.pool.as_deref(),
11638 &mut logits,
11639 );
11640 self.graph_logits = Some(logits);
11641 return vec![0.0; self.hidden_size];
11642 }
11643 if let Some(b) = &self.g3n {
11646 let _ = (task_mask, upto);
11647 return crate::g3n::g3n_forward(
11648 &b.0,
11649 &b.1,
11650 hidden,
11651 position,
11652 &mut self.kv_cache.layers,
11653 self.num_heads,
11654 self.num_kv_heads,
11655 self.head_dim,
11656 self.pool.as_deref(),
11657 );
11658 }
11659 let mut h = hidden.to_vec();
11660 let (nh, _nkv, _hd, hs, _rd, eps) = (
11663 self.num_heads,
11664 self.num_kv_heads,
11665 self.head_dim,
11666 self.hidden_size,
11667 self.rotary_dim,
11668 self.rms_eps,
11669 );
11670 let pool = self.pool.clone();
11671 let graph_env = std::env::var("CMF_GPU_WGPU_GRAPH").ok();
11683 let graph_on = match graph_env.as_deref() {
11684 Some("0") => false,
11685 Some("prefill") => false, Some(_) => true,
11687 None => crate::gpu::wgpu_graph_default(),
11693 };
11694 let graph_trusted =
11695 graph_env.is_some() || crate::gpu::wgpu_graph_default() || self.gdn_cfg.is_some();
11696 let race_eligible = graph_on
11697 && upto.is_none()
11698 && task_mask.is_none()
11699 && from == 0
11700 && !crate::gpu::graph_unsupported();
11701 let mut tail_start = 0usize;
11702 if race_eligible && crate::gpu::graph_race_use_graph(graph_trusted) {
11703 let t_graph = std::time::Instant::now();
11704 let mut lg = Vec::new();
11705 let mut gl = 0usize;
11706 let built = self.try_token_graph_wgpu(hidden, position, &mut lg, &mut gl);
11707 let declined = built.is_none();
11708 let built = match built {
11709 Some(Ok(hh)) => Some(hh),
11710 Some(Err(())) => {
11711 self.clear_sequence_state();
11715 self.graph_failed
11716 .store(true, std::sync::atomic::Ordering::Relaxed);
11717 self.cancel
11718 .store(true, std::sync::atomic::Ordering::Relaxed);
11719 tracing::error!("token graph failed after admission; sequence state cleared");
11720 return vec![0.0; self.hidden_size];
11721 }
11722 None => None,
11723 };
11724 if declined && !self.o1_active() && self.attn_softcap == 0.0 {
11729 crate::gpu::graph_mark_unsupported();
11730 }
11731 graph_note(built.is_some(), gl, self.num_layers);
11732 if let Some(hh) = built {
11733 let dur = t_graph.elapsed();
11734 if std::env::var("CMF_GRAPH_PROF").is_ok() {
11735 eprintln!("graph-call: {:.2} ms total", dur.as_secs_f64() * 1000.0);
11736 }
11737 if gl > 0 && gl < self.num_layers {
11738 h = hh;
11744 tail_start = gl;
11745 } else if graph_trusted || !crate::gpu::graph_race_first_token_hopeless(dur) {
11746 if !graph_trusted {
11747 crate::gpu::graph_race_record(true, dur);
11748 }
11749 if !lg.is_empty() {
11750 lg.resize(self.vocab_size, 0.0);
11753 if let Some(c) = self.final_softcap {
11754 for l in lg.iter_mut() {
11755 *l = c * (*l / c).tanh();
11756 }
11757 }
11758 self.graph_logits = Some(lg);
11759 }
11760 return hh;
11761 }
11762 }
11768 }
11769 let span = from > 0 || upto.is_some();
11793 if span && graph_on && task_mask.is_none() && graph_trusted {
11794 let upto_excl = upto.map_or(self.num_layers, |u| u + 1);
11795 let mut lg = Vec::new();
11796 let mut gl = 0usize;
11797 let span_res =
11798 self.try_token_graph_wgpu_span(hidden, position, &mut lg, from, upto_excl, &mut gl);
11799 let span_res = match span_res {
11800 Some(Ok(hh)) => Some(hh),
11801 Some(Err(())) => {
11802 self.clear_sequence_state();
11803 self.graph_failed
11804 .store(true, std::sync::atomic::Ordering::Relaxed);
11805 self.cancel
11806 .store(true, std::sync::atomic::Ordering::Relaxed);
11807 tracing::error!(
11808 "span token graph failed after admission; sequence state cleared"
11809 );
11810 return vec![0.0; self.hidden_size];
11811 }
11812 None => None,
11813 };
11814 graph_note(span_res.is_some(), gl, upto_excl - from);
11815 if std::env::var("CMF_GPU_DEBUG").is_ok() {
11816 static SEEN: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
11820 if SEEN.fetch_add(1, std::sync::atomic::Ordering::Relaxed) < 4 {
11821 eprintln!(
11822 "span graph: covered {gl} of {} layers [{from}..{upto_excl}) res={}",
11823 upto_excl - from,
11824 span_res.is_some()
11825 );
11826 }
11827 }
11828 if let Some(hh) = span_res {
11829 if gl == upto_excl - from {
11830 if !lg.is_empty() {
11831 lg.resize(self.vocab_size, 0.0);
11832 if let Some(c) = self.final_softcap {
11833 for l in lg.iter_mut() {
11834 *l = c * (*l / c).tanh();
11835 }
11836 }
11837 self.graph_logits = Some(lg);
11838 }
11839 crate::gpu::set_layer(-1);
11840 return hh;
11841 }
11842 h = hh;
11844 tail_start = from + gl;
11845 }
11846 }
11847 let t_race_cpu = (race_eligible && !graph_trusted).then(std::time::Instant::now);
11848
11849 let _host_tail = (tail_start > from).then(crate::gpu::enter_cpu_scope);
11855 let automatic_gpu_prefix = self.automatic_gpu_prefix();
11856
11857 let _prof_layers = crate::cpuprof::time(crate::cpuprof::Slot::Layers);
11858 #[cfg(target_os = "macos")]
11859 let mut gpu_skip_until = 0usize;
11860 for li in tail_start.max(from)..self.num_layers {
11861 let _capacity_tail = automatic_gpu_prefix
11862 .filter(|&prefix| li >= prefix)
11863 .map(|_| crate::gpu::enter_cpu_scope());
11864 crate::gpu::set_layer(li as i64); if let Some(u) = upto {
11866 if li > u {
11867 break;
11868 }
11869 }
11870 if let Some(mask) = task_mask {
11871 if !mask.layer_alive(li) {
11872 continue; }
11874 }
11875 #[cfg(target_os = "macos")]
11879 {
11880 if li < gpu_skip_until {
11881 continue;
11882 }
11883 if task_mask.is_none() {
11884 let end = self.q1_graph_gpu(li, upto, position, &mut h);
11885 if self
11886 .graph_failed
11887 .load(std::sync::atomic::Ordering::Relaxed)
11888 {
11889 return vec![0.0; self.hidden_size];
11893 }
11894 if end > li {
11895 gpu_skip_until = end;
11896 if self.is_loop_end(end - 1) && end < self.num_layers {
11899 h = inference::rms_norm(
11900 &h,
11901 &self.weights.final_norm,
11902 self.rms_eps,
11903 self.norm_style,
11904 );
11905 }
11906 continue;
11907 }
11908 }
11909 }
11910
11911 let lw = &self.weights.layers[self.phys_layer(li)];
11912 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
11913 if tp.parse::<usize>().ok() == Some(position) {
11914 let n: f32 = h.iter().map(|x| x * x).sum::<f32>().sqrt();
11915 eprintln!(
11916 "TRACE pos {position} layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
11917 h[0], h[1]
11918 );
11919 }
11920 }
11921 let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
11924 inference::rms_norm_into(
11925 &h,
11926 &lw.input_norm,
11927 self.rms_eps,
11928 self.norm_style,
11929 &mut self.ws.n1,
11930 );
11931 drop(prof);
11932
11933 let attn_out = match &lw.attn {
11934 AttnKind::Mla(w) => {
11935 let inv_freq_l = self.layer_inv_freq(li);
11936 let rs = self.layer_rope_scale(li);
11937 let eps = self.rms_eps;
11938 let pool = self.pool.clone();
11939 mla_attention(
11940 w,
11941 &self.ws.n1,
11942 &mut self.kv_cache.layers[li],
11943 position,
11944 &inv_freq_l,
11945 rs,
11946 eps,
11947 pool.as_deref(),
11948 )
11949 }
11950 AttnKind::Linear(w) => {
11951 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
11952 vmf_phase_forward(
11953 &self.ws.n1,
11954 w,
11955 &cfg,
11956 &mut self.kv_cache.layers[li].linear_state,
11957 self.pool.as_deref(),
11958 )
11959 }
11960 AttnKind::Kda(w) => {
11961 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
11962 crate::linear_core::kda_forward(
11963 &self.ws.n1,
11964 w,
11965 &cfg,
11966 &mut self.kv_cache.layers[li].linear_state,
11967 self.pool.as_deref(),
11968 )
11969 }
11970 AttnKind::LinearGdn(w) => {
11971 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
11972 gdn_forward(
11973 &self.ws.n1,
11974 w,
11975 &cfg,
11976 &mut self.kv_cache.layers[li].linear_state,
11977 self.pool.as_deref(),
11978 )
11979 }
11980 AttnKind::ShortConv(w) => {
11981 let cfg = self
11982 .short_conv_cfg
11983 .expect("short-conv layer without short_conv_cfg");
11984 short_conv_forward(
11985 &self.ws.n1,
11986 w,
11987 &cfg,
11988 &mut self.kv_cache.layers[li].linear_state,
11989 self.pool.as_deref(),
11990 )
11991 }
11992 AttnKind::Full {
11993 wq,
11994 wk,
11995 wv,
11996 wo,
11997 q_norm,
11998 k_norm,
11999 output_gate,
12000 softplus_gate,
12001 bias,
12002 } if self.kv_cache.layers[li].o1_sealed() => {
12003 let inv_freq_l = self.layer_inv_freq(li);
12006 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
12007 let cfg = QwenAttnCfg {
12008 num_heads: self.layer_num_heads(li),
12009 num_kv_heads: nkv_l,
12010 head_dim: hd_l,
12011 hidden_size: hs,
12012 position,
12013 inv_freq: &inv_freq_l,
12014 rotary_dim: rd_l,
12015 scale: self.attn_scale,
12016 softcap: self.attn_softcap,
12017 window: None,
12018 v_norm: self.attn_v_norm,
12019 qk_norm_after_rope: self.qk_norm_after_rope,
12020 q_norm: q_norm.as_deref(),
12021 k_norm: k_norm.as_deref(),
12022 output_gate: *output_gate,
12023 softplus_gate: softplus_gate
12024 .as_ref()
12025 .map(|(gate, per_head)| (gate, *per_head)),
12026 rope_scale: self.layer_rope_scale(li),
12027 bias: bias
12028 .as_ref()
12029 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
12030 rms_eps: eps,
12031 norm_style: self.norm_style,
12032 pool: pool.as_deref(),
12033 };
12034 attention::qwen_attention_nystrom(
12035 &self.ws.n1,
12036 wq,
12037 wk,
12038 wv,
12039 wo,
12040 &mut self.kv_cache.layers[li],
12041 &cfg,
12042 )
12043 }
12044 AttnKind::Full {
12045 wq,
12046 wk,
12047 wv,
12048 wo,
12049 q_norm,
12050 k_norm,
12051 output_gate,
12052 softplus_gate,
12053 bias,
12054 } => 'attn: {
12055 if graph_on
12058 && !*output_gate
12059 && softplus_gate.is_none()
12060 && self.attention_heads_per_layer.is_none()
12061 && bias.is_none()
12062 && task_mask.is_none()
12063 {
12064 let inv_freq_l = self.layer_inv_freq(li);
12065 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
12066 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
12067 if let (Some((gm, qi)), Some((_, ki)), Some((_, vi)), Some((_, oi))) = (
12068 wq.mapped_q1(),
12069 wk.mapped_q1(),
12070 wv.mapped_q1(),
12071 wo.mapped_q1(),
12072 ) {
12073 let gm = gm.clone();
12074 let mut out = vec![0f32; hs];
12075 let cache = &self.kv_cache.layers[li];
12076 if crate::gpu::attn_dropin(
12077 &gm,
12078 self.graph_kv_id,
12079 li,
12080 &self.ws.n1,
12081 qi,
12082 ki,
12083 vi,
12084 oi,
12085 q_norm.as_deref(),
12086 k_norm.as_deref(),
12087 self.qk_norm_after_rope,
12088 &inv_freq_l,
12089 nh,
12090 nkv_l,
12091 hd_l,
12092 rd_l,
12093 hs,
12094 position,
12095 self.kv_cache.max_seq_len,
12096 gemma,
12097 eps as f32,
12098 cache.k_heads(),
12099 cache.v_heads(),
12100 &mut out,
12101 ) {
12102 break 'attn out;
12103 }
12104 }
12105 }
12106 let masked = task_mask
12107 .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
12108 .unwrap_or(false);
12109 let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
12110 match (masked, f32_view) {
12111 (true, (Some(q), Some(k), Some(v), Some(o))) => {
12114 let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
12115 attention::multi_head_attention(
12116 &self.ws.n1,
12117 q,
12118 k,
12119 v,
12120 o,
12121 &mut self.kv_cache.layers[li],
12122 self.num_heads,
12123 self.num_kv_heads,
12124 self.head_dim,
12125 self.hidden_size,
12126 position,
12127 &active_heads,
12128 &self.inv_freq,
12129 )
12130 }
12131 (masked, _) => {
12132 if masked {
12133 tracing::warn!(
12134 "layer {li}: head mask on quantized weights not \
12135 supported yet — executing dense"
12136 );
12137 }
12138 let inv_freq_l = self.layer_inv_freq(li);
12139 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
12140 let cfg = QwenAttnCfg {
12141 num_heads: self.layer_num_heads(li),
12142 num_kv_heads: nkv_l,
12143 head_dim: hd_l,
12144 hidden_size: hs,
12145 position,
12146 inv_freq: &inv_freq_l,
12147 rotary_dim: rd_l,
12148 scale: self.attn_scale,
12149 softcap: self.attn_softcap,
12150 window: self.layer_window(li),
12151 v_norm: self.attn_v_norm,
12152 qk_norm_after_rope: self.qk_norm_after_rope,
12153 q_norm: q_norm.as_deref(),
12154 k_norm: k_norm.as_deref(),
12155 output_gate: *output_gate,
12156 softplus_gate: softplus_gate
12157 .as_ref()
12158 .map(|(gate, per_head)| (gate, *per_head)),
12159 rope_scale: self.layer_rope_scale(li),
12160 bias: bias
12161 .as_ref()
12162 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
12163 rms_eps: eps,
12164 norm_style: self.norm_style,
12165 pool: pool.as_deref(),
12166 };
12167 attention::qwen_attention(
12168 &self.ws.n1,
12169 wq,
12170 wk,
12171 wv,
12172 wo,
12173 &mut self.kv_cache.layers[li],
12174 &cfg,
12175 )
12176 }
12177 }
12178 }
12179 };
12180 let attn_out = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
12183 Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
12184 None => attn_out,
12185 };
12186 let lw = &self.weights.layers[self.phys_layer(li)];
12187 let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
12188 inference::add_rmsnorm_fused_into(
12189 &mut h,
12190 &attn_out,
12191 &lw.post_norm,
12192 self.rms_eps,
12193 self.norm_style,
12194 &mut self.ws.p1,
12195 );
12196 drop(prof);
12197 let mut attn_out = attn_out;
12198 attention::recycle_buf(&mut attn_out);
12199 let post_normed = &self.ws.p1;
12200
12201 let ffn_masked = task_mask
12202 .map(|m| m.ffn_active_count(li) < self.intermediate_size)
12203 .unwrap_or(false);
12204 let ffn_out = match (ffn_masked, &lw.ffn) {
12216 (_, FfnKind::Dense(d)) if !d.segs.is_empty() => {
12220 let row = task_mask
12221 .and_then(|tm| tm.ffn_masks.get(li))
12222 .map(|v| v.as_slice());
12223 tube_ffn(d, post_normed, 1, self.pool.as_deref(), row)
12224 }
12225 (true, FfnKind::Dense(d)) => {
12226 let tm = task_mask.unwrap();
12227 let alive = tm.ffn_active_count(li);
12228 let deep = alive * 2 <= self.intermediate_size;
12229 if deep && d.down_proj.sparse_col_ok() && !d.gate_proj.has_prism_contract() {
12230 let active = tm.ffn_active_indices(li);
12231 sparse_ffn_quant(
12232 d,
12233 post_normed,
12234 &active,
12235 self.hidden_size,
12236 self.pool.as_deref(),
12237 )
12238 } else if deep
12239 && let (Some(g), Some(u), Some(dn)) = (
12240 d.gate_proj.as_f32(),
12241 d.up_proj.as_f32(),
12242 d.down_proj.as_f32(),
12243 )
12244 {
12245 let active = tm.ffn_active_indices(li);
12246 inference::sparse_ffn_forward(
12247 post_normed,
12248 g,
12249 u,
12250 dn,
12251 self.hidden_size,
12252 self.intermediate_size,
12253 &active,
12254 self.pool.as_deref(),
12255 )
12256 } else {
12257 let row = tm.ffn_masks.get(li).map(|v| v.as_slice());
12258 dense_ffn_batch(d, post_normed, 1, self.pool.as_deref(), row)
12259 }
12260 }
12261 (true, FfnKind::Moe(m)) => {
12262 let allowed = task_mask.and_then(|tm| tm.expert_flags(li, m.experts.len()));
12266 ffn_forward(
12267 &lw.ffn,
12268 post_normed,
12269 self.pool.as_deref(),
12270 allowed.as_deref(),
12271 )
12272 }
12273 (true, FfnKind::DenseMoe(dm)) => dense_moe_ffn(
12274 dm,
12275 post_normed,
12276 &h,
12277 self.rms_eps,
12278 self.norm_style,
12279 self.pool.as_deref(),
12280 ),
12281 (false, _) => match &lw.ffn {
12282 FfnKind::DenseMoe(dm) => dense_moe_ffn(
12283 dm,
12284 post_normed,
12285 &h,
12286 self.rms_eps,
12287 self.norm_style,
12288 self.pool.as_deref(),
12289 ),
12290 _ => {
12291 let allowed = match (&lw.ffn, task_mask) {
12292 (FfnKind::Moe(m), Some(tm)) => tm.expert_flags(li, m.experts.len()),
12293 _ => None,
12294 };
12295 ffn_forward(
12296 &lw.ffn,
12297 post_normed,
12298 self.pool.as_deref(),
12299 allowed.as_deref(),
12300 )
12301 }
12302 },
12303 };
12304 let ffn_out = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
12305 Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
12306 None => ffn_out,
12307 };
12308 for (i, &f) in ffn_out.iter().enumerate() {
12309 h[i] += f;
12310 }
12311 let mut ffn_out = ffn_out;
12312 attention::recycle_buf(&mut ffn_out);
12313
12314 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
12316 for v in h.iter_mut() {
12317 *v *= sc;
12318 }
12319 }
12320
12321 if self.is_loop_end(li) && li + 1 < self.num_layers {
12324 h = inference::rms_norm(
12325 &h,
12326 &self.weights.final_norm,
12327 self.rms_eps,
12328 self.norm_style,
12329 );
12330 }
12331
12332 if self.dyn_phi_layer == Some(li) {
12336 self.update_dyn_phi(&h);
12337 }
12338 }
12339 crate::gpu::set_layer(-1); if let Some(t) = t_race_cpu {
12341 crate::gpu::graph_race_record(false, t.elapsed());
12342 }
12343
12344 h
12345 }
12346
12347 fn update_dyn_phi(&mut self, h: &[f32]) {
12350 const A: f32 = 0.2;
12351 if self.dyn_phi_ema.len() != h.len() {
12352 self.dyn_phi_ema = vec![0.0; h.len()];
12353 self.dyn_phi_seen = 0;
12354 }
12355 if self.dyn_phi_seen == 0 {
12356 self.dyn_phi_ema.copy_from_slice(h);
12357 } else {
12358 for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
12359 *e = (1.0 - A) * *e + A * v;
12360 }
12361 }
12362 self.dyn_phi_seen += 1;
12363 }
12364
12365 pub fn dyn_phi(&self) -> &[f32] {
12367 &self.dyn_phi_ema
12368 }
12369
12370 pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
12372 self.dyn_phi_layer = layer;
12373 self.dyn_phi_ema.clear();
12374 self.dyn_phi_seen = 0;
12375 }
12376
12377 pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
12379 let Some(model) = &self.model else {
12380 return Vec::new();
12381 };
12382 model
12383 .header
12384 .skills
12385 .iter()
12386 .enumerate()
12387 .filter_map(|(i, sk)| {
12388 let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
12389 let sel = sk.selection.as_ref()?;
12390 (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
12391 })
12392 .collect()
12393 }
12394
12395 pub fn active_skill(&self) -> Option<usize> {
12397 self.dyn_active
12398 }
12399
12400 pub fn enable_dynamic_routing(&mut self) -> usize {
12405 use crate::swarm::{DynRouter, RoutableSkill};
12406 let Some(model) = self.model.clone() else {
12407 return 0;
12408 };
12409 if self.dyn_blend_loaded {
12412 tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
12413 return 0;
12414 }
12415 if let Some(a) = self.dyn_active {
12419 if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
12420 tracing::warn!("loaded skill is not FFN-eligible — dynamic routing unavailable");
12421 return 0;
12422 }
12423 }
12424 let hidden = self.hidden_size;
12425 let mut skills = Vec::new();
12426 for (idx, id, _phi) in self.dynamic_skills() {
12427 if let Some(sel) = model.header.skills[idx].selection.as_ref() {
12428 if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
12429 skills.push(rs);
12430 }
12431 }
12432 }
12433 if skills.is_empty() {
12434 return 0;
12435 }
12436 let phi = skills[0].phi_layer;
12438 if skills.iter().any(|s| s.phi_layer != phi) {
12439 tracing::warn!("routable skills disagree on phi_layer; using {phi}");
12440 }
12441 let n = skills.len();
12442 self.set_dyn_phi_layer(Some(phi));
12443 self.dyn_router = Some(DynRouter::new(skills));
12444 n
12445 }
12446
12447 pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
12449 self.dyn_router
12450 .as_ref()
12451 .map(|r| r.switches.clone())
12452 .unwrap_or_default()
12453 }
12454
12455 fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
12458 let rows = self.weights.lm_head.rows();
12459 let mut logits = attention::take_buf(rows.min(self.vocab_size));
12460 self.weights
12461 .lm_head
12462 .matvec(hidden, &mut logits, self.pool.as_deref());
12463 logits.resize(self.vocab_size, 0.0);
12464 if let Some(m) = self.logit_multiplier {
12465 for l in logits.iter_mut() {
12466 *l *= m;
12467 }
12468 }
12469 if let Some(c) = self.final_softcap {
12470 for l in logits.iter_mut() {
12471 *l = c * (*l / c).tanh();
12472 }
12473 }
12474 if let Some(cm) = self.head_clusters.as_ref() {
12475 self.hierarchical_head_logprobs(hidden, cm, &mut logits);
12476 }
12477 logits
12478 }
12479
12480 fn hierarchical_head_logprobs(&self, hidden: &[f32], cm: &[f32], logits: &mut [f32]) {
12483 let h = hidden.len();
12484 let ncl = cm.len() / h.max(1);
12485 if ncl == 0 || logits.len() % ncl != 0 {
12486 return;
12487 }
12488 let cs = logits.len() / ncl;
12489 let mut lc = vec![0.0f32; ncl];
12491 for c in 0..ncl {
12492 let row = &cm[c * h..(c + 1) * h];
12493 let mut s = 0.0f32;
12494 for j in 0..h {
12495 s += row[j] * hidden[j];
12496 }
12497 lc[c] = s;
12498 }
12499 let mx = lc.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
12500 let lse: f32 = mx + lc.iter().map(|v| (v - mx).exp()).sum::<f32>().ln();
12501 for c in 0..ncl {
12502 let blk = &mut logits[c * cs..(c + 1) * cs];
12503 let bm = blk.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
12504 let bl: f32 = bm + blk.iter().map(|v| (v - bm).exp()).sum::<f32>().ln();
12505 let add = lc[c] - lse - bl;
12506 for v in blk.iter_mut() {
12507 *v += add;
12508 }
12509 }
12510 }
12511
12512 pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
12517 self.clear_sequence_state();
12518 crate::gpu::graph_race_begin_generation();
12522 if task_mask.is_none() {
12523 self.o1_begin();
12524 }
12525 let mut hidden = vec![0.0f32; self.hidden_size];
12526 for (pos, &id) in ids.iter().enumerate() {
12527 let emb = self.embed_single(id);
12528 hidden = self.forward_layers(&emb, pos, task_mask);
12529 }
12530 if let Err(err) = self.o1_seal_checked() {
12531 self.o1_fail(err);
12532 }
12533 inference::rms_norm_into(
12534 &hidden,
12535 &self.weights.final_norm,
12536 self.rms_eps,
12537 self.norm_style,
12538 &mut self.ws.n1,
12539 );
12540 self.lm_head_forward(&self.ws.n1)
12541 }
12542}
12543
12544pub fn create_test_pipeline(
12546 hidden_size: usize,
12547 intermediate_size: usize,
12548 num_heads: usize,
12549 num_kv_heads: usize,
12550 head_dim: usize,
12551 num_layers: usize,
12552 vocab_size: usize,
12553) -> Pipeline {
12554 let synth = |n: usize, salt: usize| -> Vec<f32> {
12557 (0..n)
12558 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
12559 .collect()
12560 };
12561 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
12562 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
12563 };
12564 let layer_weights: Vec<LayerWeights> = (0..num_layers)
12565 .map(|li| LayerWeights {
12566 input_norm: vec![1.0; hidden_size],
12567 post_norm: vec![1.0; hidden_size],
12568 attn_out_norm: None,
12569 ffn_out_norm: None,
12570 layer_scale: None,
12571 ffn: FfnKind::Dense(DenseFfn {
12572 gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
12573 up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
12574 down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
12575 act: Act::Silu,
12576 down_t: None,
12577 segs: Vec::new(),
12578 }),
12579 attn: AttnKind::Full {
12580 bias: None,
12581 wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
12582 wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
12583 wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
12584 wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
12585 q_norm: None,
12586 k_norm: None,
12587 output_gate: false,
12588 softplus_gate: None,
12589 },
12590 })
12591 .collect();
12592
12593 Pipeline::new(
12594 Tokenizer::byte_level(),
12595 PipelineWeights {
12596 embed_tokens: qt(vocab_size, hidden_size, 100),
12597 layers: layer_weights,
12598 lm_head: qt(vocab_size, hidden_size, 200),
12599 final_norm: vec![1.0; hidden_size],
12600 },
12601 hidden_size,
12602 intermediate_size,
12603 num_heads,
12604 num_kv_heads,
12605 head_dim,
12606 num_layers,
12607 num_layers, false, vocab_size,
12610 1e-6,
12611 10_000.0,
12612 NormStyle::Qwen,
12613 4096,
12614 SamplerConfig {
12615 seed: Some(42),
12616 ..Default::default()
12617 },
12618 )
12619}
12620
12621#[inline]
12626fn mask_bit(row: &[u8], j: usize) -> bool {
12627 (row.get(j >> 3).copied().unwrap_or(0) >> (j & 7)) & 1 != 0
12628}
12629
12630fn mask_gain() -> f32 {
12641 static G: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
12642 *G.get_or_init(|| {
12643 std::env::var("CMF_FFN_MASK_GAIN")
12644 .ok()
12645 .and_then(|v| v.parse().ok())
12646 .unwrap_or(1.0)
12647 })
12648}
12649
12650fn zero_masked_cols(g: &mut [f32], rows: usize, inter: usize, row: &[u8]) {
12651 let fill = meanfill().and_then(|(i, v)| {
12654 let li = crate::gpu::cur_layer();
12655 (*i == inter && li >= 0).then(|| &v[li as usize * inter..(li as usize + 1) * inter])
12656 });
12657 for r in 0..rows {
12658 let base = r * inter;
12659 for (bi, &byte) in row.iter().enumerate() {
12660 if byte == 0xFF {
12661 continue;
12662 }
12663 let j0 = bi * 8;
12664 for bit in 0..8 {
12665 let j = j0 + bit;
12666 if j < inter && byte & (1 << bit) == 0 {
12667 g[base + j] = fill.map_or(0.0, |f| f[j]);
12668 }
12669 }
12670 }
12671 }
12672 let gain = mask_gain();
12673 if gain != 1.0 {
12674 for v in g[..rows * inter].iter_mut() {
12675 *v *= gain;
12676 }
12677 }
12678}
12679
12680#[inline]
12682fn tube_bit(row: Option<&[u8]>, i: usize) -> bool {
12683 row.is_none_or(|r| mask_bit(r, i))
12684}
12685
12686fn all_bits_on(row: &[u8], n: usize) -> bool {
12689 (0..n).all(|i| mask_bit(row, i))
12690}
12691
12692fn tube_topk() -> usize {
12700 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
12701 *K.get_or_init(|| {
12702 std::env::var("CMF_TUBE_TOPK")
12703 .ok()
12704 .and_then(|v| v.parse().ok())
12705 .unwrap_or(0)
12706 })
12707}
12708
12709fn tube_score_oracle() -> bool {
12710 static O: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
12711 *O.get_or_init(|| std::env::var("CMF_TUBE_SCORE").is_ok_and(|v| v == "oracle"))
12712}
12713
12714fn tube_ffn_routed(
12721 d: &DenseFfn,
12722 xs: &[f32],
12723 b: usize,
12724 pool: Option<&Pool>,
12725 mask_row: Option<&[u8]>,
12726 k: usize,
12727) -> Vec<f32> {
12728 let hidden = d.down_proj.rows();
12729 let core = d.gate_proj.rows();
12730 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
12731 let mut out = match (b, core_full, mask_row) {
12732 (1, true, _) => dense_ffn(d, xs, pool),
12733 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
12734 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
12735 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
12736 };
12737 let cand: Vec<usize> = (0..d.segs.len())
12738 .filter(|&i| tube_bit(mask_row, d.segs[i].start))
12739 .collect();
12740 if cand.is_empty() {
12741 return out;
12742 }
12743 let oracle = tube_score_oracle();
12747 let mut acts: Vec<Vec<f32>> = Vec::with_capacity(cand.len());
12748 let mut scores = vec![0f32; b * cand.len()];
12749 for (ci, &i) in cand.iter().enumerate() {
12750 let seg = &d.segs[i];
12751 let w = seg.width;
12752 let mut g = vec![0.0f32; b * w];
12753 if b == 1 {
12754 seg.gate.matvec(xs, &mut g, pool);
12755 } else {
12756 seg.gate.matmat(xs, b, &mut g, pool);
12757 }
12758 for v in g.iter_mut() {
12759 *v = Act::Silu.combine(*v, 1.0);
12760 }
12761 if !oracle {
12762 for t in 0..b {
12763 scores[t * cand.len() + ci] =
12764 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
12765 }
12766 }
12767 if oracle || b > 1 {
12768 let mut u = vec![0.0f32; b * w];
12769 if b == 1 {
12770 seg.up.matvec(xs, &mut u, pool);
12771 } else {
12772 seg.up.matmat(xs, b, &mut u, pool);
12773 }
12774 for (a, &v) in g.iter_mut().zip(u.iter()) {
12775 *a *= v;
12776 }
12777 if oracle {
12778 for t in 0..b {
12779 scores[t * cand.len() + ci] =
12780 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
12781 }
12782 }
12783 }
12784 acts.push(g);
12785 }
12786 let keep = k.min(cand.len());
12788 let mut scratch: Vec<f32> = Vec::new();
12789 for t in 0..b {
12790 let mut sc: Vec<(f32, usize)> = (0..cand.len())
12791 .map(|ci| (scores[t * cand.len() + ci], ci))
12792 .collect();
12793 sc.sort_unstable_by(|x, y| y.0.total_cmp(&x.0));
12794 let mut alive = vec![false; cand.len()];
12795 for &(_, ci) in sc.iter().take(keep) {
12796 alive[ci] = true;
12797 }
12798 if b > 1 {
12799 for (ci, a) in acts.iter_mut().enumerate() {
12800 if !alive[ci] {
12801 let w = d.segs[cand[ci]].width;
12802 a[t * w..(t + 1) * w].fill(0.0);
12803 }
12804 }
12805 } else {
12806 for (ci, &i) in cand.iter().enumerate() {
12810 if !alive[ci] {
12811 continue;
12812 }
12813 let seg = &d.segs[i];
12814 let w = seg.width;
12815 let g = &mut acts[ci];
12816 if !tube_score_oracle() {
12817 scratch.clear();
12818 scratch.resize(w, 0.0);
12819 seg.up.matvec(xs, &mut scratch, pool);
12820 for (a, &v) in g.iter_mut().zip(scratch.iter()) {
12821 *a *= v;
12822 }
12823 }
12824 let mut acc = vec![0.0f32; hidden];
12825 seg.down.matvec(g, &mut acc, pool);
12826 for (o, a) in out.iter_mut().zip(&acc) {
12827 *o += *a;
12828 }
12829 }
12830 }
12831 }
12832 if b > 1 {
12833 for (ci, &i) in cand.iter().enumerate() {
12834 let seg = &d.segs[i];
12835 let mut acc = vec![0.0f32; b * hidden];
12836 seg.down.matmat(&acts[ci], b, &mut acc, pool);
12837 for (o, a) in out.iter_mut().zip(&acc) {
12838 *o += *a;
12839 }
12840 }
12841 }
12842 out
12843}
12844
12845fn tube_ffn(
12851 d: &DenseFfn,
12852 xs: &[f32],
12853 b: usize,
12854 pool: Option<&Pool>,
12855 mask_row: Option<&[u8]>,
12856) -> Vec<f32> {
12857 if tube_topk() > 0 {
12858 return tube_ffn_routed(d, xs, b, pool, mask_row, tube_topk());
12859 }
12860 let hidden = d.down_proj.rows();
12861 let core = d.gate_proj.rows();
12862 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
12863 let mut out = match (b, core_full, mask_row) {
12864 (1, true, _) => dense_ffn(d, xs, pool),
12865 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
12866 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
12867 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
12868 };
12869 TUBE_SCRATCH.with(|sc| {
12870 let mut sc = sc.borrow_mut();
12871 let [g, u, acc] = &mut *sc;
12872 for seg in &d.segs {
12873 if !tube_bit(mask_row, seg.start) {
12874 continue;
12875 }
12876 let w = seg.width;
12877 g.resize(b * w, 0.0);
12878 if b == 1
12879 && d.act == Act::Silu
12880 && QTensor::matvec_silu_mul(&seg.gate, &seg.up, xs, g, pool)
12881 {
12882 } else {
12884 u.resize(b * w, 0.0);
12885 if b == 1 {
12886 QTensor::matvec_many([&seg.gate, &seg.up], xs, [g, u], pool);
12887 } else {
12888 seg.gate.matmat(xs, b, g, pool);
12889 seg.up.matmat(xs, b, u, pool);
12890 }
12891 for i in 0..b * w {
12892 g[i] = d.act.combine(g[i], u[i]);
12893 }
12894 }
12895 acc.resize(b * hidden, 0.0);
12896 acc.fill(0.0);
12897 if b == 1 {
12898 seg.down.matvec(g, acc, pool);
12899 } else {
12900 seg.down.matmat(g, b, acc, pool);
12901 }
12902 for (o, a) in out.iter_mut().zip(acc.iter()) {
12903 *o += *a;
12904 }
12905 }
12906 out
12907 })
12908}
12909
12910thread_local! {
12911 static TUBE_SCRATCH: std::cell::RefCell<[Vec<f32>; 3]> =
12915 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new()]) };
12916}
12917
12918fn dense_ffn_batch(
12919 d: &DenseFfn,
12920 xs: &[f32],
12921 b: usize,
12922 pool: Option<&Pool>,
12923 mask_row: Option<&[u8]>,
12924) -> Vec<f32> {
12925 let inter = d.gate_proj.rows();
12926 let hidden = d.down_proj.rows();
12927 if mask_row.is_none()
12935 && d.act == Act::Silu
12936 && b >= 32
12937 && crate::gpu::enabled_here()
12938 && !crate::gpu::mm_killed()
12939 && refit_dir().is_none()
12944 && !ffn_probe_active()
12949 {
12950 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
12951 d.gate_proj.mapped_q4t(),
12952 d.up_proj.mapped_q4t(),
12953 d.down_proj.mapped_q4t(),
12954 ) {
12955 let mut out = vec![0.0f32; b * hidden];
12956 if crate::gpu::q4t_ffn(model, w1, w3, w2, xs, b, hidden, inter, &mut out) {
12957 return out;
12958 }
12959 }
12960 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
12965 d.gate_proj.mapped_q4tp(),
12966 d.up_proj.mapped_q4tp(),
12967 d.down_proj.mapped_q4tp(),
12968 ) {
12969 let mut out = vec![0.0f32; b * hidden];
12970 if crate::gpu::q4tp_ffn(model, w1, w3, w2, xs, b, hidden, inter, &mut out) {
12971 return out;
12972 }
12973 }
12974 }
12975 let mut g = vec![0.0f32; b * inter];
12976 d.gate_proj.matmat(xs, b, &mut g, pool);
12977 let mut u = vec![0.0f32; b * inter];
12978 d.up_proj.matmat(xs, b, &mut u, pool);
12979 if gate_topk() > 0 && d.act == Act::Silu {
12980 for t in 0..b {
12981 let row = &mut g[t * inter..(t + 1) * inter];
12982 for v in row.iter_mut() {
12983 *v = Act::Silu.combine(*v, 1.0);
12984 }
12985 keep_top_k(row, gate_topk());
12986 }
12987 for i in 0..b * inter {
12988 g[i] *= u[i];
12989 }
12990 } else {
12991 for i in 0..b * inter {
12992 g[i] = d.act.combine(g[i], u[i]);
12993 }
12994 }
12995 if let Some(row) = mask_row {
12996 zero_masked_cols(&mut g, b, inter, row);
12997 }
12998 if oracle_topk() > 0 {
12999 for t in 0..b {
13000 keep_top_k(&mut g[t * inter..(t + 1) * inter], oracle_topk());
13001 }
13002 }
13003 let mut out = vec![0.0f32; b * hidden];
13004 d.down_proj.matmat(&g, b, &mut out, pool);
13005 if refit_dir().is_some() {
13006 let li = crate::gpu::cur_layer();
13007 if li >= 0 {
13008 refit_accumulate(li as usize, &g, b, inter, &out, hidden, pool);
13009 }
13010 }
13011 FFN_PROBE.with(|pr| {
13015 if let Some(acc) = pr.borrow_mut().as_mut() {
13016 let li = crate::gpu::cur_layer();
13017 if li < 0 {
13018 return;
13019 }
13020 let Some(row) = acc.get_mut(li as usize) else {
13021 return;
13022 };
13023 let sq = probe_sq();
13024 for t in 0..b {
13025 for (a, &v) in row.iter_mut().zip(&g[t * inter..(t + 1) * inter]) {
13026 *a += if sq {
13027 (v as f64) * (v as f64)
13028 } else {
13029 (v as f64).abs()
13030 };
13031 }
13032 }
13033 }
13034 });
13035 out
13036}
13037
13038fn accumulate_act(m: &MoeFfn, xs: &[f32], b: usize) {
13043 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
13044 static DUMP: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
13045 let on = *ON.get_or_init(|| std::env::var("CMF_RMS_TRACE").is_ok());
13046 let dump = *DUMP.get_or_init(|| std::env::var("CMF_ACT_DUMP").is_ok());
13047 if (!on && !dump) || b == 0 {
13048 return;
13049 }
13050 let hidden = xs.len() / b;
13051 if on {
13052 let mut acc = m.act_sq.borrow_mut();
13053 if acc.len() < hidden {
13054 acc.resize(hidden, 0.0);
13055 }
13056 for t in 0..b {
13057 let row = &xs[t * hidden..(t + 1) * hidden];
13058 for (a, &v) in acc.iter_mut().zip(row) {
13059 *a += (v as f64) * (v as f64);
13060 }
13061 }
13062 }
13063 if dump {
13064 let cap: usize = std::env::var("CMF_ACT_DUMP_ROWS")
13067 .ok()
13068 .and_then(|v| v.parse().ok())
13069 .unwrap_or(4096);
13070 let mut rows = m.act_rows.borrow_mut();
13071 if rows.len() < cap * hidden {
13072 let take = b.min((cap * hidden - rows.len()) / hidden.max(1));
13073 rows.extend_from_slice(&xs[..take * hidden]);
13074 }
13075 }
13076}
13077
13078#[derive(Clone, Copy)]
13081struct SendVecs(*mut Vec<f32>);
13082unsafe impl Send for SendVecs {}
13083unsafe impl Sync for SendVecs {}
13084impl SendVecs {
13085 #[inline]
13086 fn at(self, i: usize) -> *mut Vec<f32> {
13087 unsafe { self.0.add(i) }
13088 }
13089}
13090
13091fn moe_ffn_batch(
13092 m: &MoeFfn,
13093 xs: &[f32],
13094 b: usize,
13095 hidden: usize,
13096 pool: Option<&Pool>,
13097 allowed: Option<&[bool]>,
13098) -> Vec<f32> {
13099 accumulate_act(m, xs, b);
13100 let ne = m.experts.len();
13101 let mut logits = vec![0.0f32; b * ne];
13102 match &m.resonance {
13103 Some(r) => {
13104 let hdim = xs.len() / b.max(1);
13105 for bi in 0..b {
13106 r.scores(
13107 &xs[bi * hdim..(bi + 1) * hdim],
13108 &mut logits[bi * ne..(bi + 1) * ne],
13109 );
13110 }
13111 }
13112 None => m.router.matmat(xs, b, &mut logits, pool),
13113 }
13114
13115 let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
13118 {
13119 let mut st = m.stats.borrow_mut();
13120 if st.len() < ne {
13121 st.resize(ne, 0);
13122 }
13123 for bi in 0..b {
13124 let (idx, p, wsum) = moe_route(&logits[bi * ne..(bi + 1) * ne], m, allowed);
13125 for &e in &idx {
13126 st[e] += 1;
13127 assign[e].push((bi, p[e] / wsum));
13128 }
13129 }
13130 }
13131
13132 let mut out = vec![0.0f32; b * hidden];
13133 let cols = m.experts[0].gate_proj.cols();
13134 let run_expert = |d: &DenseFfn, list: &[(usize, f32)], out: &mut [f32]| {
13135 let sb = list.len();
13136 let mut sub = vec![0.0f32; sb * cols];
13137 for (k, &(bi, _)) in list.iter().enumerate() {
13138 sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
13139 }
13140 let eo = dense_ffn_batch(d, &sub, sb, pool, None);
13141 for (k, &(bi, w)) in list.iter().enumerate() {
13142 for i in 0..hidden {
13143 out[bi * hidden + i] += w * eo[k * hidden + i];
13144 }
13145 }
13146 };
13147 let active: Vec<usize> = (0..ne).filter(|&e| !assign[e].is_empty()).collect();
13153 if pool.is_some() && active.len() >= 8 {
13154 let mut panels: Vec<Vec<f32>> = vec![Vec::new(); active.len()];
13155 {
13156 let panel_ptr = SendVecs(panels.as_mut_ptr());
13157 let experts = &m.experts;
13160 let (active_r, assign_r) = (&active, &assign);
13161 let run = |start: usize, end: usize| {
13162 for ai in start..end {
13163 let e = active_r[ai];
13164 let list = &assign_r[e];
13165 let sb = list.len();
13166 let mut sub = vec![0.0f32; sb * cols];
13167 for (k, &(bi, _)) in list.iter().enumerate() {
13168 sub[k * cols..(k + 1) * cols]
13169 .copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
13170 }
13171 unsafe {
13173 *panel_ptr.at(ai) = dense_ffn_batch(&experts[e], &sub, sb, None, None);
13174 }
13175 }
13176 };
13177 match pool {
13178 Some(p) => p.run_rows(active.len(), &run),
13179 None => run(0, active.len()),
13180 }
13181 }
13182 for (ai, &e) in active.iter().enumerate() {
13183 for (k, &(bi, w)) in assign[e].iter().enumerate() {
13184 let eo = &panels[ai][k * hidden..(k + 1) * hidden];
13185 for i in 0..hidden {
13186 out[bi * hidden + i] += w * eo[i];
13187 }
13188 }
13189 }
13190 } else {
13191 for &e in &active {
13192 run_expert(&m.experts[e], &assign[e], &mut out);
13193 }
13194 }
13195 if let Some((se, gate)) = &m.shared {
13196 let all: Vec<(usize, f32)> = if let Some(gate) = gate {
13197 let mut gl = vec![0.0f32; b];
13198 gate.matmat(xs, b, &mut gl, pool);
13199 (0..b)
13200 .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
13201 .collect()
13202 } else {
13203 (0..b).map(|bi| (bi, 1.0)).collect()
13204 };
13205 run_expert(se, &all, &mut out);
13206 }
13207 out
13208}
13209
13210thread_local! {
13211 static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
13215 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
13216}
13217
13218fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
13220 if gate_topk() > 0
13223 && let Some(out) = dense_ffn_dynamic(d, x, pool, gate_topk())
13224 {
13225 return out;
13226 }
13227 let prism_body = d.gate_proj.has_prism_contract()
13243 || d.up_proj.has_prism_contract()
13244 || d.down_proj.has_prism_contract();
13245 if !prism_body
13246 && crate::gpu::enabled_here()
13247 && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
13248 {
13249 let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
13250 crate::gpu::ProbeArm::Gpu
13251 } else {
13252 crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
13253 };
13254 match arm {
13255 crate::gpu::ProbeArm::Gpu => {
13256 let t0 = std::time::Instant::now();
13257 if let Some(out) = dense_ffn_gpu(d, x, pool) {
13258 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
13259 return out;
13260 }
13261 crate::gpu::probe_note_decline(crate::gpu::OpClass::Ffn);
13265 }
13266 crate::gpu::ProbeArm::CpuTimed => {
13267 let t0 = std::time::Instant::now();
13268 let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
13269 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
13270 return out;
13271 }
13272 crate::gpu::ProbeArm::Cpu => {
13273 return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
13274 }
13275 }
13276 }
13277 dense_ffn_cpu(d, x, pool)
13278}
13279
13280fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
13282 let inter = d.gate_proj.rows();
13283 FFN_SCRATCH.with(|s| {
13284 let mut s = s.borrow_mut();
13285 let [g, u, ..] = &mut *s;
13286 g.resize(inter, 0.0);
13287 if gate_topk() > 0 {
13290 u.resize(inter, 0.0);
13294 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
13295 for i in 0..inter {
13296 g[i] = Act::Silu.combine(g[i], 1.0);
13297 }
13298 keep_top_k(g, gate_topk());
13299 for i in 0..inter {
13300 g[i] *= u[i];
13301 }
13302 } else if d.act == Act::Silu && {
13303 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
13304 QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool)
13305 } {
13306 } else {
13308 u.resize(inter, 0.0);
13309 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
13311 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
13312 for i in 0..inter {
13313 g[i] = d.act.combine(g[i], u[i]);
13314 }
13315 }
13316 FFN_PROBE.with(|pr| {
13324 if let Some(acc) = pr.borrow_mut().as_mut() {
13325 let li = crate::gpu::cur_layer();
13326 if li >= 0 {
13327 if let Some(row) = acc.get_mut(li as usize) {
13328 match probe_topk() {
13329 0 if probe_sq() => {
13330 for (a, &v) in row.iter_mut().zip(g.iter()) {
13331 *a += (v as f64) * (v as f64);
13332 }
13333 }
13334 0 if probe_signed() => {
13335 for (a, &v) in row.iter_mut().zip(g.iter()) {
13336 *a += v as f64;
13337 }
13338 }
13339 0 => {
13340 for (a, &v) in row.iter_mut().zip(g.iter()) {
13341 *a += (v as f64).abs();
13342 }
13343 }
13344 k => {
13345 let n = g.len();
13346 let k = k.min(n);
13347 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
13348 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
13349 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
13350 });
13351 let thr = *kth;
13352 for (a, &v) in row.iter_mut().zip(g.iter()) {
13353 if v.abs() >= thr {
13354 *a += 1.0;
13355 }
13356 }
13357 }
13358 }
13359 }
13360 }
13361 }
13362 });
13363 if oracle_topk() > 0 {
13364 keep_top_k(g, oracle_topk());
13365 }
13366 {
13367 let li = crate::gpu::cur_layer();
13368 if li >= 0 {
13369 adump_row(li as usize, g);
13370 }
13371 }
13372 let mut out = attention::take_buf(d.down_proj.rows());
13373 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnDown);
13374 d.down_proj.matvec(g, &mut out, pool);
13375 out
13376 })
13377}
13378
13379pub struct RefitAcc {
13392 pub support: Vec<u32>,
13393 pub gss: Vec<f32>,
13394 pub ya: Vec<f32>,
13395 pub hidden: usize,
13396 pub tokens: u64,
13397 pub buf_g: Vec<f32>,
13403 pub buf_o: Vec<f32>,
13404 pub buf_t: usize,
13405}
13406
13407type RefitState = (std::collections::HashMap<usize, RefitAcc>, Vec<f32>);
13411
13412static REFIT: std::sync::OnceLock<Option<(String, std::sync::Mutex<RefitState>)>> =
13413 std::sync::OnceLock::new();
13414
13415fn ffn_probe_active() -> bool {
13418 FFN_PROBE.with(|p| p.borrow().is_some())
13419}
13420
13421fn refit_dir() -> Option<&'static (String, std::sync::Mutex<RefitState>)> {
13422 REFIT
13423 .get_or_init(|| {
13424 std::env::var("CMF_FFN_REFIT").ok().map(|d| {
13425 (
13426 d,
13427 std::sync::Mutex::new((std::collections::HashMap::new(), Vec::new())),
13428 )
13429 })
13430 })
13431 .as_ref()
13432}
13433
13434fn refit_accumulate(
13436 li: usize,
13437 g: &[f32],
13438 b: usize,
13439 inter: usize,
13440 out: &[f32],
13441 hidden: usize,
13442 pool: Option<&Pool>,
13443) {
13444 let Some((dir, map)) = refit_dir() else {
13445 return;
13446 };
13447 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
13448 let (from, to) = *SPAN.get_or_init(|| {
13449 let g = |k: &str, d: usize| {
13450 std::env::var(k)
13451 .ok()
13452 .and_then(|v| v.parse().ok())
13453 .unwrap_or(d)
13454 };
13455 (
13456 g("CMF_FFN_REFIT_FROM", 0),
13457 g("CMF_FFN_REFIT_TO", usize::MAX),
13458 )
13459 });
13460 if li < from || li > to {
13461 return;
13462 }
13463 let mut guard = map.lock().unwrap();
13464 let (map, shared) = &mut *guard;
13465 let acc = match map.entry(li) {
13466 std::collections::hash_map::Entry::Occupied(e) => e.into_mut(),
13467 std::collections::hash_map::Entry::Vacant(e) => {
13468 let path = format!("{dir}/support.{li}.u32");
13469 let Ok(bytes) = std::fs::read(&path) else {
13470 eprintln!("refit: no {path} — layer {li} skipped");
13471 return;
13472 };
13473 let n = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
13474 let support: Vec<u32> = bytes[4..4 + n * 4]
13475 .chunks_exact(4)
13476 .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
13477 .collect();
13478 eprintln!(
13479 "refit: layer {li} support {n} ({:.0} MB of accumulator)",
13480 (n * n + hidden * n) as f64 * 4.0 / 1e6
13481 );
13482 e.insert(RefitAcc {
13483 gss: vec![0.0; n * n],
13484 ya: vec![0.0; hidden * n],
13485 buf_g: Vec::new(),
13486 buf_o: Vec::new(),
13487 buf_t: 0,
13488 support,
13489 hidden,
13490 tokens: 0,
13491 })
13492 }
13493 };
13494 let ns = acc.support.len();
13495 let cap = refit_batch();
13497 if acc.buf_g.is_empty() {
13498 acc.buf_g = vec![0.0; ns * cap];
13499 acc.buf_o = vec![0.0; hidden * cap];
13500 }
13501 let take = b.min(cap - acc.buf_t);
13502 for t in 0..take {
13503 let col = acc.buf_t + t;
13504 for (j, &n) in acc.support.iter().enumerate() {
13505 acc.buf_g[j * cap + col] = g[t * inter + n as usize];
13506 }
13507 for h in 0..hidden {
13508 acc.buf_o[h * cap + col] = out[t * hidden + h];
13509 }
13510 }
13511 acc.buf_t += take;
13512 acc.tokens += take as u64;
13513 if acc.buf_t < cap {
13514 return;
13515 }
13516 let bt = acc.buf_t;
13517 acc.buf_t = 0;
13518 let RefitAcc {
13528 gss,
13529 ya,
13530 buf_g,
13531 buf_o,
13532 ..
13533 } = acc;
13534 let need = (ns * ns).max(hidden * ns);
13535 if shared.len() < need {
13536 shared.resize(need, 0.0);
13537 }
13538 let scratch = &mut shared[..];
13539 let _ = bt;
13540 if crate::gpu::gemm_nt_f32_transient(buf_g, buf_g, &mut scratch[..ns * ns], ns, cap, ns) {
13541 add_into(gss, &scratch[..ns * ns], pool);
13542 if crate::gpu::gemm_nt_f32_transient(
13543 buf_o,
13544 buf_g,
13545 &mut scratch[..hidden * ns],
13546 hidden,
13547 cap,
13548 ns,
13549 ) {
13550 add_into(ya, &scratch[..hidden * ns], pool);
13551 } else {
13552 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
13553 }
13554 } else {
13555 accum_outer_t(gss, ns, ns, cap, buf_g, buf_g, pool);
13556 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
13557 }
13558 }
13562
13563fn refit_batch() -> usize {
13565 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
13566 *B.get_or_init(|| {
13567 std::env::var("CMF_FFN_REFIT_BATCH")
13568 .ok()
13569 .and_then(|v| v.parse().ok())
13570 .unwrap_or(4096)
13571 })
13572}
13573
13574fn accum_outer_t(
13577 c: &mut [f32],
13578 m: usize,
13579 n: usize,
13580 b: usize,
13581 left: &[f32],
13582 right: &[f32],
13583 pool: Option<&Pool>,
13584) {
13585 let ptr = SendMut(c.as_mut_ptr());
13586 let body = |i: usize| {
13587 let ptr = &ptr;
13588 let row = unsafe { std::slice::from_raw_parts_mut(ptr.0.add(i * n), n) };
13589 for t in 0..b {
13590 let a = left[i * b + t];
13591 if a == 0.0 {
13592 continue;
13593 }
13594 for (j, o) in row.iter_mut().enumerate() {
13595 *o += a * right[j * b + t];
13596 }
13597 }
13598 };
13599 match pool {
13600 Some(p) if m > 1 => p.run_rows(m, &|s, e| {
13601 for i in s..e {
13602 body(i);
13603 }
13604 }),
13605 _ => {
13606 for i in 0..m {
13607 body(i);
13608 }
13609 }
13610 }
13611}
13612
13613fn add_into(dst: &mut [f32], src: &[f32], pool: Option<&Pool>) {
13616 let n = dst.len().min(src.len());
13617 match pool {
13618 Some(p) if n >= 1 << 16 => {
13619 let ptr = SendMut(dst.as_mut_ptr());
13620 let f = |s: usize, e: usize| {
13621 let ptr = &ptr;
13622 for blk in s..e {
13623 let (a, b) = (blk * 4096, ((blk + 1) * 4096).min(n));
13624 for i in a..b {
13625 unsafe { *ptr.0.add(i) += src[i] };
13626 }
13627 }
13628 };
13629 p.run_rows(n.div_ceil(4096), &f);
13630 }
13631 _ => {
13632 for (d, v) in dst.iter_mut().zip(&src[..n]) {
13633 *d += *v;
13634 }
13635 }
13636 }
13637}
13638
13639fn accum_outer(
13644 c: &mut [f32],
13645 m: usize,
13646 n: usize,
13647 b: usize,
13648 left: &[f32],
13649 right: &[f32],
13650 pool: Option<&Pool>,
13651) {
13652 const TILE: usize = 32;
13653 let tiles = m.div_ceil(TILE);
13654 let cp = SendMut(c.as_mut_ptr());
13655 let body = |ti: usize| {
13656 let cp = &cp;
13657 let i0 = ti * TILE;
13658 let i1 = (i0 + TILE).min(m);
13659 for t in 0..b {
13660 let r = &right[t * n..t * n + n];
13661 for i in i0..i1 {
13662 let a = left[i * b + t];
13663 if a == 0.0 {
13664 continue;
13665 }
13666 let row = unsafe { std::slice::from_raw_parts_mut(cp.0.add(i * n), n) };
13668 for (o, v) in row.iter_mut().zip(r) {
13669 *o += a * *v;
13670 }
13671 }
13672 }
13673 };
13674 match pool {
13675 Some(p) if tiles > 1 => p.run_rows(tiles, &|s, e| {
13676 for ti in s..e {
13677 body(ti);
13678 }
13679 }),
13680 _ => {
13681 for ti in 0..tiles {
13682 body(ti);
13683 }
13684 }
13685 }
13686}
13687
13688pub fn refit_flush() -> usize {
13690 let Some((dir, map)) = refit_dir() else {
13691 return 0;
13692 };
13693 let guard = map.lock().unwrap();
13694 let mut n = 0;
13695 for (li, acc) in guard.0.iter() {
13696 let w = |name: &str, v: &[f32]| {
13699 let path = format!("{dir}/{name}.{li}.f32");
13700 let bytes: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
13701 match std::fs::write(&path, &bytes) {
13702 Ok(()) => {}
13703 Err(e) => eprintln!(
13704 "refit: FAILED to write {path} ({} MB): {e}",
13705 bytes.len() / 1_000_000
13706 ),
13707 }
13708 };
13709 w("gss", &acc.gss);
13710 w("ya", &acc.ya);
13711 println!(
13712 "refit L{li}: {} support, {} tokens, hidden {}",
13713 acc.support.len(),
13714 acc.tokens,
13715 acc.hidden
13716 );
13717 n += 1;
13718 }
13719 n
13720}
13721
13722fn adump_row(li: usize, g: &[f32]) {
13727 use std::io::Write as _;
13728 static FILES: std::sync::OnceLock<
13729 Option<(
13730 String,
13731 std::sync::Mutex<std::collections::HashMap<usize, std::fs::File>>,
13732 )>,
13733 > = std::sync::OnceLock::new();
13734 let Some((prefix, map)) = FILES
13735 .get_or_init(|| {
13736 std::env::var("CMF_FFN_ADUMP")
13737 .ok()
13738 .map(|p| (p, std::sync::Mutex::new(std::collections::HashMap::new())))
13739 })
13740 .as_ref()
13741 else {
13742 return;
13743 };
13744 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
13747 let (from, to) = *SPAN.get_or_init(|| {
13748 let g = |k: &str, d: usize| {
13749 std::env::var(k)
13750 .ok()
13751 .and_then(|v| v.parse().ok())
13752 .unwrap_or(d)
13753 };
13754 (
13755 g("CMF_FFN_ADUMP_FROM", 0),
13756 g("CMF_FFN_ADUMP_TO", usize::MAX),
13757 )
13758 });
13759 if li < from || li > to {
13760 return;
13761 }
13762 let mut map = map.lock().unwrap();
13763 let f = map.entry(li).or_insert_with(|| {
13764 std::fs::File::create(format!("{prefix}.{li}.f16")).expect("adump file")
13765 });
13766 let mut bytes = Vec::with_capacity(g.len() * 2);
13767 for v in g {
13768 bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(*v).to_le_bytes());
13769 }
13770 let _ = f.write_all(&bytes);
13771}
13772
13773fn oracle_topk() -> usize {
13779 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
13780 *K.get_or_init(|| {
13781 std::env::var("CMF_FFN_ORACLE_TOPK")
13782 .ok()
13783 .and_then(|v| v.parse().ok())
13784 .unwrap_or(0)
13785 })
13786}
13787
13788fn gate_topk() -> usize {
13794 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
13795 *K.get_or_init(|| {
13796 std::env::var("CMF_FFN_GATE_TOPK")
13797 .ok()
13798 .and_then(|v| v.parse().ok())
13799 .unwrap_or(0)
13800 })
13801}
13802
13803fn gate_block() -> usize {
13810 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
13811 *B.get_or_init(|| {
13812 std::env::var("CMF_FFN_GATE_BLOCK")
13813 .ok()
13814 .and_then(|v| v.parse().ok())
13815 .unwrap_or(1)
13816 })
13817}
13818
13819fn keep_top_blocks(g: &mut [f32], keep_n: usize, block: usize) {
13821 let n = g.len();
13822 let nb = n.div_ceil(block);
13823 let kb = (keep_n.div_ceil(block)).clamp(1, nb);
13824 if kb >= nb {
13825 return;
13826 }
13827 let mut score: Vec<f32> = (0..nb)
13828 .map(|b| {
13829 g[b * block..((b + 1) * block).min(n)]
13830 .iter()
13831 .map(|v| v * v)
13832 .sum::<f32>()
13833 })
13834 .collect();
13835 let mut ord = score.clone();
13836 let (_, kth, _) = ord.select_nth_unstable_by(kb - 1, |a, b| {
13837 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
13838 });
13839 let thr = *kth;
13840 for b in 0..nb {
13841 if score[b] < thr {
13842 g[b * block..((b + 1) * block).min(n)].fill(0.0);
13843 }
13844 }
13845 score.clear();
13846}
13847
13848fn keep_top_k(g: &mut [f32], k: usize) {
13850 if gate_block() > 1 {
13851 return keep_top_blocks(g, k, gate_block());
13852 }
13853 let n = g.len();
13854 if k == 0 || k >= n {
13855 return;
13856 }
13857 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
13858 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
13859 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
13860 });
13861 let thr = *kth;
13862 for v in g.iter_mut() {
13863 if v.abs() < thr {
13864 *v = 0.0;
13865 }
13866 }
13867}
13868
13869fn probe_sq() -> bool {
13873 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
13874 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SQ").is_ok())
13875}
13876
13877fn probe_signed() -> bool {
13881 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
13882 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SIGNED").is_ok())
13883}
13884
13885fn meanfill() -> Option<&'static (usize, Vec<f32>)> {
13893 static M: std::sync::OnceLock<Option<(usize, Vec<f32>)>> = std::sync::OnceLock::new();
13894 M.get_or_init(|| {
13895 let p = std::env::var("CMF_FFN_MEANFILL").ok()?;
13896 let b = std::fs::read(&p).ok()?;
13897 let inter = u32::from_le_bytes(b[4..8].try_into().ok()?) as usize;
13898 let vals: Vec<f32> = b[8..]
13899 .chunks_exact(4)
13900 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
13901 .collect();
13902 eprintln!("meanfill: {} value(s), inter {inter}", vals.len());
13903 Some((inter, vals))
13904 })
13905 .as_ref()
13906}
13907
13908fn probe_topk() -> usize {
13911 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
13912 *K.get_or_init(|| {
13913 std::env::var("CMF_FFN_PROBE_TOPK")
13914 .ok()
13915 .and_then(|v| v.parse().ok())
13916 .unwrap_or(0)
13917 })
13918}
13919
13920thread_local! {
13921 static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
13924 const { std::cell::RefCell::new(None) };
13925}
13926
13927fn dense_ffn_dynamic(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, k: usize) -> Option<Vec<f32>> {
13940 if d.gate_proj.has_prism_contract()
13945 || d.up_proj.has_prism_contract()
13946 || d.down_proj.has_prism_contract()
13947 {
13948 return None;
13949 }
13950 let dt = d.down_t.as_ref()?;
13951 let inter = d.gate_proj.rows();
13952 let hidden = dt.cols();
13953 if k == 0 || k >= inter || d.act != Act::Silu {
13954 return None;
13955 }
13956 DYN_SCRATCH.with(|sc| {
13957 let mut sc = sc.borrow_mut();
13958 let DynScratch {
13959 g,
13960 mag,
13961 live,
13962 parts,
13963 } = &mut *sc;
13964 g.resize(inter, 0.0);
13965 d.gate_proj.matvec(x, g, pool);
13966 for v in g.iter_mut() {
13967 *v = inference::silu(*v);
13968 }
13969 mag.clear();
13972 mag.extend(g.iter().map(|v| v.abs()));
13973 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
13974 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
13975 });
13976 let thr = *kth;
13977 live.clear();
13978 live.extend((0..inter as u32).filter(|&n| g[n as usize].abs() >= thr));
13979 let mut out = vec![0.0f32; hidden];
13980 match pool {
13981 Some(p) if live.len() >= 64 => {
13982 let nw = p.n_workers() + 1;
13983 parts.clear();
13984 parts.resize(nw * hidden, 0.0);
13985 let ptr = SendMut(parts.as_mut_ptr());
13986 let n = live.len();
13987 let live_ref: &[u32] = live;
13988 let g_ref: &[f32] = g;
13989 p.run(&|w, workers| {
13990 let chunk = n.div_ceil(workers);
13991 let (s, e) = (w * chunk, ((w + 1) * chunk).min(n));
13992 if s >= e {
13993 return;
13994 }
13995 WORKER_SCRATCH.with(|ws| {
13996 let mut ws = ws.borrow_mut();
13997 let [scratch, acc] = &mut *ws;
13998 scratch.resize(hidden.max(x.len()), 0.0);
13999 acc.clear();
14000 acc.resize(hidden, 0.0);
14001 for (o, &nrm) in live_ref[s..e].iter().enumerate() {
14002 if let Some(&nx) = live_ref[s..e].get(o + 1) {
14005 d.up_proj.prefetch_row(nx as usize);
14006 dt.prefetch_row(nx as usize);
14007 }
14008 let idx = nrm as usize;
14009 let up = d.up_proj.row_dot(idx, x, scratch);
14010 let a = g_ref[idx] * up;
14011 if a != 0.0 {
14012 dt.add_row_scaled(idx, a, acc, scratch);
14013 }
14014 }
14015 for (j, v) in acc.iter().enumerate() {
14016 unsafe { *ptr.at(w * hidden + j) = *v };
14017 }
14018 });
14019 });
14020 for w in 0..nw {
14021 for (j, o) in out.iter_mut().enumerate() {
14022 *o += parts[w * hidden + j];
14023 }
14024 }
14025 }
14026 _ => {
14027 WORKER_SCRATCH.with(|ws| {
14028 let mut ws = ws.borrow_mut();
14029 let [scratch, _acc] = &mut *ws;
14030 scratch.resize(hidden.max(x.len()), 0.0);
14031 for &nrm in live.iter() {
14032 let idx = nrm as usize;
14033 let up = d.up_proj.row_dot(idx, x, scratch);
14034 let a = g[idx] * up;
14035 if a != 0.0 {
14036 dt.add_row_scaled(idx, a, &mut out, scratch);
14037 }
14038 }
14039 });
14040 }
14041 }
14042 Some(out)
14043 })
14044}
14045
14046struct DynScratch {
14049 g: Vec<f32>,
14050 mag: Vec<f32>,
14051 live: Vec<u32>,
14052 parts: Vec<f32>,
14053}
14054
14055thread_local! {
14056 static DYN_SCRATCH: std::cell::RefCell<DynScratch> = const {
14057 std::cell::RefCell::new(DynScratch {
14058 g: Vec::new(),
14059 mag: Vec::new(),
14060 live: Vec::new(),
14061 parts: Vec::new(),
14062 })
14063 };
14064 static WORKER_SCRATCH: std::cell::RefCell<[Vec<f32>; 2]> =
14066 const { std::cell::RefCell::new([Vec::new(), Vec::new()]) };
14067}
14068
14069fn dense_ffn_masked(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, mask_row: &[u8]) -> Vec<f32> {
14074 let inter = d.gate_proj.rows();
14075 FFN_SCRATCH.with(|s| {
14076 let mut s = s.borrow_mut();
14077 let [g, u, ..] = &mut *s;
14078 g.resize(inter, 0.0);
14079 if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
14080 } else {
14082 u.resize(inter, 0.0);
14083 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
14084 for i in 0..inter {
14085 g[i] = d.act.combine(g[i], u[i]);
14086 }
14087 }
14088 zero_masked_cols(g, 1, inter, mask_row);
14089 let mut out = attention::take_buf(d.down_proj.rows());
14090 d.down_proj.matvec(g, &mut out, pool);
14091 out
14092 })
14093}
14094
14095fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
14101 if d.gate_proj.has_prism_contract()
14102 || d.up_proj.has_prism_contract()
14103 || d.down_proj.has_prism_contract()
14104 {
14105 return None;
14106 }
14107 if d.act != Act::Silu {
14109 return None;
14110 }
14111 if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
14114 return None;
14115 }
14116 let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
14117 let mut model_ref = None;
14118 moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
14119 let model = model_ref?;
14120 let hidden = jobs[0].down.1;
14121 let mut out = attention::take_buf(hidden);
14122 if crate::gpu::moe_block(&model, &jobs, &mut out) {
14123 Some(out)
14124 } else {
14125 let mut out = out;
14126 attention::recycle_buf(&mut out);
14127 None
14128 }
14129}
14130
14131#[allow(clippy::type_complexity)]
14136#[allow(clippy::type_complexity)]
14137pub(crate) fn moe_parts(
14138 t: &QTensor,
14139) -> Option<(
14140 &std::sync::Arc<cortiq_core::CmfModel>,
14141 usize,
14142 usize,
14143 usize,
14144 &[f32],
14145 &[f32],
14146 bool,
14147 bool,
14148 bool,
14149)> {
14150 match t {
14151 QTensor::Mapped {
14152 model,
14153 idx,
14154 dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
14155 rows,
14156 cols,
14157 row_scale,
14158 col_field,
14159 ..
14160 } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => Some((
14161 model, *idx, *rows, *cols, row_scale, col_field, false, false, false,
14162 )),
14163 QTensor::Mapped {
14165 model,
14166 idx,
14167 dtype: cortiq_core::TensorDtype::Q1,
14168 rows,
14169 cols,
14170 ..
14171 } => Some((
14172 model,
14173 *idx,
14174 *rows,
14175 *cols,
14176 &[][..],
14177 &[][..],
14178 true,
14179 false,
14180 false,
14181 )),
14182 QTensor::Mapped {
14184 model,
14185 idx,
14186 dtype: cortiq_core::TensorDtype::Q4Tiled,
14187 rows,
14188 cols,
14189 ..
14190 } => Some((
14191 model,
14192 *idx,
14193 *rows,
14194 *cols,
14195 &[][..],
14196 &[][..],
14197 false,
14198 true,
14199 false,
14200 )),
14201 QTensor::Mapped {
14203 model,
14204 idx,
14205 dtype: cortiq_core::TensorDtype::Q4TiledP,
14206 rows,
14207 cols,
14208 ..
14209 } => Some((
14210 model,
14211 *idx,
14212 *rows,
14213 *cols,
14214 &[][..],
14215 &[][..],
14216 false,
14217 true,
14218 false,
14219 )),
14220 QTensor::Mapped {
14224 model,
14225 idx,
14226 dtype: cortiq_core::TensorDtype::Q2TiledP,
14227 rows,
14228 cols,
14229 ..
14230 } => Some((
14231 model,
14232 *idx,
14233 *rows,
14234 *cols,
14235 &[][..],
14236 &[][..],
14237 false,
14238 true,
14239 true,
14240 )),
14241 _ => None,
14242 }
14243}
14244
14245#[cfg(target_os = "macos")]
14253fn metal_moe_graph_parts(m: &MoeFfn, hidden: usize) -> Option<crate::gpu::GpuMoe<'_>> {
14254 if m.router_input_norm
14255 || m.route_tau.is_some()
14256 || m.mask.is_some()
14257 || m.per_expert_scale.is_some()
14258 || m.experts.is_empty()
14259 || m.top_k == 0
14260 || m.resonance.is_some()
14261 {
14262 return None;
14263 }
14264 let (sh, sg) = match &m.shared {
14267 Some((sh, sg)) => (sh, sg.as_ref()),
14268 None => return None,
14269 };
14270 let (rf, rr, rc) = m.router.f32_parts()?;
14271 if rr != m.experts.len() || rc != hidden {
14272 return None;
14273 }
14274 let shared_gated = sg.is_some();
14275 let sf = match sg {
14276 Some(sg) => {
14277 let (sf, sr, sc) = sg.f32_parts()?;
14278 if sr * sc != hidden {
14279 return None;
14280 }
14281 sf
14282 }
14283 None => &rf[..hidden],
14286 };
14287 if let Some(b) = &m.expert_bias {
14288 if b.len() != m.experts.len() {
14289 return None;
14290 }
14291 }
14292 let inter = m.experts[0].gate_proj.rows();
14293 let gu_q2 = m.experts[0].gate_proj.mapped_q2tp().is_some();
14296 let trio = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
14297 if e.act != Act::Silu
14298 || e.gate_proj.rows() != inter
14299 || e.gate_proj.cols() != hidden
14300 || e.up_proj.rows() != inter
14301 || e.up_proj.cols() != hidden
14302 || e.down_proj.rows() != hidden
14303 || e.down_proj.cols() != inter
14304 {
14305 return None;
14306 }
14307 let pick = |t: &QTensor| -> Option<usize> {
14308 if gu_q2 {
14309 t.mapped_q2tp().map(|(_, i)| i)
14310 } else {
14311 t.mapped_q4tp().map(|(_, i)| i)
14312 }
14313 };
14314 Some((
14315 pick(&e.gate_proj)?,
14316 pick(&e.up_proj)?,
14317 e.down_proj.mapped_q4tp().map(|(_, i)| i)?,
14318 ))
14319 };
14320 let experts = m.experts.iter().map(trio).collect::<Option<Vec<_>>>()?;
14321 let shared = trio(sh)?;
14322 Some(crate::gpu::GpuMoe {
14323 router: rf,
14324 sgate: sf,
14325 experts,
14326 shared,
14327 n_exp: m.experts.len(),
14328 top_k: m.top_k,
14329 inter,
14330 norm_topk: m.norm_topk_prob,
14331 route_scale: m.routed_scaling,
14332 gu_q2,
14333 sigmoid: m.router_sigmoid,
14334 bias: m.expert_bias.as_deref(),
14335 shared_gated,
14336 })
14337}
14338
14339pub(crate) fn moe_push_job_parts<'a>(
14343 gate: &'a QTensor,
14344 up: &'a QTensor,
14345 down: &'a QTensor,
14346 x: &[f32],
14347 w: f32,
14348 swiglu_limit: f32,
14349 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
14350 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
14351) -> Option<()> {
14352 use crate::qtensor::prescale;
14353 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(gate)?;
14354 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(up)?;
14355 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(down)?;
14356 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
14357 return None; }
14359 if gq2 && (dq2 || !dq4 || down.mapped_q4tp().is_none()) {
14362 return None;
14363 }
14364 if !gq2 && dq2 {
14365 return None;
14366 }
14367 model_ref.get_or_insert_with(|| gm.clone());
14368 let dt = |cf: &[f32]| {
14369 if cf.is_empty() {
14370 cortiq_core::TensorDtype::Q8Row
14371 } else {
14372 cortiq_core::TensorDtype::Q8_2f
14373 }
14374 };
14375 jobs.push(crate::gpu::MoeJob {
14376 gate: (gi, gr, gc, grs),
14377 up: (ui, ur, uc, urs),
14378 down: (di, dr, dc, drs),
14379 xs_gate: prescale(x, gcf, dt(gcf)).into_owned(),
14380 xs_up: prescale(x, ucf, dt(ucf)).into_owned(),
14381 down_col: dcf,
14382 w,
14383 q1: gq1,
14384 q4t: gq4 && !gq2 && gate.mapped_q4tp().is_none(),
14385 q4tp: gq4 && (gq2 || gate.mapped_q4tp().is_some()),
14386 gu_q2: gq2,
14387 swiglu_limit,
14388 });
14389 Some(())
14390}
14391
14392fn moe_push_job<'a>(
14394 d: &'a DenseFfn,
14395 x: &[f32],
14396 w: f32,
14397 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
14398 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
14399) -> Option<()> {
14400 use crate::qtensor::prescale;
14401 if d.act != Act::Silu {
14402 return None; }
14404 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(&d.gate_proj)?;
14405 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(&d.up_proj)?;
14406 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(&d.down_proj)?;
14407 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
14408 return None; }
14410 if gq2 && (dq2 || !dq4 || d.down_proj.mapped_q4tp().is_none()) {
14411 return None;
14412 }
14413 if !gq2 && dq2 {
14414 return None;
14415 }
14416 model_ref.get_or_insert_with(|| gm.clone());
14417 let gdt = if gcf.is_empty() {
14418 cortiq_core::TensorDtype::Q8Row
14419 } else {
14420 cortiq_core::TensorDtype::Q8_2f
14421 };
14422 let udt = if ucf.is_empty() {
14423 cortiq_core::TensorDtype::Q8Row
14424 } else {
14425 cortiq_core::TensorDtype::Q8_2f
14426 };
14427 jobs.push(crate::gpu::MoeJob {
14428 gate: (gi, gr, gc, grs),
14429 up: (ui, ur, uc, urs),
14430 down: (di, dr, dc, drs),
14431 xs_gate: prescale(x, gcf, gdt).into_owned(),
14432 xs_up: prescale(x, ucf, udt).into_owned(),
14433 down_col: dcf,
14434 w,
14435 q1: gq1,
14436 q4t: gq4 && !gq2 && d.gate_proj.mapped_q4tp().is_none(),
14437 q4tp: gq4 && (gq2 || d.gate_proj.mapped_q4tp().is_some()),
14438 gu_q2: gq2,
14439 swiglu_limit: 0.0,
14440 });
14441 Some(())
14442}
14443
14444fn sparse_ffn_quant(
14451 d: &DenseFfn,
14452 x: &[f32],
14453 active: &[u16],
14454 hidden: usize,
14455 pool: Option<&Pool>,
14456) -> Vec<f32> {
14457 let n = active.len();
14458 let inter = d.gate_proj.rows();
14459 let mut act = vec![0.0f32; n];
14460 let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
14463 let compute = |ai: usize| -> f32 {
14464 let idx = active[ai] as usize;
14465 if idx >= inter {
14466 return 0.0; }
14468 let mut s = if need_scratch {
14469 vec![0.0f32; hidden]
14470 } else {
14471 Vec::new()
14472 };
14473 let gate = d.gate_proj.row_dot(idx, x, &mut s);
14474 let up = d.up_proj.row_dot(idx, x, &mut s);
14475 d.act.combine(gate, up)
14476 };
14477 match pool {
14478 Some(p) if n >= 256 => {
14479 let ptr = SendMut(act.as_mut_ptr());
14480 p.run(&|widx, nw| {
14481 let chunk = n.div_ceil(nw);
14482 let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
14483 for ai in s..e {
14484 unsafe { *ptr.at(ai) = compute(ai) };
14485 }
14486 });
14487 }
14488 _ => {
14489 for (ai, a) in act.iter_mut().enumerate() {
14490 *a = compute(ai);
14491 }
14492 }
14493 }
14494 let mut out = vec![0.0f32; hidden];
14496 for (ai, &idx) in active.iter().enumerate() {
14497 let w = act[ai];
14498 if w.abs() >= 1e-12 && (idx as usize) < inter {
14499 d.down_proj.add_col_scaled(idx as usize, w, &mut out);
14500 }
14501 }
14502 out
14503}
14504
14505#[doc(hidden)]
14507pub fn sparse_ffn_quant_for_test(
14508 d: &DenseFfn,
14509 x: &[f32],
14510 active: &[u16],
14511 hidden: usize,
14512) -> Vec<f32> {
14513 sparse_ffn_quant(d, x, active, hidden, None)
14514}
14515
14516fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
14520 let deq = |t: &QTensor| -> Vec<f32> {
14521 let (rows, cols) = (t.rows(), t.cols());
14522 let mut out = vec![0.0f32; rows * cols];
14523 for r in 0..rows {
14524 t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
14525 }
14526 out
14527 };
14528 (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
14529}
14530
14531struct SendMut(*mut f32);
14533unsafe impl Send for SendMut {}
14534unsafe impl Sync for SendMut {}
14535impl SendMut {
14536 #[inline]
14537 #[allow(clippy::mut_from_ref)]
14540 unsafe fn at(&self, i: usize) -> &mut f32 {
14541 unsafe { &mut *self.0.add(i) }
14542 }
14543}
14544
14545pub(crate) fn moe_route(
14555 logits: &[f32],
14556 m: &MoeFfn,
14557 allowed: Option<&[bool]>,
14558) -> (Vec<usize>, Vec<f32>, f32) {
14559 let ne = logits.len();
14560 let p: Vec<f32> = if m.router_sigmoid {
14561 logits.iter().map(|&l| 1.0 / (1.0 + (-l).exp())).collect()
14562 } else {
14563 let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
14564 let mut e: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
14565 let s: f32 = e.iter().sum();
14566 for v in &mut e {
14567 *v /= s;
14568 }
14569 e
14570 };
14571 let admit = |e: usize| {
14577 m.mask.as_ref().is_none_or(|mk| mk[e])
14578 && allowed.is_none_or(|a| a.get(e).copied().unwrap_or(false))
14579 };
14580 let mut idx: Vec<usize> = (0..ne).filter(|&e| admit(e)).collect();
14581 match &m.expert_bias {
14583 Some(b) => idx.sort_unstable_by(|&x, &y| {
14584 (p[y] + b[y])
14585 .partial_cmp(&(p[x] + b[x]))
14586 .unwrap()
14587 .then(x.cmp(&y))
14588 }),
14589 None => idx.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y))),
14590 }
14591 idx.truncate(m.top_k);
14592 if let Some(tau) = m.route_tau {
14596 let total: f32 = idx.iter().map(|&e| p[e]).sum();
14597 if total > 0.0 {
14598 let mut acc = 0.0f32;
14599 let mut keep = idx.len();
14600 for (i, &e) in idx.iter().enumerate() {
14601 acc += p[e];
14602 if acc >= tau * total {
14603 keep = i + 1;
14604 break;
14605 }
14606 }
14607 idx.truncate(keep);
14608 }
14609 }
14610 let wsum: f32 = if m.norm_topk_prob {
14611 let s: f32 = idx.iter().map(|&e| p[e]).sum();
14612 (if m.router_sigmoid { s + 1e-6 } else { s }) / m.routed_scaling
14615 } else {
14616 1.0 / m.routed_scaling
14617 };
14618 (idx, p, wsum)
14619}
14620
14621fn moe_trace(idx: &[usize]) {
14623 moe_trace_at(crate::gpu::cur_layer() as i32, idx)
14624}
14625
14626pub(crate) fn moe_trace_at(li: i32, idx: &[usize]) {
14629 use std::io::Write;
14630 static F: std::sync::OnceLock<Option<std::sync::Mutex<std::fs::File>>> =
14631 std::sync::OnceLock::new();
14632 let Some(f) = F.get_or_init(|| {
14633 let p = std::env::var("CMF_MOE_TRACE").ok()?;
14634 Some(std::sync::Mutex::new(
14635 std::fs::OpenOptions::new()
14636 .create(true)
14637 .append(true)
14638 .open(p)
14639 .ok()?,
14640 ))
14641 }) else {
14642 return;
14643 };
14644 let ids: Vec<String> = idx.iter().map(|e| e.to_string()).collect();
14645 let _ = writeln!(f.lock().unwrap(), "{li}:{}", ids.join(","));
14646}
14647
14648pub(crate) fn moe_ffn(
14651 m: &MoeFfn,
14652 x: &[f32],
14653 pool: Option<&Pool>,
14654 allowed: Option<&[bool]>,
14655) -> Vec<f32> {
14656 accumulate_act(m, x, 1);
14657 let ne = m.experts.len();
14658 let mut logits = vec![0.0f32; ne];
14659 match &m.resonance {
14660 Some(r) => r.scores(x, &mut logits),
14661 None => m.router.matvec(x, &mut logits, pool),
14662 }
14663 let (idx, p, wsum) = moe_route(&logits, m, allowed);
14664 {
14665 let mut st = m.stats.borrow_mut();
14666 if st.len() < ne {
14667 st.resize(ne, 0);
14668 }
14669 for &e in &idx {
14670 st[e] += 1;
14671 }
14672 }
14673 moe_trace(&idx);
14679 if crate::gpu::enabled_here() {
14684 match crate::gpu::probe_arm(crate::gpu::OpClass::Ffn) {
14685 crate::gpu::ProbeArm::Gpu => {
14686 let t0 = std::time::Instant::now();
14687 if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
14688 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
14689 return out;
14690 }
14691 }
14692 crate::gpu::ProbeArm::CpuTimed => {
14693 let t0 = std::time::Instant::now();
14694 let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
14695 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
14696 return out;
14697 }
14698 crate::gpu::ProbeArm::Cpu => {
14699 return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
14700 }
14701 }
14702 }
14703 moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
14704}
14705
14706fn graph_note(built: bool, layers_run: usize, total_layers: usize) {
14711 use std::sync::atomic::{AtomicBool, Ordering};
14712 if built {
14713 GRAPH_TOK_OK.fetch_add(1, Ordering::Relaxed);
14714 if total_layers > 0 && layers_run < total_layers {
14715 GRAPH_TOK_PREFIX.fetch_add(1, Ordering::Relaxed);
14716 } else {
14717 GRAPH_TOK_FULL.fetch_add(1, Ordering::Relaxed);
14718 }
14719 } else {
14720 GRAPH_TOK_MISS.fetch_add(1, Ordering::Relaxed);
14721 }
14722 static SAID: AtomicBool = AtomicBool::new(false);
14723 if !SAID.swap(true, Ordering::Relaxed) {
14724 if built {
14725 tracing::info!("wgpu whole-token graph: ACTIVE");
14726 } else {
14727 tracing::warn!("wgpu whole-token graph refused — per-op path");
14728 }
14729 }
14730}
14731
14732pub static GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
14736pub static GRAPH_TOK_MISS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
14737pub static GRAPH_TOK_PREFIX: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
14741pub static GRAPH_TOK_FULL: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
14743
14744pub static METAL_GRAPH_TOK_OK: std::sync::atomic::AtomicU64 =
14748 std::sync::atomic::AtomicU64::new(0);
14749pub static METAL_GRAPH_HEAD_OK: std::sync::atomic::AtomicU64 =
14750 std::sync::atomic::AtomicU64::new(0);
14751pub static METAL_GRAPH_HEAD_MISS: std::sync::atomic::AtomicU64 =
14752 std::sync::atomic::AtomicU64::new(0);
14753pub static METAL_GRAPH_LAYERS: std::sync::atomic::AtomicU64 =
14754 std::sync::atomic::AtomicU64::new(0);
14755pub static METAL_GRAPH_ERRORS: std::sync::atomic::AtomicU64 =
14756 std::sync::atomic::AtomicU64::new(0);
14757pub static METAL_PREFILL_CHUNKS: std::sync::atomic::AtomicU64 =
14761 std::sync::atomic::AtomicU64::new(0);
14762pub static METAL_PREFILL_ROWS: std::sync::atomic::AtomicU64 =
14763 std::sync::atomic::AtomicU64::new(0);
14764pub static METAL_PREFILL_HEAD_ROWS: std::sync::atomic::AtomicU64 =
14765 std::sync::atomic::AtomicU64::new(0);
14766pub static METAL_PREFILL_ERRORS: std::sync::atomic::AtomicU64 =
14767 std::sync::atomic::AtomicU64::new(0);
14768
14769fn moe_batch_enabled() -> bool {
14772 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
14773 *ON.get_or_init(|| std::env::var("CMF_MOE_BATCH").as_deref() != Ok("0"))
14774}
14775
14776fn moe_ffn_cpu_batched(
14782 m: &MoeFfn,
14783 x: &[f32],
14784 idx: &[usize],
14785 p: &[f32],
14786 wsum: f32,
14787 pool: Option<&Pool>,
14788) -> Option<Vec<f32>> {
14789 if idx.is_empty() || !moe_batch_enabled() {
14790 return None;
14791 }
14792 if FFN_PROBE.with(|pr| pr.borrow().is_some()) {
14796 return None;
14797 }
14798 let n = idx.len() + usize::from(m.shared.is_some());
14799 let mut pairs = Vec::with_capacity(n);
14800 let mut downs = Vec::with_capacity(n);
14801 let mut ws = Vec::with_capacity(n);
14802 for &e in idx {
14803 let d = &m.experts[e];
14804 if d.act != Act::Silu {
14805 return None;
14806 }
14807 pairs.push((&d.gate_proj, &d.up_proj));
14808 downs.push(&d.down_proj);
14809 ws.push(p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]));
14810 }
14811 if let Some((se, gate)) = &m.shared {
14814 if se.act != Act::Silu {
14815 return None;
14816 }
14817 let g = gate.as_ref().map_or(1.0, |gate| {
14818 let mut gl = [0.0f32; 1];
14819 gate.matvec(x, &mut gl, pool);
14820 1.0 / (1.0 + (-gl[0]).exp())
14821 });
14822 pairs.push((&se.gate_proj, &se.up_proj));
14823 downs.push(&se.down_proj);
14824 ws.push(g);
14825 }
14826 let inter = pairs[0].0.rows();
14827 let mut gs: Vec<Vec<f32>> = (0..pairs.len()).map(|_| vec![0f32; inter]).collect();
14828 if !QTensor::moe_gate_up_many(&pairs, x, &mut gs, pool) {
14829 return None;
14830 }
14831 let mut out = attention::take_buf(x.len());
14832 if !QTensor::moe_down_many(&downs, &gs, &ws, &mut out, pool) {
14833 attention::recycle_buf(&mut out);
14834 return None;
14835 }
14836 Some(out)
14837}
14838
14839pub(crate) fn moe_cold_experts_cpu(
14845 experts: &[(&DenseFfn, f32)],
14846 x: &[f32],
14847 pool: Option<&Pool>,
14848) -> Vec<f32> {
14849 let mut out = attention::take_buf(x.len());
14850 if experts.is_empty() {
14851 return out;
14852 }
14853 let pairs: Vec<_> = experts
14854 .iter()
14855 .map(|(e, _)| (&e.gate_proj, &e.up_proj))
14856 .collect();
14857 let downs: Vec<_> = experts.iter().map(|(e, _)| &e.down_proj).collect();
14858 let weights: Vec<_> = experts.iter().map(|(_, w)| *w).collect();
14859 let inter = experts[0].0.gate_proj.rows();
14860 let mut activations: Vec<Vec<f32>> = (0..experts.len()).map(|_| vec![0.0; inter]).collect();
14861 if QTensor::moe_gate_up_many(&pairs, x, &mut activations, pool)
14862 && QTensor::moe_down_many(&downs, &activations, &weights, &mut out, pool)
14863 {
14864 return out;
14865 }
14866 out.fill(0.0);
14867 for &(expert, weight) in experts {
14868 let mut one = dense_ffn(expert, x, pool);
14869 for (o, v) in out.iter_mut().zip(&one) {
14870 *o += weight * v;
14871 }
14872 attention::recycle_buf(&mut one);
14873 }
14874 out
14875}
14876
14877fn moe_ffn_cpu(
14879 m: &MoeFfn,
14880 x: &[f32],
14881 idx: &[usize],
14882 p: &[f32],
14883 wsum: f32,
14884 pool: Option<&Pool>,
14885) -> Vec<f32> {
14886 if let Some(out) = moe_ffn_cpu_batched(m, x, idx, p, wsum, pool) {
14887 return out;
14888 }
14889 let mut out = attention::take_buf(x.len());
14890 for &e in idx {
14891 let mut eo = dense_ffn(&m.experts[e], x, pool);
14892 let w = p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]);
14893 for i in 0..out.len() {
14894 out[i] += w * eo[i];
14895 }
14896 attention::recycle_buf(&mut eo);
14897 }
14898 if let Some((se, gate)) = &m.shared {
14899 let mut so = dense_ffn(se, x, pool);
14900 let g = gate.as_ref().map_or(1.0, |gate| {
14901 let mut gl = [0.0f32; 1];
14902 gate.matvec(x, &mut gl, pool);
14903 1.0 / (1.0 + (-gl[0]).exp())
14904 });
14905 for i in 0..out.len() {
14906 out[i] += g * so[i];
14907 }
14908 attention::recycle_buf(&mut so);
14909 }
14910 out
14911}
14912
14913#[allow(clippy::too_many_arguments)]
14921fn mla_attention(
14922 w: &MlaWeights,
14923 normed: &[f32],
14924 cache: &mut crate::kv_cache::LayerKvCache,
14925 position: usize,
14926 inv_freq: &[f32],
14927 rope_scale: f32,
14928 eps: f64,
14929 pool: Option<&Pool>,
14930) -> Vec<f32> {
14931 let (nh, dr, dn, dv, lora) = (w.nh, w.qk_rope, w.qk_nope, w.v_dim, w.lora);
14932 let hd = dr + dn;
14933 let mut q = vec![0.0f32; nh * hd];
14934 match (&w.q_a, &w.q_a_norm) {
14935 (Some(qa), Some(qn)) => {
14936 let mut t = vec![0.0f32; qa.rows()];
14937 qa.matvec(normed, &mut t, pool);
14938 let tn = inference::rms_norm(&t, qn, eps, NormStyle::Qwen);
14939 w.q_proj.matvec(&tn, &mut q, pool);
14940 }
14941 _ => w.q_proj.matvec(normed, &mut q, pool),
14942 }
14943 let mut ca = vec![0.0f32; lora + dr];
14944 w.kv_a.matvec(normed, &mut ca, pool);
14945 let (c_lat, k_rope) = ca.split_at_mut(lora);
14946 let latn = inference::rms_norm(c_lat, &w.kv_a_norm, eps, NormStyle::Qwen);
14947 let mut kvb = vec![0.0f32; nh * (dn + dv)];
14948 w.kv_b.matvec(&latn, &mut kvb, pool);
14949 if !w.nope {
14950 attention::rope_rotate_scaled(k_rope, position, inv_freq, rope_scale);
14951 }
14952 for h in 0..nh {
14953 if !w.nope {
14954 attention::rope_rotate_scaled(
14955 &mut q[h * hd..h * hd + dr],
14956 position,
14957 inv_freq,
14958 rope_scale,
14959 );
14960 }
14961 }
14962 let mut k = vec![0.0f32; nh * hd];
14963 let mut v = vec![0.0f32; nh * hd];
14964 for h in 0..nh {
14965 k[h * hd..h * hd + dr].copy_from_slice(k_rope);
14966 k[h * hd + dr..(h + 1) * hd].copy_from_slice(&kvb[h * (dn + dv)..h * (dn + dv) + dn]);
14967 v[h * hd..h * hd + dv].copy_from_slice(&kvb[h * (dn + dv) + dn..(h + 1) * (dn + dv)]);
14968 }
14969 cache.append(&k, &v, &vec![true; nh]);
14970 let (ao, mut imp) = attention::attend_all_heads(&q, cache, nh, 1, hd, w.scale, None, 0.0);
14971 attention::recycle_buf(&mut imp);
14972 let mut ov = vec![0.0f32; nh * dv];
14973 for h in 0..nh {
14974 ov[h * dv..(h + 1) * dv].copy_from_slice(&ao[h * hd..h * hd + dv]);
14975 }
14976 let mut out = vec![0.0f32; w.o_proj.rows()];
14977 w.o_proj.matvec(&ov, &mut out, pool);
14978 out
14979}
14980
14981fn dense_moe_ffn(
14988 dm: &DenseMoeFfn,
14989 x_normed: &[f32],
14990 h_raw: &[f32],
14991 eps: f64,
14992 norm_style: NormStyle,
14993 pool: Option<&Pool>,
14994) -> Vec<f32> {
14995 let mut d = dense_ffn(&dm.dense, x_normed, pool);
14996 d = inference::rms_norm(&d, &dm.post_norm_1, eps, norm_style);
14997 let m = &dm.moe;
14998 let ne = m.experts.len();
14999 let mut logits = vec![0.0f32; ne];
15000 if m.router_input_norm {
15001 let ss: f32 = h_raw.iter().map(|v| v * v).sum::<f32>() / h_raw.len() as f32;
15002 let inv = 1.0 / (ss + eps as f32).sqrt();
15003 let xr: Vec<f32> = h_raw.iter().map(|v| v * inv).collect();
15004 m.router.matvec(&xr, &mut logits, pool);
15005 } else {
15006 m.router.matvec(h_raw, &mut logits, pool);
15007 }
15008 let (idx, p, wsum) = moe_route(&logits, m, None);
15009 {
15010 let mut st = m.stats.borrow_mut();
15011 if st.len() < ne {
15012 st.resize(ne, 0);
15013 }
15014 for &e in &idx {
15015 st[e] += 1;
15016 }
15017 }
15018 let x2 = inference::rms_norm(h_raw, &dm.pre_norm_2, eps, norm_style);
15019 let mo = moe_ffn_cpu(m, &x2, &idx, &p, wsum, pool);
15020 let mo = inference::rms_norm(&mo, &dm.post_norm_2, eps, norm_style);
15021 for (di, mi) in d.iter_mut().zip(&mo) {
15022 *di += mi;
15023 }
15024 d
15025}
15026
15027fn moe_gpu_refused(why: &'static str) {
15034 use std::sync::atomic::{AtomicBool, Ordering};
15035 static SAID: AtomicBool = AtomicBool::new(false);
15036 if !SAID.swap(true, Ordering::Relaxed) {
15037 tracing::warn!("MoE GPU block refused ({why}) — experts run on the CPU");
15038 }
15039}
15040
15041fn moe_ffn_gpu(
15042 m: &MoeFfn,
15043 x: &[f32],
15044 idx: &[usize],
15045 p: &[f32],
15046 wsum: f32,
15047 pool: Option<&Pool>,
15048) -> Option<Vec<f32>> {
15049 use crate::gpu::MoeJob;
15050
15051 let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
15052 let mut model_ref = None;
15053 for &e in idx {
15054 if moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref).is_none() {
15055 moe_gpu_refused("push_job(expert)");
15056 return None;
15057 }
15058 }
15059 if let Some((se, gate)) = &m.shared {
15060 let g = gate.as_ref().map_or(1.0, |gate| {
15061 let mut gl = [0.0f32; 1];
15062 gate.matvec(x, &mut gl, pool);
15063 1.0 / (1.0 + (-gl[0]).exp())
15064 });
15065 if moe_push_job(se, x, g, &mut jobs, &mut model_ref).is_none() {
15066 moe_gpu_refused("push_job(shared)");
15067 return None;
15068 }
15069 }
15070 let Some(model) = model_ref else {
15071 moe_gpu_refused("no model_ref");
15072 return None;
15073 };
15074 let hidden = jobs[0].down.1;
15075 let mut out = vec![0.0f32; hidden];
15076 if crate::gpu::moe_block(&model, &jobs, &mut out) {
15077 Some(out)
15078 } else {
15079 moe_gpu_refused("gpu::moe_block");
15080 None
15081 }
15082}
15083
15084fn ffn_forward(
15086 ffn: &FfnKind,
15087 x: &[f32],
15088 pool: Option<&Pool>,
15089 experts_allowed: Option<&[bool]>,
15090) -> Vec<f32> {
15091 match ffn {
15092 FfnKind::Dense(d) if !d.segs.is_empty() => tube_ffn(d, x, 1, pool, None),
15093 FfnKind::Dense(d) => dense_ffn(d, x, pool),
15094 FfnKind::Moe(m) => moe_ffn(m, x, pool, experts_allowed),
15095 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
15099 }
15100}
15101
15102fn ffn_forward_pair(
15106 ffn: &FfnKind,
15107 x1: &[f32],
15108 x2: &[f32],
15109 pool: Option<&Pool>,
15110 experts_allowed: Option<&[bool]>,
15111) -> (Vec<f32>, Vec<f32>) {
15112 let d = match ffn {
15113 FfnKind::Dense(d) if !d.segs.is_empty() => {
15116 return (
15117 tube_ffn(d, x1, 1, pool, None),
15118 tube_ffn(d, x2, 1, pool, None),
15119 );
15120 }
15121 FfnKind::Dense(d) => d,
15122 FfnKind::Moe(m) => {
15123 return (
15124 moe_ffn(m, x1, pool, experts_allowed),
15125 moe_ffn(m, x2, pool, experts_allowed),
15126 );
15127 }
15128 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
15129 };
15130 let inter = d.gate_proj.rows();
15131 FFN_SCRATCH.with(|s| {
15132 let mut s = s.borrow_mut();
15133 let [g1, g2, u1, u2] = &mut *s;
15134 g1.resize(inter, 0.0);
15135 g2.resize(inter, 0.0);
15136 u1.resize(inter, 0.0);
15137 u2.resize(inter, 0.0);
15138 QTensor::matvec2_many(
15141 [&d.gate_proj, &d.up_proj],
15142 x1,
15143 x2,
15144 [g1.as_mut_slice(), u1.as_mut_slice()],
15145 [g2.as_mut_slice(), u2.as_mut_slice()],
15146 pool,
15147 );
15148 for i in 0..inter {
15149 g1[i] = d.act.combine(g1[i], u1[i]);
15150 g2[i] = d.act.combine(g2[i], u2[i]);
15151 }
15152 let mut o1 = attention::take_buf(d.down_proj.rows());
15153 let mut o2 = attention::take_buf(d.down_proj.rows());
15154 d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
15155 (o1, o2)
15156 })
15157}
15158
15159#[cfg(test)]
15160mod tests {
15161
15162 #[test]
15167 fn prefill_chunk_rule_widens_only_dense_on_discrete() {
15168 use super::{
15169 prefill_chunk_rule, ChunkHost, ChunkStackFacts, DISCRETE_DENSE_PREFILL_CHUNK,
15170 };
15171 let dense_card = ChunkStackFacts {
15172 plain_dense: true,
15173 discrete: true,
15174 gpu_on: true,
15175 ..Default::default()
15176 };
15177 assert!(dense_card.dense_on_discrete());
15178 assert_eq!(
15180 prefill_chunk_rule(None, ChunkHost::Other, dense_card.dense_on_discrete()),
15181 DISCRETE_DENSE_PREFILL_CHUNK
15182 );
15183 assert!(DISCRETE_DENSE_PREFILL_CHUNK > 48);
15184 for (label, facts) in [
15185 ("GDN hybrid / MoE / DeepSeek stack", ChunkStackFacts { plain_dense: false, ..dense_card }),
15186 ("integrated GPU", ChunkStackFacts { discrete: false, ..dense_card }),
15187 ("CPU only", ChunkStackFacts { gpu_on: false, discrete: false, ..dense_card }),
15188 ("capacity split", ChunkStackFacts { capacity_split: true, ..dense_card }),
15189 ("multi-GPU plan", ChunkStackFacts { multi_gpu: true, ..dense_card }),
15190 ("O(1) layers", ChunkStackFacts { o1: true, ..dense_card }),
15191 ] {
15192 assert!(!facts.dense_on_discrete(), "{label}");
15193 assert_eq!(
15194 prefill_chunk_rule(None, ChunkHost::Other, facts.dense_on_discrete()),
15195 48,
15196 "{label} keeps the historical x86 chunk"
15197 );
15198 }
15199 for dense in [false, true] {
15201 assert_eq!(prefill_chunk_rule(None, ChunkHost::Macos, dense), 512);
15202 assert_eq!(prefill_chunk_rule(None, ChunkHost::Aarch64, dense), 256);
15203 }
15204 for host in [ChunkHost::Macos, ChunkHost::Aarch64, ChunkHost::Other] {
15206 for dense in [false, true] {
15207 assert_eq!(prefill_chunk_rule(Some(48), host, dense), 48);
15208 assert_eq!(prefill_chunk_rule(Some(0), host, dense), 1);
15209 }
15210 }
15211 }
15212
15213 #[test]
15214 fn kv_reuse_plan_pulls_rows_decode_wrote_only_on_the_device() {
15215 use super::{ReuseLayer, ReusePlan, kv_reuse_plan};
15216 let full = |host_rows, device_rows| ReuseLayer {
15217 full: true,
15218 host_rows,
15219 device_rows,
15220 device_state: false,
15221 };
15222 assert_eq!(
15225 kv_reuse_plan(339, &[full(300, Some(339)), full(300, Some(339))]),
15226 ReusePlan::Pull(vec![(0, 300, 339), (1, 300, 339)])
15227 );
15228 assert_eq!(kv_reuse_plan(339, &[full(339, None)]), ReusePlan::Ready);
15230 assert_eq!(kv_reuse_plan(339, &[full(339, Some(345))]), ReusePlan::Ready);
15232 assert_eq!(
15234 kv_reuse_plan(339, &[full(300, Some(339)), full(339, None)]),
15235 ReusePlan::Pull(vec![(0, 300, 339)])
15236 );
15237 assert_eq!(kv_reuse_plan(339, &[full(300, Some(320))]), ReusePlan::Fresh);
15239 assert_eq!(kv_reuse_plan(339, &[full(300, None)]), ReusePlan::Fresh);
15240 assert_eq!(kv_reuse_plan(339, &[full(350, None)]), ReusePlan::Fresh);
15241 let conv = |device_state| ReuseLayer {
15244 full: false,
15245 host_rows: 0,
15246 device_rows: None,
15247 device_state,
15248 };
15249 assert_eq!(
15250 kv_reuse_plan(339, &[conv(true), full(300, Some(339))]),
15251 ReusePlan::Fresh
15252 );
15253 assert_eq!(kv_reuse_plan(339, &[conv(false), full(339, None)]), ReusePlan::Ready);
15254 }
15255
15256 #[test]
15257 fn nll_graph_policy_scopes_only_the_fused_head() {
15258 for (label, unmasked, prefer_graph, native_metal, want_graph, want_head) in [
15259 ("vulkan graph", true, true, false, true, false),
15261 ("native Metal graph", true, true, true, true, true),
15263 ("masked", false, true, false, false, false),
15265 ("graph disabled", true, false, true, false, false),
15266 ] {
15267 let (graph_quality, graph_head_required) =
15268 super::nll_graph_policy(unmasked, prefer_graph, native_metal);
15269 assert_eq!(graph_quality, want_graph, "{label}: graph quality");
15270 assert_eq!(graph_head_required, want_head, "{label}: fused head");
15271 }
15272 }
15273
15274 #[test]
15275 fn mtp_prefill_pair_boundaries_skip_only_final_prompt_row() {
15276 assert_eq!(mtp_prefill_pair_count(0, 128, 256), 128);
15277 assert_eq!(mtp_prefill_pair_count(128, 256, 256), 127);
15278 assert_eq!(mtp_prefill_pair_count(0, 256, 256), 255);
15279 assert_eq!(mtp_prefill_pair_count(256, 256, 256), 0);
15280 assert_eq!(mtp_prefill_pair_count(300, 320, 256), 0);
15281 }
15282
15283 #[test]
15284 fn cancel_flag_stops_generation() {
15285 let mut p = create_test_pipeline(16, 32, 2, 2, 8, 2, 32);
15286 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
15289 let r = p.generate_from_ids(&[1, 2, 3], 8, None, None).unwrap();
15290 assert_eq!(r.finish_reason, "cancelled");
15291 assert!(
15292 r.token_ids.is_empty(),
15293 "no tokens after cancel: {:?}",
15294 r.token_ids
15295 );
15296 assert_eq!(p.kv_cache.seq_len(), 0);
15297 assert!(p.kv_history.is_empty());
15298 assert!(!p.graph_want_logits);
15299 assert!(p.graph_logits.is_none());
15300 let r2 = p.generate_from_ids(&[1, 2, 3], 4, None, None).unwrap();
15302 assert_ne!(r2.finish_reason, "cancelled");
15303 }
15304 use super::*;
15305
15306 #[test]
15314 fn dynamic_ffn_equals_the_zeroing_arm() {
15315 let (hidden, inter) = (8usize, 32usize);
15316 let synth = |n: usize, salt: usize| -> Vec<f32> {
15317 (0..n)
15318 .map(|i| (((i * 29 + salt * 13 + 7) % 89) as f32 / 89.0 - 0.5) * 0.6)
15319 .collect()
15320 };
15321 let down = synth(hidden * inter, 3);
15322 let mut down_t = vec![0.0f32; inter * hidden];
15323 for r in 0..hidden {
15324 for c in 0..inter {
15325 down_t[c * hidden + r] = down[r * inter + c];
15326 }
15327 }
15328 let d = DenseFfn {
15329 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
15330 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
15331 down_proj: QTensor::from_f32(down.clone(), hidden, inter),
15332 act: Act::Silu,
15333 down_t: Some(QTensor::from_f32(down_t, inter, hidden)),
15334 segs: Vec::new(),
15335 };
15336 let x = synth(hidden, 11);
15337 let k = 12usize;
15338 let got = dense_ffn_dynamic(&d, &x, None, k).expect("down_t present");
15339 let mut g = vec![0.0f32; inter];
15341 d.gate_proj.matvec(&x, &mut g, None);
15342 let mut u = vec![0.0f32; inter];
15343 d.up_proj.matvec(&x, &mut u, None);
15344 for v in g.iter_mut() {
15345 *v = inference::silu(*v);
15346 }
15347 keep_top_k(&mut g, k);
15348 for i in 0..inter {
15349 g[i] *= u[i];
15350 }
15351 let mut want = vec![0.0f32; hidden];
15352 d.down_proj.matvec(&g, &mut want, None);
15353 for (a, b) in want.iter().zip(&got) {
15354 assert!((a - b).abs() < 1e-5, "dynamic {b} vs reference {a}");
15355 }
15356 }
15357
15358 #[test]
15364 fn tube_ffn_open_equals_dense_and_closed_equals_masked() {
15365 let (hidden, core, tube) = (8usize, 12usize, 8usize);
15366 let inter = core + tube;
15367 let synth = |n: usize, salt: usize| -> Vec<f32> {
15368 (0..n)
15369 .map(|i| (((i * 41 + salt * 17 + 5) % 97) as f32 / 97.0 - 0.5) * 0.5)
15370 .collect()
15371 };
15372 let (g_all, u_all) = (synth(inter * hidden, 1), synth(inter * hidden, 2));
15373 let d_all = synth(hidden * inter, 3);
15374 let dense = DenseFfn {
15376 gate_proj: QTensor::from_f32(g_all.clone(), inter, hidden),
15377 up_proj: QTensor::from_f32(u_all.clone(), inter, hidden),
15378 down_proj: QTensor::from_f32(d_all.clone(), hidden, inter),
15379 act: Act::Silu,
15380 down_t: None,
15381 segs: Vec::new(),
15382 };
15383 let rows =
15384 |v: &[f32], a: usize, b: usize| -> Vec<f32> { v[a * hidden..b * hidden].to_vec() };
15385 let cols = |v: &[f32], a: usize, b: usize| -> Vec<f32> {
15386 let mut o = Vec::with_capacity(hidden * (b - a));
15387 for r in 0..hidden {
15388 o.extend_from_slice(&v[r * inter + a..r * inter + b]);
15389 }
15390 o
15391 };
15392 let tubed = DenseFfn {
15393 down_t: None,
15394 gate_proj: QTensor::from_f32(rows(&g_all, 0, core), core, hidden),
15395 up_proj: QTensor::from_f32(rows(&u_all, 0, core), core, hidden),
15396 down_proj: QTensor::from_f32(cols(&d_all, 0, core), hidden, core),
15397 act: Act::Silu,
15398 segs: vec![FfnSeg {
15399 gate: QTensor::from_f32(rows(&g_all, core, inter), tube, hidden),
15400 up: QTensor::from_f32(rows(&u_all, core, inter), tube, hidden),
15401 down: QTensor::from_f32(cols(&d_all, core, inter), hidden, tube),
15402 start: core,
15403 width: tube,
15404 }],
15405 };
15406 let x = synth(hidden, 7);
15407 let want = dense_ffn(&dense, &x, None);
15408 let got = tube_ffn(&tubed, &x, 1, None, None);
15409 for (a, b) in want.iter().zip(&got) {
15410 assert!((a - b).abs() < 1e-5, "open tube: {a} vs {b}");
15411 }
15412 let mut bits = vec![0u8; inter.div_ceil(8)];
15414 for n in 0..core {
15415 bits[n / 8] |= 1 << (n % 8);
15416 }
15417 let closed = tube_ffn(&tubed, &x, 1, None, Some(&bits));
15418 let masked = dense_ffn_masked(&dense, &x, None, &bits);
15419 for (a, b) in masked.iter().zip(&closed) {
15420 assert!((a - b).abs() < 1e-5, "closed tube: {a} vs {b}");
15421 }
15422 let batch = tube_ffn(&tubed, &x, 1, None, Some(&bits));
15424 for (a, b) in closed.iter().zip(&batch) {
15425 assert_eq!(a, b, "batch arm disagrees with decode arm");
15426 }
15427 }
15428
15429 #[test]
15431 fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
15432 let (hidden, inter) = (16usize, 40usize);
15433 let synth = |n: usize, salt: usize| -> Vec<f32> {
15434 (0..n)
15435 .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
15436 .collect()
15437 };
15438 let d = DenseFfn {
15439 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
15440 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
15441 down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
15442 act: Act::Silu,
15443 down_t: None,
15444 segs: Vec::new(),
15445 };
15446 let x = synth(hidden, 9);
15447 let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
15449
15450 let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
15451
15452 let mut g = vec![0.0f32; inter];
15454 d.gate_proj.matvec(&x, &mut g, None);
15455 let mut u = vec![0.0f32; inter];
15456 d.up_proj.matvec(&x, &mut u, None);
15457 let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
15458 for i in 0..inter {
15459 g[i] = if act_set.contains(&(i as u16)) {
15460 inference::silu(g[i]) * u[i]
15461 } else {
15462 0.0
15463 };
15464 }
15465 let mut reference = vec![0.0f32; hidden];
15466 d.down_proj.matvec(&g, &mut reference, None);
15467
15468 let max_d = sparse
15469 .iter()
15470 .zip(&reference)
15471 .map(|(a, b)| (a - b).abs())
15472 .fold(0.0f32, f32::max);
15473 assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
15474 }
15475
15476 fn attach_test_mtp(p: &mut Pipeline) {
15478 let (h, inter, heads, kv, hd) = (
15479 p.hidden_size,
15480 p.intermediate_size,
15481 p.num_heads,
15482 p.num_kv_heads,
15483 p.head_dim,
15484 );
15485 let synth = |n: usize, salt: usize| -> Vec<f32> {
15486 (0..n)
15487 .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
15488 .collect()
15489 };
15490 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
15491 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
15492 };
15493 p.mtp = Some(MtpModule {
15494 enorm: vec![1.0; h],
15495 hnorm: vec![1.0; h],
15496 eh_proj: qt(h, 2 * h, 301),
15497 layer: LayerWeights {
15498 input_norm: vec![1.0; h],
15499 post_norm: vec![1.0; h],
15500 attn_out_norm: None,
15501 ffn_out_norm: None,
15502 layer_scale: None,
15503 ffn: FfnKind::Dense(DenseFfn {
15504 gate_proj: qt(inter, h, 315),
15505 up_proj: qt(inter, h, 316),
15506 down_proj: qt(h, inter, 317),
15507 act: Act::Silu,
15508 down_t: None,
15509 segs: Vec::new(),
15510 }),
15511 attn: AttnKind::Full {
15512 bias: None,
15513 wq: qt(heads * hd, h, 311),
15514 wk: qt(kv * hd, h, 312),
15515 wv: qt(kv * hd, h, 313),
15516 wo: qt(h, heads * hd, 314),
15517 q_norm: None,
15518 k_norm: None,
15519 output_gate: false,
15520 softplus_gate: None,
15521 },
15522 },
15523 final_norm: vec![1.0; h],
15524 kv: crate::kv_cache::LayerKvCache::new(kv, hd),
15525 });
15526 }
15527
15528 #[test]
15529 fn speculative_equals_vanilla_greedy() {
15530 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
15534 let run = |spec: bool| {
15535 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
15536 p.sampler_config.temperature = 0.0;
15537 attach_test_mtp(&mut p);
15538 p.speculative = spec;
15539 let r = p.generate("abcdef", 12, None, None).unwrap();
15540 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
15541 };
15542 let (vanilla, d0, _) = run(false);
15543 let (spec, d1, a1) = run(true);
15544 assert_eq!(d0, 0, "vanilla path must not draft");
15545 assert!(d1 > 0, "speculative path must draft");
15546 assert_eq!(
15547 vanilla, spec,
15548 "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
15549 );
15550 }
15551
15552 #[test]
15553 fn speculative_accepts_constant_oracle() {
15554 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
15556 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
15557 p.sampler_config.temperature = 0.0;
15558 p.sampler_config.repetition_penalty = 1.0;
15559 p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
15562 attach_test_mtp(&mut p);
15563 p.speculative = true;
15564 let r = p.generate("abcd", 10, None, None).unwrap();
15565 assert!(r.mtp_drafted > 0);
15566 assert_eq!(
15567 r.mtp_accepted, r.mtp_drafted,
15568 "constant logits → every draft accepted"
15569 );
15570 assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
15573 }
15574
15575 #[test]
15576 fn empty_prompt_is_an_error_not_a_panic() {
15577 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
15578 let r = p.generate("", 4, None, None);
15579 assert!(r.is_err(), "empty prompt must be a clean error");
15580 }
15581
15582 #[test]
15583 fn every_token_enters_kv_exactly_once() {
15584 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
15585 p.sampler_config.temperature = 0.0;
15587 let r = p.generate("abc", 2, None, None).unwrap();
15588 assert_eq!(r.prompt_tokens, 3);
15589 assert_eq!(
15593 p.kv_cache.seq_len(),
15594 3 + r.tokens_generated - 1,
15595 "each token must be cached exactly once (v1 cached the last prompt token twice)"
15596 );
15597 }
15598
15599 #[test]
15600 fn generation_is_reproducible_with_seed() {
15601 let run = || {
15602 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
15603 p.generate("hello", 8, None, None).unwrap().token_ids
15604 };
15605 assert_eq!(run(), run());
15606 }
15607
15608 #[test]
15609 fn resetting_sampler_restarts_the_seeded_stream() {
15610 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
15611 let config = SamplerConfig {
15612 seed: Some(1234),
15613 ..SamplerConfig::default()
15614 };
15615 p.set_sampler_config(config.clone());
15616 let first = p.generate("hello", 8, None, None).unwrap().token_ids;
15617 p.set_sampler_config(config);
15618 let second = p.generate("hello", 8, None, None).unwrap().token_ids;
15619 assert_eq!(first, second);
15620 }
15621
15622 #[test]
15623 fn eviction_bounds_the_cache() {
15624 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
15625 p.kv_cache.max_seq_len = 6;
15626 p.sampler_config.temperature = 0.0;
15627 let _ = p.generate("abcd", 12, None, None).unwrap();
15628 assert!(
15629 p.kv_cache.seq_len() <= 6 + 1,
15630 "cache must stay bounded by max_seq_len (got {})",
15631 p.kv_cache.seq_len()
15632 );
15633 }
15634
15635 #[test]
15636 fn confidence_matches_tokens_and_is_a_probability() {
15637 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
15638 p.sampler_config.temperature = 0.0;
15639 p.sampler_config.repetition_penalty = 1.0;
15640 let r = p.generate("abcd", 10, None, None).unwrap();
15641 assert_eq!(
15642 r.token_confidence.len(),
15643 r.token_ids.len(),
15644 "one confidence per emitted token"
15645 );
15646 for &c in &r.token_confidence {
15647 assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
15648 }
15649 let logits = [1.0f32, 3.0, 0.5, 3.0];
15651 let p0 = top1_prob_t(&logits, 1, 1.0);
15652 let p1 = top1_prob_t(&logits, 3, 1.0);
15653 assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
15654 assert!(p0 > 0.0 && p0 < 1.0);
15655 let sharp = top1_prob_t(&logits, 1, 1.0);
15657 let soft = top1_prob_t(&logits, 1, 2.0);
15658 assert!(soft < sharp, "higher temperature lowers peak confidence");
15659 }
15660
15661 #[test]
15662 fn trace_is_opt_in_and_parallels_the_output() {
15663 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
15665 p.sampler_config.temperature = 0.0;
15666 p.sampler_config.repetition_penalty = 1.0;
15667 let r = p.generate("abcd", 10, None, None).unwrap();
15668 assert!(r.traces.is_empty(), "trace must be empty unless enabled");
15669
15670 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
15672 p.sampler_config.temperature = 0.0;
15673 p.sampler_config.repetition_penalty = 1.0;
15674 p.set_trace(true);
15675 let r = p.generate("abcd", 10, None, None).unwrap();
15676 assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
15677 for (i, tr) in r.traces.iter().enumerate() {
15678 assert_eq!(tr.t, i, "trace index is sequential");
15679 assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
15680 assert_eq!(
15681 tr.confidence, r.token_confidence[i],
15682 "trace confidence matches the confidence channel"
15683 );
15684 assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
15686 }
15687 }
15688
15689 #[test]
15690 fn explain_prefill_logits_match_greedy_first_token() {
15691 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
15695 p.sampler_config.temperature = 0.0;
15696 p.sampler_config.repetition_penalty = 1.0;
15697 let ids = p.tokenizer.encode("abcd");
15698 let logits = p.prefill_next_logits(&ids, None);
15699 let argmax = logits
15700 .iter()
15701 .enumerate()
15702 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
15703 .unwrap()
15704 .0 as u32;
15705 let r = p.generate("abcd", 1, None, None).unwrap();
15706 assert_eq!(
15707 argmax, r.token_ids[0],
15708 "explain preview must match greedy emit"
15709 );
15710 }
15711
15712 #[test]
15713 fn laguna_shared_expert_is_unconditionally_added() {
15714 let matrix = |values: Vec<f32>| QTensor::from_f32(values, 2, 2);
15715 let identity = || matrix(vec![1.0, 0.0, 0.0, 1.0]);
15716 let zero_dense = || DenseFfn {
15717 gate_proj: matrix(vec![0.0; 4]),
15718 up_proj: matrix(vec![0.0; 4]),
15719 down_proj: matrix(vec![0.0; 4]),
15720 act: Act::Silu,
15721 down_t: None,
15722 segs: Vec::new(),
15723 };
15724 let shared = DenseFfn {
15725 gate_proj: identity(),
15726 up_proj: identity(),
15727 down_proj: identity(),
15728 act: Act::Silu,
15729 down_t: None,
15730 segs: Vec::new(),
15731 };
15732 let x = [1.0, 2.0];
15733 let expected = dense_ffn(&shared, &x, None);
15734 let moe = MoeFfn {
15735 router: QTensor::from_f32(vec![0.0, 0.0], 1, 2),
15736 experts: vec![zero_dense()],
15737 top_k: 1,
15738 norm_topk_prob: true,
15739 router_sigmoid: true,
15740 expert_bias: None,
15741 routed_scaling: 1.0,
15742 route_tau: None,
15743 shared: Some((shared, None)),
15744 stats: std::cell::RefCell::new(Vec::new()),
15745 act_sq: std::cell::RefCell::new(Vec::new()),
15746 act_rows: std::cell::RefCell::new(Vec::new()),
15747 mask: None,
15748 per_expert_scale: None,
15749 router_input_norm: false,
15750 resonance: None,
15751 };
15752 let actual = moe_ffn_cpu(&moe, &x, &[0], &[0.0], 1.0, None);
15753 for (actual, expected) in actual.iter().zip(expected) {
15754 assert!((actual - expected).abs() < 1e-6);
15755 }
15756 }
15757
15758 #[test]
15759 fn o1_batch_transition_publishes_one_epoch_before_serial_handoff() {
15760 const B: usize = 19;
15761 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
15762 p.set_o1(Some(crate::nystrom::O1Cfg {
15763 layers: crate::nystrom::O1Layers::All,
15764 m: 4,
15765 w: 8,
15766 sink: 2,
15767 rect: crate::nystrom::O1Rect::Aggregate,
15768 }));
15769 p.o1_begin_with_prefix(Some(B));
15770 let ids: Vec<u32> = (0..B as u32).collect();
15771 let _ = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
15772
15773 assert_eq!(p.o1_epoch, 1, "all layers publish one completed transition");
15774 assert!(p.kv_cache.layers.iter().all(|l| l.o1_sealed()));
15775 let next = p.embed_single(B as u32);
15776 let _ = p.forward_layers(&next, B, None);
15777 assert_eq!(p.o1_epoch, 1, "sealed handoff must not republish the epoch");
15778 }
15779
15780 #[test]
15781 fn o1_pair_transition_commits_scratch_before_epoch_publication() {
15782 const B: usize = 19;
15783 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
15784 let gdn_cfg = crate::linear_core::GdnCfg {
15788 num_v_heads: 2,
15789 num_k_heads: 1,
15790 key_head_dim: 2,
15791 value_head_dim: 4,
15792 conv_kernel: 3,
15793 hidden_size: 8,
15794 rms_eps: 1e-6,
15795 output_gate_sigmoid: false,
15796 };
15797 let synth = |n: usize, salt: usize| -> Vec<f32> {
15798 (0..n)
15799 .map(|i| (((i * 13 + salt * 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
15800 .collect()
15801 };
15802 let qt = |rows: usize, cols: usize, salt: usize| {
15803 crate::qtensor::QTensor::from_f32(synth(rows * cols, salt), rows, cols)
15804 };
15805 let c_dim = gdn_cfg.conv_dim();
15806 let vd = gdn_cfg.num_v_heads * gdn_cfg.value_head_dim;
15807 p.weights.layers[0].attn = AttnKind::LinearGdn(crate::linear_core::GdnWeights {
15808 in_proj_qkv: qt(c_dim, 8, 1),
15809 in_proj_z: qt(vd, 8, 2),
15810 in_proj_a: qt(gdn_cfg.num_v_heads, 8, 3),
15811 in_proj_b: qt(gdn_cfg.num_v_heads, 8, 4),
15812 conv1d: synth(c_dim * gdn_cfg.conv_kernel, 5),
15813 a_log: vec![0.2, 0.5],
15814 dt_bias: synth(gdn_cfg.num_v_heads, 6),
15815 norm: vec![1.0; gdn_cfg.value_head_dim],
15816 out_proj: qt(8, vd, 7),
15817 });
15818 p.gdn_cfg = Some(gdn_cfg);
15819 p.set_o1(Some(crate::nystrom::O1Cfg {
15820 layers: crate::nystrom::O1Layers::All,
15821 m: 4,
15822 w: 8,
15823 sink: 2,
15824 rect: crate::nystrom::O1Rect::Aggregate,
15825 }));
15826 p.o1_begin_with_prefix(Some(B));
15827 for pos in 0..B - 2 {
15828 let emb = p.embed_single(pos as u32);
15829 let _ = p.forward_layers(&emb, pos, None);
15830 }
15831 let lane1_state = p.kv_cache.layers[0].linear_state.clone();
15832
15833 let e1 = p.embed_single((B - 2) as u32);
15834 let e2 = p.embed_single((B - 1) as u32);
15835 let _ = p.forward_pair(&e1, &e2, B - 2);
15836
15837 assert_eq!(p.o1_epoch, 1, "pair crossing B publishes one epoch");
15838 assert!(
15839 p.kv_cache
15840 .layers
15841 .iter()
15842 .enumerate()
15843 .all(|(li, l)| !p.o1_flags[li] || l.o1_sealed())
15844 );
15845 assert!(!p.kv_cache.layers[0].linear_state.is_empty());
15846 assert_ne!(
15847 p.kv_cache.layers[0].linear_state, lane1_state,
15848 "real pair must commit GDN lane 2 before returning"
15849 );
15850 assert!(p.kv_cache.layers[0].linear_scratch.is_empty());
15851 let next = p.embed_single(B as u32);
15852 let _ = p.forward_layers(&next, B, None);
15853 assert_eq!(p.o1_epoch, 1, "serial continuation must reuse the epoch");
15854 }
15855
15856 #[test]
15857 fn o1_error_observation_stays_terminal_until_reset() {
15858 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
15859 p.set_o1(Some(crate::nystrom::O1Cfg {
15860 layers: crate::nystrom::O1Layers::All,
15861 m: 4,
15862 w: 8,
15863 sink: 2,
15864 rect: crate::nystrom::O1Rect::Aggregate,
15865 }));
15866 p.o1_begin();
15867 p.kv_cache.layers[0].o1_abort("synthetic transition failure".into());
15868
15869 assert!(p.o1_seal_checked().is_err());
15870 assert!(
15871 p.o1_seal_checked().is_err(),
15872 "retry must see the sticky error"
15873 );
15874 let k = vec![0.2f32; 4];
15875 let v = vec![0.3f32; 4];
15876 p.kv_cache.layers[0].append(&k, &v, &[]);
15877 assert_eq!(p.kv_cache.layers[0].seq_len, 0);
15878
15879 p.reset_session();
15880 p.o1_begin();
15881 p.kv_cache.layers[0].append(&k, &v, &[]);
15882 assert_eq!(p.kv_cache.layers[0].seq_len, 1);
15883 }
15884
15885 #[test]
15886 fn nll_graph_failure_is_terminal_and_request_is_reusable() {
15887 let ids = vec![1u32, 2, 3, 4, 5, 6];
15888 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
15889 p.graph_logits = Some(vec![123.0]);
15890 p.graph_want_logits = true;
15891 p.graph_failed
15892 .store(true, std::sync::atomic::Ordering::Relaxed);
15893 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
15894 let err = p.nll_ids_from(&ids, 0).expect_err("prior graph failure");
15895 assert!(err.contains("before NLL"));
15896 assert!(p.graph_logits.is_none());
15897 assert!(!p.graph_want_logits);
15898 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
15899 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
15900
15901 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
15902 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
15903 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
15904 assert_eq!(actual.1, expected.1);
15905 assert!((actual.0 - expected.0).abs() < 1e-9);
15906 }
15907
15908 #[test]
15909 fn nll_forward_failure_discards_partial_score_and_clears_sidechannels() {
15910 let ids = vec![1u32, 2, 3, 4, 5, 6];
15911 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
15912 p.nll_test_fail_at = Some(1);
15913 let err = p
15914 .nll_ids_from(&ids, 0)
15915 .expect_err("one-shot forward failure");
15916 assert!(err.contains("forward") || err.contains("score row"));
15917 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
15918 assert!(!p.graph_want_logits);
15919 assert!(p.graph_logits.is_none());
15920 assert!(p.kv_history.is_empty());
15921
15922 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
15923 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
15924 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
15925 assert_eq!(actual.1, expected.1);
15926 assert!((actual.0 - expected.0).abs() < 1e-9);
15927 }
15928
15929 #[test]
15930 fn nll_serial_failure_before_first_row_is_reported() {
15931 let ids = vec![1u32, 2, 3, 4];
15932 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
15933 p.nll_test_force_serial = true;
15934 p.nll_test_fail_at = Some(0);
15935 let err = p.nll_ids_from(&ids, 0).expect_err("serial forward failure");
15936 assert!(err.contains("serial forward"));
15937 assert!(p.kv_history.is_empty());
15938 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
15939 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
15940 }
15941
15942 #[test]
15943 fn ffn_probe_failure_discards_recorder_and_state() {
15944 let ids = vec![1u32, 2, 3, 4];
15945 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
15946 p.nll_test_fail_at = Some(0);
15947 let err = p
15948 .probe_ffn_mass_batch(&ids)
15949 .expect_err("probe forward failure");
15950 assert!(err.contains("NLL"));
15951 assert!(FFN_PROBE.with(|probe| probe.borrow().is_none()));
15952 assert!(p.kv_history.is_empty());
15953 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
15954 }
15955
15956 #[test]
15957 fn nll_test_controls_are_pipeline_scoped() {
15958 let ids = vec![1u32, 2, 3, 4];
15959 let mut failing = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
15960 let mut unaffected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
15961 failing.nll_test_force_serial = true;
15962 failing.nll_test_fail_at = Some(0);
15963
15964 assert!(!failing.can_prefill_batched());
15965 assert!(unaffected.can_prefill_batched());
15966 let expected = unaffected
15967 .nll_ids_from(&ids, 0)
15968 .expect("unaffected pipeline remains usable");
15969 let err = failing
15970 .nll_ids_from(&ids, 0)
15971 .expect_err("failure injection belongs to failing pipeline");
15972 assert!(err.contains("serial forward"));
15973 assert!(failing.nll_test_fail_at.is_none());
15974 assert!(unaffected.can_prefill_batched());
15975 let actual = unaffected
15976 .nll_ids_from(&ids, 0)
15977 .expect("unaffected pipeline remains reusable");
15978 assert_eq!(actual.1, expected.1);
15979 assert!((actual.0 - expected.0).abs() < 1e-9);
15980 }
15981
15982 #[test]
15983 fn forward_ids_failure_channel_is_terminal_and_reusable() {
15984 let ids = vec![1u32, 2, 3, 4, 5, 6];
15985 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
15986 p.graph_logits = Some(vec![123.0]);
15987 p.graph_want_logits = true;
15988 p.graph_failed
15989 .store(true, std::sync::atomic::Ordering::Relaxed);
15990 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
15991
15992 let err = p
15993 .forward_ids(&ids, None)
15994 .expect_err("a failed forward must not become a valid head result");
15995 assert!(err.contains("forward_ids setup"));
15996 assert!(p.graph_logits.is_none());
15997 assert!(!p.graph_want_logits);
15998 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
15999 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
16000 assert_eq!(p.kv_cache.seq_len(), 0);
16001
16002 let expected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64)
16003 .forward_ids(&ids, None)
16004 .expect("fresh forward_ids");
16005 let actual = p
16006 .forward_ids(&ids, None)
16007 .expect("pipeline remains reusable after a failed forward");
16008 assert_eq!(actual.len(), expected.len());
16009 assert!(
16010 actual
16011 .iter()
16012 .zip(expected)
16013 .all(|(a, b)| (a - b).abs() < 1e-9)
16014 );
16015 assert_eq!(p.kv_cache.seq_len(), ids.len());
16016 }
16017}