1use crate::linear_core::{GdnCfg, GdnWeights, gdn_forward};
9use crate::loader::{Overlay, build_ffn_at, load_f32, load_matrix};
10use crate::pipeline::{FfnKind, MoeFfn, moe_ffn};
11use crate::pool::Pool;
12use crate::qtensor::QTensor;
13use cortiq_core::{CmfError, CmfModel, LayerType, ModelArch, Qwen4ExpConfig, TensorDtype};
14use std::cmp::Ordering;
15use std::sync::Arc;
16
17const PRIME_1: u64 = 10_007;
18const SPLITMIX_GAMMA: u64 = 0x9E37_79B9_7F4A_7C15;
19const SPLITMIX_M1: u64 = 0xBF58_476D_1CE4_E5B9;
20const SPLITMIX_M2: u64 = 0x94D0_49BB_1331_11EB;
21
22pub struct GatedResidual {
23 norm: Vec<f32>,
24 down: QTensor,
25 up: QTensor,
26 inject: Option<QTensor>,
27 down_idx: Option<usize>,
31 up_idx: Option<usize>,
32 inject_idx: Option<usize>,
33}
34
35struct LayerIdx {
39 gdn: Option<[usize; 5]>,
41 qsa: Option<[usize; 5]>,
43 ple: Option<(usize, usize)>,
45 router: usize,
46 shared_gate: Option<usize>,
47}
48
49pub struct QsaWeights {
50 q_proj: QTensor,
51 k_proj: QTensor,
52 v_proj: QTensor,
53 o_proj: QTensor,
54 q_norm: Vec<f32>,
55 k_norm: Vec<f32>,
56 index_qk: QTensor,
57 index_q_norm: Vec<f32>,
58 index_k_norm: Vec<f32>,
59}
60
61pub struct PleWeights {
62 shards: Vec<QTensor>,
63 rows_per_shard: usize,
64 row_dim: usize,
65 key_proj: QTensor,
66 value_proj: QTensor,
67 norm_key: Vec<f32>,
68 norm_query: Vec<f32>,
69 norm_conv: Vec<f32>,
70 conv: Vec<f32>,
71 multipliers: Vec<i64>,
72 vocab_sizes: Vec<i64>,
73 offsets: Vec<i64>,
74}
75
76pub enum Mixer {
77 Gdn(GdnWeights),
78 Qsa(QsaWeights),
79}
80
81pub struct Layer {
82 attn_hc: GatedResidual,
83 mlp_hc: GatedResidual,
84 mixer: Mixer,
85 moe: MoeFfn,
86 ple: Option<PleWeights>,
87 expert_ids: Vec<(usize, usize, usize)>,
91 shared_ids: Option<(usize, usize, usize)>,
95 idx: Option<LayerIdx>,
96}
97
98pub struct Globals {
99 embed: QTensor,
100 lm_head: QTensor,
101 head_hc: GatedResidual,
102 lm_head_idx: Option<usize>,
103 embed_idx: Option<usize>,
105 skeleton_idxs: Vec<usize>,
108}
109
110#[derive(Clone)]
111pub struct Cfg {
112 hidden: usize,
113 hc: usize,
114 eps: f64,
115 n_heads: usize,
116 n_kv_heads: usize,
117 head_dim: usize,
118 rotary_dim: usize,
119 index_heads: usize,
120 index_kv_heads: usize,
121 index_dim: usize,
122 index_budget: usize,
123 compress_ratio: usize,
124 gdn: GdnCfg,
125 ngram_size: usize,
126 heads_per_ngram: usize,
127 ple_kernel: usize,
128 ple_dilation: usize,
129 eos: u32,
130}
131
132#[derive(Default)]
133struct QsaState {
134 raw_keys: Vec<f32>,
135 keys: Vec<f32>,
136 values: Vec<f32>,
137}
138
139#[derive(Default)]
140struct LayerState {
141 gdn: Vec<f32>,
142 qsa: QsaState,
143 ple_history: Vec<f32>,
145 ple_history_rows: usize,
146}
147
148pub struct State {
149 hyper: Vec<f32>,
150 layers: Vec<LayerState>,
151 token_history: Vec<u32>,
152 gpu_pool: Option<QwenGpuPool>,
153 pub pos: usize,
154 #[cfg(feature = "gpu")]
157 dev: Option<crate::gpu_wgpu::qwen4::Dev>,
158 picks_prev: Vec<Vec<usize>>,
162 device_off: bool,
164 host_pos: usize,
169 #[cfg(feature = "gpu")]
170 profile: Option<ExpertProfile>,
171 verify_window: bool,
174 window_logits: Vec<Vec<f32>>,
175 #[cfg(feature = "gpu")]
176 mtp: Option<MtpHead>,
177 mtp_tried: bool,
178}
179
180pub(crate) static FILL_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
184pub(crate) static FILL_N: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
185pub(crate) static FILLS_WALL_NS: std::sync::atomic::AtomicU64 =
187 std::sync::atomic::AtomicU64::new(0);
188
189#[cfg(feature = "gpu")]
191fn expert_slices(model: &CmfModel, t: (usize, usize, usize)) -> Option<[&[u8]; 3]> {
192 let bytes = model.primary_bytes();
193 let part = |i: usize| -> Option<&[u8]> {
194 let e = model.tensors.get(i)?;
195 let abs = model.entry_abs_offset(e)?;
196 bytes.get(abs..abs + e.nbytes as usize)
197 };
198 Some([part(t.0)?, part(t.1)?, part(t.2)?])
199}
200
201fn fill_trace() -> bool {
203 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
204 *S.get_or_init(|| std::env::var("CMF_QWEN_FILL_TRACE").as_deref() == Ok("1"))
205}
206
207pub(crate) struct QwenGpuPool {
208 pub(crate) hold_layers: [Option<usize>; 2],
211 staging_base: usize,
215 staging: usize,
216 #[cfg(feature = "gpu")]
219 stager: Option<crate::gpu_wgpu::qwen4::Stager>,
220 #[cfg(feature = "gpu")]
223 pub(crate) tier: Option<Arc<crate::gpu_wgpu::qwen4::HostTier>>,
224 admit_pool: Option<Arc<Pool>>,
227 pub(crate) store: Option<Arc<crate::expert_store::ExpertStore>>,
230 pub(crate) segment_slots: usize,
231 floor: usize,
232 n_experts: usize,
233 fetch_quota: usize,
234 fetch_min_seen: u16,
235 fetch_max_env: &'static str,
236 fetch_min_env: &'static str,
237 owner: Vec<Option<(usize, usize)>>,
238 slot_for: Vec<u32>,
242 shared_slot: Vec<u32>,
243 seen: Vec<u16>,
244 seen_epoch: Vec<u32>,
245 free: Vec<usize>,
248 occupancy: Vec<usize>,
249 last: Vec<u64>,
250 clock: u64,
251}
252
253#[cfg(feature = "gpu")]
263#[must_use = "a pending admission must be finished or deliberately dropped"]
264pub(crate) struct QwenGpuPendingEnsure<'pool, 'input> {
265 pool: Option<&'pool mut QwenGpuPool>,
266 model: &'input Arc<CmfModel>,
267 picks: &'input [usize],
268 triples: &'input [(usize, usize, usize)],
269 layer: usize,
270 base: usize,
271 shared_slot: u32,
272 plan: Vec<(usize, usize)>,
273}
274
275#[cfg(feature = "gpu")]
276impl QwenGpuPendingEnsure<'_, '_> {
277 pub(crate) fn cold_ids(&self) -> Option<Vec<usize>> {
281 let pool = self.pool.as_deref()?;
282 cold_picks_from_slots(&pool.slot_for, self.base, pool.n_experts, self.picks)
283 }
284
285 pub(crate) fn finish(self) -> Option<(Vec<u32>, u32)> {
290 self.finish_inner(false)
291 .map(|(remap, shared_slot, _)| (remap, shared_slot))
292 }
293
294 pub(crate) fn finish_and_flush(self) -> Option<(Vec<u32>, u32, usize)> {
300 self.finish_inner(true)
301 }
302
303 fn finish_inner(mut self, flush_uploads: bool) -> Option<(Vec<u32>, u32, usize)> {
304 let pool = self.pool.take()?;
308 let model = self.model;
309 let triples = self.triples;
310 let layer = self.layer;
311 let base = self.base;
312 let shared_slot = self.shared_slot;
313 let plan = std::mem::take(&mut self.plan);
314 let ok = pool.run_fills(model, layer, &plan, triples, false);
317 let mut failed = false;
318 for (&(slot, e), ok) in plan.iter().zip(&ok) {
319 if !ok {
320 pool.owner[slot] = None;
321 pool.slot_for[base + e] = u32::MAX;
322 pool.occupancy[layer] = pool.occupancy[layer].saturating_sub(1);
323 pool.free.push(slot);
324 failed = true;
325 }
326 }
327 let result = if failed || triples.len() != pool.n_experts {
328 None
329 } else {
330 let remap = pool.slot_for[base..base + pool.n_experts].to_vec();
331 Some((remap, shared_slot, pool.segment_slots))
332 };
333 if flush_uploads {
334 pool.flush_uploads();
335 }
336 result
337 }
338}
339
340#[cfg(feature = "gpu")]
341impl Drop for QwenGpuPendingEnsure<'_, '_> {
342 fn drop(&mut self) {
343 let Some(pool) = self.pool.as_deref_mut() else {
344 return;
345 };
346 for &(slot, expert) in &self.plan {
352 let Some(key) = self.base.checked_add(expert) else {
353 continue;
354 };
355 if pool.owner.get(slot).copied().flatten() == Some((self.layer, expert))
356 && pool.slot_for.get(key) == Some(&(slot as u32))
357 {
358 pool.owner[slot] = None;
359 pool.slot_for[key] = u32::MAX;
360 if let Some(occupancy) = pool.occupancy.get_mut(self.layer) {
361 *occupancy = occupancy.saturating_sub(1);
362 }
363 pool.free.push(slot);
364 }
365 }
366 }
367}
368
369#[cfg(feature = "gpu")]
373fn cold_picks_from_slots(
374 slot_for: &[u32],
375 base: usize,
376 n_experts: usize,
377 picks: &[usize],
378) -> Option<Vec<usize>> {
379 let slots = slot_for.get(base..base.checked_add(n_experts)?)?;
380 let mut cold = Vec::with_capacity(picks.len());
381 for &expert in picks {
382 let slot = *slots.get(expert)?;
383 if slot == u32::MAX {
384 cold.push(expert);
385 }
386 }
387 Some(cold)
388}
389
390fn bounded_pool_pct(raw: Option<&str>, default: usize, min: usize, max: usize) -> usize {
394 raw.and_then(|v| v.parse::<usize>().ok())
395 .unwrap_or(default)
396 .clamp(min, max)
397}
398
399fn bounded_pool_slots(raw: Option<&str>, safe: usize, cap_override: bool) -> usize {
400 let floor = if cap_override { safe.min(8) } else { 8 };
401 raw.and_then(|v| v.parse::<usize>().ok())
402 .map(|v| if cap_override { v.min(safe) } else { v })
403 .unwrap_or(safe)
404 .max(floor)
405}
406
407fn active_layer_floor(capacity: usize, active_layers: usize) -> usize {
415 capacity / active_layers.max(1)
416}
417
418impl State {
419 pub fn new(n_layers: usize) -> Self {
420 Self {
421 hyper: Vec::new(),
422 layers: (0..n_layers).map(|_| LayerState::default()).collect(),
423 token_history: Vec::new(),
424 gpu_pool: None,
425 pos: 0,
426 #[cfg(feature = "gpu")]
427 dev: None,
428 picks_prev: vec![Vec::new(); n_layers],
429 device_off: false,
430 host_pos: 0,
431 #[cfg(feature = "gpu")]
432 profile: None,
433 verify_window: false,
434 window_logits: Vec::new(),
435 #[cfg(feature = "gpu")]
436 mtp: None,
437 mtp_tried: false,
438 }
439 }
440
441 fn reset(&mut self) {
442 self.hyper.clear();
443 self.token_history.clear();
444 self.pos = 0;
445 self.host_pos = 0;
446 for st in &mut self.layers {
447 *st = LayerState::default();
448 }
449 }
450}
451
452#[cfg(feature = "gpu")]
453impl QwenGpuPool {
454 pub(crate) fn create(
455 model: &Arc<CmfModel>,
456 inter: usize,
457 hidden: usize,
458 n_layers: usize,
459 n_experts: usize,
460 gu_q2: bool,
461 ) -> Option<Self> {
462 Self::create_with_policy(
463 model,
464 inter,
465 hidden,
466 n_layers,
467 n_experts,
468 gu_q2,
469 "CMF_QWEN_POOL_PCT",
470 75,
471 25,
472 85,
473 "CMF_QWEN_EXPERT_SLOTS",
474 "CMF_QWEN_FETCH_MAX",
475 "CMF_QWEN_FETCH_MIN_SEEN",
476 None,
477 0,
478 )
479 }
480
481 pub(crate) fn create_explicit(
485 model: &Arc<CmfModel>,
486 inter: usize,
487 hidden: usize,
488 n_layers: usize,
489 n_experts: usize,
490 gu_q2: bool,
491 slots: usize,
492 staging: usize,
493 ) -> Option<Self> {
494 Self::create_with_policy(
495 model,
496 inter,
497 hidden,
498 n_layers,
499 n_experts,
500 gu_q2,
501 "CMF_QWEN_POOL_PCT",
502 75,
503 25,
504 85,
505 "CMF_QWEN_EXPERT_SLOTS",
506 "CMF_QWEN_FETCH_MAX",
507 "CMF_QWEN_FETCH_MIN_SEEN",
508 Some(slots),
509 staging,
510 )
511 .map(|mut pool| {
512 let stage_mb = std::env::var("CMF_QWEN_STAGE_MB")
523 .ok()
524 .and_then(|v| v.parse::<u64>().ok())
525 .unwrap_or(0);
526 pool.stager = crate::gpu_wgpu::qwen4::Stager::new(stage_mb);
527 let admit_threads = std::env::var("CMF_QWEN_ADMIT_THREADS")
531 .ok()
532 .and_then(|v| v.parse::<usize>().ok())
533 .unwrap_or(16);
534 pool.admit_pool = (admit_threads > 0).then(|| Arc::new(Pool::new(admit_threads)));
535 pool.fetch_min_seen = std::env::var("CMF_QWEN_FETCH_MIN_SEEN")
539 .ok()
540 .and_then(|v| v.parse().ok())
541 .unwrap_or(1);
542 pool.fetch_quota = std::env::var("CMF_QWEN_FETCH_MAX")
543 .ok()
544 .and_then(|v| v.parse().ok())
545 .unwrap_or(pool.fetch_quota.max(16));
546 pool
547 })
548 }
549
550 pub(crate) fn configure_minicpm_tail(&mut self, active_layers: usize) {
561 let layers = active_layers.clamp(1, self.occupancy.len().max(1));
562 self.floor = active_layer_floor(self.staging_base, layers);
563 if std::env::var_os("CMF_QWEN_ADMIT_THREADS").is_none() {
564 self.admit_pool = Some(Arc::new(Pool::new(2)));
569 }
570 }
571
572 pub(crate) fn create_exact(
576 model: &Arc<CmfModel>,
577 inter: usize,
578 hidden: usize,
579 n_experts: usize,
580 gu_q2: bool,
581 slots: usize,
582 ) -> Option<Self> {
583 Self::create_with_policy(
584 model,
585 inter,
586 hidden,
587 1,
588 n_experts,
589 gu_q2,
590 "CMF_QWEN_MTP_BANK",
591 75,
592 25,
593 85,
594 "CMF_QWEN_MTP_BANK_SLOTS",
595 "CMF_QWEN_FETCH_MAX",
596 "CMF_QWEN_FETCH_MIN_SEEN",
597 Some(slots),
598 0,
599 )
600 }
601
602 pub(crate) fn per_expert_bytes(inter: usize, hidden: usize, gu_q2: bool) -> Option<usize> {
604 let gu = cortiq_core::quant::expected_nbytes(
605 if gu_q2 {
606 TensorDtype::Q2TiledP
607 } else {
608 TensorDtype::Q4TiledP
609 },
610 &[inter, hidden],
611 )?;
612 let dn = cortiq_core::quant::expected_nbytes(TensorDtype::Q4TiledP, &[hidden, inter])?;
613 2usize.checked_mul(gu)?.checked_add(dn)
614 }
615
616 fn victim(&mut self, layer: usize, protect: &[usize]) -> Option<usize> {
620 if let Some(s) = self.free.pop() {
621 return Some(s);
622 }
623 let hold = self.hold_layers;
624 let eligible = |owner: (usize, usize)| {
625 owner.1 != usize::MAX
626 && (owner.0 != layer || !protect.contains(&owner.1))
627 && !hold.contains(&Some(owner.0))
630 };
631 self.owner
636 .iter()
637 .enumerate()
638 .filter_map(|(slot, &o)| {
639 o.filter(|&x| {
640 eligible(x) && self.occupancy.get(x.0).copied().unwrap_or(0) > self.floor
641 })
642 .map(|_| slot)
643 })
644 .min_by_key(|&slot| self.last[slot])
645 .or_else(|| {
646 self.owner
647 .iter()
648 .enumerate()
649 .filter_map(|(slot, &o)| {
650 o.filter(|&x| eligible(x) && x.0 == layer).map(|_| slot)
651 })
652 .min_by_key(|&slot| self.last[slot])
653 })
654 .or_else(|| {
655 self.owner
656 .iter()
657 .enumerate()
658 .filter_map(|(slot, &o)| o.filter(|&x| eligible(x)).map(|_| slot))
659 .min_by_key(|&slot| self.last[slot])
660 })
661 }
662
663 pub(crate) fn admit_now(
666 &mut self,
667 model: &Arc<CmfModel>,
668 layer: usize,
669 expert: usize,
670 triples: &[(usize, usize, usize)],
671 protect: &[usize],
672 ) -> Option<u32> {
673 if expert >= self.n_experts || triples.len() != self.n_experts {
674 return None;
675 }
676 let key = layer * self.n_experts + expert;
677 if self.slot_for[key] != u32::MAX {
678 return Some(self.slot_for[key]);
679 }
680 let victim = self.victim(layer, protect)?;
681 if !self.fill_slot(model, victim, layer, expert, triples[expert]) {
682 if self.owner[victim].is_none() {
683 self.free.push(victim);
684 }
685 return None;
686 }
687 if let Some(old) = self.owner[victim] {
688 self.dropped(old);
689 if old.1 == usize::MAX {
690 self.shared_slot[old.0] = u32::MAX;
691 } else {
692 self.slot_for[old.0 * self.n_experts + old.1] = u32::MAX;
693 self.occupancy[old.0] = self.occupancy[old.0].saturating_sub(1);
694 }
695 }
696 self.clock = self.clock.saturating_add(1);
697 self.owner[victim] = Some((layer, expert));
698 self.slot_for[key] = victim as u32;
699 self.occupancy[layer] += 1;
700 self.seen[key] = self.seen[key].max(1);
701 self.last[victim] = self.clock;
702 Some(victim as u32)
703 }
704
705 #[cfg(feature = "gpu")]
708 fn fill_slot(
709 &self,
710 model: &Arc<CmfModel>,
711 slot: usize,
712 layer: usize,
713 expert: usize,
714 triple: (usize, usize, usize),
715 ) -> bool {
716 let t0 = std::time::Instant::now();
717 let key = layer * self.n_experts + expert;
718 let tier = self.tier.as_ref();
722 if tier.is_some_and(|t| t.hit(key, slot)) {
723 if slot < self.staging_base
724 && let Some(store) = self.store.as_ref()
725 {
726 store.note_vram(layer, expert, true);
727 }
728 return true;
729 }
730 let up = |parts: [&[u8]; 3]| -> bool {
731 if tier.is_some_and(|t| t.fill(key, Some(slot), parts)) {
732 return true;
733 }
734 crate::gpu_wgpu::qwen4::upload_expert_parts(self.stager.as_ref(), model, slot, parts)
735 };
736 let stored = self
737 .store
738 .as_ref()
739 .and_then(|store| store.with_expert(layer, expert, true, up));
740 let ok = match stored {
741 Some(ok) => ok,
742 None => {
743 if tier.is_some()
744 && let Some(parts) = expert_slices(model, triple)
745 {
746 up(parts)
747 } else if let Some(st) = self.stager.as_ref()
748 && crate::gpu_wgpu::qwen4::stage_expert(st, model, slot, triple)
749 {
750 true
751 } else {
752 crate::gpu_wgpu::dsv4_global_slot_fill(model, slot, triple)
753 }
754 }
755 };
756 if ok && slot < self.staging_base
759 && let Some(store) = self.store.as_ref()
760 {
761 store.note_vram(layer, expert, true);
762 }
763 FILL_NS.fetch_add(
764 t0.elapsed().as_nanos() as u64,
765 std::sync::atomic::Ordering::Relaxed,
766 );
767 FILL_N.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
768 ok
769 }
770
771 #[cfg(feature = "gpu")]
775 fn run_fills(
776 &self,
777 model: &Arc<CmfModel>,
778 layer: usize,
779 jobs: &[(usize, usize)],
780 triples: &[(usize, usize, usize)],
781 chunked: bool,
782 ) -> Vec<bool> {
783 let t0 = std::time::Instant::now();
784 let me = self;
785 let device = crate::gpu::current_device();
792 let out: Vec<bool> = if jobs.len() <= 1 {
793 jobs.iter()
794 .map(|&(slot, e)| me.fill_slot(model, slot, layer, e, triples[e]))
795 .collect()
796 } else if let Some(pool) = self.admit_pool.as_ref() {
797 let done: Vec<std::sync::atomic::AtomicBool> = jobs
798 .iter()
799 .map(|_| std::sync::atomic::AtomicBool::new(false))
800 .collect();
801 let next = std::sync::atomic::AtomicUsize::new(0);
802 pool.run(&|_, _| {
803 loop {
804 let i = next.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
805 let Some(&(slot, e)) = jobs.get(i) else { break };
806 let ok = me.fill_slot(model, slot, layer, e, triples[e]);
807 done[i].store(ok, std::sync::atomic::Ordering::Relaxed);
808 }
809 });
810 done.iter()
811 .map(|d| d.load(std::sync::atomic::Ordering::Relaxed))
812 .collect()
813 } else if chunked {
814 const ADMIT_THREADS: usize = 32;
815 let nth = jobs.len().min(ADMIT_THREADS);
816 let per = jobs.len().div_ceil(nth);
817 std::thread::scope(|scope| {
818 let handles: Vec<_> = jobs
819 .chunks(per)
820 .map(|part| {
821 let model = model.clone();
822 let h = scope.spawn(move || {
823 crate::gpu::with_device(device, || {
824 part.iter()
825 .map(|&(slot, e)| {
826 me.fill_slot(&model, slot, layer, e, triples[e])
827 })
828 .collect::<Vec<bool>>()
829 })
830 });
831 (part.len(), h)
832 })
833 .collect();
834 handles
837 .into_iter()
838 .flat_map(|(n, h)| h.join().unwrap_or_else(|_| vec![false; n]))
839 .collect()
840 })
841 } else {
842 std::thread::scope(|scope| {
843 let hs: Vec<_> = jobs
844 .iter()
845 .map(|&(slot, e)| {
846 let model = model.clone();
847 let triple = triples[e];
848 scope.spawn(move || {
849 crate::gpu::with_device(device, || {
850 me.fill_slot(&model, slot, layer, e, triple)
851 })
852 })
853 })
854 .collect();
855 hs.into_iter().map(|h| h.join().unwrap_or(false)).collect()
856 })
857 };
858 FILLS_WALL_NS.fetch_add(
859 t0.elapsed().as_nanos() as u64,
860 std::sync::atomic::Ordering::Relaxed,
861 );
862 out
863 }
864
865 fn dropped(&self, old: (usize, usize)) {
867 if old.1 != usize::MAX
868 && let Some(store) = self.store.as_ref()
869 {
870 store.note_vram(old.0, old.1, false);
871 }
872 }
873
874 #[cfg(feature = "gpu")]
877 pub(crate) fn flush_uploads(&mut self) {
878 if let Some(st) = self.stager.as_mut() {
879 st.flush();
880 }
881 if let Some(cb) = self.tier.as_ref().and_then(|t| t.take()) {
882 crate::gpu_wgpu::qwen4::submit_cb(cb);
883 }
884 }
885
886 pub(crate) fn take_uploads(&mut self) -> Option<wgpu::CommandBuffer> {
891 let staged = self.stager.as_mut().and_then(|st| st.take());
892 let tiered = self.tier.as_ref().and_then(|t| t.take());
893 match (staged, tiered) {
894 (Some(a), Some(b)) => {
895 crate::gpu_wgpu::qwen4::submit_cb(a);
896 Some(b)
897 }
898 (a, b) => a.or(b),
899 }
900 }
901
902 pub(crate) fn uploads_done(&self) {
905 if let Some(t) = self.tier.as_ref() {
906 t.done();
907 }
908 }
909
910 pub(crate) fn rearm_uploads(&mut self) {
911 if let Some(st) = self.stager.as_mut() {
912 st.rearm();
913 }
914 }
915
916 pub(crate) fn stage_cold(
920 &self,
921 model: &Arc<CmfModel>,
922 layer: usize,
923 tok: usize,
924 experts: &[usize],
925 triples: &[(usize, usize, usize)],
926 top_k: usize,
927 ) -> Vec<Option<u32>> {
928 if self.staging == 0 || (tok + 1) * top_k > self.staging || triples.len() != self.n_experts
929 {
930 return vec![None; experts.len()];
931 }
932 let base = self.staging_base + tok * top_k;
933 if fill_trace() {
934 eprintln!("fill staging tok={tok} experts={experts:?}");
935 }
936 let jobs: Vec<(usize, usize)> = experts
937 .iter()
938 .enumerate()
939 .filter(|&(_, &e)| e < self.n_experts)
940 .map(|(j, &e)| (base + j, e))
941 .collect();
942 let ok = self.run_fills(model, layer, &jobs, triples, false);
943 let mut out = vec![None; experts.len()];
944 let mut ji = 0;
945 for (j, &e) in experts.iter().enumerate() {
946 if e < self.n_experts {
947 if ok[ji] {
948 out[j] = Some(jobs[ji].0 as u32);
949 }
950 ji += 1;
951 }
952 }
953 out
954 }
955
956 pub(crate) fn admit_many(
960 &mut self,
961 model: &Arc<CmfModel>,
962 layer: usize,
963 experts: &[usize],
964 triples: &[(usize, usize, usize)],
965 protect: &[usize],
966 ) -> Vec<Option<u32>> {
967 if triples.len() != self.n_experts {
968 return vec![None; experts.len()];
969 }
970 let plan = self.reserve_many(layer, experts, protect);
971 let uploads: Vec<(usize, usize)> = plan
972 .iter()
973 .filter_map(|(_, p)| p.filter(|&(_, e)| e != usize::MAX))
974 .collect();
975 let failed = self.upload_reserved(model, layer, &uploads, triples, false);
976 Self::plan_slots(plan, &failed)
977 }
978
979 fn reserve_many(
985 &mut self,
986 layer: usize,
987 experts: &[usize],
988 protect: &[usize],
989 ) -> Vec<(usize, Option<(usize, usize)>)> {
990 let mut plan: Vec<(usize, Option<(usize, usize)>)> = Vec::with_capacity(experts.len());
991 for &expert in experts {
992 if expert >= self.n_experts {
993 plan.push((expert, None));
994 continue;
995 }
996 let key = layer * self.n_experts + expert;
997 if self.slot_for[key] != u32::MAX {
998 plan.push((expert, Some((self.slot_for[key] as usize, usize::MAX))));
999 continue;
1000 }
1001 let Some(victim) = self.victim(layer, protect) else {
1002 plan.push((expert, None));
1003 continue;
1004 };
1005 let old = self.owner[victim].take();
1007 if let Some(old) = old {
1008 self.dropped(old);
1009 if old.1 == usize::MAX {
1010 self.shared_slot[old.0] = u32::MAX;
1011 } else {
1012 self.slot_for[old.0 * self.n_experts + old.1] = u32::MAX;
1013 self.occupancy[old.0] = self.occupancy[old.0].saturating_sub(1);
1014 }
1015 }
1016 self.owner[victim] = Some((layer, expert));
1017 self.slot_for[key] = victim as u32;
1018 self.occupancy[layer] += 1;
1019 self.seen[key] = self.seen[key].max(1);
1020 self.clock = self.clock.saturating_add(1);
1021 self.last[victim] = self.clock;
1022 if fill_trace() {
1023 eprintln!("fill cold layer={layer} expert={expert} slot={victim}");
1024 }
1025 plan.push((expert, Some((victim, expert))));
1026 }
1027 plan
1028 }
1029
1030 fn upload_reserved(
1035 &mut self,
1036 model: &Arc<CmfModel>,
1037 layer: usize,
1038 uploads: &[(usize, usize)],
1039 triples: &[(usize, usize, usize)],
1040 pooled: bool,
1041 ) -> Vec<usize> {
1042 if let Some(store) = self.store.as_ref() {
1043 let es: Vec<usize> = uploads.iter().map(|&(_, e)| e).collect();
1044 store.prefetch(layer, &es);
1045 }
1046 let mut hit = vec![false; uploads.len()];
1048 if let Some(t) = self.tier.as_ref() {
1049 for (i, &(slot, e)) in uploads.iter().enumerate() {
1050 if t.hit(layer * self.n_experts + e, slot) {
1051 hit[i] = true;
1052 if slot < self.staging_base
1053 && let Some(store) = self.store.as_ref()
1054 {
1055 store.note_vram(layer, e, true);
1056 }
1057 }
1058 }
1059 }
1060 let all = uploads;
1061 let rest: Vec<(usize, usize)> = all
1062 .iter()
1063 .zip(&hit)
1064 .filter(|(_, h)| !**h)
1065 .map(|(&u, _)| u)
1066 .collect();
1067 let results = self.run_fills(model, layer, &rest, triples, pooled);
1068 let mut rest_ok = results.into_iter();
1069 let results: Vec<bool> = hit
1070 .iter()
1071 .map(|&h| h || rest_ok.next().unwrap_or(false))
1072 .collect();
1073 let uploads = all;
1074 let mut failed: Vec<usize> = Vec::new();
1075 for (i, &(slot, e)) in uploads.iter().enumerate() {
1076 if !results.get(i).copied().unwrap_or(false) {
1077 self.owner[slot] = None;
1079 self.slot_for[layer * self.n_experts + e] = u32::MAX;
1080 self.occupancy[layer] = self.occupancy[layer].saturating_sub(1);
1081 self.free.push(slot);
1082 failed.push(e);
1083 }
1084 }
1085 failed
1086 }
1087
1088 fn plan_slots(plan: Vec<(usize, Option<(usize, usize)>)>, failed: &[usize]) -> Vec<Option<u32>> {
1089 plan.into_iter()
1090 .map(|(expert, p)| match p {
1091 Some((slot, _)) if !failed.contains(&expert) => Some(slot as u32),
1092 _ => None,
1093 })
1094 .collect()
1095 }
1096
1097 pub(crate) fn admit_frame(
1104 &mut self,
1105 model: &Arc<CmfModel>,
1106 layer: usize,
1107 reqs: &[(Vec<usize>, Vec<usize>)],
1108 triples: &[(usize, usize, usize)],
1109 ) -> Vec<Vec<Option<u32>>> {
1110 if triples.len() != self.n_experts {
1111 return reqs.iter().map(|(e, _)| vec![None; e.len()]).collect();
1112 }
1113 let plans: Vec<_> = reqs
1114 .iter()
1115 .map(|(experts, protect)| self.reserve_many(layer, experts, protect))
1116 .collect();
1117 let mut uploads: Vec<(usize, usize)> = Vec::new();
1121 for plan in &plans {
1122 for &(_, p) in plan {
1123 if let Some((slot, e)) = p.filter(|&(_, e)| e != usize::MAX) {
1124 uploads.retain(|&(s, _)| s != slot);
1125 uploads.push((slot, e));
1126 }
1127 }
1128 }
1129 let failed = self.upload_reserved(model, layer, &uploads, triples, true);
1130 plans
1131 .into_iter()
1132 .map(|plan| Self::plan_slots(plan, &failed))
1133 .collect()
1134 }
1135
1136 pub(crate) fn prefill(
1139 &mut self,
1140 model: &Arc<CmfModel>,
1141 ranked: &[(usize, usize)],
1142 triples: &[Vec<(usize, usize, usize)>],
1143 ) -> usize {
1144 if let Some(store) = self.store.clone() {
1145 return self.prefill_parallel(model, ranked, triples, &store);
1146 }
1147 let mut n = 0;
1148 for &(layer, expert) in ranked {
1149 if self.free.is_empty() {
1150 break;
1151 }
1152 if layer >= triples.len() || expert >= self.n_experts {
1153 continue;
1154 }
1155 let key = layer * self.n_experts + expert;
1156 if self.slot_for[key] != u32::MAX {
1157 continue;
1158 }
1159 let Some(&triple) = triples[layer].get(expert) else {
1160 continue;
1161 };
1162 let Some(slot) = self.free.pop() else { break };
1163 if !crate::gpu_wgpu::dsv4_global_slot_fill(model, slot, triple) {
1164 self.free.push(slot);
1165 break;
1166 }
1167 self.owner[slot] = Some((layer, expert));
1168 self.slot_for[key] = slot as u32;
1169 self.occupancy[layer] += 1;
1170 self.seen[key] = self.seen[key].max(1);
1171 self.clock = self.clock.saturating_add(1);
1172 self.last[slot] = self.clock;
1173 n += 1;
1174 }
1175 n
1176 }
1177
1178 fn prefill_parallel(
1183 &mut self,
1184 model: &Arc<CmfModel>,
1185 ranked: &[(usize, usize)],
1186 triples: &[Vec<(usize, usize, usize)>],
1187 store: &Arc<crate::expert_store::ExpertStore>,
1188 ) -> usize {
1189 let mut plan: Vec<(usize, usize, usize)> = Vec::new();
1190 for &(layer, expert) in ranked {
1191 if layer >= triples.len() || expert >= self.n_experts {
1192 continue;
1193 }
1194 let key = layer * self.n_experts + expert;
1195 if self.slot_for[key] != u32::MAX {
1196 continue;
1197 }
1198 let Some(slot) = self.free.pop() else { break };
1199 plan.push((slot, layer, expert));
1200 }
1201 let threads = std::thread::available_parallelism()
1202 .map_or(8, |n| n.get())
1203 .clamp(4, 32);
1204 let next = std::sync::atomic::AtomicUsize::new(0);
1205 let done: Vec<std::sync::atomic::AtomicBool> = (0..plan.len())
1206 .map(|_| std::sync::atomic::AtomicBool::new(false))
1207 .collect();
1208 std::thread::scope(|scope| {
1209 for _ in 0..threads {
1210 scope.spawn(|| {
1211 loop {
1212 let i = next.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1213 let Some(&(slot, layer, expert)) = plan.get(i) else {
1214 break;
1215 };
1216 let ok = store
1219 .with_expert(layer, expert, false, |parts| {
1220 crate::gpu_wgpu::qwen4::upload_expert_parts(
1221 None, model, slot, parts,
1222 )
1223 })
1224 .unwrap_or(false);
1225 done[i].store(ok, std::sync::atomic::Ordering::Relaxed);
1226 if i % 384 == 383 {
1227 crate::gpu_wgpu::qwen4::flush_writes();
1228 }
1229 }
1230 });
1231 }
1232 });
1233 crate::gpu_wgpu::qwen4::flush_writes();
1234 let mut n = 0;
1235 for (i, &(slot, layer, expert)) in plan.iter().enumerate() {
1236 if !done[i].load(std::sync::atomic::Ordering::Relaxed) {
1237 self.free.push(slot);
1238 continue;
1239 }
1240 let key = layer * self.n_experts + expert;
1241 self.owner[slot] = Some((layer, expert));
1242 self.slot_for[key] = slot as u32;
1243 self.occupancy[layer] += 1;
1244 self.seen[key] = self.seen[key].max(1);
1245 self.clock = self.clock.saturating_add(1);
1246 self.last[slot] = self.clock;
1247 store.note_vram(layer, expert, true);
1248 n += 1;
1249 }
1250 n
1251 }
1252
1253 pub(crate) fn remap_snapshot(&self, layer: usize) -> (Vec<u32>, u32) {
1256 let base = layer * self.n_experts;
1257 (
1258 self.slot_for[base..base + self.n_experts].to_vec(),
1259 self.shared_slot.get(layer).copied().unwrap_or(u32::MAX),
1260 )
1261 }
1262
1263 pub(crate) fn create_for_dsv41(
1268 model: &Arc<CmfModel>,
1269 inter: usize,
1270 hidden: usize,
1271 n_layers: usize,
1272 n_experts: usize,
1273 gu_q2: bool,
1274 ) -> Option<Self> {
1275 Self::create_with_policy(
1276 model,
1277 inter,
1278 hidden,
1279 n_layers,
1280 n_experts,
1281 gu_q2,
1282 "CMF_DSV41_POOL_PCT",
1283 75,
1284 25,
1285 85,
1286 "CMF_DSV41_EXPERT_SLOTS",
1287 "CMF_DSV41_FETCH_MAX",
1288 "CMF_DSV41_FETCH_MIN_SEEN",
1289 None,
1290 0,
1291 )
1292 }
1293
1294 pub(crate) fn create_for_glm(
1302 model: &Arc<CmfModel>,
1303 inter: usize,
1304 hidden: usize,
1305 n_layers: usize,
1306 n_experts: usize,
1307 gu_q2: bool,
1308 ) -> Option<Self> {
1309 Self::create_with_policy(
1310 model,
1311 inter,
1312 hidden,
1313 n_layers,
1314 n_experts,
1315 gu_q2,
1316 "CMF_GLM_POOL_PCT",
1317 40,
1318 20,
1319 40,
1320 "CMF_GLM_EXPERT_SLOTS",
1321 "CMF_GLM_FETCH_MAX",
1322 "CMF_GLM_FETCH_MIN_SEEN",
1323 None,
1324 0,
1325 )
1326 }
1327
1328 fn create_with_policy(
1329 model: &Arc<CmfModel>,
1330 inter: usize,
1331 hidden: usize,
1332 n_layers: usize,
1333 n_experts: usize,
1334 gu_q2: bool,
1335 pct_env: &str,
1336 default_pct: usize,
1337 min_pct: usize,
1338 max_pct: usize,
1339 slots_env: &str,
1340 fetch_max_env: &'static str,
1341 fetch_min_env: &'static str,
1342 explicit_slots: Option<usize>,
1343 staging: usize,
1344 ) -> Option<Self> {
1345 if !crate::gpu_wgpu::dsv4_global_moe_supported() {
1346 return None;
1347 }
1348 let gu = cortiq_core::quant::expected_nbytes(
1349 if gu_q2 {
1350 TensorDtype::Q2TiledP
1351 } else {
1352 TensorDtype::Q4TiledP
1353 },
1354 &[inter, hidden],
1355 )?;
1356 let dn = cortiq_core::quant::expected_nbytes(
1357 cortiq_core::TensorDtype::Q4TiledP,
1358 &[hidden, inter],
1359 )?;
1360 let per = 2usize.checked_mul(gu)?.checked_add(dn)?;
1361 let budget = crate::gpu_wgpu::dsv4_vram_budget()? as usize;
1362 let max_pct = if pct_env == "CMF_GLM_POOL_PCT" && budget >= 20_000_000_000 {
1372 100
1377 } else {
1378 max_pct
1379 };
1380 let default_pct = if pct_env == "CMF_GLM_POOL_PCT" && budget >= 20_000_000_000 {
1381 100
1382 } else {
1383 default_pct
1384 };
1385 let pool_pct = bounded_pool_pct(
1386 std::env::var(pct_env).ok().as_deref(),
1387 default_pct,
1388 min_pct,
1389 max_pct,
1390 );
1391 let safe_requested = budget.saturating_mul(pool_pct) / 100 / per.max(1);
1392 let bounded_override = pct_env == "CMF_GLM_POOL_PCT" || pct_env == "CMF_DSV41_POOL_PCT";
1398 let requested = match explicit_slots {
1399 Some(n) => n.max(1),
1400 None => bounded_pool_slots(
1401 std::env::var(slots_env).ok().as_deref(),
1402 safe_requested,
1403 bounded_override,
1404 ),
1405 };
1406 let (capacity, segment_slots) = if pct_env == "CMF_DSV41_POOL_PCT" {
1407 crate::gpu_wgpu::dsv4_global_moe_create_for_dsv41(
1408 model, requested, inter, hidden, gu_q2,
1409 )?
1410 } else if explicit_slots.is_some() && pct_env == "CMF_QWEN_POOL_PCT" {
1411 crate::gpu_wgpu::dsv4_global_moe_create_s8_exact(
1414 model, requested, inter, hidden, gu_q2,
1415 )?
1416 } else if pct_env == "CMF_QWEN_MTP_BANK" {
1417 crate::gpu_wgpu::dsv4_global_moe_create_slots(model, requested, inter, hidden, gu_q2)?
1420 } else {
1421 crate::gpu_wgpu::dsv4_global_moe_create(model, requested, inter, hidden, gu_q2)?
1423 };
1424 let (auto_quota, auto_min_seen) = crate::gpu_wgpu::dsv4_fetch_defaults();
1425 let fetch_quota = std::env::var(fetch_max_env)
1426 .ok()
1427 .and_then(|v| v.parse::<usize>().ok())
1428 .unwrap_or(auto_quota);
1429 let glm_policy = pct_env == "CMF_GLM_POOL_PCT";
1435 let fetch_min_seen = std::env::var(fetch_min_env)
1436 .ok()
1437 .and_then(|v| v.parse::<u16>().ok())
1438 .unwrap_or(if glm_policy || pct_env == "CMF_DSV41_POOL_PCT" {
1444 1
1445 } else {
1446 auto_min_seen.max(2)
1447 });
1448 if std::env::var_os("CMF_QWEN_PROF").is_some()
1449 || std::env::var_os("CMF_GLM_PROF").is_some()
1450 || std::env::var_os("CMF_DSV41_PROF").is_some()
1451 {
1452 eprintln!(
1453 "dynamic-pool capacity={capacity} segment_slots={segment_slots} requested={requested} pct={pool_pct} env={pct_env} fetch_quota={fetch_quota} min_seen={fetch_min_seen} gu={}",
1454 if gu_q2 { "q2tp" } else { "q4tp" }
1455 );
1456 }
1457 let staging = staging.min(capacity / 2);
1458 let staging_base = capacity - staging;
1459 Some(Self {
1460 hold_layers: [None, None],
1461 staging_base,
1462 staging,
1463 #[cfg(feature = "gpu")]
1464 stager: None,
1465 #[cfg(feature = "gpu")]
1466 tier: None,
1467 admit_pool: None,
1468 store: None,
1469 segment_slots,
1470 floor: (staging_base / n_layers.max(1)).max(2),
1471 n_experts,
1472 fetch_quota,
1473 fetch_min_seen,
1474 fetch_max_env,
1475 fetch_min_env,
1476 owner: vec![None; capacity],
1477 slot_for: vec![u32::MAX; n_layers.checked_mul(n_experts)?],
1478 shared_slot: vec![u32::MAX; n_layers],
1479 seen: vec![0; n_layers.checked_mul(n_experts)?],
1480 seen_epoch: vec![0; n_layers.checked_mul(n_experts)?],
1481 free: (0..staging_base).rev().collect(),
1482 occupancy: vec![0; n_layers.max(1)],
1483 last: vec![0; capacity],
1484 clock: 0,
1485 })
1486 }
1487
1488 pub(crate) fn begin_ensure<'pool, 'input>(
1493 &'pool mut self,
1494 model: &'input Arc<CmfModel>,
1495 layer: usize,
1496 picks: &'input [usize],
1497 triples: &'input [(usize, usize, usize)],
1498 shared: Option<(usize, usize, usize)>,
1499 ) -> Option<QwenGpuPendingEnsure<'pool, 'input>> {
1500 self.clock = self.clock.saturating_add(1);
1501 let now = self.clock;
1502 let shared_slot = if let Some(triple) = shared {
1503 let slot = *self.shared_slot.get(layer)?;
1504 if slot != u32::MAX {
1505 self.last[slot as usize] = now;
1506 slot
1507 } else {
1508 let slot = self.free.pop().or_else(|| {
1509 self.owner
1510 .iter()
1511 .enumerate()
1512 .filter(|(_, o)| o.is_some_and(|(_, e)| e != usize::MAX))
1514 .min_by_key(|(slot, _)| self.last[*slot])
1515 .map(|(slot, _)| slot)
1516 })?;
1517 if !crate::gpu_wgpu::dsv4_global_slot_fill(model, slot, triple) {
1518 if self.owner[slot].is_none() {
1519 self.free.push(slot);
1520 }
1521 return None;
1522 }
1523 if let Some(old) = self.owner[slot] {
1524 if old.1 == usize::MAX {
1525 self.shared_slot[old.0] = u32::MAX;
1526 } else {
1527 self.slot_for[old.0 * self.n_experts + old.1] = u32::MAX;
1528 self.occupancy[old.0] = self.occupancy[old.0].saturating_sub(1);
1529 }
1530 }
1531 self.owner[slot] = Some((layer, usize::MAX));
1532 self.shared_slot[layer] = slot as u32;
1533 self.occupancy[layer] += 1;
1534 self.last[slot] = now;
1535 slot as u32
1536 }
1537 } else {
1538 0
1539 };
1540 let epoch = (now / 64).min(u32::MAX as u64) as u32;
1546 let base = layer.checked_mul(self.n_experts)?;
1547 for &expert in picks {
1548 if expert >= self.n_experts {
1549 return None;
1550 }
1551 let key = base + expert;
1552 let delta = epoch.saturating_sub(self.seen_epoch[key]);
1553 self.seen[key] = if delta >= u16::BITS {
1554 0
1555 } else {
1556 self.seen[key] >> delta
1557 };
1558 self.seen_epoch[key] = epoch;
1559 self.seen[key] = self.seen[key].saturating_add(1);
1560 let slot = self.slot_for[key];
1561 if slot != u32::MAX {
1562 self.last[slot as usize] = now;
1563 }
1564 }
1565 let quota = std::env::var(self.fetch_max_env)
1566 .ok()
1567 .and_then(|v| v.parse::<usize>().ok())
1568 .unwrap_or(self.fetch_quota);
1569 let min_seen = std::env::var(self.fetch_min_env)
1570 .ok()
1571 .and_then(|v| v.parse::<u16>().ok())
1572 .unwrap_or(self.fetch_min_seen);
1573 let mut plan: Vec<(usize, usize)> = Vec::new();
1575 for &expert in picks {
1576 let key = base + expert;
1577 if self.slot_for[key] != u32::MAX || plan.len() >= quota || self.seen[key] < min_seen {
1578 continue;
1579 }
1580 if triples.get(expert).is_none() {
1581 return None;
1582 }
1583 let victim = self.victim(layer, picks)?;
1584 if let Some(old) = self.owner[victim] {
1585 self.dropped(old);
1586 if old.1 == usize::MAX {
1587 self.shared_slot[old.0] = u32::MAX;
1588 } else {
1589 self.slot_for[old.0 * self.n_experts + old.1] = u32::MAX;
1590 self.occupancy[old.0] = self.occupancy[old.0].saturating_sub(1);
1591 }
1592 }
1593 self.owner[victim] = Some((layer, expert));
1594 self.slot_for[key] = victim as u32;
1595 self.occupancy[layer] += 1;
1596 self.last[victim] = now;
1597 if fill_trace() {
1598 eprintln!(
1599 "fill prefetch layer={layer} expert={expert} slot={victim} seen={}",
1600 self.seen[key]
1601 );
1602 }
1603 plan.push((victim, expert));
1604 }
1605 Some(QwenGpuPendingEnsure {
1606 pool: Some(self),
1607 model,
1608 picks,
1609 triples,
1610 layer,
1611 base,
1612 shared_slot,
1613 plan,
1614 })
1615 }
1616
1617 pub(crate) fn ensure(
1618 &mut self,
1619 model: &Arc<CmfModel>,
1620 layer: usize,
1621 picks: &[usize],
1622 triples: &[(usize, usize, usize)],
1623 shared: Option<(usize, usize, usize)>,
1624 ) -> Option<(Vec<u32>, u32)> {
1625 self.begin_ensure(model, layer, picks, triples, shared)?.finish()
1626 }
1627}
1628
1629fn err(s: impl Into<String>) -> CmfError {
1630 CmfError::Parse(format!("qwen4_exp: {}", s.into()))
1631}
1632
1633fn f(model: &CmfModel, name: &str) -> Result<Vec<f32>, CmfError> {
1634 load_f32(model, name, &Overlay::None).map_err(err)
1635}
1636
1637fn t(model: &Arc<CmfModel>, name: &str) -> Result<QTensor, CmfError> {
1638 load_matrix(model, name, false, &Overlay::None)
1639}
1640
1641fn load_hc(model: &Arc<CmfModel>, prefix: &str, inject: bool) -> Result<GatedResidual, CmfError> {
1642 Ok(GatedResidual {
1643 norm: f(model, &format!("{prefix}hc_norm.weight"))?,
1644 down: t(model, &format!("{prefix}input_mix_weight_down.weight"))?,
1645 up: t(model, &format!("{prefix}input_mix_weight_up.weight"))?,
1646 inject: inject
1647 .then(|| t(model, &format!("{prefix}block_inject_weight.weight")))
1648 .transpose()?,
1649 down_idx: model.tensor_index(&format!("{prefix}input_mix_weight_down.weight")),
1650 up_idx: model.tensor_index(&format!("{prefix}input_mix_weight_up.weight")),
1651 inject_idx: inject
1652 .then(|| model.tensor_index(&format!("{prefix}block_inject_weight.weight")))
1653 .flatten(),
1654 })
1655}
1656
1657impl GatedResidual {
1658 fn idxs(&self) -> Vec<usize> {
1659 let mut v = Vec::new();
1660 v.extend(self.down_idx);
1661 v.extend(self.up_idx);
1662 v.extend(self.inject_idx);
1663 v
1664 }
1665}
1666
1667fn layer_idx(model: &CmfModel, p: &str, is_gdn: bool, has_ple: bool) -> Option<LayerIdx> {
1669 let ix = |n: String| model.tensor_index(&n);
1670 let gdn = if is_gdn {
1671 let q = format!("{p}linear_attn.");
1672 Some([
1673 ix(format!("{q}in_proj_qkv.weight"))?,
1674 ix(format!("{q}in_proj_z.weight"))?,
1675 ix(format!("{q}in_proj_a.weight"))?,
1676 ix(format!("{q}in_proj_b.weight"))?,
1677 ix(format!("{q}out_proj.weight"))?,
1678 ])
1679 } else {
1680 None
1681 };
1682 let qsa = if !is_gdn {
1683 let q = format!("{p}self_attn.");
1684 Some([
1685 ix(format!("{q}q_proj.weight"))?,
1686 ix(format!("{q}k_proj.weight"))?,
1687 ix(format!("{q}v_proj.weight"))?,
1688 ix(format!("{q}o_proj.weight"))?,
1689 ix(format!("{q}indexer.index_qk_proj.weight"))?,
1690 ])
1691 } else {
1692 None
1693 };
1694 let ple = if has_ple {
1695 Some((
1696 ix(format!("{p}ple.key_proj.weight"))?,
1697 ix(format!("{p}ple.value_proj.weight"))?,
1698 ))
1699 } else {
1700 None
1701 };
1702 Some(LayerIdx {
1703 gdn,
1704 qsa,
1705 ple,
1706 router: ix(format!("{p}mlp.gate.weight"))?,
1707 shared_gate: ix(format!("{p}mlp.shared_expert_gate.weight")),
1708 })
1709}
1710
1711fn load_gdn(model: &Arc<CmfModel>, prefix: &str) -> Result<GdnWeights, CmfError> {
1712 Ok(GdnWeights {
1713 in_proj_qkv: t(model, &format!("{prefix}in_proj_qkv.weight"))?,
1714 in_proj_z: t(model, &format!("{prefix}in_proj_z.weight"))?,
1715 in_proj_a: t(model, &format!("{prefix}in_proj_a.weight"))?,
1716 in_proj_b: t(model, &format!("{prefix}in_proj_b.weight"))?,
1717 conv1d: f(model, &format!("{prefix}conv1d.weight"))?,
1718 a_log: f(model, &format!("{prefix}A_log"))?,
1719 dt_bias: f(model, &format!("{prefix}dt_bias"))?,
1720 norm: f(model, &format!("{prefix}norm.weight"))?,
1721 out_proj: t(model, &format!("{prefix}out_proj.weight"))?,
1722 })
1723}
1724
1725fn load_qsa(model: &Arc<CmfModel>, prefix: &str) -> Result<QsaWeights, CmfError> {
1726 Ok(QsaWeights {
1727 q_proj: t(model, &format!("{prefix}q_proj.weight"))?,
1728 k_proj: t(model, &format!("{prefix}k_proj.weight"))?,
1729 v_proj: t(model, &format!("{prefix}v_proj.weight"))?,
1730 o_proj: t(model, &format!("{prefix}o_proj.weight"))?,
1731 q_norm: f(model, &format!("{prefix}q_norm.weight"))?,
1732 k_norm: f(model, &format!("{prefix}k_norm.weight"))?,
1733 index_qk: t(model, &format!("{prefix}indexer.index_qk_proj.weight"))?,
1734 index_q_norm: f(model, &format!("{prefix}indexer.q_norm.weight"))
1735 .or_else(|_| f(model, &format!("{prefix}indexer.q_layernorm.weight")))?,
1736 index_k_norm: f(model, &format!("{prefix}indexer.k_norm.weight"))
1737 .or_else(|_| f(model, &format!("{prefix}indexer.k_layernorm.weight")))?,
1738 })
1739}
1740
1741fn splitmix64(mut value: u64) -> u64 {
1742 value = value.wrapping_add(SPLITMIX_GAMMA);
1743 value = (value ^ (value >> 30)).wrapping_mul(SPLITMIX_M1);
1744 value = (value ^ (value >> 27)).wrapping_mul(SPLITMIX_M2);
1745 value ^ (value >> 31)
1746}
1747
1748fn is_prime(value: usize) -> bool {
1749 if value < 2 {
1750 return false;
1751 }
1752 if value.is_multiple_of(2) {
1753 return value == 2;
1754 }
1755 let mut d = 3usize;
1756 while d <= value / d {
1757 if value.is_multiple_of(d) {
1758 return false;
1759 }
1760 d += 2;
1761 }
1762 true
1763}
1764
1765fn nth_prime_after(start: usize, count: usize) -> usize {
1766 let mut p = start;
1767 for _ in 0..count {
1768 p += 1;
1769 while !is_prime(p) {
1770 p += 1;
1771 }
1772 }
1773 p
1774}
1775
1776fn ple_tables(
1777 qc: &Qwen4ExpConfig,
1778 vocab: usize,
1779 ple_index: usize,
1780) -> (Vec<i64>, Vec<i64>, Vec<i64>) {
1781 let max_mul = i64::MAX as u64 / vocab.max(1) as u64;
1782 let half_bound = (max_mul / 2).max(1);
1783 let base = qc.seed.wrapping_add(PRIME_1.wrapping_mul(ple_index as u64));
1784 let multipliers = (0..qc.ngram_size)
1785 .map(|i| {
1786 let v = base.wrapping_add(SPLITMIX_GAMMA.wrapping_mul((i + 1) as u64));
1787 (2 * (splitmix64(v) % half_bound) + 1) as i64
1788 })
1789 .collect();
1790 let heads = (qc.ngram_size - 1) * qc.heads_per_ngram;
1791 let mut sizes = Vec::with_capacity(heads);
1792 let mut offsets = Vec::with_capacity(heads);
1793 let mut off = 0i64;
1794 for head in 0..heads {
1795 let global = ple_index * heads + head;
1796 let sz = nth_prime_after(qc.ngram_vocab_size_base - 1, global + 1) as i64;
1797 sizes.push(sz);
1798 offsets.push(off);
1799 off += sz;
1800 }
1801 (multipliers, sizes, offsets)
1802}
1803
1804fn load_ple(
1805 model: &Arc<CmfModel>,
1806 prefix: &str,
1807 qc: &Qwen4ExpConfig,
1808 vocab: usize,
1809 ple_index: usize,
1810) -> Result<PleWeights, CmfError> {
1811 let mut shards = Vec::with_capacity(qc.split_ngram_parts);
1812 for si in 0..qc.split_ngram_parts {
1813 shards.push(t(
1814 model,
1815 &format!("{prefix}ple_embedding.ngram_embedding.shard_{si}.weight"),
1816 )?);
1817 }
1818 let first = shards
1819 .first()
1820 .ok_or_else(|| err("PLE has no embedding shards"))?;
1821 let (rows_per_shard, row_dim) = (first.rows(), first.cols());
1822 if shards
1823 .iter()
1824 .any(|s| s.rows() != rows_per_shard || s.cols() != row_dim)
1825 {
1826 return Err(err("PLE embedding shard shapes disagree"));
1827 }
1828 let (multipliers, vocab_sizes, offsets) = ple_tables(qc, vocab, ple_index);
1829 Ok(PleWeights {
1830 shards,
1831 rows_per_shard,
1832 row_dim,
1833 key_proj: t(model, &format!("{prefix}key_proj.weight"))?,
1834 value_proj: t(model, &format!("{prefix}value_proj.weight"))?,
1835 norm_key: f(model, &format!("{prefix}norm_key.weight"))?,
1836 norm_query: f(model, &format!("{prefix}norm_query.weight"))?,
1837 norm_conv: f(model, &format!("{prefix}norm_conv.weight"))?,
1838 conv: f(model, &format!("{prefix}conv1d.weight"))?,
1839 multipliers,
1840 vocab_sizes,
1841 offsets,
1842 })
1843}
1844
1845pub fn load(
1848 model: &Arc<CmfModel>,
1849 arch: &ModelArch,
1850) -> Result<(Globals, Vec<Layer>, Cfg, State), CmfError> {
1851 let qc = arch
1852 .qwen4_exp
1853 .as_ref()
1854 .ok_or_else(|| err("missing qwen4_exp descriptor"))?;
1855 if qc.hc_count == 0 || qc.indexer_compress_ratio == 0 || qc.ngram_size < 2 {
1856 return Err(err("invalid zero geometry"));
1857 }
1858 let gdn = GdnCfg {
1859 num_v_heads: arch.linear_num_value_heads.unwrap_or(48),
1860 num_k_heads: arch.linear_num_key_heads.unwrap_or(16),
1861 key_head_dim: arch.linear_key_head_dim.unwrap_or(128),
1862 value_head_dim: arch.linear_value_head_dim.unwrap_or(128),
1863 conv_kernel: arch.linear_conv_kernel_dim.unwrap_or(4),
1864 hidden_size: arch.hidden_size,
1865 rms_eps: arch.rms_norm_eps,
1866 output_gate_sigmoid: true,
1867 };
1868 let eos = model
1869 .header
1870 .tokenizer_config
1871 .as_ref()
1872 .and_then(|tc| {
1876 tc.pad_token_id
1877 .or(tc.bos_token_id)
1878 .or_else(|| tc.eos_token_ids.last().copied())
1879 })
1880 .unwrap_or(248_044);
1881 let cfg = Cfg {
1882 hidden: arch.hidden_size,
1883 hc: qc.hc_count,
1884 eps: arch.rms_norm_eps,
1885 n_heads: arch.num_attention_heads,
1886 n_kv_heads: arch.num_kv_heads,
1887 head_dim: arch.head_dim,
1888 rotary_dim: ((arch.head_dim as f32 * arch.partial_rotary_factor) as usize).max(2),
1889 index_heads: qc.indexer_n_heads,
1890 index_kv_heads: qc.indexer_kv_heads,
1891 index_dim: qc.indexer_head_dim,
1892 index_budget: qc.indexer_budget,
1893 compress_ratio: qc.indexer_compress_ratio,
1894 gdn,
1895 ngram_size: qc.ngram_size,
1896 heads_per_ngram: qc.heads_per_ngram,
1897 ple_kernel: qc.ple_conv_kernel_size,
1898 ple_dilation: qc.ngram_size,
1899 eos,
1900 };
1901 if cfg.n_heads % cfg.n_kv_heads != 0 || cfg.index_kv_heads != 1 {
1902 return Err(err("unsupported QSA head grouping"));
1903 }
1904
1905 let head_hc = load_hc(model, "model.hyper_connection_mixer.", false)?;
1906 let lm_head_idx = model.tensor_index("lm_head.weight");
1907 let embed_idx = model.tensor_index("model.embed_tokens.weight");
1908 let mut skeleton_idxs: Vec<usize> = head_hc.idxs();
1909 skeleton_idxs.extend(lm_head_idx);
1910 if std::env::var("CMF_QWEN_MTP").as_deref() != Ok("0")
1913 && std::env::var("CMF_QWEN_MTP_DEVICE_DRAFT").as_deref() == Ok("1")
1914 {
1915 let side = cortiq_core::mtp_sidecar_path(&model.path);
1916 if side != model.path && side.exists() {
1917 skeleton_idxs.extend(embed_idx);
1918 }
1919 }
1920 let globals_partial = (
1921 t(model, "model.embed_tokens.weight")?,
1922 t(model, "lm_head.weight")?,
1923 head_hc,
1924 );
1925 let mut layers = Vec::with_capacity(arch.num_layers);
1926 let mut ple_index = 0usize;
1927 for li in 0..arch.num_layers {
1928 let p = format!("model.layers.{li}.");
1929 let mixer = match arch.layer_types.get(li) {
1930 Some(LayerType::LinearAttention) => {
1931 Mixer::Gdn(load_gdn(model, &format!("{p}linear_attn."))?)
1932 }
1933 _ => Mixer::Qsa(load_qsa(model, &format!("{p}self_attn."))?),
1934 };
1935 let ple = if qc.ple_layer_ids.contains(&(li + 1)) {
1936 let w = load_ple(model, &format!("{p}ple."), qc, arch.vocab_size, ple_index)?;
1937 ple_index += 1;
1938 Some(w)
1939 } else {
1940 None
1941 };
1942 let moe = match build_ffn_at(model, arch, &p, false, &Overlay::None)? {
1943 FfnKind::Moe(m) => m,
1944 _ => return Err(err(format!("layer {li} is not MoE"))),
1945 };
1946 let expert_ids: Vec<_> = moe
1947 .experts
1948 .iter()
1949 .map(|e| {
1950 Some((
1951 e.gate_proj.model_idx()?,
1952 e.up_proj.model_idx()?,
1953 e.down_proj.model_idx()?,
1954 ))
1955 })
1956 .collect::<Option<_>>()
1957 .ok_or_else(|| err(format!("layer {li} experts are not mmap-backed")))?;
1958 let shared_ids = moe
1959 .shared
1960 .as_ref()
1961 .and_then(|(e, _)| {
1962 Some((
1963 e.gate_proj.model_idx()?,
1964 e.up_proj.model_idx()?,
1965 e.down_proj.model_idx()?,
1966 ))
1967 })
1968 .ok_or_else(|| err(format!("layer {li} shared expert is not mmap-backed")))?;
1969 let attn_hc = load_hc(model, &format!("{p}attn_hyper_connection."), true)?;
1970 let mlp_hc = load_hc(model, &format!("{p}mlp_hyper_connection."), true)?;
1971 let idx = layer_idx(model, &p, matches!(mixer, Mixer::Gdn(_)), ple.is_some());
1972 skeleton_idxs.extend(attn_hc.idxs());
1973 skeleton_idxs.extend(mlp_hc.idxs());
1974 if let Some(ix) = &idx {
1975 skeleton_idxs.extend(ix.gdn.iter().flatten().copied());
1976 skeleton_idxs.extend(ix.qsa.iter().flatten().copied());
1977 if let Some((k, v)) = ix.ple {
1978 skeleton_idxs.extend([k, v]);
1979 }
1980 skeleton_idxs.push(ix.router);
1981 skeleton_idxs.extend(ix.shared_gate);
1982 }
1983 layers.push(Layer {
1984 attn_hc,
1985 mlp_hc,
1986 mixer,
1987 moe,
1988 ple,
1989 expert_ids,
1990 shared_ids: Some(shared_ids),
1991 idx,
1992 });
1993 }
1994 let globals = Globals {
1995 embed: globals_partial.0,
1996 lm_head: globals_partial.1,
1997 head_hc: globals_partial.2,
1998 lm_head_idx,
1999 embed_idx,
2000 skeleton_idxs,
2001 };
2002 let state = State::new(layers.len());
2003 Ok((globals, layers, cfg, state))
2004}
2005
2006#[inline]
2007fn sigmoid(x: f32) -> f32 {
2008 1.0 / (1.0 + (-x).exp())
2009}
2010
2011#[inline]
2012fn silu(x: f32) -> f32 {
2013 x * sigmoid(x)
2014}
2015
2016fn group_rms_zero_into(x: &[f32], weight: &[f32], group: usize, eps: f64, out: &mut [f32]) {
2017 debug_assert_eq!(x.len(), weight.len());
2018 debug_assert_eq!(x.len(), out.len());
2019 debug_assert!(group > 0 && x.len().is_multiple_of(group));
2020 for (gi, chunk) in x.chunks(group).enumerate() {
2021 let ss = chunk.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>();
2022 let inv = (ss / group as f64 + eps).sqrt().recip() as f32;
2023 let off = gi * group;
2024 for j in 0..group {
2025 out[off + j] = chunk[j] * inv * (1.0 + weight[off + j]);
2026 }
2027 }
2028}
2029
2030fn group_rms_zero(x: &[f32], weight: &[f32], group: usize, eps: f64) -> Vec<f32> {
2031 let mut out = vec![0.0f32; x.len()];
2032 group_rms_zero_into(x, weight, group, eps, &mut out);
2033 out
2034}
2035
2036fn hc_mix(
2037 w: &GatedResidual,
2038 hyper: &[f32],
2039 cfg: &Cfg,
2040 pool: Option<&Pool>,
2041) -> (Vec<f32>, Option<Vec<f32>>) {
2042 let mut normed = crate::attention::take_buf(hyper.len());
2043 group_rms_zero_into(hyper, &w.norm, cfg.hidden, cfg.eps, &mut normed);
2044 let mut low = crate::attention::take_buf(w.down.rows());
2045 let mut inject = w.inject.as_ref().map(|iw| vec![0.0f32; iw.rows()]);
2050 match (&w.inject, inject.as_mut()) {
2051 (Some(iw), Some(inj)) => {
2052 QTensor::matvec_many([&w.down, iw], &normed, [&mut low, inj], pool)
2053 }
2054 _ => w.down.matvec(&normed, &mut low, pool),
2055 }
2056 for v in &mut low {
2057 *v = silu(*v / cfg.hc as f32);
2058 }
2059 let mut mix = crate::attention::take_buf(hyper.len());
2060 w.up.matvec(&low, &mut mix, pool);
2061 let mut folded = vec![0.0f32; cfg.hidden];
2062 for stream in 0..cfg.hc {
2063 let off = stream * cfg.hidden;
2064 for d in 0..cfg.hidden {
2065 folded[d] += sigmoid(mix[off + d]) * normed[off + d] / cfg.hc as f32;
2066 }
2067 }
2068 let inject = inject.map(|mut v| {
2069 for x in &mut v {
2070 *x = 2.0 * sigmoid(*x / cfg.hc as f32);
2071 }
2072 v
2073 });
2074 crate::attention::recycle_buf(&mut mix);
2075 crate::attention::recycle_buf(&mut low);
2076 crate::attention::recycle_buf(&mut normed);
2077 (folded, inject)
2078}
2079
2080fn inject(hyper: &mut [f32], block: &[f32], weights: &[f32], cfg: &Cfg) {
2081 for stream in 0..cfg.hc {
2082 let off = stream * cfg.hidden;
2083 for d in 0..cfg.hidden {
2084 hyper[off + d] += weights[stream] * block[d];
2085 }
2086 }
2087}
2088
2089fn trace_stats(label: &str, li: usize, position: usize, values: &[f32]) {
2090 if std::env::var_os("CMF_QWEN_TRACE").is_none() {
2091 return;
2092 }
2093 let mut sumsq = 0.0f64;
2094 let mut max = 0.0f32;
2095 let mut finite = 0usize;
2096 for &v in values {
2097 if v.is_finite() {
2098 sumsq += (v as f64) * (v as f64);
2099 max = max.max(v.abs());
2100 finite += 1;
2101 }
2102 }
2103 let rms = if finite == 0 {
2104 f64::NAN
2105 } else {
2106 (sumsq / finite as f64).sqrt()
2107 };
2108 eprintln!(
2109 "qwen4_exp pos={position} layer={li:02} {label}: rms={rms:.6} max={max:.6} finite={finite}/{}",
2110 values.len()
2111 );
2112}
2113
2114fn dump_values(label: &str, li: usize, position: usize, values: &[f32]) {
2115 let Some(root) = std::env::var_os("CMF_QWEN_DUMP") else {
2116 return;
2117 };
2118 if let Ok(wanted) = std::env::var("CMF_QWEN_DUMP_LAYER")
2119 && wanted.parse::<usize>().ok() != Some(li)
2120 {
2121 return;
2122 }
2123 let root = std::path::PathBuf::from(root);
2124 if std::fs::create_dir_all(&root).is_err() {
2125 return;
2126 }
2127 let bytes = unsafe {
2128 std::slice::from_raw_parts(values.as_ptr().cast::<u8>(), std::mem::size_of_val(values))
2129 };
2130 let _ = std::fs::write(
2131 root.join(format!("p{position:06}_l{li:02}_{label}.f32")),
2132 bytes,
2133 );
2134}
2135
2136static TAP: std::sync::Mutex<Vec<(String, usize, usize, Vec<f32>)>> =
2139 std::sync::Mutex::new(Vec::new());
2140
2141fn tap_layer() -> Option<usize> {
2142 std::env::var("CMF_QWEN_DEVICE_TAP").ok()?.parse().ok()
2143}
2144
2145fn observe(label: &str, li: usize, position: usize, values: &[f32]) {
2146 if tap_layer() == Some(li) && position < 3 {
2147 TAP.lock()
2148 .unwrap()
2149 .push((label.to_string(), li, position, values.to_vec()));
2150 }
2151 trace_stats(label, li, position, values);
2152 dump_values(label, li, position, values);
2153}
2154
2155fn rms_zero_head(v: &mut [f32], weight: &[f32], head_dim: usize, eps: f64) {
2156 for head in v.chunks_mut(head_dim) {
2157 let ss = head.iter().map(|&x| (x as f64) * (x as f64)).sum::<f64>();
2158 let inv = (ss / head_dim as f64 + eps).sqrt().recip() as f32;
2159 for (x, &w) in head.iter_mut().zip(weight) {
2160 *x *= inv * (1.0 + w);
2161 }
2162 }
2163}
2164
2165fn rope(v: &mut [f32], pos: usize, inv_freq: &[f32], rotary_dim: usize) {
2166 let rd = rotary_dim.min(v.len()).min(inv_freq.len() * 2);
2167 let half = rd / 2;
2168 for i in 0..half {
2169 let a = (pos as f32 * inv_freq[i]).cos();
2170 let b = (pos as f32 * inv_freq[i]).sin();
2171 let x1 = v[i];
2172 let x2 = v[i + half];
2173 v[i] = x1 * a - x2 * b;
2174 v[i + half] = x2 * a + x1 * b;
2175 }
2176}
2177
2178fn selected_tokens(
2179 q: &[f32],
2180 raw_keys: &[f32],
2181 npos: usize,
2182 w: &QsaWeights,
2183 cfg: &Cfg,
2184 inv_freq: &[f32],
2185) -> Vec<usize> {
2186 let cr = cfg.compress_ratio;
2187 let complete = npos / cr;
2188 let mut scores = Vec::with_capacity(complete);
2189 for block in 0..complete {
2190 let mut k = vec![0.0f32; cfg.index_dim];
2191 for ti in block * cr..(block + 1) * cr {
2192 let src = &raw_keys[ti * cfg.index_dim..(ti + 1) * cfg.index_dim];
2193 for (d, &x) in src.iter().enumerate() {
2194 k[d] += x / cr as f32;
2195 }
2196 }
2197 rms_zero_head(&mut k, &w.index_k_norm, cfg.index_dim, cfg.eps);
2198 rope(
2199 &mut k,
2200 block * cr,
2201 inv_freq,
2202 cfg.rotary_dim.min(cfg.index_dim),
2203 );
2204 let mut s = 0.0f32;
2205 for h in 0..cfg.index_heads {
2206 let qh = &q[h * cfg.index_dim..(h + 1) * cfg.index_dim];
2207 let dot = qh.iter().zip(&k).map(|(&a, &b)| a * b).sum::<f32>();
2208 s += dot.max(0.0);
2209 }
2210 scores.push((block, s / (cfg.index_dim as f32).sqrt()));
2211 }
2212 let keep = (cfg.index_budget / cr).min(complete);
2213 if complete > keep {
2214 scores.select_nth_unstable_by(keep, |a, b| {
2215 b.1.partial_cmp(&a.1).unwrap_or(Ordering::Equal)
2216 });
2217 scores.truncate(keep);
2218 }
2219 let mut out = Vec::with_capacity(keep * cr + cr.saturating_sub(1));
2220 for (block, _) in scores {
2221 out.extend(block * cr..(block + 1) * cr);
2222 }
2223 out.extend(complete * cr..npos);
2224 out
2225}
2226
2227fn qsa_forward(
2228 x: &[f32],
2229 w: &QsaWeights,
2230 cfg: &Cfg,
2231 st: &mut QsaState,
2232 position: usize,
2233 inv_freq: &[f32],
2234 pool: Option<&Pool>,
2235) -> Vec<f32> {
2236 let mut iqk =
2237 crate::attention::take_buf((cfg.index_heads + cfg.index_kv_heads) * cfg.index_dim);
2238 let mut qg = crate::attention::take_buf(cfg.n_heads * cfg.head_dim * 2);
2239 let mut k = crate::attention::take_buf(cfg.n_kv_heads * cfg.head_dim);
2240 let mut v = crate::attention::take_buf(cfg.n_kv_heads * cfg.head_dim);
2241 QTensor::matvec_many(
2245 [&w.index_qk, &w.q_proj, &w.k_proj, &w.v_proj],
2246 x,
2247 [&mut iqk, &mut qg, &mut k, &mut v],
2248 pool,
2249 );
2250 let qlen = cfg.index_heads * cfg.index_dim;
2251 let mut iq = crate::attention::take_buf(qlen);
2252 iq.copy_from_slice(&iqk[..qlen]);
2253 rms_zero_head(&mut iq, &w.index_q_norm, cfg.index_dim, cfg.eps);
2254 for h in 0..cfg.index_heads {
2255 rope(
2256 &mut iq[h * cfg.index_dim..(h + 1) * cfg.index_dim],
2257 position,
2258 inv_freq,
2259 cfg.rotary_dim.min(cfg.index_dim),
2260 );
2261 }
2262 st.raw_keys.extend_from_slice(&iqk[qlen..]);
2263
2264 let mut q = crate::attention::take_buf(cfg.n_heads * cfg.head_dim);
2265 let mut gate = crate::attention::take_buf(q.len());
2266 for h in 0..cfg.n_heads {
2267 let src = h * cfg.head_dim * 2;
2268 let dst = h * cfg.head_dim;
2269 q[dst..dst + cfg.head_dim].copy_from_slice(&qg[src..src + cfg.head_dim]);
2270 gate[dst..dst + cfg.head_dim]
2271 .copy_from_slice(&qg[src + cfg.head_dim..src + cfg.head_dim * 2]);
2272 }
2273 rms_zero_head(&mut q, &w.q_norm, cfg.head_dim, cfg.eps);
2274 rms_zero_head(&mut k, &w.k_norm, cfg.head_dim, cfg.eps);
2275 for h in 0..cfg.n_heads {
2276 rope(
2277 &mut q[h * cfg.head_dim..(h + 1) * cfg.head_dim],
2278 position,
2279 inv_freq,
2280 cfg.rotary_dim,
2281 );
2282 }
2283 for h in 0..cfg.n_kv_heads {
2284 rope(
2285 &mut k[h * cfg.head_dim..(h + 1) * cfg.head_dim],
2286 position,
2287 inv_freq,
2288 cfg.rotary_dim,
2289 );
2290 }
2291 st.keys.extend_from_slice(&k);
2292 st.values.extend_from_slice(&v);
2293 let npos = position + 1;
2294 let selected = selected_tokens(&iq, &st.raw_keys, npos, w, cfg, inv_freq);
2295 let groups = cfg.n_heads / cfg.n_kv_heads;
2296 let scale = (cfg.head_dim as f32).sqrt().recip();
2297 let mut merged = crate::attention::take_buf(cfg.n_heads * cfg.head_dim);
2298 for qh in 0..cfg.n_heads {
2299 let kvh = qh / groups;
2300 let qs = &q[qh * cfg.head_dim..(qh + 1) * cfg.head_dim];
2301 let mut scores = Vec::with_capacity(selected.len());
2302 let mut max = f32::NEG_INFINITY;
2303 for &ti in &selected {
2304 let ko = (ti * cfg.n_kv_heads + kvh) * cfg.head_dim;
2305 let s = qs
2306 .iter()
2307 .zip(&st.keys[ko..ko + cfg.head_dim])
2308 .map(|(&a, &b)| a * b)
2309 .sum::<f32>()
2310 * scale;
2311 max = max.max(s);
2312 scores.push(s);
2313 }
2314 let z = scores.iter().map(|&s| (s - max).exp()).sum::<f32>();
2315 let out = &mut merged[qh * cfg.head_dim..(qh + 1) * cfg.head_dim];
2316 for (&ti, &score) in selected.iter().zip(&scores) {
2317 let p = (score - max).exp() / z.max(f32::MIN_POSITIVE);
2318 let vo = (ti * cfg.n_kv_heads + kvh) * cfg.head_dim;
2319 for d in 0..cfg.head_dim {
2320 out[d] += p * st.values[vo + d];
2321 }
2322 }
2323 let go = qh * cfg.head_dim;
2324 for d in 0..cfg.head_dim {
2325 out[d] *= sigmoid(gate[go + d]);
2326 }
2327 }
2328 let mut out = vec![0.0f32; cfg.hidden];
2329 w.o_proj.matvec(&merged, &mut out, pool);
2330 crate::attention::recycle_buf(&mut merged);
2331 crate::attention::recycle_buf(&mut gate);
2332 crate::attention::recycle_buf(&mut q);
2333 crate::attention::recycle_buf(&mut iq);
2334 crate::attention::recycle_buf(&mut v);
2335 crate::attention::recycle_buf(&mut k);
2336 crate::attention::recycle_buf(&mut qg);
2337 crate::attention::recycle_buf(&mut iqk);
2338 out
2339}
2340
2341fn shifted_token(history: &[u32], current: u32, shift: usize, eos: u32) -> u32 {
2342 if shift == 0 {
2343 return current;
2344 }
2345 if history.len() < shift {
2346 return eos;
2347 }
2348 let source = history.len() - shift;
2349 if history[source + 1..].contains(&eos) || history.last() == Some(&eos) {
2350 eos
2351 } else {
2352 history[source]
2353 }
2354}
2355
2356fn ple_embedding(w: &PleWeights, cfg: &Cfg, history: &[u32], token: u32) -> Vec<f32> {
2357 let ids = ple_row_ids(w, cfg, history, token);
2358 let mut out = vec![0.0f32; ids.len() * w.row_dim];
2359 for (hi, &id) in ids.iter().enumerate() {
2360 ple_row(w, id, &mut out[hi * w.row_dim..(hi + 1) * w.row_dim]);
2361 }
2362 out
2363}
2364
2365fn ple_row(w: &PleWeights, id: i64, dst: &mut [f32]) {
2367 let global = id as usize;
2368 let shard = global / w.rows_per_shard;
2369 let local = global % w.rows_per_shard;
2370 debug_assert!(shard < w.shards.len());
2371 w.shards[shard].row_f32(local, dst);
2372}
2373
2374fn ple_row_ids(w: &PleWeights, cfg: &Cfg, history: &[u32], token: u32) -> Vec<i64> {
2376 let shifted: Vec<i64> = (0..cfg.ngram_size)
2377 .map(|s| shifted_token(history, token, s, cfg.eos) as i64)
2378 .collect();
2379 let mut ids = Vec::with_capacity((cfg.ngram_size - 1) * cfg.heads_per_ngram);
2380 for ngram in 2..=cfg.ngram_size {
2381 let mut mixed = shifted[0].wrapping_mul(w.multipliers[0]);
2382 for p in 1..ngram {
2383 mixed ^= shifted[p].wrapping_mul(w.multipliers[p]);
2384 }
2385 let h0 = (ngram - 2) * cfg.heads_per_ngram;
2386 for hi in h0..h0 + cfg.heads_per_ngram {
2387 ids.push(mixed.rem_euclid(w.vocab_sizes[hi]) + w.offsets[hi]);
2388 }
2389 }
2390 ids
2391}
2392
2393fn ple_frame_rows(
2400 layers: &[Layer],
2401 cfg: &Cfg,
2402 history: &[u32],
2403 ids: &[u32],
2404 pool: Option<&Pool>,
2405) -> Vec<Vec<Vec<f32>>> {
2406 let mut out: Vec<Vec<Vec<f32>>> = Vec::with_capacity(layers.len());
2407 let mut jobs: Vec<(usize, usize, usize, i64)> = Vec::new();
2409 let mut hist: Vec<u32> = history.to_vec();
2410 for (li, l) in layers.iter().enumerate() {
2411 let Some(pw) = l.ple.as_ref() else {
2412 out.push(Vec::new());
2413 continue;
2414 };
2415 hist.truncate(history.len());
2416 let mut rows = Vec::with_capacity(ids.len());
2417 for (t, &id) in ids.iter().enumerate() {
2418 let rid = ple_row_ids(pw, cfg, &hist, id);
2419 rows.push(vec![0.0f32; rid.len() * pw.row_dim]);
2420 jobs.extend(rid.into_iter().enumerate().map(|(h, r)| (li, t, h, r)));
2421 hist.push(id);
2422 }
2423 out.push(rows);
2424 }
2425 let tables: Vec<Option<&PleWeights>> = layers.iter().map(|l| l.ple.as_ref()).collect();
2427 let dst: Vec<crate::pool::SendMut> = jobs
2428 .iter()
2429 .map(|&(li, t, h, _)| {
2430 let dim = tables[li].map_or(0, |w| w.row_dim);
2431 crate::pool::SendMut::new(out[li][t][h * dim..].as_mut_ptr())
2432 })
2433 .collect();
2434 let read = |s: usize, e: usize| {
2435 for j in s..e {
2436 let (li, _, _, id) = jobs[j];
2437 let Some(w) = tables[li] else {
2438 continue;
2439 };
2440 let row = unsafe { std::slice::from_raw_parts_mut(dst[j].at(0), w.row_dim) };
2443 ple_row(w, id, row);
2444 }
2445 };
2446 match pool {
2447 Some(p) if jobs.len() > 1 => p.run_rows(jobs.len(), &read),
2448 _ => read(0, jobs.len()),
2449 }
2450 out
2451}
2452
2453fn ple_forward(
2454 hyper: &[f32],
2455 token: u32,
2456 history: &[u32],
2457 w: &PleWeights,
2458 cfg: &Cfg,
2459 st: &mut LayerState,
2460 pool: Option<&Pool>,
2461) -> Vec<f32> {
2462 let emb = ple_embedding(w, cfg, history, token);
2463 let mut key = vec![0.0f32; cfg.hc * cfg.hidden];
2464 let mut value = vec![0.0f32; cfg.hidden];
2465 w.key_proj.matvec(&emb, &mut key, pool);
2466 w.value_proj.matvec(&emb, &mut value, pool);
2467 let key = group_rms_zero(&key, &w.norm_key, cfg.hidden, cfg.eps);
2468 let query = group_rms_zero(hyper, &w.norm_query, cfg.hidden, cfg.eps);
2469 let mut gated = vec![0.0f32; cfg.hc * cfg.hidden];
2470 for stream in 0..cfg.hc {
2471 let off = stream * cfg.hidden;
2472 let dot = key[off..off + cfg.hidden]
2473 .iter()
2474 .zip(&query[off..off + cfg.hidden])
2475 .map(|(&a, &b)| a * b)
2476 .sum::<f32>()
2477 / (cfg.hidden as f32).sqrt();
2478 let signed_root = dot.signum() * dot.abs().max(1e-6).sqrt();
2479 let g = sigmoid(signed_root);
2480 for d in 0..cfg.hidden {
2481 gated[off + d] = g * value[d];
2482 }
2483 }
2484 let normed = group_rms_zero(&gated, &w.norm_conv, cfg.hidden, cfg.eps);
2485 let hist_cap = (cfg.ple_kernel - 1) * cfg.ple_dilation;
2486 let width = gated.len();
2487 let mut conv = vec![0.0f32; width];
2488 for channel in 0..width {
2489 let mut sum = w.conv[channel * cfg.ple_kernel + cfg.ple_kernel - 1] * normed[channel];
2490 for tap in 0..cfg.ple_kernel - 1 {
2491 let lag = (cfg.ple_kernel - 1 - tap) * cfg.ple_dilation;
2492 if lag <= st.ple_history_rows {
2493 let row = st.ple_history_rows - lag;
2494 sum +=
2495 w.conv[channel * cfg.ple_kernel + tap] * st.ple_history[row * width + channel];
2496 }
2497 }
2498 conv[channel] = silu(sum);
2499 }
2500 if hist_cap > 0 {
2501 if st.ple_history_rows < hist_cap {
2502 st.ple_history.extend_from_slice(&normed);
2503 st.ple_history_rows += 1;
2504 } else {
2505 st.ple_history.copy_within(width.., 0);
2506 let off = (hist_cap - 1) * width;
2507 st.ple_history[off..off + width].copy_from_slice(&normed);
2508 }
2509 }
2510 for (o, &g) in conv.iter_mut().zip(&gated) {
2511 *o += g;
2512 }
2513 conv
2514}
2515
2516#[cfg(feature = "gpu")]
2517fn dynamic_moe_gpu(
2518 layer: &Layer,
2519 li: usize,
2520 x: &[f32],
2521 state: &mut State,
2522 pool: Option<&Pool>,
2523) -> Option<(Vec<f32>, Vec<usize>)> {
2524 let m = &layer.moe;
2525 if !crate::gpu::enabled_here()
2526 || m.router_sigmoid
2527 || !m.norm_topk_prob
2528 || (m.routed_scaling - 1.0).abs() > 1e-9
2529 || m.per_expert_scale.is_some()
2530 || m.route_tau.is_some()
2531 || m.mask.is_some()
2532 {
2533 return None;
2534 }
2535 let model = m.experts.first()?.gate_proj.model_arc()?;
2536 let gu_q2 = m
2537 .experts
2538 .first()
2539 .is_some_and(|e| e.gate_proj.model_dtype() == Some(TensorDtype::Q2TiledP));
2540 let dynamic_mode = std::env::var("CMF_QWEN_DYNAMIC_MOE").ok();
2541 let dynamic_enabled = match dynamic_mode.as_deref() {
2542 Some("1") => true,
2543 Some("0") => false,
2544 None | Some("auto") => {
2550 gu_q2
2551 && crate::gpu_wgpu::dsv4_global_moe_supported()
2552 && crate::gpu_wgpu::dsv4_vram_budget().is_some_and(|b| b >= 14_000_000_000)
2553 }
2554 Some(_) => false,
2555 };
2556 if !dynamic_enabled {
2557 return None;
2558 }
2559 if state.gpu_pool.is_none() {
2560 state.gpu_pool = QwenGpuPool::create(
2561 &model,
2562 m.experts.first()?.gate_proj.rows(),
2563 x.len(),
2564 state.layers.len(),
2565 m.experts.len(),
2566 gu_q2,
2567 );
2568 }
2569 let mut logits = vec![0.0f32; m.experts.len()];
2570 m.router.matvec(x, &mut logits, pool);
2571 let (picks, probabilities, wsum) = crate::pipeline::moe_route(&logits, m, None);
2572 let (remap, shared_slot) =
2573 state
2574 .gpu_pool
2575 .as_mut()?
2576 .ensure(&model, li, &picks, &layer.expert_ids, layer.shared_ids)?;
2577 let shared_weight = m.shared.as_ref().map_or(1.0, |(_, gate)| {
2578 gate.as_ref().map_or(1.0, |gate| {
2579 let mut y = [0.0f32; 1];
2580 gate.matvec(x, &mut y, pool);
2581 sigmoid(y[0])
2582 })
2583 });
2584 let has_shared = m.shared.is_some();
2585 let mut mix_weights = vec![0.0f32; m.experts.len()];
2586 for &expert in &picks {
2587 mix_weights[expert] =
2588 probabilities[expert] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[expert]);
2589 }
2590 let cold_ids: Vec<_> = picks
2591 .iter()
2592 .copied()
2593 .filter(|&expert| remap[expert] == u32::MAX)
2594 .collect();
2595 let cold_jobs: Vec<_> = cold_ids
2596 .iter()
2597 .copied()
2598 .map(|expert| (&m.experts[expert], mix_weights[expert]))
2599 .collect();
2600 let gp = state.gpu_pool.as_ref()?;
2601 let weights = crate::gpu_wgpu::Dsv4MoeW {
2602 router: &[],
2603 experts: &layer.expert_ids,
2604 logits: &logits,
2605 bias: Some(&mix_weights),
2608 mask: None,
2609 forced: Some(&picks),
2610 remap: Some(&remap),
2611 global: Some(crate::gpu_wgpu::Dsv4GlobalMoe {
2612 pool_uid: model.uid(),
2613 shared_slot,
2614 segment_slots: gp.segment_slots as u32,
2615 }),
2616 has_shared,
2617 shared_weight,
2618 preweighted: true,
2619 qwen_softmax: true,
2620 };
2621 let geom = crate::gpu_wgpu::Dsv4MoeGeom {
2622 hidden: x.len(),
2623 inter: m.experts.first()?.gate_proj.rows(),
2624 top_k: m.top_k,
2625 route_scale: 1.0,
2626 swiglu_limit: 0.0,
2627 gu_q2,
2628 bf16: false,
2629 };
2630 let mut out = vec![0.0f32; x.len()];
2631 let mut cold = Vec::new();
2632 let mut cold_x = Vec::new();
2633 let (frame_ok, mut cold_cpu) = std::thread::scope(|scope| {
2634 let cpu = (!cold_jobs.is_empty())
2635 .then(|| scope.spawn(|| crate::pipeline::moe_cold_experts_cpu(&cold_jobs, x, pool)));
2636 let ok = crate::gpu_wgpu::dsv4_moe_frame(
2637 &model,
2638 &weights,
2639 geom,
2640 x,
2641 &mut cold,
2642 &mut cold_x,
2643 None,
2644 None,
2645 &mut out,
2646 );
2647 let early = cpu
2648 .map(|job| job.join().ok())
2649 .flatten()
2650 .unwrap_or_else(|| vec![0.0; x.len()]);
2651 (ok, early)
2652 });
2653 if !frame_ok
2654 || cold.len() != cold_jobs.len()
2655 || cold.iter().map(|&(e, _)| e).ne(cold_ids.iter().copied())
2656 {
2657 return None;
2658 }
2659 for (o, c) in out.iter_mut().zip(&cold_cpu) {
2662 *o += c;
2663 }
2664 crate::attention::recycle_buf(&mut cold_cpu);
2665 Some((out, picks))
2666}
2667
2668pub fn forward_token(
2672 globals: &Globals,
2673 layers: &[Layer],
2674 cfg: &Cfg,
2675 state: &mut State,
2676 token_id: u32,
2677 position: usize,
2678 inv_freq: &[f32],
2679 pool: Option<&Pool>,
2680 logits: &mut Vec<f32>,
2681 want_logits: bool,
2682) {
2683 #[cfg(feature = "gpu")]
2684 if forward_token_device(
2685 globals,
2686 layers,
2687 cfg,
2688 state,
2689 token_id,
2690 position,
2691 inv_freq,
2692 pool,
2693 logits,
2694 want_logits,
2695 ) {
2696 return;
2697 }
2698 let prof = std::env::var_os("CMF_QWEN_PROF").is_some();
2699 #[cfg(target_arch = "x86_64")]
2700 if position == 0
2701 && std::env::var_os("CMF_POOL_SPIN").is_none()
2702 && let Some(workers) = pool
2703 {
2704 workers.set_spin_budget((workers.n_workers() * 10_000).clamp(30_000, 200_000));
2710 }
2711 #[cfg(feature = "gpu")]
2712 let gpu_moe_before = if prof {
2713 use std::sync::atomic::Ordering;
2714 Some((
2715 crate::gpu_wgpu::MOE_ENC_NS.load(Ordering::Relaxed),
2716 crate::gpu_wgpu::MOE_WAIT_NS.load(Ordering::Relaxed),
2717 crate::gpu_wgpu::MOE_GPU_NS[0].load(Ordering::Relaxed),
2718 crate::gpu_wgpu::MOE_GPU_N.load(Ordering::Relaxed),
2719 crate::gpu_wgpu::DSV4_FILLS.load(Ordering::Relaxed),
2720 crate::gpu_wgpu::DSV4_FILL_BYTES.load(Ordering::Relaxed),
2721 ))
2722 } else {
2723 None
2724 };
2725 let token_t0 = std::time::Instant::now();
2726 let mut ple_dt = std::time::Duration::ZERO;
2727 let mut attn_hc_dt = std::time::Duration::ZERO;
2728 let mut mixer_dt = std::time::Duration::ZERO;
2729 let mut mlp_hc_dt = std::time::Duration::ZERO;
2730 let mut moe_dt = std::time::Duration::ZERO;
2731 if position == 0 || state.pos != position {
2732 state.reset();
2733 }
2734 if state.host_pos != position && state.token_history.len() == position {
2735 tracing::warn!(
2741 "qwen4: host caches rebuilt over {position} tokens after the device path turned off"
2742 );
2743 if std::env::var_os("CMF_QWEN_PROF").is_some() {
2744 eprintln!("qwen4: replaying {position} tokens on the host path");
2745 }
2746 let history = std::mem::take(&mut state.token_history);
2747 state.reset();
2748 for (p, &id) in history.iter().enumerate() {
2749 let mut lg = Vec::new();
2750 forward_token(
2751 globals, layers, cfg, state, id, p, inv_freq, pool, &mut lg, false,
2752 );
2753 }
2754 }
2755 let mut emb = vec![0.0f32; cfg.hidden];
2759 if (token_id as usize) < globals.embed.rows() {
2760 globals.embed.row_f32(token_id as usize, &mut emb);
2761 }
2762 observe("embedding", 0, position, &emb);
2763 state.hyper.clear();
2764 state.hyper.reserve(cfg.hc * cfg.hidden);
2765 for _ in 0..cfg.hc {
2766 state.hyper.extend_from_slice(&emb);
2767 }
2768 for (li, layer) in layers.iter().enumerate() {
2769 crate::gpu::set_layer(li as i64);
2773 let st = &mut state.layers[li];
2774 if let Some(ple) = &layer.ple
2775 && std::env::var_os("CMF_QWEN_NO_PLE").is_none()
2776 {
2777 let t0 = std::time::Instant::now();
2778 let side = ple_forward(
2779 &state.hyper,
2780 token_id,
2781 &state.token_history,
2782 ple,
2783 cfg,
2784 st,
2785 pool,
2786 );
2787 for (h, s) in state.hyper.iter_mut().zip(side) {
2788 *h += s;
2789 }
2790 ple_dt += t0.elapsed();
2791 observe("post_ple", li, position, &state.hyper);
2792 }
2793 let t0 = std::time::Instant::now();
2794 let (mixed, inject_w) = hc_mix(&layer.attn_hc, &state.hyper, cfg, pool);
2795 attn_hc_dt += t0.elapsed();
2796 observe("attn_in", li, position, &mixed);
2797 let t0 = std::time::Instant::now();
2798 let block = match &layer.mixer {
2799 Mixer::Gdn(w) => gdn_forward(&mixed, w, &cfg.gdn, &mut st.gdn, pool),
2800 Mixer::Qsa(w) => qsa_forward(&mixed, w, cfg, &mut st.qsa, position, inv_freq, pool),
2801 };
2802 mixer_dt += t0.elapsed();
2803 observe("attn_out", li, position, &block);
2804 inject(
2805 &mut state.hyper,
2806 &block,
2807 inject_w.as_deref().expect("layer HC has injection"),
2808 cfg,
2809 );
2810 observe("post_attn", li, position, &state.hyper);
2811 let t0 = std::time::Instant::now();
2812 let (mixed, inject_w) = hc_mix(&layer.mlp_hc, &state.hyper, cfg, pool);
2813 mlp_hc_dt += t0.elapsed();
2814 observe("moe_in", li, position, &mixed);
2815 let t0 = std::time::Instant::now();
2816 #[cfg(feature = "gpu")]
2817 let gpu_moe = dynamic_moe_gpu(layer, li, &mixed, state, pool);
2818 #[cfg(not(feature = "gpu"))]
2819 let gpu_moe: Option<(Vec<f32>, Vec<usize>)> = None;
2820 let block = match gpu_moe {
2821 Some((block, _)) => block,
2822 None => {
2823 moe_ffn(&layer.moe, &mixed, pool, None)
2829 }
2830 };
2831 moe_dt += t0.elapsed();
2832 observe("moe_out", li, position, &block);
2833 inject(
2834 &mut state.hyper,
2835 &block,
2836 inject_w.as_deref().expect("layer HC has injection"),
2837 cfg,
2838 );
2839 observe("post_moe", li, position, &state.hyper);
2840 }
2841 crate::gpu::set_layer(-1);
2842 let head_t0 = std::time::Instant::now();
2843 if want_logits {
2844 let (hidden, _) = hc_mix(&globals.head_hc, &state.hyper, cfg, pool);
2845 observe("head_in", layers.len(), position, &hidden);
2846 logits.resize(globals.lm_head.rows(), 0.0);
2847 globals.lm_head.matvec(&hidden, logits, pool);
2848 observe("logits", layers.len(), position, logits);
2849 } else {
2850 logits.clear();
2851 }
2852 let head_dt = head_t0.elapsed();
2853 state.token_history.push(token_id);
2854 state.pos = position + 1;
2855 state.host_pos = position + 1;
2856 if prof {
2857 eprintln!(
2858 "qwen-prof pos={position} total={:.3}s ple={:.3}s attn_hc={:.3}s mixer={:.3}s mlp_hc={:.3}s moe={:.3}s head={:.3}s",
2859 token_t0.elapsed().as_secs_f64(),
2860 ple_dt.as_secs_f64(),
2861 attn_hc_dt.as_secs_f64(),
2862 mixer_dt.as_secs_f64(),
2863 mlp_hc_dt.as_secs_f64(),
2864 moe_dt.as_secs_f64(),
2865 head_dt.as_secs_f64(),
2866 );
2867 #[cfg(feature = "gpu")]
2868 if let Some((enc0, wait0, card0, calls0, fills0, fill_bytes0)) = gpu_moe_before {
2869 use std::sync::atomic::Ordering;
2870 let enc = crate::gpu_wgpu::MOE_ENC_NS.load(Ordering::Relaxed) - enc0;
2871 let wait = crate::gpu_wgpu::MOE_WAIT_NS.load(Ordering::Relaxed) - wait0;
2872 let card = crate::gpu_wgpu::MOE_GPU_NS[0].load(Ordering::Relaxed) - card0;
2873 let calls = crate::gpu_wgpu::MOE_GPU_N.load(Ordering::Relaxed) - calls0;
2874 let fills = crate::gpu_wgpu::DSV4_FILLS.load(Ordering::Relaxed) - fills0;
2875 let fill_bytes = crate::gpu_wgpu::DSV4_FILL_BYTES.load(Ordering::Relaxed) - fill_bytes0;
2876 eprintln!(
2877 "qwen-moe-gpu pos={position} calls={calls} fills={fills} fill={:.1}MB encode={:.1}ms wait={:.1}ms card={:.1}ms",
2878 fill_bytes as f64 / 1e6,
2879 enc as f64 / 1e6,
2880 wait as f64 / 1e6,
2881 card as f64 / 1e6,
2882 );
2883 }
2884 }
2885}
2886
2887#[cfg(feature = "gpu")]
2888fn device_geom(cfg: &Cfg, moe: &MoeFfn, gu_q2: bool) -> crate::gpu_wgpu::qwen4::Geom {
2889 use crate::gpu_wgpu::qwen4::{GdnGeom, Geom};
2890 Geom {
2891 hidden: cfg.hidden,
2892 hc: cfg.hc,
2893 eps: cfg.eps as f32,
2894 n_heads: cfg.n_heads,
2895 n_kv_heads: cfg.n_kv_heads,
2896 head_dim: cfg.head_dim,
2897 rotary_dim: cfg.rotary_dim,
2898 index_heads: cfg.index_heads,
2899 index_dim: cfg.index_dim,
2900 index_budget: cfg.index_budget,
2901 compress_ratio: cfg.compress_ratio,
2902 gdn: GdnGeom {
2903 nv: cfg.gdn.num_v_heads,
2904 nk: cfg.gdn.num_k_heads,
2905 dk: cfg.gdn.key_head_dim,
2906 dv: cfg.gdn.value_head_dim,
2907 kk: cfg.gdn.conv_kernel,
2908 },
2909 ple_kernel: cfg.ple_kernel,
2910 ple_dilation: cfg.ple_dilation,
2911 top_k: moe.top_k,
2912 n_experts: moe.experts.len(),
2913 inter: moe.experts.first().map_or(0, |e| e.gate_proj.rows()),
2914 gu_q2,
2915 }
2916}
2917
2918#[cfg(feature = "gpu")]
2919fn device_layer_w<'a>(
2920 layer: &'a Layer,
2921 ix: &LayerIdx,
2922) -> Option<crate::gpu_wgpu::qwen4::LayerW<'a>> {
2923 use crate::gpu_wgpu::qwen4::{HcW, LayerW, MixerW, PleW};
2924 let hc = |g: &'a GatedResidual| -> Option<HcW<'a>> {
2925 Some(HcW {
2926 norm: &g.norm,
2927 down: g.down_idx?,
2928 up: g.up_idx?,
2929 inject: g.inject_idx,
2930 })
2931 };
2932 let mixer = match &layer.mixer {
2933 Mixer::Gdn(w) => {
2934 let [qkv, z, a, b, out] = ix.gdn?;
2935 MixerW::Gdn {
2936 qkv,
2937 z,
2938 a,
2939 b,
2940 out,
2941 conv1d: &w.conv1d,
2942 a_log: &w.a_log,
2943 dt_bias: &w.dt_bias,
2944 norm: &w.norm,
2945 }
2946 }
2947 Mixer::Qsa(w) => {
2948 let [q, k, v, o, index_qk] = ix.qsa?;
2949 MixerW::Qsa {
2950 q,
2951 k,
2952 v,
2953 o,
2954 index_qk,
2955 q_norm: &w.q_norm,
2956 k_norm: &w.k_norm,
2957 iq_norm: &w.index_q_norm,
2958 ik_norm: &w.index_k_norm,
2959 }
2960 }
2961 };
2962 let ple = match (&layer.ple, ix.ple) {
2963 (Some(w), Some((key_proj, value_proj))) => Some(PleW {
2964 key_proj,
2965 value_proj,
2966 norm_key: &w.norm_key,
2967 norm_query: &w.norm_query,
2968 norm_conv: &w.norm_conv,
2969 conv: &w.conv,
2970 }),
2971 (None, None) => None,
2972 _ => return None,
2973 };
2974 Some(LayerW {
2975 attn_hc: hc(&layer.attn_hc)?,
2976 mlp_hc: hc(&layer.mlp_hc)?,
2977 mixer,
2978 ple,
2979 router: ix.router,
2980 shared_gate: ix.shared_gate,
2981 })
2982}
2983
2984#[cfg(feature = "gpu")]
2985#[allow(clippy::type_complexity)]
2986static DEV_TAP: std::sync::Mutex<Vec<(usize, Vec<f32>, Vec<f32>, Vec<f32>, Vec<f32>, Vec<f32>)>> =
2987 std::sync::Mutex::new(Vec::new());
2988
2989#[cfg(feature = "gpu")]
2990fn tap_compare(label: &str, host: &[f32], dev: &[f32]) {
2991 let n = host.len().min(dev.len());
2992 let mut max_abs = 0.0f32;
2993 let mut at = 0usize;
2994 let (mut dot, mut na, mut nb) = (0.0f64, 0.0f64, 0.0f64);
2995 for i in 0..n {
2996 let d = (host[i] - dev[i]).abs();
2997 if d > max_abs {
2998 max_abs = d;
2999 at = i;
3000 }
3001 dot += host[i] as f64 * dev[i] as f64;
3002 na += host[i] as f64 * host[i] as f64;
3003 nb += dev[i] as f64 * dev[i] as f64;
3004 }
3005 eprintln!(
3006 "qwen4-tap {label:>10}: cos={:.6} max_abs={max_abs:.4} at {at} (host {:.4} dev {:.4}) |host|={:.3} |dev|={:.3} n={n}",
3007 dot / (na.sqrt() * nb.sqrt()).max(1e-30),
3008 host.get(at).copied().unwrap_or(0.0),
3009 dev.get(at).copied().unwrap_or(0.0),
3010 na.sqrt(),
3011 nb.sqrt()
3012 );
3013}
3014
3015#[cfg(feature = "gpu")]
3019struct ExpertProfile {
3020 n_layers: usize,
3021 n_experts: usize,
3022 counts: Vec<u32>,
3023 tokens: u64,
3024}
3025
3026#[cfg(feature = "gpu")]
3027impl ExpertProfile {
3028 const MAGIC: &'static [u8; 8] = b"CMFQ4PF\0";
3029
3030 fn new(n_layers: usize, n_experts: usize) -> Self {
3031 Self {
3032 n_layers,
3033 n_experts,
3034 counts: vec![0; n_layers * n_experts],
3035 tokens: 0,
3036 }
3037 }
3038
3039 fn load(path: &str, n_layers: usize, n_experts: usize) -> Option<Self> {
3040 let bytes = std::fs::read(path).ok()?;
3041 if bytes.len() < 24 || &bytes[..8] != Self::MAGIC {
3042 return None;
3043 }
3044 let rd = |o: usize| u64::from_le_bytes(bytes[o..o + 8].try_into().unwrap());
3045 let (nl, ne) = (rd(8) as usize, rd(16) as usize);
3046 if nl != n_layers || ne != n_experts || bytes.len() < 32 + nl * ne * 4 {
3047 return None;
3048 }
3049 let tokens = rd(24);
3050 let counts = bytes[32..32 + nl * ne * 4]
3051 .chunks_exact(4)
3052 .map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
3053 .collect();
3054 Some(Self {
3055 n_layers,
3056 n_experts,
3057 counts,
3058 tokens,
3059 })
3060 }
3061
3062 fn save(&self, path: &str) {
3063 let mut out = Vec::with_capacity(32 + self.counts.len() * 4);
3064 out.extend_from_slice(Self::MAGIC);
3065 out.extend_from_slice(&(self.n_layers as u64).to_le_bytes());
3066 out.extend_from_slice(&(self.n_experts as u64).to_le_bytes());
3067 out.extend_from_slice(&self.tokens.to_le_bytes());
3068 for &c in &self.counts {
3069 out.extend_from_slice(&c.to_le_bytes());
3070 }
3071 if let Err(e) = std::fs::write(path, out) {
3072 tracing::warn!("expert profile not saved to {path}: {e}");
3073 }
3074 }
3075
3076 fn note(&mut self, layer: usize, picks: &[usize]) {
3077 for &e in picks {
3078 if let Some(c) = self.counts.get_mut(layer * self.n_experts + e) {
3079 *c = c.saturating_add(1);
3080 }
3081 }
3082 }
3083
3084 fn ranked(&self) -> Vec<(usize, usize)> {
3086 let mut v: Vec<(u32, usize, usize)> = self
3087 .counts
3088 .iter()
3089 .enumerate()
3090 .filter(|&(_, &c)| c > 0)
3091 .map(|(i, &c)| (c, i / self.n_experts, i % self.n_experts))
3092 .collect();
3093 v.sort_unstable_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1)).then(a.2.cmp(&b.2)));
3094 v.into_iter().map(|(_, l, e)| (l, e)).collect()
3095 }
3096}
3097
3098#[cfg(feature = "gpu")]
3102fn admit_batch_env() -> bool {
3103 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
3104 *ON.get_or_init(|| std::env::var("CMF_QWEN_ADMIT_BATCH").as_deref() != Ok("0"))
3105}
3106
3107#[cfg(feature = "gpu")]
3109#[derive(Default)]
3110struct DevProf {
3111 encode: std::time::Duration,
3112 wait: std::time::Duration,
3113 cold_cpu: std::time::Duration,
3114 admit: std::time::Duration,
3115 head: std::time::Duration,
3116 cold_experts: usize,
3117 cold_dev: usize,
3118 layers_with_cold: usize,
3119 chains: usize,
3120 aborted: usize,
3121 finish: std::time::Duration,
3123 spin: std::time::Duration,
3125 post: std::time::Duration,
3127 ple: std::time::Duration,
3129 gpu: [f64; 17],
3132 gpu_last_end: Option<f64>,
3133}
3134
3135#[cfg(feature = "gpu")]
3136const TS_NAMES: [&str; 17] = [
3137 "pending", "ple", "attn_hc", "gdn_proj", "gdn_rec", "gdn_out", "qsa_proj", "qsa_attn",
3138 "qsa_out", "moe_hc", "router", "route", "gu", "dn", "tail", "head", "idle",
3139];
3140
3141#[cfg(feature = "gpu")]
3142impl DevProf {
3143 fn gpu_note(&mut self, fi: usize, n: usize, m: &[f64], gdn: Option<bool>) {
3145 if m.len() < 13 {
3146 return;
3147 }
3148 let d = |a: usize, b: usize| (m[b] - m[a]).max(0.0);
3149 if let Some(prev) = self.gpu_last_end {
3150 self.gpu[16] += (m[0] - prev).max(0.0);
3151 }
3152 self.gpu_last_end = Some(m[8]);
3153 self.gpu[0] += d(0, 1);
3154 if fi >= n {
3155 self.gpu[15] += d(7, 8);
3156 return;
3157 }
3158 self.gpu[1] += d(1, 2);
3159 self.gpu[2] += d(2, 3);
3160 let o = if gdn == Some(true) { 3 } else { 6 };
3161 self.gpu[o] += d(3, 9);
3162 self.gpu[o + 1] += d(9, 10);
3163 self.gpu[o + 2] += d(10, 4);
3164 self.gpu[9] += d(4, 5);
3165 self.gpu[10] += d(5, 11);
3166 self.gpu[11] += d(11, 6);
3167 self.gpu[12] += d(6, 12);
3168 self.gpu[13] += d(12, 7);
3169 self.gpu[14] += d(7, 8);
3170 }
3171}
3172
3173#[cfg(feature = "gpu")]
3178pub fn prefill_chunk() -> usize {
3179 std::env::var("CMF_QWEN_PREFILL_CHUNK")
3180 .ok()
3181 .and_then(|v| v.parse::<usize>().ok())
3182 .unwrap_or(crate::gpu_wgpu::qwen4::TMAX)
3183 .clamp(1, crate::gpu_wgpu::qwen4::TMAX)
3184}
3185
3186#[cfg(not(feature = "gpu"))]
3187pub fn prefill_chunk() -> usize {
3188 1
3189}
3190
3191#[allow(clippy::too_many_arguments)]
3195pub fn forward_tokens(
3196 globals: &Globals,
3197 layers: &[Layer],
3198 cfg: &Cfg,
3199 state: &mut State,
3200 ids: &[u32],
3201 pos0: usize,
3202 inv_freq: &[f32],
3203 pool: Option<&Pool>,
3204 logits: &mut Vec<f32>,
3205 want_logits: bool,
3206) {
3207 #[cfg(feature = "gpu")]
3208 if !ids.is_empty()
3209 && ids.len() <= crate::gpu_wgpu::qwen4::TMAX
3210 && forward_tokens_device(
3211 globals,
3212 layers,
3213 cfg,
3214 state,
3215 ids,
3216 pos0,
3217 inv_freq,
3218 pool,
3219 logits,
3220 want_logits,
3221 )
3222 {
3223 return;
3224 }
3225 for (i, &id) in ids.iter().enumerate() {
3226 let last = i + 1 == ids.len();
3227 let mut lg = Vec::new();
3228 forward_token(
3229 globals,
3230 layers,
3231 cfg,
3232 state,
3233 id,
3234 pos0 + i,
3235 inv_freq,
3236 pool,
3237 &mut lg,
3238 want_logits && last,
3239 );
3240 if last {
3241 *logits = lg;
3242 }
3243 }
3244}
3245
3246#[cfg(feature = "gpu")]
3250#[allow(clippy::too_many_arguments)]
3251pub fn capture_window(
3252 globals: &Globals,
3253 layers: &[Layer],
3254 cfg: &Cfg,
3255 state: &mut State,
3256 ids: &[u32],
3257 lo: usize,
3258 hi: usize,
3259 inv_freq: &[f32],
3260 pool: Option<&Pool>,
3261) -> Result<Vec<Vec<f32>>, String> {
3262 use crate::gpu_wgpu::qwen4 as q4;
3263 let first = layers.first().ok_or("no layers")?;
3264 let hidden = first.moe.experts.first().ok_or("no experts")?.gate_proj.cols();
3265 let ne = first.moe.experts.len();
3266 let hi = hi.min(layers.len());
3267 if lo >= hi || !q4::capture_begin(lo, hi, hidden, ne) {
3268 return Err("capture unavailable".into());
3269 }
3270 let mut out = vec![Vec::with_capacity(ids.len() * (hidden + ne)); hi - lo];
3271 let chunk = prefill_chunk();
3272 let mut pos = 0;
3273 let res = (|| {
3274 while pos < ids.len() {
3275 let end = (pos + chunk).min(ids.len());
3276 let mut lg = Vec::new();
3277 forward_tokens(globals, layers, cfg, state, &ids[pos..end], pos, inv_freq, pool, &mut lg, false);
3278 let (rows, written) = q4::capture_read(end - pos).ok_or("capture readback")?;
3279 if written < hi - lo {
3280 return Err(format!("the device path ran {written} of {} captured layer frames (host fallback?)", hi - lo));
3281 }
3282 for (k, r) in rows.into_iter().enumerate() {
3283 out[k].extend(r);
3284 }
3285 pos = end;
3286 }
3287 Ok(())
3288 })();
3289 q4::capture_end();
3290 res.map(|_| out)
3291}
3292
3293pub fn expert_dirs(layers: &[Layer], li: usize) -> Option<(Vec<(usize, usize, usize)>, Option<(usize, usize, usize)>)> {
3296 let l = layers.get(li)?;
3297 Some((l.expert_ids.clone(), l.shared_ids))
3298}
3299
3300#[cfg(feature = "gpu")]
3301#[allow(clippy::too_many_arguments)]
3302fn forward_token_device(
3303 globals: &Globals,
3304 layers: &[Layer],
3305 cfg: &Cfg,
3306 state: &mut State,
3307 token_id: u32,
3308 position: usize,
3309 inv_freq: &[f32],
3310 pool: Option<&Pool>,
3311 logits: &mut Vec<f32>,
3312 want_logits: bool,
3313) -> bool {
3314 forward_tokens_device(
3315 globals,
3316 layers,
3317 cfg,
3318 state,
3319 &[token_id],
3320 position,
3321 inv_freq,
3322 pool,
3323 logits,
3324 want_logits,
3325 )
3326}
3327
3328#[cfg(feature = "gpu")]
3329#[allow(clippy::too_many_arguments)]
3330fn forward_tokens_device(
3331 globals: &Globals,
3332 layers: &[Layer],
3333 cfg: &Cfg,
3334 state: &mut State,
3335 ids: &[u32],
3336 pos0: usize,
3337 inv_freq: &[f32],
3338 pool: Option<&Pool>,
3339 logits: &mut Vec<f32>,
3340 want_logits: bool,
3341) -> bool {
3342 use crate::gpu_wgpu::qwen4 as q4;
3343 let ntok = ids.len();
3344 if ntok == 0 || ntok > q4::TMAX {
3345 return false;
3346 }
3347 let position = pos0;
3348 let token_id = ids[0];
3349 if state.device_off
3350 || std::env::var("CMF_QWEN_DEVICE").as_deref() == Ok("0")
3351 || !crate::gpu::enabled_here()
3352 {
3353 return false;
3354 }
3355 fn off(state: &mut State, why: &str) -> bool {
3356 if !state.device_off {
3357 tracing::warn!("qwen4 device path off: {why}");
3358 if std::env::var_os("CMF_QWEN_PROF").is_some() {
3359 eprintln!("qwen4-device: off ({why})");
3360 }
3361 }
3362 state.device_off = true;
3363 false
3364 }
3365 if !q4::available() {
3366 return off(state, "kernels or the global expert arena unavailable");
3367 }
3368 let Some(first) = layers.first() else {
3369 return off(state, "no layers");
3370 };
3371 let Some(e0) = first.moe.experts.first() else {
3372 return off(state, "no experts");
3373 };
3374 let Some(model) = e0.gate_proj.model_arc() else {
3375 return off(state, "experts are not mmap-backed");
3376 };
3377 let gu_q2 = e0.gate_proj.model_dtype() == Some(TensorDtype::Q2TiledP);
3378 if !gu_q2 && e0.gate_proj.model_dtype() != Some(TensorDtype::Q4TiledP) {
3379 return off(state, "expert gate/up are neither q2tp nor q4tp");
3380 }
3381 if first.moe.router_sigmoid
3382 || !first.moe.norm_topk_prob
3383 || first.moe.route_tau.is_some()
3384 || first.moe.mask.is_some()
3385 || (first.moe.routed_scaling - 1.0).abs() > 1e-9
3386 {
3387 return off(state, "non-Qwen routing settings");
3388 }
3389 let g = device_geom(cfg, &first.moe, gu_q2);
3390 let prof = std::env::var_os("CMF_QWEN_PROF").is_some();
3391 let check = std::env::var("CMF_QWEN_DEVICE_CHECK").as_deref() == Ok("1");
3392 let t_token = std::time::Instant::now();
3393
3394 if state.dev.is_none() {
3396 if layers.iter().any(|l| l.idx.is_none()) || globals.lm_head_idx.is_none() {
3397 return off(state, "skeleton tensors missing from the directory");
3398 }
3399 let kinds: Vec<(bool, bool)> = layers
3400 .iter()
3401 .map(|l| (matches!(l.mixer, Mixer::Gdn(_)), l.ple.is_some()))
3402 .collect();
3403 let Some(dev) = q4::Dev::new(model.uid(), &g, &kinds) else {
3404 return off(state, "device state allocation refused");
3405 };
3406 let t0 = std::time::Instant::now();
3407 let Some(bytes) = q4::prewarm(&model, &globals.skeleton_idxs) else {
3408 return off(state, "skeleton does not fit the VRAM budget");
3409 };
3410 tracing::info!(
3411 "qwen4 device: {} skeleton tensors resident ({} MB) in {:.2}s",
3412 globals.skeleton_idxs.len(),
3413 bytes >> 20,
3414 t0.elapsed().as_secs_f64()
3415 );
3416 if prof {
3417 eprintln!(
3418 "qwen4-device: skeleton {} MB resident in {:.2}s",
3419 bytes >> 20,
3420 t0.elapsed().as_secs_f64()
3421 );
3422 }
3423 state.dev = Some(dev);
3424 state.picks_prev = vec![Vec::new(); layers.len()];
3425 }
3426 if state.gpu_pool.is_none() {
3427 if state.mtp.is_none() && !state.mtp_tried {
3430 state.mtp_tried = true;
3431 state.mtp = MtpHead::load(&model, model.arch());
3432 if let Some(head) = state.mtp.as_mut()
3433 && let Err(why) = head.setup(&g, gu_q2)
3434 {
3435 tracing::warn!("qwen4 MTP off: {why}");
3436 state.mtp = None;
3437 }
3438 }
3439 let Some(budget) = q4::vram_budget() else {
3440 return off(state, "no VRAM budget");
3441 };
3442 let resident = q4::resident_bytes();
3443 let Some(per) = QwenGpuPool::per_expert_bytes(g.inter, g.hidden, gu_q2) else {
3444 return off(state, "expert geometry");
3445 };
3446 let reserve_mb = std::env::var("CMF_QWEN_KV_RESERVE_MB")
3447 .ok()
3448 .and_then(|v| v.parse::<u64>().ok())
3449 .unwrap_or(768);
3453 let workspace = std::env::var("CMF_QWEN_WORKSPACE_MB")
3460 .ok()
3461 .and_then(|v| v.parse::<u64>().ok())
3462 .map(|m| m << 20)
3463 .unwrap_or(if budget >= 24 << 30 {
3464 0
3465 } else {
3466 (budget / 10).clamp(2 << 30, 4 << 30)
3467 });
3468 let window = state.mtp.as_ref().map_or(0, |h| h.k + 1);
3474 let snap_extra = {
3475 let kinds: Vec<(bool, bool)> = layers
3476 .iter()
3477 .map(|l| (matches!(l.mixer, Mixer::Gdn(_)), l.ple.is_some()))
3478 .collect();
3479 q4::Dev::snap_bytes(&g, &kinds, window.saturating_sub(MTP_MEASURED_WINDOW))
3480 };
3481 let free = budget
3482 .saturating_sub(resident)
3483 .saturating_sub(reserve_mb << 20)
3484 .saturating_sub(workspace)
3485 .saturating_sub(snap_extra);
3486 let slots = (free / per as u64) as usize;
3487 let n_layers = layers.len();
3488 let staging = q4::TMAX * g.top_k;
3490 let Some(arena) = QwenGpuPool::create_explicit(
3491 &model,
3492 g.inter,
3493 g.hidden,
3494 n_layers,
3495 g.n_experts,
3496 gu_q2,
3497 slots,
3498 staging,
3499 ) else {
3500 return off(state, "expert arena allocation refused");
3501 };
3502 if prof {
3503 eprintln!(
3504 "qwen4-device: budget {} MB, skeleton resident {} MB, reserve {} MB, workspace {} MB, window snapshots beyond {MTP_MEASURED_WINDOW} rows {} MB, arena request {} slots ({} MB), got {} slots",
3505 budget >> 20,
3506 resident >> 20,
3507 reserve_mb,
3508 workspace >> 20,
3509 snap_extra >> 20,
3510 slots,
3511 (slots as u64 * per as u64) >> 20,
3512 arena.owner.len()
3513 );
3514 }
3515 let (cap_slots, cap_staging) = (arena.staging_base, arena.staging);
3516 state.gpu_pool = Some(arena);
3517 let pinned = {
3518 let arena = state.gpu_pool.as_mut().unwrap();
3519 layers.iter().enumerate().all(|(li, l)| {
3520 arena
3521 .ensure(&model, li, &[], &l.expert_ids, l.shared_ids)
3522 .is_some_and(|(_, s)| s != u32::MAX)
3523 })
3524 };
3525 if !pinned {
3526 return off(state, "shared expert could not be pinned");
3527 }
3528 let n_experts = g.n_experts;
3529 let triples: Vec<Vec<(usize, usize, usize)>> =
3530 layers.iter().map(|l| l.expert_ids.clone()).collect();
3531 let store = (std::env::var("CMF_QWEN_STORE").as_deref() != Ok("0"))
3534 .then(|| {
3535 crate::expert_store::ExpertStore::new(&model, &triples, n_experts, cap_slots)
3536 })
3537 .flatten();
3538 let advised =
3540 crate::expert_store::advise_random(&model, |n| n.contains("ngram_embedding.shard_"));
3541 if let Some(store) = store.as_ref() {
3542 state.gpu_pool.as_mut().unwrap().store = Some(store.clone());
3543 if prof {
3544 eprintln!(
3545 "qwen4-device: expert store io {:?}, RAM tier {} experts, n-gram table {} MiB advised random",
3546 store.io_mode(),
3547 store.tier_capacity(),
3548 advised >> 20
3549 );
3550 }
3551 }
3552 let mut profile = std::env::var("CMF_QWEN_PROFILE")
3553 .ok()
3554 .and_then(|path| ExpertProfile::load(&path, layers.len(), n_experts));
3555 let mut warm_order: Vec<(usize, usize)> = Vec::new();
3556 if let Some(pr) = profile.as_ref() {
3557 let ranked = pr.ranked();
3558 warm_order = ranked.clone();
3559 let t0 = std::time::Instant::now();
3560 let n = state
3561 .gpu_pool
3562 .as_mut()
3563 .map_or(0, |a| a.prefill(&model, &ranked, &triples));
3564 tracing::info!(
3565 "qwen4 device: arena prefilled with {n} of {} profiled experts ({} tokens of history) in {:.2}s",
3566 ranked.len(),
3567 pr.tokens,
3568 t0.elapsed().as_secs_f64()
3569 );
3570 if prof {
3571 eprintln!(
3572 "qwen4-device: arena prefilled with {n} of {} profiled experts in {:.2}s (capacity {} slots, {} staging)",
3573 ranked.len(),
3574 t0.elapsed().as_secs_f64(),
3575 cap_slots,
3576 cap_staging
3577 );
3578 }
3579 }
3580 {
3592 let n_keys = layers.len() * n_experts;
3593 let want_slots = n_keys.saturating_sub(cap_slots) + cap_slots / 8;
3594 let stride = (per as u64).div_ceil(256) * 256 + 1024;
3595 let want = want_slots as u64 * stride;
3596 let asked = match std::env::var("CMF_QWEN_HTIER_MB").ok().as_deref() {
3597 None | Some("") => 0,
3598 Some("auto") => want,
3599 Some(v) => v.parse::<u64>().map_or(0, |mb| mb << 20),
3600 };
3601 let cap_bytes = if asked == 0 {
3602 0
3603 } else {
3604 crate::expert_store::host_available_bytes().map_or(0, |avail| {
3605 asked
3606 .min(avail / 2)
3607 .min(avail.saturating_sub(16 << 30))
3608 })
3609 };
3610 if cap_bytes > 0 {
3611 let t0 = std::time::Instant::now();
3612 let tier = q4::HostTier::new(&model, n_keys, cap_bytes);
3613 if prof {
3614 match tier.as_ref() {
3615 Some(t) => eprintln!(
3616 "qwen4-device: host tier {} slots ({} MiB, first GiB in {:.0} ms, the rest in the background after the first frame)",
3617 t.capacity(),
3618 (t.capacity() as u64 * t.stride()) >> 20,
3619 t0.elapsed().as_secs_f64() * 1e3
3620 ),
3621 None => eprintln!("qwen4-device: host tier unavailable"),
3622 }
3623 }
3624 let fill_threads = std::env::var("CMF_QWEN_HTIER_FILL")
3629 .ok()
3630 .and_then(|v| v.parse::<usize>().ok())
3631 .unwrap_or(8);
3632 if let Some(t) = tier.as_ref()
3633 && fill_threads > 0
3634 {
3635 let arena = state.gpu_pool.as_ref().unwrap();
3636 let mut order: Vec<usize> = Vec::with_capacity(n_keys);
3637 let mut seen = vec![false; n_keys];
3638 let ranked = profile.as_ref().map(|p| p.ranked()).unwrap_or_default();
3639 for &(l, e) in &ranked {
3640 if l < layers.len() && e < n_experts {
3641 let k = l * n_experts + e;
3642 if !seen[k] && arena.slot_for[k] == u32::MAX {
3643 seen[k] = true;
3644 order.push(k);
3645 }
3646 }
3647 }
3648 for e in 0..n_experts {
3649 for l in 0..layers.len() {
3650 let k = l * n_experts + e;
3651 if !seen[k] && arena.slot_for[k] == u32::MAX {
3652 seen[k] = true;
3653 order.push(k);
3654 }
3655 }
3656 }
3657 let flat: Vec<(usize, usize, usize)> =
3658 triples.iter().flat_map(|r| r.iter().copied()).collect();
3659 if flat.len() == n_keys {
3660 t.start_fill(model.clone(), Arc::new(flat), order, fill_threads);
3661 }
3662 }
3663 state.gpu_pool.as_mut().unwrap().tier = tier;
3664 }
3665 }
3666 if std::env::var_os("CMF_QWEN_PROFILE_SAVE").is_some() && profile.is_none() {
3667 profile = Some(ExpertProfile::new(layers.len(), n_experts));
3668 }
3669 state.profile = profile;
3670 if let Some(store) = store.as_ref() {
3674 let mut seen = vec![false; layers.len() * n_experts];
3675 for &(l, e) in &warm_order {
3676 if l < layers.len() && e < n_experts {
3677 seen[l * n_experts + e] = true;
3678 }
3679 }
3680 for e in 0..n_experts {
3681 for l in 0..layers.len() {
3682 if !seen[l * n_experts + e] {
3683 warm_order.push((l, e));
3684 }
3685 }
3686 }
3687 store.start_background(warm_order);
3688 }
3689 }
3690 if position == 0 || state.pos != position {
3691 state.reset();
3692 let ok = state.dev.as_mut().is_some_and(|d| d.reset());
3693 if !ok {
3694 return off(state, "device reset failed");
3695 }
3696 }
3697 let dev = state.dev.as_mut().unwrap();
3698 let arena = state.gpu_pool.as_mut().unwrap();
3699
3700 let mut pf = DevProf::default();
3702 let cold_on_device = std::env::var("CMF_QWEN_COLD_DEVICE").as_deref() != Ok("0");
3703 let chain_len = if ntok > 1 {
3705 1
3706 } else {
3707 std::env::var("CMF_QWEN_CHAIN")
3708 .ok()
3709 .and_then(|v| v.parse::<usize>().ok())
3710 .unwrap_or(1)
3711 .max(1)
3712 };
3713 let n = layers.len();
3714 let top_k = g.top_k;
3715 let cs = q4::cold_stride(&g);
3718 let hidden_bytes = cfg.hidden * 4;
3719 let cold_part = (ntok * cs).div_ceil(16) * 16;
3720 let frame_bytes = cold_part + (ntok * hidden_bytes).div_ceil(16) * 16;
3721 let snapshot = state.verify_window;
3722 if snapshot {
3723 dev.ensure_snaps(&g, ntok);
3724 }
3725 let mut cold_vec: Vec<Option<Vec<f32>>> = vec![None; ntok];
3726 let mut cold_slots: Vec<Vec<(u32, f32)>> = vec![Vec::with_capacity(top_k); ntok];
3727 let mut failed: Option<&'static str> = None;
3728 let head_hc = device_hc_w(&globals.head_hc);
3729 let lm_head_idx = globals.lm_head_idx.unwrap_or(usize::MAX);
3730 let logit_rows = match (want_logits, snapshot) {
3735 (false, _) => 0,
3736 (true, false) => 1,
3737 (true, true) => ntok,
3738 };
3739 let stage_need = q4::chain_stage_bytes(
3740 chain_len.min(n),
3741 frame_bytes,
3742 logit_rows,
3743 q4::head_stride(&model, lm_head_idx).unwrap_or(0),
3744 );
3745 if !dev.ensure_stage(stage_need) {
3746 failed = Some("readback stage allocation");
3747 }
3748 for (t, &id) in ids.iter().enumerate() {
3749 let mut emb = vec![0.0f32; cfg.hidden];
3750 if (id as usize) < globals.embed.rows() {
3751 globals.embed.row_f32(id as usize, &mut emb);
3752 }
3753 dev.seed(t, &emb, cfg.hc);
3754 }
3755 let tp = std::time::Instant::now();
3757 let ple_rows = ple_frame_rows(layers, cfg, &state.token_history, ids, pool);
3758 pf.ple += tp.elapsed();
3759 let frames_total = if want_logits { n + 1 } else { n };
3764 dev.gated = chain_len > 1;
3765 let mut fi = 0usize;
3766 let mut vocab_out = 0usize;
3767 let mut prepared: Option<(wgpu::CommandBuffer, u64, Option<usize>, usize)> = None;
3774 let dump_hyper = std::env::var("CMF_QWEN_DUMP_HYPER").ok();
3775 let mut t_fence: Option<std::time::Instant> = None;
3776 let mut lstride_out = 0usize;
3778 while fi < frames_total && failed.is_none() {
3779 let hi = (fi + chain_len).min(frames_total);
3780 let t0 = std::time::Instant::now();
3781 let encode_chain = |lo: usize,
3782 hi: usize,
3783 dev: &mut q4::Dev,
3784 arena: &mut QwenGpuPool,
3785 picks_prev: &[Vec<usize>],
3786 pf: &mut DevProf,
3787 vocab_out: &mut usize|
3788 -> Result<
3789 (wgpu::CommandEncoder, u64, Option<usize>, usize),
3790 &'static str,
3791 > {
3792 let mut enc = q4::new_encoder("qwen4-chain").ok_or("encoder")?;
3793 let merge = q4::merge_guard(&enc);
3794 q4::ts_mark(&mut enc, 0);
3795 if !q4::encode_pending(&mut enc, dev, &g, ntok) {
3797 return Err("pending inject");
3798 }
3799 q4::pending_done(dev);
3800 q4::ts_mark(&mut enc, 1);
3801 let mut stage_off = 0u64;
3802 let mut logits_off = None;
3803 let mut lstride = 0usize;
3804 for f in lo..hi {
3805 let inject_prev = f > lo;
3806 if f == n {
3807 for i in 2..8 {
3808 q4::ts_mark(&mut enc, i);
3809 }
3810 dev.keep_r(&mut enc, ntok - 1);
3812 let hw = head_hc.as_ref().ok_or("head hyper-connection indices")?;
3813 let first = if snapshot { 0 } else { ntok - 1 };
3816 let (lb, vocab, ls) = q4::encode_head(
3817 &mut enc,
3818 dev,
3819 &model,
3820 &g,
3821 hw,
3822 lm_head_idx,
3823 inject_prev,
3824 ntok,
3825 )
3826 .ok_or("head frame declined")?;
3827 let bytes = ((ntok - first) * ls) as u64;
3828 if !q4::copy_to_stage(&mut enc, dev, &lb, (first * ls) as u64, stage_off, bytes)
3829 {
3830 return Err("logits overrun the readback stage");
3831 }
3832 logits_off = Some(stage_off as usize);
3833 *vocab_out = vocab;
3834 lstride = ls;
3835 stage_off += bytes.div_ceil(16) * 16;
3836 continue;
3837 }
3838 let layer = &layers[f];
3839 let ix = layer.idx.as_ref().ok_or("layer index")?;
3840 let w = device_layer_w(layer, ix).ok_or("layer weights")?;
3841 let ta = std::time::Instant::now();
3842 let (remap, shared_slot) = match arena.ensure(
3843 &model,
3844 f,
3845 &picks_prev[f],
3846 &layer.expert_ids,
3847 layer.shared_ids,
3848 ) {
3849 Some(r) => r,
3850 None => arena.remap_snapshot(f),
3851 };
3852 pf.admit += ta.elapsed();
3853 if shared_slot == u32::MAX {
3854 return Err("shared expert slot");
3855 }
3856 let out = q4::encode_layer(
3857 &mut enc,
3858 dev,
3859 &model,
3860 &g,
3861 &w,
3862 f,
3863 pos0,
3864 ntok,
3865 inv_freq,
3866 &remap,
3867 shared_slot,
3868 &ple_rows[f],
3869 inject_prev,
3870 snapshot,
3871 )
3872 .ok_or("layer frame declined")?;
3873 if !q4::copy_to_stage(&mut enc, dev, &out.cold, 0, stage_off, (ntok * cs) as u64)
3874 || !q4::copy_to_stage(
3875 &mut enc,
3876 dev,
3877 &out.x2,
3878 0,
3879 stage_off + cold_part as u64,
3880 (ntok * hidden_bytes) as u64,
3881 )
3882 {
3883 return Err("layer frame overruns the readback stage");
3884 }
3885 stage_off += frame_bytes as u64;
3886 }
3887 q4::ts_mark(&mut enc, 8);
3888 q4::ts_resolve(&mut enc);
3889 dev.arm_chain(lo, hi);
3890 drop(merge);
3891 Ok((enc, stage_off, logits_off, lstride))
3892 };
3893 let (cb, stage_off, logits_off, ls) = match prepared.take() {
3894 Some(p) => p,
3895 None => match encode_chain(
3896 fi,
3897 hi,
3898 dev,
3899 arena,
3900 &state.picks_prev,
3901 &mut pf,
3902 &mut vocab_out,
3903 ) {
3904 Ok((e, a, b, c)) => {
3905 let tf = std::time::Instant::now();
3906 let cb = q4::finish_frame(e);
3907 pf.finish += tf.elapsed();
3908 (cb, a, b, c)
3909 }
3910 Err(why) => {
3911 failed = Some(why);
3912 break;
3913 }
3914 },
3915 };
3916 if ls != 0 {
3917 lstride_out = ls;
3918 }
3919 pf.encode += t0.elapsed();
3920 if !q4::finalize_pending(dev, &g, ntok, &cold_vec, &cold_slots) {
3922 failed = Some("finalize");
3923 break;
3924 }
3925 let t0 = std::time::Instant::now();
3926 let uploads = arena.take_uploads();
3929 let Some(pend) = q4::submit_frame(dev, uploads, cb, stage_off) else {
3930 failed = Some("submit");
3931 break;
3932 };
3933 arena.rearm_uploads();
3934 if let Some(tf) = t_fence.take() {
3935 pf.post += tf.elapsed();
3936 }
3937 arena.hold_layers = [None, None];
3939 if chain_len == 1 && hi < frames_total {
3940 let te = std::time::Instant::now();
3941 match encode_chain(
3942 hi,
3943 hi + 1,
3944 dev,
3945 arena,
3946 &state.picks_prev,
3947 &mut pf,
3948 &mut vocab_out,
3949 ) {
3950 Ok((e, a, b, c)) => {
3951 let tf = std::time::Instant::now();
3952 prepared = Some((q4::finish_frame(e), a, b, c));
3953 pf.finish += tf.elapsed();
3954 }
3955 Err(why) => {
3956 failed = Some(why);
3957 break;
3958 }
3959 }
3960 pf.encode += te.elapsed();
3961 arena.hold_layers = [(hi < n).then_some(hi), (fi < n).then_some(fi)];
3964 }
3965 let ts = std::time::Instant::now();
3966 let Some(bytes) = pend.wait() else {
3967 failed = Some("readback");
3968 break;
3969 };
3970 arena.uploads_done();
3971 pf.spin += ts.elapsed();
3972 pf.wait += t0.elapsed();
3973 if let Some(m) = q4::ts_read() {
3974 pf.gpu_note(fi, n, &m, layers.get(fi).map(|l| matches!(l.mixer, Mixer::Gdn(_))));
3975 }
3976 t_fence = Some(std::time::Instant::now());
3977 pf.chains += 1;
3978 let mut miss_at: Option<usize> = None;
3980 let mut off = 0usize;
3981 'frames: for f in fi..hi {
3982 if f == n {
3983 break;
3984 }
3985 let layer = &layers[f];
3986 dev.commit(f);
3987 if let Some(dir) = dump_hyper.as_deref()
3988 && let Some(rows) = q4::read_hyper(dev, ntok, cfg.hc * cfg.hidden)
3989 {
3990 let _ = std::fs::create_dir_all(&dir);
3991 for (t, row) in rows.iter().enumerate() {
3992 let path = std::path::Path::new(dir)
3993 .join(format!("pos{:05}_layer{f:02}.f32", pos0 + t));
3994 let bytes: Vec<u8> = row.iter().flat_map(|v| v.to_le_bytes()).collect();
3995 let _ = std::fs::write(path, bytes);
3996 }
3997 }
3998 let mut any_cold_in_frame = false;
3999 let frame_off = off;
4000 off += frame_bytes;
4001 let mut pre_slots: Vec<Option<Vec<Option<u32>>>> = vec![None; ntok];
4005 if cold_on_device && ntok > 1 && admit_batch_env() {
4006 let ta = std::time::Instant::now();
4007 let mut toks = Vec::new();
4008 let mut reqs = Vec::new();
4009 for t in 0..ntok {
4010 let w_off = frame_off + t * cs;
4011 let word = |j: usize| {
4012 let b = &bytes[w_off + 4 * j..w_off + 4 * j + 4];
4013 u32::from_le_bytes([b[0], b[1], b[2], b[3]])
4014 };
4015 let eids: Vec<usize> = (0..top_k)
4016 .map(|j| word(2 * j))
4017 .filter(|&e| e != u32::MAX && (e as usize) < layer.moe.experts.len())
4018 .map(|e| e as usize)
4019 .collect();
4020 if eids.is_empty() {
4021 continue;
4022 }
4023 let picks: Vec<usize> = (0..top_k)
4024 .map(|j| word(2 * top_k + 2 * j))
4025 .filter(|&e| e != u32::MAX && (e as usize) < g.n_experts)
4026 .map(|e| e as usize)
4027 .collect();
4028 toks.push(t);
4029 reqs.push((eids, picks));
4030 }
4031 if !reqs.is_empty() {
4032 let got = arena.admit_frame(&model, f, &reqs, &layer.expert_ids);
4033 for (t, sl) in toks.into_iter().zip(got) {
4034 pre_slots[t] = Some(sl);
4035 }
4036 }
4037 pf.admit += ta.elapsed();
4038 }
4039 for t in 0..ntok {
4040 let w_off = frame_off + t * cs;
4041 let words: Vec<u32> = bytes[w_off..w_off + 4 * top_k * 4]
4042 .chunks_exact(4)
4043 .map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
4044 .collect();
4045 let x_off = frame_off + cold_part + t * hidden_bytes;
4046 let picks: Vec<usize> = (0..top_k)
4047 .map(|j| words[2 * top_k + 2 * j])
4048 .filter(|&e| e != u32::MAX && (e as usize) < g.n_experts)
4049 .map(|e| e as usize)
4050 .collect();
4051 if let Some(pr) = state.profile.as_mut() {
4052 pr.note(f, &picks);
4053 }
4054 if check && ntok == 1 && tap_layer() == Some(f) && position < 3 && f + 1 == hi {
4055 let bufs = q4::tap_bufs(dev, &g);
4057 let h = cfg.hidden;
4058 let parts: Vec<(&wgpu::Buffer, u64)> = vec![
4059 (&bufs[0], (h * 4) as u64),
4060 (&bufs[1], (h * 4) as u64),
4061 (&bufs[2], (h * 4) as u64),
4062 (&bufs[3], (h * 4) as u64),
4063 (&bufs[4], (cfg.hc * h * 4) as u64),
4064 ];
4065 if let Some(enc) = q4::new_encoder("qwen4-tap")
4066 && let Some(tb) = q4::submit_readback(enc, &parts)
4067 {
4068 let fl = |o: usize, cnt: usize| -> Vec<f32> {
4069 tb[o..o + cnt * 4]
4070 .chunks_exact(4)
4071 .map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]]))
4072 .collect()
4073 };
4074 let al = |cnt: usize| ((cnt * 4) as u64).div_ceil(16) as usize * 16;
4075 let mut o = 0;
4076 let x = fl(o, h);
4077 o += al(h);
4078 let blk = fl(o, h);
4079 o += al(h);
4080 let x2v = fl(o, h);
4081 o += al(h);
4082 let mo = fl(o, h);
4083 o += al(h);
4084 let hyper = fl(o, cfg.hc * h);
4085 DEV_TAP
4086 .lock()
4087 .unwrap()
4088 .push((position, x, blk, x2v, mo, hyper));
4089 }
4090 }
4091 let any_cold = (0..top_k).any(|j| words[2 * j] != u32::MAX);
4092 if t + 1 == ntok {
4093 state.picks_prev[f] = picks.clone();
4094 }
4095 cold_slots[t].clear();
4096 cold_vec[t] = None;
4097 if !any_cold {
4098 continue;
4099 }
4100 any_cold_in_frame = true;
4101 let ta = std::time::Instant::now();
4105 let x2: Vec<f32> = bytes[x_off..x_off + cfg.hidden * 4]
4106 .chunks_exact(4)
4107 .map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]]))
4108 .collect();
4109 let mut jobs: Vec<(&crate::pipeline::DenseFfn, f32)> = Vec::new();
4110 let cold_list: Vec<(usize, f32)> = (0..top_k)
4111 .filter_map(|j| {
4112 let e = words[2 * j];
4113 (e != u32::MAX && (e as usize) < layer.moe.experts.len())
4114 .then(|| (e as usize, f32::from_bits(words[2 * j + 1])))
4115 })
4116 .collect();
4117 let eids: Vec<usize> = cold_list.iter().map(|&(e, _)| e).collect();
4118 let slots = if cold_on_device {
4121 let mut s = match pre_slots[t].take() {
4122 Some(s) if s.len() == eids.len() => s,
4123 _ => arena.admit_many(&model, f, &eids, &layer.expert_ids, &picks),
4124 };
4125 if s.iter().any(Option::is_none) {
4126 let rest: Vec<usize> = eids
4127 .iter()
4128 .zip(&s)
4129 .filter(|(_, x)| x.is_none())
4130 .map(|(&e, _)| e)
4131 .collect();
4132 let mut staged = arena
4133 .stage_cold(&model, f, t, &rest, &layer.expert_ids, top_k)
4134 .into_iter();
4135 for x in s.iter_mut().filter(|x| x.is_none()) {
4136 *x = staged.next().flatten();
4137 }
4138 }
4139 s
4140 } else {
4141 vec![None; eids.len()]
4142 };
4143 for (&(e, wgt), slot) in cold_list.iter().zip(slots) {
4144 match slot {
4145 Some(sl) => cold_slots[t].push((sl, wgt)),
4146 None => jobs.push((&layer.moe.experts[e], wgt)),
4147 }
4148 }
4149 pf.admit += ta.elapsed();
4150 pf.cold_experts += cold_slots[t].len() + jobs.len();
4151 pf.cold_dev += cold_slots[t].len();
4152 if !jobs.is_empty() {
4153 let tc = std::time::Instant::now();
4154 cold_vec[t] = Some(crate::pipeline::moe_cold_experts_cpu(&jobs, &x2, pool));
4155 pf.cold_cpu += tc.elapsed();
4156 }
4157 if check
4158 && ntok == 1
4159 && tap_layer() == Some(f)
4160 && position < 3
4161 && let Some(cv) = cold_vec[t].as_ref()
4162 && let Some(last) = DEV_TAP.lock().unwrap().last_mut()
4163 {
4164 for (m, c) in last.4.iter_mut().zip(cv) {
4165 *m += c;
4166 }
4167 }
4168 }
4169 if any_cold_in_frame {
4170 pf.layers_with_cold += 1;
4171 miss_at = Some(f);
4172 break 'frames;
4173 }
4174 }
4175 match miss_at {
4176 Some(j) => {
4177 dev.discard(j + 1, hi);
4179 pf.aborted += hi - (j + 1);
4180 fi = j + 1;
4181 }
4182 None => {
4183 if let Some(lo) = logits_off {
4184 let stride = lstride_out;
4185 let read = |o: usize| -> Vec<f32> {
4186 bytes[o..o + vocab_out * 4]
4187 .chunks_exact(4)
4188 .map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]]))
4189 .collect()
4190 };
4191 if snapshot {
4192 state.window_logits = (0..ntok).map(|t| read(lo + t * stride)).collect();
4193 *logits = state.window_logits.last().cloned().unwrap_or_default();
4194 } else {
4195 *logits = read(lo);
4196 }
4197 }
4198 fi = hi;
4199 }
4200 }
4201 }
4202 arena.flush_uploads();
4203 if let Some(why) = failed {
4204 return off(state, why);
4205 }
4206 dev.clear_pending();
4211 if !want_logits {
4212 logits.clear();
4213 }
4214 if !check {
4215 state.token_history.extend_from_slice(ids);
4216 state.pos = pos0 + ntok;
4217 }
4218 if let Some(d) = state.dev.as_mut() {
4219 d.pos = pos0 + ntok;
4220 }
4221 if let (Some(pr), Ok(path)) = (
4222 state.profile.as_mut(),
4223 std::env::var("CMF_QWEN_PROFILE_SAVE"),
4224 ) {
4225 pr.tokens += ntok as u64;
4226 if pr.tokens % 32 < ntok as u64 {
4227 pr.save(&path);
4228 }
4229 }
4230 if prof {
4231 let store_line = arena
4232 .store
4233 .as_ref()
4234 .map(|s| s.report())
4235 .unwrap_or_default();
4236 let fill_ns = FILL_NS.swap(0, std::sync::atomic::Ordering::Relaxed);
4237 let fill_n = FILL_N.swap(0, std::sync::atomic::Ordering::Relaxed);
4238 let fills_wall = FILLS_WALL_NS.swap(0, std::sync::atomic::Ordering::Relaxed) as f64 / 1e6;
4239 let store_line = match arena.tier.as_ref() {
4240 Some(t) => {
4241 use std::sync::atomic::Ordering::Relaxed;
4242 format!(
4243 "{store_line} htier hits={} fills={} fill_sum={:.1}ms nofit={} online={} bg_fills={}",
4244 t.hits.swap(0, Relaxed),
4245 t.fills.swap(0, Relaxed),
4246 t.fill_ns.swap(0, Relaxed) as f64 / 1e6,
4247 t.misses.swap(0, Relaxed),
4248 t.online(),
4249 t.bg_fills.load(Relaxed)
4250 )
4251 }
4252 None => store_line,
4253 };
4254 eprintln!(
4255 "qwen4-device pos={position} ntok={ntok} total={:.1}ms encode={:.1}ms finish={:.1}ms wait={:.1}ms spin={:.1}ms post={:.1}ms ple={:.1}ms cold_cpu={:.1}ms admit={:.1}ms chains={} aborted_frames={} cold={} experts ({} on device) in {} layers fills={fill_n} fill_sum={:.1}ms fill_wall={fills_wall:.1}ms {store_line}",
4256 t_token.elapsed().as_secs_f64() * 1e3,
4257 pf.encode.as_secs_f64() * 1e3,
4258 pf.finish.as_secs_f64() * 1e3,
4259 pf.wait.as_secs_f64() * 1e3,
4260 pf.spin.as_secs_f64() * 1e3,
4261 pf.post.as_secs_f64() * 1e3,
4262 pf.ple.as_secs_f64() * 1e3,
4263 pf.cold_cpu.as_secs_f64() * 1e3,
4264 pf.admit.as_secs_f64() * 1e3,
4265 pf.chains,
4266 pf.aborted,
4267 pf.cold_experts,
4268 pf.cold_dev,
4269 pf.layers_with_cold,
4270 fill_ns as f64 / 1e6,
4271 );
4272 }
4273 if prof && pf.gpu_last_end.is_some() {
4274 let mut line = format!("qwen4-ts pos={position} ntok={ntok}");
4275 for (name, v) in TS_NAMES.iter().zip(pf.gpu) {
4276 line.push_str(&format!(" {name}={:.2}", v / 1e6));
4277 }
4278 line.push_str(&format!(" busy={:.2} ms", pf.gpu[..16].iter().sum::<f64>() / 1e6));
4279 eprintln!("{line}");
4280 }
4281 if check && ntok == 1 {
4282 let dev_logits = logits.clone();
4286 let mut host = Vec::new();
4287 state.device_off = true;
4288 forward_token(
4289 globals,
4290 layers,
4291 cfg,
4292 state,
4293 token_id,
4294 position,
4295 inv_freq,
4296 pool,
4297 &mut host,
4298 want_logits,
4299 );
4300 state.device_off = false;
4301 if let Some(tl) = tap_layer() {
4302 let dev_taps = std::mem::take(&mut *DEV_TAP.lock().unwrap());
4303 let host_taps = std::mem::take(&mut *TAP.lock().unwrap());
4304 for (pos, x, blk, x2, mo, hyper) in &dev_taps {
4305 let find = |lab: &str| {
4306 host_taps
4307 .iter()
4308 .find(|(l, li, p, _)| l == lab && *li == tl && p == pos)
4309 .map(|(_, _, _, v)| v.as_slice())
4310 };
4311 eprintln!("qwen4-tap layer {tl} pos {pos}:");
4312 if let Some(h) = find("attn_in") {
4313 tap_compare("attn_in", h, x);
4314 }
4315 if let Some(h) = find("attn_out") {
4316 tap_compare("attn_out", h, blk);
4317 }
4318 if let Some(h) = find("post_attn") {
4319 tap_compare("post_attn", h, hyper);
4320 }
4321 if let Some(h) = find("moe_in") {
4322 tap_compare("moe_in", h, x2);
4323 }
4324 if let Some(h) = find("moe_out") {
4325 tap_compare("moe_res", h, mo);
4326 }
4327 }
4328 }
4329 if !want_logits {
4330 return true;
4331 }
4332 let n = host.len().min(dev_logits.len());
4333 let mut max_abs = 0.0f32;
4334 let (mut dot, mut na, mut nb) = (0.0f64, 0.0f64, 0.0f64);
4335 for i in 0..n {
4336 max_abs = max_abs.max((host[i] - dev_logits[i]).abs());
4337 dot += host[i] as f64 * dev_logits[i] as f64;
4338 na += host[i] as f64 * host[i] as f64;
4339 nb += dev_logits[i] as f64 * dev_logits[i] as f64;
4340 }
4341 let argmax = |v: &[f32]| {
4342 v.iter()
4343 .enumerate()
4344 .fold((0usize, f32::NEG_INFINITY), |a, (i, &x)| {
4345 if x > a.1 { (i, x) } else { a }
4346 })
4347 .0
4348 };
4349 eprintln!(
4350 "qwen4-check pos={position} cos={:.6} max_abs={max_abs:.4} argmax host={} dev={}",
4351 dot / (na.sqrt() * nb.sqrt()).max(1e-30),
4352 argmax(&host),
4353 argmax(&dev_logits)
4354 );
4355 *logits = dev_logits;
4356 }
4357 true
4358}
4359
4360#[cfg(feature = "gpu")]
4361fn device_hc_w(g: &GatedResidual) -> Option<crate::gpu_wgpu::qwen4::HcW<'_>> {
4362 Some(crate::gpu_wgpu::qwen4::HcW {
4363 norm: &g.norm,
4364 down: g.down_idx?,
4365 up: g.up_idx?,
4366 inject: g.inject_idx,
4367 })
4368}
4369
4370#[cfg(feature = "gpu")]
4373const MTP_MEASURED_WINDOW: usize = 4;
4374
4375#[cfg(feature = "gpu")]
4379fn mtp_k(raw: Option<&str>) -> (usize, bool) {
4380 let max = crate::gpu_wgpu::qwen4::TMAX - 1;
4381 match raw.map(|v| v.trim().parse::<usize>()) {
4382 None => (3, false),
4383 Some(Ok(v)) => (v.clamp(1, max), !(1..=max).contains(&v)),
4384 Some(Err(_)) => (3, true),
4385 }
4386}
4387
4388#[cfg(feature = "gpu")]
4392pub struct SpecRound {
4393 pub accepted: Vec<u32>,
4394 pub logits: Vec<f32>,
4395 pub drafted: usize,
4396}
4397
4398#[cfg(feature = "gpu")]
4412pub struct MtpHead {
4413 side: Arc<CmfModel>,
4414 layer: Layer,
4415 enorm: Vec<f32>,
4416 hnorm: Vec<f32>,
4417 fc_e: usize,
4418 fc_h: usize,
4419 mixer: GatedResidual,
4420 skeleton: Vec<usize>,
4421 dev: Option<crate::gpu_wgpu::qwen4::Dev>,
4422 arena: Option<QwenGpuPool>,
4423 pub k: usize,
4424 pub drafted: u64,
4425 pub accepted: u64,
4426 pub rounds: u64,
4427}
4428
4429#[cfg(feature = "gpu")]
4430impl MtpHead {
4431 pub fn load(main: &Arc<CmfModel>, arch: &ModelArch) -> Option<Self> {
4433 if std::env::var("CMF_QWEN_MTP").as_deref() == Ok("0") {
4434 return None;
4435 }
4436 let path = cortiq_core::mtp_sidecar_path(&main.path);
4437 if path == main.path || !path.exists() {
4438 return None;
4439 }
4440 let side = match CmfModel::open(&path) {
4441 Ok(m) => Arc::new(m),
4442 Err(e) => {
4443 tracing::warn!("qwen4 MTP sidecar {}: {e}", path.display());
4444 return None;
4445 }
4446 };
4447 let sa = side.arch();
4448 if sa.arch_name != arch.arch_name
4449 || sa.hidden_size != arch.hidden_size
4450 || sa.vocab_size != arch.vocab_size
4451 {
4452 tracing::warn!(
4453 "qwen4 MTP sidecar {}: geometry differs from the backbone",
4454 path.display()
4455 );
4456 return None;
4457 }
4458 let p = "model.mtp.layers.0.";
4459 let build =
4460 || -> Result<(Layer, GatedResidual, Vec<f32>, Vec<f32>, usize, usize), CmfError> {
4461 let mixer = Mixer::Qsa(load_qsa(&side, &format!("{p}self_attn."))?);
4462 let moe = match build_ffn_at(&side, sa, p, false, &Overlay::None)? {
4463 FfnKind::Moe(m) => m,
4464 _ => return Err(err("MTP layer is not MoE")),
4465 };
4466 let expert_ids: Vec<_> = moe
4467 .experts
4468 .iter()
4469 .map(|e| {
4470 Some((
4471 e.gate_proj.model_idx()?,
4472 e.up_proj.model_idx()?,
4473 e.down_proj.model_idx()?,
4474 ))
4475 })
4476 .collect::<Option<_>>()
4477 .ok_or_else(|| err("MTP experts are not mmap-backed"))?;
4478 let shared_ids = moe
4479 .shared
4480 .as_ref()
4481 .and_then(|(e, _)| {
4482 Some((
4483 e.gate_proj.model_idx()?,
4484 e.up_proj.model_idx()?,
4485 e.down_proj.model_idx()?,
4486 ))
4487 })
4488 .ok_or_else(|| err("MTP shared expert is not mmap-backed"))?;
4489 let attn_hc = load_hc(&side, &format!("{p}attn_hyper_connection."), true)?;
4490 let mlp_hc = load_hc(&side, &format!("{p}mlp_hyper_connection."), true)?;
4491 let idx = layer_idx(&side, p, false, false);
4492 let layer = Layer {
4493 attn_hc,
4494 mlp_hc,
4495 mixer,
4496 moe,
4497 ple: None,
4498 expert_ids,
4499 shared_ids: Some(shared_ids),
4500 idx,
4501 };
4502 let mixer_hc = load_hc(&side, "model.mtp.hyper_connection_mixer.", false)?;
4503 let enorm = f(&side, "model.mtp.enorm.weight")?;
4504 let hnorm = f(&side, "model.mtp.hnorm.weight")?;
4505 let fc_e = side
4506 .tensor_index("model.mtp.fc_embedding.weight")
4507 .ok_or_else(|| err("fc_embedding"))?;
4508 let fc_h = side
4509 .tensor_index("model.mtp.fc_hidden.weight")
4510 .ok_or_else(|| err("fc_hidden"))?;
4511 Ok((layer, mixer_hc, enorm, hnorm, fc_e, fc_h))
4512 };
4513 let (layer, mixer, enorm, hnorm, fc_e, fc_h) = match build() {
4514 Ok(v) => v,
4515 Err(e) => {
4516 tracing::warn!("qwen4 MTP sidecar {}: {e}", path.display());
4517 return None;
4518 }
4519 };
4520 let ix = layer.idx.as_ref()?;
4521 let mut skeleton: Vec<usize> = Vec::new();
4522 skeleton.extend(layer.attn_hc.idxs());
4523 skeleton.extend(layer.mlp_hc.idxs());
4524 skeleton.extend(mixer.idxs());
4525 skeleton.extend(ix.qsa.iter().flatten().copied());
4526 skeleton.push(ix.router);
4527 skeleton.extend(ix.shared_gate);
4528 skeleton.extend([fc_e, fc_h]);
4529 let raw = std::env::var("CMF_QWEN_MTP_K").ok();
4530 let (k, clamped) = mtp_k(raw.as_deref());
4531 if clamped {
4532 tracing::warn!(
4533 "CMF_QWEN_MTP_K={} is outside 1..={}: the verify window (k + 1 positions) is one device frame of at most {} tokens; using k = {k}",
4534 raw.as_deref().unwrap_or(""),
4535 crate::gpu_wgpu::qwen4::TMAX - 1,
4536 crate::gpu_wgpu::qwen4::TMAX
4537 );
4538 }
4539 tracing::info!("qwen4 MTP: draft head from {} (k = {k})", path.display());
4540 Some(Self {
4541 side,
4542 layer,
4543 enorm,
4544 hnorm,
4545 fc_e,
4546 fc_h,
4547 mixer,
4548 skeleton,
4549 dev: None,
4550 arena: None,
4551 k,
4552 drafted: 0,
4553 accepted: 0,
4554 rounds: 0,
4555 })
4556 }
4557
4558 fn setup(&mut self, g: &crate::gpu_wgpu::qwen4::Geom, gu_q2: bool) -> Result<(), &'static str> {
4560 use crate::gpu_wgpu::qwen4 as q4;
4561 if self.dev.is_some() && self.arena.is_some() {
4562 return Ok(());
4563 }
4564 let dev = q4::Dev::new(self.side.uid(), g, &[(false, false)]).ok_or("MTP device state")?;
4565 q4::prewarm(&self.side, &self.skeleton).ok_or("MTP skeleton does not fit")?;
4566 let n_exp = self.layer.moe.experts.len();
4567 let mut arena =
4569 QwenGpuPool::create_exact(&self.side, g.inter, g.hidden, n_exp, gu_q2, n_exp + 8)
4570 .ok_or("MTP expert bank")?;
4571 arena
4572 .ensure(
4573 &self.side,
4574 0,
4575 &[],
4576 &self.layer.expert_ids,
4577 self.layer.shared_ids,
4578 )
4579 .filter(|(_, s)| *s != u32::MAX)
4580 .ok_or("MTP shared expert")?;
4581 let all: Vec<(usize, usize)> = (0..n_exp).map(|e| (0, e)).collect();
4582 let got = arena.prefill(
4583 &self.side,
4584 &all,
4585 std::slice::from_ref(&self.layer.expert_ids),
4586 );
4587 if got < n_exp {
4588 return Err("MTP experts do not all fit their bank");
4589 }
4590 self.dev = Some(dev);
4591 self.arena = Some(arena);
4592 Ok(())
4593 }
4594}
4595
4596#[cfg(feature = "gpu")]
4601#[allow(clippy::too_many_arguments)]
4602pub fn spec_round(
4603 globals: &Globals,
4604 layers: &[Layer],
4605 cfg: &Cfg,
4606 state: &mut State,
4607 next_pos: usize,
4608 all_ids: &[u32],
4609 inv_freq: &[f32],
4610 pool: Option<&Pool>,
4611) -> Option<SpecRound> {
4612 use crate::gpu_wgpu::qwen4 as q4;
4613 if state.device_off || state.dev.is_none() || next_pos == 0 || all_ids.len() != next_pos + 1 {
4614 return None;
4615 }
4616 if state.mtp.is_none() && !state.mtp_tried {
4617 state.mtp_tried = true;
4618 let model = layers.first()?.moe.experts.first()?.gate_proj.model_arc()?;
4619 state.mtp = MtpHead::load(&model, model.arch());
4620 }
4621 let first = layers.first()?;
4622 let e0 = first.moe.experts.first()?;
4623 let gu_q2 = e0.gate_proj.model_dtype() == Some(TensorDtype::Q2TiledP);
4624 let g = device_geom(cfg, &first.moe, gu_q2);
4625 let prof = std::env::var_os("CMF_QWEN_PROF").is_some();
4626 let t_round = std::time::Instant::now();
4627 let (k, drafts) = {
4629 let head = state.mtp.as_mut()?;
4630 if let Err(why) = head.setup(&g, gu_q2) {
4631 tracing::warn!("qwen4 MTP off: {why}");
4632 state.mtp = None;
4633 return None;
4634 }
4635 let k = head.k;
4636 let mdev = head.dev.as_mut()?;
4637 let arena = head.arena.as_mut()?;
4638 let (remap, shared_slot) = arena.remap_snapshot(0);
4639 let ix = head.layer.idx.as_ref()?;
4640 let w = device_layer_w(&head.layer, ix)?;
4641 let mixer_w = device_hc_w(&head.mixer)?;
4642 let main_dev = state.dev.as_ref()?;
4643 let main_model = e0.gate_proj.model_arc()?;
4644 let lm_head = globals.lm_head_idx?;
4645 if !mdev.ensure_stage(q4::chain_stage_bytes(
4647 0,
4648 0,
4649 1,
4650 q4::head_stride(&main_model, lm_head)?,
4651 )) {
4652 return None;
4653 }
4654 let mut drafts: Vec<u32> = Vec::with_capacity(k);
4655 let tok_in = all_ids[next_pos];
4656 let device_chain = std::env::var("CMF_QWEN_MTP_DEVICE_DRAFT").as_deref() == Ok("1");
4661 let mut chained = false;
4662 if device_chain
4663 && let Some(embed_idx) = globals.embed_idx
4664 && let Some(ids_buf) = q4::draft_ids(tok_in)
4665 {
4666 let mut run = || -> Option<Vec<u32>> {
4667 let mut enc = q4::new_encoder("qwen4-mtp-draft")?;
4668 let merge = q4::merge_guard(&enc);
4669 for j in 0..k {
4670 let cell = next_pos - 1 + j;
4671 let _salt = q4::frame_salt(j);
4674 let r = if j == 0 {
4675 q4::whole(&main_dev.r_last)
4676 } else {
4677 mdev.hyper(0)
4678 };
4679 q4::encode_draft_gather(
4680 &mut enc,
4681 mdev,
4682 &main_model,
4683 &g,
4684 embed_idx,
4685 &ids_buf,
4686 j,
4687 )?;
4688 if !q4::encode_mtp_input(
4689 &mut enc,
4690 mdev,
4691 &head.side,
4692 &g,
4693 None,
4694 &head.enorm,
4695 &head.hnorm,
4696 head.fc_e,
4697 head.fc_h,
4698 r,
4699 ) {
4700 return None;
4701 }
4702 q4::encode_layer(
4703 &mut enc,
4704 mdev,
4705 &head.side,
4706 &g,
4707 &w,
4708 0,
4709 cell,
4710 1,
4711 inv_freq,
4712 &remap,
4713 shared_slot,
4714 &[],
4715 false,
4716 false,
4717 )?;
4718 if !q4::encode_pending(&mut enc, mdev, &g, 1)
4719 || !q4::finalize_pending(mdev, &g, 1, &[None], &[Vec::new()])
4720 {
4721 return None;
4722 }
4723 q4::pending_done(mdev);
4724 let (lb, vocab, _) = q4::encode_head_with(
4725 &mut enc,
4726 mdev,
4727 &head.side,
4728 &main_model,
4729 &g,
4730 &mixer_w,
4731 lm_head,
4732 false,
4733 1,
4734 )?;
4735 q4::encode_argmax(&mut enc, mdev, &lb, vocab, &ids_buf, j + 1)?;
4736 }
4737 if !q4::copy_to_stage(&mut enc, mdev, &ids_buf, 4, 0, (k * 4) as u64) {
4738 return None;
4739 }
4740 drop(merge);
4741 let bytes = q4::submit_chain(mdev, enc, (k * 4) as u64)?.wait()?;
4742 Some(
4743 bytes[..k * 4]
4744 .chunks_exact(4)
4745 .map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
4746 .collect(),
4747 )
4748 };
4749 if let Some(d) = run() {
4750 drafts = d;
4751 chained = true;
4752 }
4753 }
4754 if !chained {
4755 let mut tok_in = tok_in;
4757 for j in 0..k {
4758 let cell = next_pos - 1 + j;
4759 let mut emb = vec![0.0f32; cfg.hidden];
4760 if (tok_in as usize) < globals.embed.rows() {
4761 globals.embed.row_f32(tok_in as usize, &mut emb);
4762 }
4763 let r = if j == 0 {
4764 q4::whole(&main_dev.r_last)
4765 } else {
4766 mdev.hyper(0)
4767 };
4768 let mut enc = q4::new_encoder("qwen4-mtp-draft")?;
4769 let merge = q4::merge_guard(&enc);
4770 if !q4::encode_mtp_input(
4771 &mut enc,
4772 mdev,
4773 &head.side,
4774 &g,
4775 Some(&emb),
4776 &head.enorm,
4777 &head.hnorm,
4778 head.fc_e,
4779 head.fc_h,
4780 r,
4781 ) {
4782 return None;
4783 }
4784 q4::encode_layer(
4785 &mut enc,
4786 mdev,
4787 &head.side,
4788 &g,
4789 &w,
4790 0,
4791 cell,
4792 1,
4793 inv_freq,
4794 &remap,
4795 shared_slot,
4796 &[],
4797 false,
4798 false,
4799 )?;
4800 if !q4::encode_pending(&mut enc, mdev, &g, 1)
4801 || !q4::finalize_pending(mdev, &g, 1, &[None], &[Vec::new()])
4802 {
4803 return None;
4804 }
4805 q4::pending_done(mdev);
4806 let (lb, vocab, _) = q4::encode_head_with(
4807 &mut enc,
4808 mdev,
4809 &head.side,
4810 &main_model,
4811 &g,
4812 &mixer_w,
4813 lm_head,
4814 false,
4815 1,
4816 )?;
4817 if !q4::copy_to_stage(&mut enc, mdev, &lb, 0, 0, (vocab * 4) as u64) {
4818 return None;
4819 }
4820 drop(merge);
4821 let bytes = q4::submit_chain(mdev, enc, (vocab * 4) as u64)?.wait()?;
4822 let mut best = (0usize, f32::NEG_INFINITY);
4823 for (i, ch) in bytes[..vocab * 4].chunks_exact(4).enumerate() {
4824 let v = f32::from_le_bytes([ch[0], ch[1], ch[2], ch[3]]);
4825 if v > best.1 {
4826 best = (i, v);
4827 }
4828 }
4829 drafts.push(best.0 as u32);
4830 tok_in = best.0 as u32;
4831 }
4832 }
4833 (k, drafts)
4834 };
4835 let t_draft = t_round.elapsed();
4836 let mut ids = Vec::with_capacity(k + 1);
4838 ids.push(all_ids[next_pos]);
4839 ids.extend_from_slice(&drafts);
4840 state.verify_window = true;
4841 let mut last_logits = Vec::new();
4842 let ok = forward_tokens_device(
4843 globals,
4844 layers,
4845 cfg,
4846 state,
4847 &ids,
4848 next_pos,
4849 inv_freq,
4850 pool,
4851 &mut last_logits,
4852 true,
4853 );
4854 state.verify_window = false;
4855 if !ok || state.window_logits.len() != k + 1 {
4856 return None;
4857 }
4858 let argmax = |v: &[f32]| {
4859 v.iter()
4860 .enumerate()
4861 .fold((0usize, f32::NEG_INFINITY), |a, (i, &x)| {
4862 if x > a.1 { (i, x) } else { a }
4863 })
4864 .0
4865 };
4866 let mut a = 0usize;
4867 while a < k && argmax(&state.window_logits[a]) == drafts[a] as usize {
4868 a += 1;
4869 }
4870 let logits = std::mem::take(&mut state.window_logits).swap_remove(a);
4871 if a < k {
4873 let dev = state.dev.as_mut()?;
4874 if !dev.restore(&g, a + 1) {
4875 return None;
4876 }
4877 let keep = state.token_history.len() - (k - a);
4878 state.token_history.truncate(keep);
4879 state.pos = next_pos + a + 1;
4880 dev.pos = state.pos;
4881 if let Some(mut enc) = q4::new_encoder("qwen4-keep-r") {
4883 dev.keep_r(&mut enc, a);
4884 q4::submit_only(enc);
4885 }
4886 }
4887 if let Some(head) = state.mtp.as_mut() {
4888 head.rounds += 1;
4889 head.drafted += k as u64;
4890 head.accepted += a as u64;
4891 if prof {
4892 eprintln!(
4893 "qwen4-mtp round={} drafted={k} accepted={a} draft={:.1}ms total={:.1}ms (acceptance {:.1}%) ids={:?}",
4894 head.rounds,
4895 t_draft.as_secs_f64() * 1e3,
4896 t_round.elapsed().as_secs_f64() * 1e3,
4897 head.accepted as f64 * 100.0 / head.drafted.max(1) as f64,
4898 drafts
4899 );
4900 }
4901 }
4902 Some(SpecRound {
4903 accepted: drafts[..a].to_vec(),
4904 logits,
4905 drafted: k,
4906 })
4907}
4908
4909#[cfg(test)]
4910mod tests {
4911 use super::*;
4912
4913 #[cfg(feature = "gpu")]
4914 #[test]
4915 fn pending_admission_cold_picks_keep_route_order() {
4916 let slots = [3, u32::MAX, 7, u32::MAX, 11, u32::MAX, 13, u32::MAX];
4917 assert_eq!(
4918 cold_picks_from_slots(&slots, 4, 4, &[3, 1, 0, 1]),
4919 Some(vec![3, 1, 1])
4920 );
4921 assert_eq!(cold_picks_from_slots(&slots, 4, 4, &[4]), None);
4922 assert_eq!(cold_picks_from_slots(&slots, 5, 4, &[0]), None);
4923 }
4924
4925 #[cfg(feature = "gpu")]
4926 #[test]
4927 fn mtp_k_parses_and_clamps_to_the_frame() {
4928 assert_eq!(mtp_k(None), (3, false));
4929 for k in 1..=7 {
4930 assert_eq!(mtp_k(Some(&k.to_string())), (k, false));
4931 }
4932 assert_eq!(mtp_k(Some("0")), (1, true));
4933 assert_eq!(mtp_k(Some("8")), (7, true));
4934 assert_eq!(mtp_k(Some("four")), (3, true));
4935 assert!(mtp_k(Some("99")).0 < crate::gpu_wgpu::qwen4::TMAX);
4937 }
4938
4939 #[test]
4940 fn dynamic_pool_percent_is_bounded_per_policy() {
4941 assert_eq!(bounded_pool_pct(None, 40, 20, 60), 40);
4945 assert_eq!(bounded_pool_pct(Some("55"), 40, 20, 40), 40);
4946 assert_eq!(bounded_pool_pct(Some("999"), 40, 20, 40), 40);
4947 assert_eq!(bounded_pool_pct(Some("0"), 40, 20, 40), 20);
4948 assert_eq!(bounded_pool_pct(Some("bad"), 40, 20, 40), 40);
4949 assert_eq!(bounded_pool_pct(Some("90"), 75, 25, 85), 85);
4951 assert_eq!(bounded_pool_slots(Some("9999"), 752, true), 752);
4952 assert_eq!(bounded_pool_slots(Some("9999"), 752, false), 9999);
4953 }
4954
4955 #[test]
4956 fn active_tail_floor_uses_only_reachable_layers() {
4957 assert_eq!(active_layer_floor(512, 40), 12);
4961 assert_eq!(active_layer_floor(512, 13), 39);
4962 assert!(active_layer_floor(512, 13) * 13 <= 512);
4963 assert_eq!(active_layer_floor(16, 13), 1);
4966 assert_eq!(active_layer_floor(12, 13), 0);
4967 }
4968
4969 #[test]
4970 fn deterministic_hash_tables_match_contract() {
4971 let q = Qwen4ExpConfig {
4972 hc_count: 4,
4973 hc_lowrank: 320,
4974 indexer_n_heads: 4,
4975 indexer_kv_heads: 1,
4976 indexer_head_dim: 128,
4977 indexer_budget: 2048,
4978 indexer_compress_ratio: 4,
4979 ple_layer_ids: vec![2],
4980 ple_embed_dim: 2560,
4981 ple_conv_kernel_size: 4,
4982 ngram_size: 3,
4983 heads_per_ngram: 8,
4984 ngram_vocab_size_base: 20_000_000,
4985 make_ngram_vocab_size_divisible_by: 128,
4986 split_ngram_parts: 128,
4987 seed: 1234,
4988 };
4989 let (m, sizes, offsets) = ple_tables(&q, 248_320, 0);
4990 assert_eq!(
4991 m,
4992 [23_703_573_157_769, 20_109_073_645_365, 8_052_911_324_071]
4993 );
4994 assert!(m.iter().all(|x| x & 1 == 1));
4995 assert_eq!(sizes.len(), 16);
4996 assert!(sizes.iter().all(|&x| is_prime(x as usize)));
4997 assert_eq!(offsets[0], 0);
4998 assert_eq!(offsets[1], sizes[0]);
4999 }
5000
5001 #[test]
5002 fn eos_breaks_ngram_context_only_after_it() {
5003 let eos = 99;
5004 assert_eq!(shifted_token(&[1, 2], 3, 1, eos), 2);
5005 assert_eq!(shifted_token(&[1, eos], 3, 1, eos), eos);
5006 assert_eq!(shifted_token(&[1, 2], eos, 1, eos), 2);
5007 assert_eq!(shifted_token(&[], 3, 2, eos), eos);
5008 }
5009}