1use crate::pipeline::{MoeFfn, MoeRoute};
41use crate::pool::Pool;
42#[cfg(feature = "gpu")]
43use cortiq_core::{CmfModel, TensorDtype};
44#[cfg(feature = "gpu")]
45use std::sync::Arc;
46
47#[derive(Clone, Copy, Debug, PartialEq, Eq)]
49pub enum MoeMode {
50 Prefix,
51 Dynamic,
52 Hybrid,
53}
54
55impl MoeMode {
56 pub fn name(self) -> &'static str {
57 match self {
58 Self::Prefix => "prefix",
59 Self::Dynamic => "dynamic",
60 Self::Hybrid => "hybrid",
61 }
62 }
63
64 #[cfg_attr(not(feature = "gpu"), allow(dead_code))]
65 fn parse(s: &str) -> Option<Option<Self>> {
66 match s.trim().to_ascii_lowercase().as_str() {
67 "prefix" => Some(Some(Self::Prefix)),
68 "dynamic" => Some(Some(Self::Dynamic)),
69 "hybrid" => Some(Some(Self::Hybrid)),
70 "auto" | "" => Some(None),
71 _ => None,
72 }
73 }
74}
75
76#[derive(Clone, Debug)]
79pub struct PlacementInputs {
80 pub budget: u64,
82 pub non_expert: u64,
85 pub per_expert: u64,
87 pub moe_layers: usize,
88 pub n_experts: usize,
89 pub top_k: usize,
90 pub attn_per_layer: u64,
92 pub graph_prefix: bool,
96}
97
98#[derive(Clone, Debug, PartialEq)]
101pub struct Placement {
102 pub mode: MoeMode,
103 pub prefix_layers: usize,
104 pub bank_slots: usize,
105 pub predicted_s: f64,
107 pub reason: String,
108}
109
110#[derive(Clone, Debug)]
112pub struct Costs {
113 pub dev_bytes_per_s: f64,
115 pub cpu_bytes_per_s: f64,
117 pub dyn_layer_s: f64,
121 pub graph_submit_s: f64,
123 pub cpu_layer_s: f64,
125 pub fill_bytes_per_s: f64,
127 pub hit_curve: Vec<(f64, f64)>,
130}
131
132impl Costs {
133 pub fn measured() -> Self {
146 Self {
147 dev_bytes_per_s: 1.2e12,
148 cpu_bytes_per_s: 60e9,
149 dyn_layer_s: 0.6e-3,
150 graph_submit_s: 0.3e-3,
151 cpu_layer_s: 0.1e-3,
152 fill_bytes_per_s: 20e9,
153 hit_curve: vec![
154 (8.0, 0.341),
155 (16.0, 0.460),
156 (32.0, 0.603),
157 (64.0, 0.777),
158 (96.0, 0.875),
159 (128.0, 0.930),
160 (160.0, 0.959),
161 (192.0, 0.972),
162 (224.0, 0.975),
163 (256.0, 1.0),
164 ],
165 }
166 }
167
168 pub fn hit_rate(&self, slots: f64, n_experts: usize) -> f64 {
170 if slots >= n_experts as f64 {
171 return 1.0;
172 }
173 if slots <= 0.0 {
174 return 0.0;
175 }
176 let c = &self.hit_curve;
177 if c.is_empty() {
178 return 0.0;
179 }
180 if slots <= c[0].0 {
181 return c[0].1 * slots / c[0].0;
182 }
183 for w in c.windows(2) {
184 let ((s0, h0), (s1, h1)) = (w[0], w[1]);
185 if slots <= s1 {
186 let t = (slots.ln() - s0.ln()) / (s1.ln() - s0.ln());
187 return h0 + t * (h1 - h0);
188 }
189 }
190 c[c.len() - 1].1
191 }
192}
193
194pub fn device_reserve(budget: u64) -> u64 {
198 let gib = 1u64 << 30;
199 (budget / 10).clamp(2 * gib, 4 * gib) + gib
200}
201
202pub fn place(inp: &PlacementInputs, costs: &Costs, forced: Option<MoeMode>) -> Placement {
204 let gb = |b: u64| b as f64 / 1e9;
205 let l = inp.moe_layers.max(1);
206 let ne = inp.n_experts.max(1);
207 let k = inp.top_k as f64;
208 let e = inp.per_expert as f64;
209 let a = inp.attn_per_layer as f64;
210 let total_experts = l * ne;
211 let room = inp
212 .budget
213 .saturating_sub(inp.non_expert)
214 .saturating_sub(device_reserve(inp.budget));
215 let slots = (room / inp.per_expert.max(1)) as usize;
216 let whole = (slots / ne).min(l);
217 let dev_layer = (a + k * e) / costs.dev_bytes_per_s;
220 let prefix_cost = |p: usize| -> f64 {
221 if costs.dev_bytes_per_s <= 0.0 {
222 return f64::INFINITY;
223 }
224 let dev = if inp.graph_prefix {
225 costs.graph_submit_s + p as f64 * dev_layer
226 } else {
227 p as f64 * (costs.dyn_layer_s + dev_layer)
228 };
229 dev + (l - p) as f64 * ((a + k * e) / costs.cpu_bytes_per_s + costs.cpu_layer_s)
230 };
231 let dyn_cost = |p: usize| -> (f64, f64) {
232 let rest = l - p;
234 if rest == 0 {
235 return (prefix_cost(p), 1.0);
236 }
237 let per = slots.saturating_sub(p * ne) as f64 / rest as f64;
238 let h = costs.hit_rate(per, ne);
239 let hot = k * h * e / costs.dev_bytes_per_s;
240 let cold = k * (1.0 - h) * e / costs.cpu_bytes_per_s;
241 let layer = costs.dyn_layer_s + a / costs.dev_bytes_per_s + hot.max(cold);
242 let head = if p == 0 {
243 0.0
244 } else if inp.graph_prefix {
245 costs.graph_submit_s + p as f64 * dev_layer
246 } else {
247 p as f64 * (costs.dyn_layer_s + dev_layer)
248 };
249 (head + rest as f64 * layer, h)
250 };
251 let describe = |mode: MoeMode, p: usize, t: f64, h: f64, why: &str| -> String {
252 format!(
253 "{why}; budget {:.1} GB, non-expert {:.1} GB, experts {:.1} GB ({} × {:.1} MB), \
254 room {} slots = {:.0}/layer, whole layers {whole}/{l}{}; predicted {:.1} ms/token \
255 ({}{})",
256 gb(inp.budget),
257 gb(inp.non_expert),
258 gb(inp.per_expert * total_experts as u64),
259 total_experts,
260 e / 1e6,
261 slots,
262 slots as f64 / l as f64,
263 if inp.graph_prefix {
264 ", graph prefix"
265 } else {
266 ", no graph prefix"
267 },
268 t * 1e3,
269 mode.name(),
270 match mode {
271 MoeMode::Prefix => format!(" P={p}"),
272 MoeMode::Dynamic => format!(" hit≈{:.0}%", h * 100.0),
273 MoeMode::Hybrid => format!(" P={p} hit≈{:.0}%", h * 100.0),
274 },
275 )
276 };
277 let mk = |mode: MoeMode, p: usize, bank: usize, t: f64, why: String| Placement {
278 mode,
279 prefix_layers: p,
280 bank_slots: bank,
281 predicted_s: t,
282 reason: why,
283 };
284 if slots >= total_experts && forced.is_none() {
285 let t = prefix_cost(l);
286 let why = describe(MoeMode::Prefix, l, t, 1.0, "every expert fits");
287 return mk(MoeMode::Prefix, l, 0, t, why);
288 }
289 let (t_prefix, t_dyn) = (prefix_cost(whole), dyn_cost(0));
290 let hybrid = (1..whole.min(l.saturating_sub(1)) + 1)
292 .map(|p| (p, dyn_cost(p)))
293 .min_by(|x, y| {
294 x.1.0
295 .partial_cmp(&y.1.0)
296 .unwrap_or(std::cmp::Ordering::Equal)
297 });
298 let bank_for = |p: usize| slots.saturating_sub(p * ne);
299 match forced {
300 Some(MoeMode::Prefix) => {
301 let why = describe(MoeMode::Prefix, whole, t_prefix, 0.0, "CMF_MIMO_MOE=prefix");
302 mk(MoeMode::Prefix, whole, 0, t_prefix, why)
303 }
304 Some(MoeMode::Dynamic) => {
305 let why = describe(
306 MoeMode::Dynamic,
307 0,
308 t_dyn.0,
309 t_dyn.1,
310 "CMF_MIMO_MOE=dynamic",
311 );
312 mk(MoeMode::Dynamic, 0, bank_for(0), t_dyn.0, why)
313 }
314 Some(MoeMode::Hybrid) => {
315 let (p, (t, h)) = hybrid.unwrap_or((0, t_dyn));
316 let why = describe(MoeMode::Hybrid, p, t, h, "CMF_MIMO_MOE=hybrid");
317 mk(MoeMode::Hybrid, p, bank_for(p), t, why)
318 }
319 None => {
320 let mut best = (MoeMode::Prefix, whole, t_prefix, 0.0);
321 if t_dyn.0 < best.2 {
322 best = (MoeMode::Dynamic, 0, t_dyn.0, t_dyn.1);
323 }
324 if let Some((p, (t, h))) = hybrid
325 && t < best.2
326 {
327 best = (MoeMode::Hybrid, p, t, h);
328 }
329 let (mode, p, t, h) = best;
330 let alt = format!(
331 "auto: prefix {:.1} ms, dynamic {:.1} ms{}",
332 t_prefix * 1e3,
333 t_dyn.0 * 1e3,
334 hybrid.map_or(String::new(), |(p, (t, _))| format!(
335 ", best hybrid P={p} {:.1} ms",
336 t * 1e3
337 )),
338 );
339 let bank = if mode == MoeMode::Prefix {
340 0
341 } else {
342 bank_for(p)
343 };
344 let why = describe(mode, p, t, h, &alt);
345 mk(mode, p, bank, t, why)
346 }
347 }
348}
349
350#[derive(Clone, Copy, Debug, Default)]
352pub struct Stats {
353 pub attn_graph_calls: u64,
355 pub attn_graph_rows: u64,
356 pub attn_graph_ns: u64,
357 pub calls: u64,
359 pub picks: u64,
361 pub hits: u64,
363 pub fills: u64,
365 pub cold: u64,
367 pub frame_ns: u64,
369 pub call_ns: u64,
371 pub fallbacks: u64,
373 pub route_ns: u64,
375}
376
377static STATS: std::sync::Mutex<Stats> = std::sync::Mutex::new(Stats {
378 attn_graph_calls: 0,
379 attn_graph_rows: 0,
380 attn_graph_ns: 0,
381 calls: 0,
382 picks: 0,
383 hits: 0,
384 fills: 0,
385 cold: 0,
386 frame_ns: 0,
387 call_ns: 0,
388 fallbacks: 0,
389 route_ns: 0,
390});
391
392pub(crate) fn note_attention_graph(rows: usize, ns: u64) {
393 let mut s = STATS.lock().unwrap();
394 s.attn_graph_calls += 1;
395 s.attn_graph_rows += rows as u64;
396 s.attn_graph_ns += ns;
397}
398
399pub fn stats() -> Stats {
401 *STATS.lock().unwrap()
402}
403
404static LAST_DECISION: std::sync::Mutex<String> = std::sync::Mutex::new(String::new());
405
406pub fn last_decision() -> String {
408 LAST_DECISION.lock().unwrap().clone()
409}
410
411#[cfg(feature = "gpu")]
431pub(crate) struct Bank {
432 pub(crate) segment_slots: usize,
433 n_experts: usize,
434 slot_for: Vec<u32>,
437 owner: Vec<u32>,
439 last: Vec<u64>,
441 occupancy: Vec<u32>,
442 free: Vec<u32>,
443 floor: u32,
444 seen: Vec<u16>,
446 seen_tok: Vec<u32>,
447 tok: u64,
448 pending: usize,
449 admitted: u64,
451 max_pending: usize,
452 prime_queue: usize,
455 min_seen: u16,
456 decay_tokens: u64,
457 tx: Option<std::sync::mpsc::Sender<(u32, (usize, usize, usize))>>,
458 done: Arc<std::sync::Mutex<Vec<(u32, bool)>>>,
459 filler: Option<std::thread::JoinHandle<()>>,
460}
461
462pub fn default_min_seen(slots_per_layer: usize) -> u64 {
469 if slots_per_layer >= 48 { 2 } else { 3 }
470}
471
472#[cfg(feature = "gpu")]
473const NONE: u32 = u32::MAX;
474#[cfg(feature = "gpu")]
475const PENDING: u32 = u32::MAX - 1;
476
477#[cfg(feature = "gpu")]
478impl Bank {
479 fn create(
482 model: &Arc<CmfModel>,
483 inter: usize,
484 hidden: usize,
485 n_layers: usize,
486 moe_layers: usize,
487 n_experts: usize,
488 slots: usize,
489 ) -> Option<Self> {
490 let keys = n_layers.checked_mul(n_experts)?;
491 if keys >= PENDING as usize {
492 return None;
493 }
494 let (capacity, segment_slots) =
495 crate::gpu_wgpu::dsv4_global_moe_create_slots(model, slots, inter, hidden, false)?;
496 let env = |k: &str| std::env::var(k).ok().and_then(|v| v.parse::<u64>().ok());
497 let done = Arc::new(std::sync::Mutex::new(Vec::new()));
498 let (tx, rx) = std::sync::mpsc::channel::<(u32, (usize, usize, usize))>();
499 let filler = {
500 let (model, done) = (model.clone(), done.clone());
501 let dev = crate::gpu::current_device();
502 std::thread::Builder::new()
503 .name("mimo-bank-fill".into())
504 .spawn(move || {
505 crate::gpu::set_current_device(dev);
506 while let Ok((slot, triple)) = rx.recv() {
507 let ok =
508 crate::gpu_wgpu::dsv4_global_slot_fill(&model, slot as usize, triple);
509 done.lock().unwrap().push((slot, ok));
510 }
511 })
512 .ok()?
513 };
514 Some(Self {
515 segment_slots,
516 n_experts,
517 slot_for: vec![NONE; keys],
518 owner: vec![NONE; capacity],
519 last: vec![0; capacity],
520 occupancy: vec![0; n_layers],
521 free: (0..capacity as u32).rev().collect(),
522 floor: (capacity / moe_layers.max(1) / 2) as u32,
523 seen: vec![0; keys],
524 seen_tok: vec![0; keys],
525 tok: 1,
526 pending: 0,
527 admitted: 0,
528 max_pending: env("CMF_MIMO_FILL_QUEUE").unwrap_or(256) as usize,
529 prime_queue: env("CMF_MIMO_PRIME_QUEUE").unwrap_or(4096) as usize,
530 min_seen: env("CMF_MIMO_FETCH_MIN_SEEN")
531 .unwrap_or(default_min_seen(capacity / moe_layers.max(1)))
532 as u16,
533 decay_tokens: env("CMF_MIMO_SEEN_DECAY").unwrap_or(16).max(1),
534 tx: Some(tx),
535 done,
536 filler: Some(filler),
537 })
538 }
539
540 pub(crate) fn capacity(&self) -> usize {
541 self.owner.len()
542 }
543
544 pub(crate) fn free_slots(&self) -> usize {
545 self.free.len()
546 }
547
548 pub(crate) fn pending(&self) -> usize {
549 self.pending
550 }
551
552 fn next_token(&mut self) {
554 self.tok += 1;
555 }
556
557 fn drain(&mut self) {
559 let done = std::mem::take(&mut *self.done.lock().unwrap());
560 for (slot, ok) in done {
561 self.pending = self.pending.saturating_sub(1);
562 let key = self.owner[slot as usize];
563 if key == NONE {
564 continue;
565 }
566 if ok {
567 self.slot_for[key as usize] = slot;
568 self.last[slot as usize] = self.tok;
569 } else {
570 self.slot_for[key as usize] = NONE;
571 self.owner[slot as usize] = NONE;
572 let layer = key as usize / self.n_experts;
573 self.occupancy[layer] = self.occupancy[layer].saturating_sub(1);
574 self.free.push(slot);
575 }
576 }
577 }
578
579 fn victim(&mut self, layer: usize) -> Option<u32> {
580 if let Some(s) = self.free.pop() {
581 return Some(s);
582 }
583 let ne = self.n_experts as u32;
584 let tok = self.tok;
585 let pick = |over_floor: bool, me: &Self| -> Option<u32> {
586 let mut best: Option<(u64, u32)> = None;
587 for (slot, &key) in me.owner.iter().enumerate() {
588 if key == NONE || me.slot_for[key as usize] != slot as u32 {
589 continue; }
591 let l = me.last[slot];
592 if l >= tok {
593 continue; }
595 let kl = (key / ne) as usize;
596 if over_floor && me.occupancy[kl] <= me.floor && kl != layer {
597 continue;
598 }
599 if best.is_none_or(|(bl, _)| l < bl) {
600 best = Some((l, slot as u32));
601 }
602 }
603 best.map(|(_, s)| s)
604 };
605 pick(true, self).or_else(|| pick(false, self))
606 }
607
608 fn resolve(
611 &mut self,
612 layer: usize,
613 picks: &[usize],
614 triples: &[(usize, usize, usize)],
615 ) -> Option<Vec<u32>> {
616 self.drain();
617 let base = layer.checked_mul(self.n_experts)?;
618 let mut remap = vec![NONE; self.n_experts];
619 for &e in picks {
620 let key = base + *(e < self.n_experts).then_some(&e)?;
621 self.see(key, 1);
622 let slot = self.slot_for[key];
623 if slot < PENDING {
624 remap[e] = slot;
625 self.last[slot as usize] = self.tok;
626 }
627 }
628 for &e in picks {
629 if !self.admit(layer, e, triples[e], self.max_pending)? {
630 break;
631 }
632 }
633 Some(remap)
634 }
635
636 fn see(&mut self, key: usize, n: u16) {
639 let tok32 = (self.tok / self.decay_tokens).min(u32::MAX as u64) as u32;
640 let shift = tok32.saturating_sub(self.seen_tok[key]);
641 self.seen[key] = if shift >= 16 {
642 0
643 } else {
644 self.seen[key] >> shift
645 };
646 self.seen_tok[key] = tok32;
647 self.seen[key] = self.seen[key].saturating_add(n);
648 }
649
650 fn admit(
654 &mut self,
655 layer: usize,
656 e: usize,
657 triple: (usize, usize, usize),
658 queue_cap: usize,
659 ) -> Option<bool> {
660 let key = layer * self.n_experts + e;
661 if self.slot_for[key] != NONE || self.pending >= queue_cap {
662 return Some(true);
663 }
664 if self.free.is_empty() && self.seen[key] < self.min_seen {
665 return Some(true);
666 }
667 let Some(slot) = self.victim(layer) else {
668 return Some(false);
669 };
670 let old = self.owner[slot as usize];
671 if old != NONE {
672 self.slot_for[old as usize] = NONE;
673 let ol = old as usize / self.n_experts;
674 self.occupancy[ol] = self.occupancy[ol].saturating_sub(1);
675 }
676 self.owner[slot as usize] = key as u32;
677 self.slot_for[key] = PENDING;
678 self.occupancy[layer] += 1;
679 self.pending += 1;
680 self.admitted += 1;
681 self.tx
682 .as_ref()
683 .is_some_and(|tx| tx.send((slot, triple)).is_ok())
684 .then_some(true)
685 }
686
687 fn prime(&mut self, layer: usize, counts: &[u64], triples: &[(usize, usize, usize)]) {
692 self.drain();
693 let base = layer * self.n_experts;
694 let mut used: Vec<(usize, u64)> = counts
695 .iter()
696 .enumerate()
697 .filter(|&(e, &c)| c > 0 && e < self.n_experts && e < triples.len())
698 .map(|(e, &c)| (e, c))
699 .collect();
700 used.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
701 for &(e, c) in &used {
702 self.see(base + e, c.min(u16::MAX as u64) as u16);
703 }
704 for &(e, _) in &used {
705 match self.admit(layer, e, triples[e], self.prime_queue) {
706 Some(true) => {}
707 _ => break,
708 }
709 }
710 }
711}
712
713#[cfg(feature = "gpu")]
714impl Drop for Bank {
715 fn drop(&mut self) {
716 self.stop_filler();
717 }
718}
719
720#[cfg(feature = "gpu")]
721impl Bank {
722 fn stop_filler(&mut self) {
723 self.tx = None;
727 if let Some(h) = self.filler.take() {
728 let _ = h.join();
729 }
730 }
731}
732
733#[cfg(feature = "gpu")]
737pub(crate) fn shutdown_banks() {
738 let banks = std::mem::take(&mut *BANKS.lock().unwrap());
739 for (_, bank) in banks {
740 bank.lock().unwrap().stop_filler();
741 }
742}
743
744#[cfg(feature = "gpu")]
747static BANKS: std::sync::Mutex<Vec<(u64, Arc<std::sync::Mutex<Bank>>)>> =
748 std::sync::Mutex::new(Vec::new());
749
750#[derive(Default)]
753pub enum Slot {
754 #[default]
755 Undecided,
756 Off,
757 #[cfg(feature = "gpu")]
758 On(Box<Dynamic>),
759}
760
761#[cfg(feature = "gpu")]
763pub struct Dynamic {
764 pub placement: Placement,
765 model: Arc<CmfModel>,
766 bank: Arc<std::sync::Mutex<Bank>>,
767 ids: Vec<Vec<(usize, usize, usize)>>,
770 pub dyn_from: usize,
772 failed: bool,
773 fast: Option<bool>,
775 last_moe: usize,
778 prof: bool,
779 prof_mark: (std::time::Instant, Stats, [u64; 7]),
780}
781
782#[cfg(feature = "gpu")]
786fn device_counters() -> [u64; 7] {
787 use std::sync::atomic::Ordering::Relaxed;
788 [
789 crate::gpu_wgpu::MOE_ENC_NS.load(Relaxed),
790 crate::gpu_wgpu::MOE_WAIT_NS.load(Relaxed),
791 crate::gpu_wgpu::MOE_UP_NS.load(Relaxed),
792 crate::gpu_wgpu::MOE_PASS_NS.load(Relaxed),
793 crate::gpu_wgpu::SUBMITS.load(Relaxed),
794 crate::gpu_wgpu::MOE_GPU_NS[0].load(Relaxed),
795 crate::gpu_wgpu::MOE_GPU_N.load(Relaxed),
796 ]
797}
798
799#[cfg(feature = "gpu")]
800fn env_mode() -> Option<MoeMode> {
801 let raw = std::env::var("CMF_MIMO_MOE").ok()?;
802 match MoeMode::parse(&raw) {
803 Some(m) => m,
804 None => {
805 tracing::warn!("CMF_MIMO_MOE={raw}: not prefix|dynamic|hybrid|auto — using auto");
806 None
807 }
808 }
809}
810
811#[cfg(feature = "gpu")]
813fn bank_refusal(layers: &[(usize, &MoeFfn)]) -> Option<String> {
814 let first = layers.first()?.1;
815 let model = first
816 .experts
817 .first()
818 .and_then(|e| e.gate_proj.model_arc())?;
819 let inter = first.experts[0].gate_proj.rows();
820 let hidden = first.experts[0].gate_proj.cols();
821 for &(li, m) in layers {
822 if m.shared.is_some()
823 || m.per_expert_scale.is_some()
824 || m.resonance.is_some()
825 || m.route_tau.is_some()
826 || m.mask.is_some()
827 || m.router_input_norm
828 {
829 return Some(format!(
830 "layer {li}: routing extras the bank frame does not carry"
831 ));
832 }
833 for (ei, d) in m.experts.iter().enumerate() {
834 let q4tp = |t: &crate::qtensor::QTensor| {
835 t.model_dtype() == Some(TensorDtype::Q4TiledP)
836 && t.model_arc().is_some_and(|a| a.uid() == model.uid())
837 };
838 if !(q4tp(&d.gate_proj) && q4tp(&d.up_proj) && q4tp(&d.down_proj))
839 || d.act != crate::pipeline::Act::Silu
840 || d.gate_proj.rows() != inter
841 || d.gate_proj.cols() != hidden
842 || d.down_proj.rows() != hidden
843 {
844 return Some(format!(
845 "layer {li} expert {ei}: not a mapped q4tp SiLU expert of the common shape"
846 ));
847 }
848 }
849 }
850 if hidden % 32 != 0 || inter % 32 != 0 {
851 return Some(format!(
852 "hidden {hidden} / inter {inter} not multiples of 32"
853 ));
854 }
855 None
856}
857
858impl Slot {
859 pub fn is_undecided(&self) -> bool {
860 matches!(self, Self::Undecided)
861 }
862
863 pub fn is_on(&self) -> bool {
865 match self {
866 #[cfg(feature = "gpu")]
867 Self::On(d) => !d.failed,
868 _ => false,
869 }
870 }
871
872 pub(crate) fn graph_prefix_end(&self) -> Option<usize> {
875 match self {
876 #[cfg(feature = "gpu")]
877 Self::On(d) => Some(d.dyn_from),
878 _ => None,
879 }
880 }
881
882 pub fn is_dynamic(&self, li: usize, host_tail: bool) -> bool {
887 match self {
888 #[cfg(feature = "gpu")]
889 Self::On(d) => {
890 !d.failed
891 && d.ids.get(li).is_some_and(|v| !v.is_empty())
892 && (li >= d.dyn_from || host_tail)
893 }
894 _ => {
895 let _ = (li, host_tail);
896 false
897 }
898 }
899 }
900
901 #[cfg(not(feature = "gpu"))]
904 pub fn decide(layers: &[(usize, &MoeFfn)], n_layers: usize, graph_prefix: bool) -> Self {
905 let _ = (layers, n_layers, graph_prefix);
906 Self::Off
907 }
908
909 #[cfg(feature = "gpu")]
912 pub fn decide(layers: &[(usize, &MoeFfn)], n_layers: usize, graph_prefix: bool) -> Self {
913 let exact = std::env::var("CMF_MIMO_EXPERT_SLOTS")
914 .ok()
915 .and_then(|v| v.parse::<usize>().ok());
916 Self::decide_with(layers, n_layers, graph_prefix, env_mode(), exact)
917 }
918
919 #[cfg(feature = "gpu")]
923 pub fn decide_with(
924 layers: &[(usize, &MoeFfn)],
925 n_layers: usize,
926 graph_prefix: bool,
927 forced: Option<MoeMode>,
928 exact: Option<usize>,
929 ) -> Self {
930 let Some(model) = layers
931 .first()
932 .and_then(|(_, m)| m.experts.first())
933 .and_then(|e| e.gate_proj.model_arc())
934 else {
935 return Self::Off;
936 };
937 if model.arch().arch_name != "mimo_v2" {
938 return Self::Off;
939 }
940 let say = |msg: &str| {
941 tracing::info!("MiMo MoE placement: {msg}");
942 *LAST_DECISION.lock().unwrap() = msg.to_string();
943 };
944 if !crate::gpu::enabled() || !crate::gpu::wgpu_active() {
945 say("prefix — no wgpu device (experts on the host path)");
946 return Self::Off;
947 }
948 if let Some(why) = bank_refusal(layers) {
949 say(&format!("prefix — {why}"));
950 return Self::Off;
951 }
952 let first = layers[0].1;
953 let inter = first.experts[0].gate_proj.rows();
954 let hidden = first.experts[0].gate_proj.cols();
955 let n_experts = first.experts.len();
956 if layers.iter().any(|(_, m)| m.experts.len() != n_experts) {
957 say("prefix — MoE layers differ in expert count");
958 return Self::Off;
959 }
960 let Some(budget) = crate::gpu_wgpu::dsv4_vram_budget() else {
961 say("prefix — no device budget");
962 return Self::Off;
963 };
964 if budget == u64::MAX {
965 say("prefix — unified memory: the host pages experts, nothing to place");
966 return Self::Off;
967 }
968 if !crate::gpu_wgpu::dsv4_global_moe_supported() {
969 say("prefix — this adapter has no segmented expert bank (descriptor arrays)");
970 return Self::Off;
971 }
972 let per_expert = {
973 let gu = cortiq_core::quant::expected_nbytes(TensorDtype::Q4TiledP, &[inter, hidden]);
974 let dn = cortiq_core::quant::expected_nbytes(TensorDtype::Q4TiledP, &[hidden, inter]);
975 match (gu, dn) {
976 (Some(gu), Some(dn)) => (2 * gu + dn) as u64,
977 _ => {
978 say("prefix — expert size unknown");
979 return Self::Off;
980 }
981 }
982 };
983 let is_expert = |name: &str| name.contains(".mlp.experts.");
984 let non_expert: u64 = model
985 .tensors
986 .iter()
987 .filter(|t| !is_expert(&t.name) && !t.name.starts_with("model.embed_tokens."))
988 .map(|t| t.nbytes)
989 .sum();
990 let layer_non_expert: u64 = model
991 .tensors
992 .iter()
993 .filter(|t| t.name.starts_with("model.layers.") && !is_expert(&t.name))
994 .map(|t| t.nbytes)
995 .sum();
996 let inp = PlacementInputs {
997 budget,
998 non_expert,
999 per_expert,
1000 moe_layers: layers.len(),
1001 n_experts,
1002 top_k: first.top_k,
1003 attn_per_layer: layer_non_expert / n_layers.max(1) as u64,
1004 graph_prefix,
1005 };
1006 let costs = Costs::measured();
1007 let mut placement = place(&inp, &costs, forced);
1008 if placement.mode == MoeMode::Prefix {
1009 say(&placement.reason);
1010 return Self::Off;
1011 }
1012 if let Some(n) = exact {
1013 placement.bank_slots = n;
1014 placement.reason = format!("{} [CMF_MIMO_EXPERT_SLOTS={n}]", placement.reason);
1015 }
1016 let bank = {
1017 let mut reg = BANKS.lock().unwrap();
1018 match reg.iter().find(|(uid, _)| *uid == model.uid()) {
1019 Some((_, b)) => Some(b.clone()),
1020 None => Bank::create(
1021 &model,
1022 inter,
1023 hidden,
1024 n_layers,
1025 layers.len().saturating_sub(placement.prefix_layers),
1026 n_experts,
1027 placement.bank_slots,
1028 )
1029 .map(|b| {
1030 let b = Arc::new(std::sync::Mutex::new(b));
1031 reg.push((model.uid(), b.clone()));
1032 b
1033 }),
1034 }
1035 };
1036 let Some(bank) = bank else {
1037 say(&format!(
1038 "prefix — the {}-slot expert bank could not be allocated ({})",
1039 placement.bank_slots, placement.reason
1040 ));
1041 return Self::Off;
1042 };
1043 let mut ids = vec![Vec::new(); n_layers];
1044 for &(li, m) in layers {
1045 let triples: Option<Vec<_>> = m
1046 .experts
1047 .iter()
1048 .map(|d| {
1049 Some((
1050 d.gate_proj.model_idx()?,
1051 d.up_proj.model_idx()?,
1052 d.down_proj.model_idx()?,
1053 ))
1054 })
1055 .collect();
1056 match (triples, ids.get_mut(li)) {
1057 (Some(t), Some(slot)) => *slot = t,
1058 _ => {
1059 say("prefix — an expert is not mmap-backed");
1060 return Self::Off;
1061 }
1062 }
1063 }
1064 let dyn_from = layers
1066 .get(placement.prefix_layers)
1067 .map_or(n_layers, |&(li, _)| li);
1068 let cap = bank.lock().unwrap().capacity();
1069 say(&format!(
1070 "{} — {}; bank {cap} slots ({:.1} GB), bank layers from {dyn_from}",
1071 placement.mode.name(),
1072 placement.reason,
1073 cap as f64 * per_expert as f64 / 1e9,
1074 ));
1075 Self::On(Box::new(Dynamic {
1076 placement,
1077 model,
1078 bank,
1079 ids,
1080 dyn_from,
1081 failed: false,
1082 fast: None,
1083 last_moe: layers.last().map_or(0, |&(li, _)| li),
1084 prof: std::env::var_os("CMF_MIMO_PROF").is_some(),
1085 prof_mark: (std::time::Instant::now(), stats(), device_counters()),
1086 }))
1087 }
1088
1089 pub(crate) fn prime(&mut self, li: usize, m: &MoeFfn, before: &[u64]) {
1093 #[cfg(feature = "gpu")]
1094 if let Self::On(d) = self
1095 && !d.failed
1096 && let Some(triples) = d.ids.get(li).filter(|t| !t.is_empty())
1097 && std::env::var("CMF_MIMO_PRIME").as_deref() != Ok("0")
1098 {
1099 let now = m.stats.borrow();
1100 let counts: Vec<u64> = (0..now.len())
1101 .map(|e| now[e].saturating_sub(before.get(e).copied().unwrap_or(0)))
1102 .collect();
1103 drop(now);
1104 d.bank.lock().unwrap().prime(li, &counts, triples);
1105 }
1106 #[cfg(not(feature = "gpu"))]
1107 let _ = (li, m, before);
1108 }
1109
1110 pub(crate) fn note_route(&self, ns: u64) {
1112 if self.is_on() {
1113 STATS.lock().unwrap().route_ns += ns;
1114 }
1115 }
1116
1117 pub(crate) fn forward(
1120 &mut self,
1121 li: usize,
1122 m: &MoeFfn,
1123 x: &[f32],
1124 route: &MoeRoute,
1125 pool: Option<&Pool>,
1126 ) -> Option<Vec<f32>> {
1127 #[cfg(not(feature = "gpu"))]
1128 {
1129 let _ = (li, m, x, route, pool);
1130 None
1131 }
1132 #[cfg(feature = "gpu")]
1133 self.forward_bank(li, m, x, route, pool)
1134 }
1135
1136 pub(crate) fn forward_rows(
1140 &mut self,
1141 li: usize,
1142 m: &MoeFfn,
1143 xs: &[f32],
1144 routes: &[MoeRoute],
1145 pool: Option<&Pool>,
1146 ) -> Option<Vec<f32>> {
1147 #[cfg(feature = "gpu")]
1148 {
1149 let Self::On(d) = self else { return None };
1150 if d.failed || d.fast == Some(false) {
1151 return None;
1152 }
1153 let t0 = std::time::Instant::now();
1154 let out = d.run_rows(li, m, xs, routes, pool);
1155 STATS.lock().unwrap().call_ns += t0.elapsed().as_nanos() as u64;
1156 out
1157 }
1158 #[cfg(not(feature = "gpu"))]
1159 {
1160 let _ = (li, m, xs, routes, pool);
1161 None
1162 }
1163 }
1164
1165 #[cfg(feature = "gpu")]
1166 fn forward_bank(
1167 &mut self,
1168 li: usize,
1169 m: &MoeFfn,
1170 x: &[f32],
1171 route: &MoeRoute,
1172 pool: Option<&Pool>,
1173 ) -> Option<Vec<f32>> {
1174 let Self::On(d) = self else { return None };
1175 if d.failed {
1176 return None;
1177 }
1178 let t0 = std::time::Instant::now();
1179 let out = d.run(li, m, x, route, pool);
1180 {
1181 let mut st = STATS.lock().unwrap();
1182 st.call_ns += t0.elapsed().as_nanos() as u64;
1183 if out.is_none() {
1184 st.fallbacks += 1;
1185 }
1186 }
1187 if d.prof && li == d.last_moe {
1188 let (t, s0, c0) = d.prof_mark;
1189 let s1 = stats();
1190 let c1 = device_counters();
1191 let ms = |i: usize| (c1[i] - c0[i]) as f64 / 1e6;
1192 let picks = (s1.picks - s0.picks).max(1);
1193 eprintln!(
1194 "mimo-moe token: {:.1} ms wall, bank calls {} | hits {:.1}% fills {} cold {} of {} \
1195 picks | frame {:.2} ms (encode {:.2} wait {:.2} upload {:.2} pass {:.2}), bank \
1196 calls {:.2} ms, route {:.2} ms | submits {} | card {:.2} ms over {} frames | bank \
1197 free {} of {}",
1198 t.elapsed().as_secs_f64() * 1e3,
1199 s1.calls - s0.calls,
1200 (s1.hits - s0.hits) as f64 / picks as f64 * 100.0,
1201 s1.fills - s0.fills,
1202 s1.cold - s0.cold,
1203 picks,
1204 (s1.frame_ns - s0.frame_ns) as f64 / 1e6,
1205 ms(0),
1206 ms(1),
1207 ms(2),
1208 ms(3),
1209 (s1.call_ns - s0.call_ns) as f64 / 1e6,
1210 (s1.route_ns - s0.route_ns) as f64 / 1e6,
1211 c1[4] - c0[4],
1212 ms(5),
1213 c1[6] - c0[6],
1214 {
1215 let b = d.bank.lock().unwrap();
1216 format!("{} (filling {})", b.free_slots(), b.pending())
1217 },
1218 d.bank.lock().unwrap().capacity(),
1219 );
1220 d.prof_mark = (std::time::Instant::now(), s1, c1);
1221 }
1222 out
1223 }
1224}
1225
1226#[cfg(feature = "gpu")]
1227impl Dynamic {
1228 fn run_rows(
1229 &mut self,
1230 li: usize,
1231 m: &MoeFfn,
1232 xs: &[f32],
1233 routes: &[MoeRoute],
1234 pool: Option<&Pool>,
1235 ) -> Option<Vec<f32>> {
1236 let rows = routes.len();
1237 let top_k = routes.first()?.idx.len();
1238 let hidden = m.experts.first()?.gate_proj.cols();
1239 let inter = m.experts[0].gate_proj.rows();
1240 if rows > 4
1241 || top_k == 0
1242 || xs.len() != rows * hidden
1243 || routes
1244 .iter()
1245 .any(|r| r.idx.len() != top_k || r.logits.len() != m.experts.len())
1246 || std::env::var("CMF_MIMO_BANK_KERNEL").as_deref() == Ok("generic")
1247 {
1248 return None;
1249 }
1250 let triples = self.ids.get(li).filter(|t| t.len() == m.experts.len())?;
1251 let mut union = Vec::new();
1252 for r in routes {
1253 for &e in &r.idx {
1254 if e >= triples.len() {
1255 return None;
1256 }
1257 if !union.contains(&e) {
1258 union.push(e);
1259 }
1260 }
1261 }
1262 let mut bank = self.bank.lock().unwrap();
1263 if li == self.dyn_from {
1264 bank.next_token();
1265 }
1266 let admitted0 = bank.admitted;
1267 let remap = bank.resolve(li, &union, triples)?;
1268 let mut sel = Vec::with_capacity(rows * top_k);
1269 let mut wt = Vec::with_capacity(rows * top_k);
1270 let mut cold_jobs = Vec::with_capacity(rows);
1271 for r in routes {
1272 let mut jobs = Vec::new();
1273 for &e in &r.idx {
1274 let w = r.p[e] / r.wsum;
1275 sel.push(remap[e]);
1276 wt.push(w);
1277 if remap[e] == u32::MAX {
1278 jobs.push((&m.experts[e], w));
1279 }
1280 }
1281 cold_jobs.push(jobs);
1282 }
1283 let cold = sel.iter().filter(|&&s| s == u32::MAX).count();
1284 let host_rows = || {
1285 crate::gpu::cpu_scope(|| {
1286 crate::qtensor::float_activations_scope(|| {
1287 crate::pipeline::moe_cold_experts_rows_cpu(&cold_jobs, xs, hidden, pool)
1288 })
1289 })
1290 };
1291 let t_frame = std::time::Instant::now();
1292 let mut out = vec![0.0; xs.len()];
1293 if cold == sel.len() {
1294 out = host_rows();
1295 } else {
1296 let (ok, host) = std::thread::scope(|scope| {
1297 let host = (cold > 0).then(|| scope.spawn(host_rows));
1298 let ok = crate::gpu_wgpu::mimo_bank::mimo_bank_rows(
1299 &self.model,
1300 xs,
1301 &sel,
1302 &wt,
1303 inter,
1304 rows,
1305 &mut out,
1306 );
1307 (
1308 ok,
1309 host.map(|h| h.join().expect("MiMo cold-expert worker panicked")),
1310 )
1311 });
1312 if !ok {
1313 return None;
1314 }
1315 if let Some(host) = host {
1316 for (o, h) in out.iter_mut().zip(host) {
1317 *o += h;
1318 }
1319 }
1320 }
1321 let mut st = STATS.lock().unwrap();
1322 st.calls += 1;
1323 st.picks += sel.len() as u64;
1324 st.hits += (sel.len() - cold) as u64;
1325 st.cold += cold as u64;
1326 st.fills += bank.admitted - admitted0;
1327 st.frame_ns += t_frame.elapsed().as_nanos() as u64;
1328 Some(out)
1329 }
1330
1331 fn run(
1332 &mut self,
1333 li: usize,
1334 m: &MoeFfn,
1335 x: &[f32],
1336 route: &MoeRoute,
1337 pool: Option<&Pool>,
1338 ) -> Option<Vec<f32>> {
1339 let triples = self.ids.get(li).filter(|t| t.len() == m.experts.len())?;
1340 let picks = &route.idx;
1341 if picks.is_empty() || route.logits.len() != m.experts.len() {
1342 return None;
1343 }
1344 let hidden = x.len();
1345 let mut mix = vec![0.0f32; m.experts.len()];
1348 for &e in picks {
1349 mix[e] = route.p[e] / route.wsum;
1350 }
1351 let bank_arc = self.bank.clone();
1355 let mut bank = bank_arc.lock().unwrap();
1356 if li == self.dyn_from {
1357 bank.next_token();
1358 }
1359 let admitted0 = bank.admitted;
1360 let Some(remap) = bank.resolve(li, picks, triples) else {
1361 tracing::warn!("MiMo MoE bank: slot map refused layer {li} — host path from now on");
1362 self.failed = true;
1363 return None;
1364 };
1365 let cold_ids: Vec<usize> = picks
1366 .iter()
1367 .copied()
1368 .filter(|&e| remap[e] == u32::MAX)
1369 .collect();
1370 let cold_jobs: Vec<(&crate::pipeline::DenseFfn, f32)> =
1371 cold_ids.iter().map(|&e| (&m.experts[e], mix[e])).collect();
1372 let weights = crate::gpu_wgpu::Dsv4MoeW {
1373 router: &[],
1374 experts: triples,
1375 logits: &route.logits,
1376 bias: Some(&mix),
1379 mask: None,
1380 forced: Some(picks),
1381 remap: Some(&remap),
1382 global: Some(crate::gpu_wgpu::Dsv4GlobalMoe {
1383 pool_uid: self.model.uid(),
1384 shared_slot: 0,
1385 segment_slots: bank.segment_slots as u32,
1386 }),
1387 has_shared: false,
1388 shared_weight: 1.0,
1389 preweighted: true,
1390 qwen_softmax: false,
1391 };
1392 let geom = crate::gpu_wgpu::Dsv4MoeGeom {
1393 hidden,
1394 inter: m.experts[0].gate_proj.rows(),
1395 top_k: picks.len(),
1396 route_scale: 1.0,
1397 swiglu_limit: 0.0,
1398 gu_q2: false,
1399 bf16: false,
1400 };
1401 let mut out = vec![0.0f32; hidden];
1402 let mut cold_seen = Vec::new();
1403 let mut cold_x = Vec::new();
1404 let model = self.model.clone();
1405 let t_frame = std::time::Instant::now();
1406 if cold_ids.len() == picks.len() {
1407 let fills = bank.admitted - admitted0;
1409 drop(bank);
1410 let out = crate::gpu::cpu_scope(|| {
1411 crate::qtensor::float_activations_scope(|| {
1412 crate::pipeline::moe_cold_experts_cpu(&cold_jobs, x, pool)
1413 })
1414 });
1415 let mut st = STATS.lock().unwrap();
1416 st.calls += 1;
1417 st.picks += picks.len() as u64;
1418 st.cold += picks.len() as u64;
1419 st.fills += fills;
1420 return Some(out);
1421 }
1422 let inter = geom.inter;
1426 let fast = *self.fast.get_or_insert_with(|| {
1427 std::env::var("CMF_MIMO_BANK_KERNEL").as_deref() != Ok("generic")
1428 && crate::gpu_wgpu::mimo_bank::mimo_bank_ready(&model, hidden, inter, picks.len())
1429 });
1430 let sel: Vec<u32> = picks.iter().map(|&e| remap[e]).collect();
1431 let wt: Vec<f32> = picks.iter().map(|&e| mix[e]).collect();
1432 let (ok, cold_out) = std::thread::scope(|s| {
1433 let host = (!cold_jobs.is_empty()).then(|| {
1436 s.spawn(|| {
1437 crate::gpu::cpu_scope(|| {
1438 crate::qtensor::float_activations_scope(|| {
1439 crate::pipeline::moe_cold_experts_cpu(&cold_jobs, x, pool)
1440 })
1441 })
1442 })
1443 });
1444 let ok = if fast {
1445 crate::gpu_wgpu::mimo_bank::mimo_bank_frame(&model, x, &sel, &wt, inter, &mut out)
1446 } else {
1447 crate::gpu_wgpu::dsv4_moe_frame(
1448 &model,
1449 &weights,
1450 geom,
1451 x,
1452 &mut cold_seen,
1453 &mut cold_x,
1454 None,
1455 None,
1456 &mut out,
1457 ) && cold_seen
1458 .iter()
1459 .map(|&(e, _)| e)
1460 .eq(cold_ids.iter().copied())
1461 };
1462 let cold = host.and_then(|h| h.join().ok());
1463 (ok, cold)
1464 });
1465 let frame_ns = t_frame.elapsed().as_nanos() as u64;
1466 let fills = bank.admitted - admitted0;
1467 drop(bank);
1468 if !ok || (!cold_ids.is_empty() && cold_out.is_none()) {
1471 tracing::warn!(
1472 "MiMo MoE bank: frame refused at layer {li} (ok={ok}, {} kernels, {} cold) — host \
1473 path from now on",
1474 if fast { "bank" } else { "generic" },
1475 cold_ids.len()
1476 );
1477 self.failed = true;
1478 return None;
1479 }
1480 if let Some(mut c) = cold_out {
1481 for (o, v) in out.iter_mut().zip(&c) {
1482 *o += v;
1483 }
1484 crate::attention::recycle_buf(&mut c);
1485 }
1486 let mut st = STATS.lock().unwrap();
1487 st.calls += 1;
1488 st.picks += picks.len() as u64;
1489 st.hits += (picks.len() - cold_ids.len()) as u64;
1490 st.cold += cold_ids.len() as u64;
1491 st.fills += fills;
1492 st.frame_ns += frame_ns;
1493 Some(out)
1494 }
1495}
1496
1497#[cfg(test)]
1498mod tests {
1499 use super::*;
1500
1501 const GB: u64 = 1_000_000_000;
1502
1503 fn mimo(budget: u64, graph: bool) -> PlacementInputs {
1504 PlacementInputs {
1505 budget,
1506 non_expert: 5_500_000_000,
1507 per_expert: 13_107_200,
1508 moe_layers: 47,
1509 n_experts: 256,
1510 top_k: 8,
1511 attn_per_layer: 93_000_000,
1512 graph_prefix: graph,
1513 }
1514 }
1515
1516 #[test]
1517 fn everything_fits_means_prefix() {
1518 let p = place(&mimo(400 * GB, true), &Costs::measured(), None);
1519 assert_eq!(p.mode, MoeMode::Prefix);
1520 assert_eq!(p.bank_slots, 0);
1521 assert_eq!(p.prefix_layers, 47);
1522 }
1523
1524 #[test]
1525 fn forced_modes_are_honoured_and_sized_from_the_budget() {
1526 let c = Costs::measured();
1527 let inp = mimo(98 * GB, false);
1528 let room = inp.budget - inp.non_expert - device_reserve(inp.budget);
1529 let slots = (room / inp.per_expert) as usize;
1530 let d = place(&inp, &c, Some(MoeMode::Dynamic));
1531 assert_eq!(
1532 (d.mode, d.prefix_layers, d.bank_slots),
1533 (MoeMode::Dynamic, 0, slots)
1534 );
1535 let p = place(&inp, &c, Some(MoeMode::Prefix));
1536 assert_eq!((p.mode, p.bank_slots), (MoeMode::Prefix, 0));
1537 assert_eq!(p.prefix_layers, slots / 256);
1538 let h = place(&inp, &c, Some(MoeMode::Hybrid));
1539 assert_eq!(h.mode, MoeMode::Hybrid);
1540 assert!(h.prefix_layers >= 1 && h.prefix_layers <= slots / 256);
1541 assert_eq!(h.bank_slots, slots - h.prefix_layers * 256);
1542 }
1543
1544 #[test]
1545 fn auto_prefers_the_cheapest_prediction() {
1546 let c = Costs::measured();
1547 for budget in [16 * GB, 24 * GB, 48 * GB, 80 * GB, 98 * GB] {
1548 for graph in [false, true] {
1549 let inp = mimo(budget, graph);
1550 let auto = place(&inp, &c, None);
1551 for m in [MoeMode::Prefix, MoeMode::Dynamic, MoeMode::Hybrid] {
1552 let f = place(&inp, &c, Some(m));
1553 assert!(
1554 auto.predicted_s <= f.predicted_s + 1e-12,
1555 "budget {budget} graph {graph}: auto {:?} {} > {m:?} {}",
1556 auto.mode,
1557 auto.predicted_s,
1558 f.predicted_s
1559 );
1560 }
1561 }
1562 }
1563 }
1564
1565 #[test]
1571 fn ladder_choices_for_mimo() {
1572 let c = Costs::measured();
1573 for mb in [16_000u64, 24_000, 48_000, 80_000, 93_791] {
1574 let budget = mb * 1024 * 1024;
1575 let no_graph = place(&mimo(budget, false), &c, None);
1576 let graph = place(&mimo(budget, true), &c, None);
1577 println!(
1578 "{mb} MB: no graph → {:?} P={} bank {} ({:.1} ms); graph → {:?} P={} bank {} ({:.1} ms)",
1579 no_graph.mode,
1580 no_graph.prefix_layers,
1581 no_graph.bank_slots,
1582 no_graph.predicted_s * 1e3,
1583 graph.mode,
1584 graph.prefix_layers,
1585 graph.bank_slots,
1586 graph.predicted_s * 1e3,
1587 );
1588 assert_eq!(
1589 no_graph.mode,
1590 MoeMode::Dynamic,
1591 "{mb} MB: {}",
1592 no_graph.reason
1593 );
1594 assert!(graph.predicted_s <= no_graph.predicted_s + 1e-12);
1595 }
1596 }
1597
1598 #[test]
1599 fn default_min_seen_follows_bank_size() {
1600 assert_eq!(default_min_seen(142), 2);
1601 assert_eq!(default_min_seen(48), 2);
1602 assert_eq!(default_min_seen(47), 3);
1603 assert_eq!(default_min_seen(13), 3);
1604 }
1605
1606 #[test]
1607 fn a_budget_below_the_non_expert_weights_leaves_no_bank() {
1608 let inp = mimo(4 * GB, false);
1609 let d = place(&inp, &Costs::measured(), Some(MoeMode::Dynamic));
1610 assert_eq!(d.bank_slots, 0);
1611 }
1612
1613 #[test]
1614 fn hit_curve_interpolates_monotonically() {
1615 let c = Costs::measured();
1616 let mut last = 0.0;
1617 for s in [
1618 1.0, 4.0, 8.0, 20.0, 32.0, 50.0, 64.0, 100.0, 128.0, 160.0, 192.0, 255.0,
1619 ] {
1620 let h = c.hit_rate(s, 256);
1621 assert!(h >= last && (0.0..=1.0).contains(&h), "{s}: {h}");
1622 last = h;
1623 }
1624 assert_eq!(c.hit_rate(256.0, 256), 1.0);
1625 assert_eq!(c.hit_rate(0.0, 256), 0.0);
1626 }
1627
1628 #[test]
1629 fn env_mode_names_parse() {
1630 assert_eq!(MoeMode::parse("dynamic"), Some(Some(MoeMode::Dynamic)));
1631 assert_eq!(MoeMode::parse("HYBRID"), Some(Some(MoeMode::Hybrid)));
1632 assert_eq!(MoeMode::parse("prefix"), Some(Some(MoeMode::Prefix)));
1633 assert_eq!(MoeMode::parse("auto"), Some(None));
1634 assert_eq!(MoeMode::parse("fast"), None);
1635 }
1636}
1637
1638#[cfg(all(test, feature = "gpu"))]
1645mod bank_tests {
1646 use super::*;
1647 use crate::pipeline::{FfnKind, Pipeline};
1648 use crate::sampler::SamplerConfig;
1649 use cortiq_core::CMF_VERSION;
1650 use cortiq_core::format::{CmfHeader, TensorSpec};
1651 use cortiq_core::quant::{
1652 GROUP_SIZE, dequant_q4tp, f32_to_f16, q4tp_code_stride, q4tp_put_code, q4tp_sections,
1653 };
1654 use cortiq_core::types::{ModelArch, QuantType};
1655 use std::collections::HashMap;
1656
1657 const HS: usize = 256;
1658 const INTER: usize = 64;
1659 const NE: usize = 16;
1660 const TOPK: usize = 4;
1661 const NH: usize = 4;
1662 const HD: usize = 32;
1663 const VD: usize = 16;
1664 const VOCAB: usize = 64;
1665 const DENSE_INTER: usize = 96;
1666 const KVH: [usize; 4] = [1, 2, 2, 1];
1668 const SLOTS: usize = 8;
1670
1671 struct Rng(u64);
1672 impl Rng {
1673 fn next(&mut self) -> u64 {
1674 self.0 = self
1675 .0
1676 .wrapping_mul(6_364_136_223_846_793_005)
1677 .wrapping_add(1_442_695_040_888_963_407);
1678 self.0 >> 33
1679 }
1680 fn f(&mut self) -> f32 {
1682 (self.next() & 0xFF_FFFF) as f32 / (1u32 << 24) as f32 - 0.5
1683 }
1684 }
1685
1686 fn f32_spec(name: String, shape: &[usize], rng: &mut Rng, scale: f32, bias: f32) -> TensorSpec {
1687 let n: usize = shape.iter().product();
1688 TensorSpec {
1689 name,
1690 dtype: TensorDtype::F32,
1691 shape: shape.to_vec(),
1692 data: (0..n)
1693 .flat_map(|_| (bias + scale * rng.f()).to_le_bytes())
1694 .collect(),
1695 }
1696 }
1697
1698 fn q4tp_bytes(rows: usize, cols: usize, rng: &mut Rng) -> Vec<u8> {
1701 let gpr = cols / GROUP_SIZE;
1702 let stride = q4tp_code_stride(gpr);
1703 let (params_off, codes_off, _) = q4tp_sections(rows, cols);
1704 let mut b = vec![0u8; codes_off + rows * stride];
1705 for byte in b[..params_off].iter_mut() {
1706 *byte = rng.next() as u8;
1707 }
1708 for r in 0..rows {
1709 let p = params_off + r * 4;
1710 let lo = -7.0 + 0.5 * rng.f();
1711 let st = 0.06 + 0.02 * rng.f();
1712 b[p..p + 2].copy_from_slice(&f32_to_f16(lo).to_le_bytes());
1713 b[p + 2..p + 4].copy_from_slice(&f32_to_f16(st).to_le_bytes());
1714 let crow = &mut b[codes_off + r * stride..codes_off + (r + 1) * stride];
1715 for g in 0..gpr {
1716 q4tp_put_code(crow, g, (rng.next() % 32) as usize);
1717 }
1718 }
1719 b
1720 }
1721
1722 fn arch() -> ModelArch {
1723 serde_json::from_value(serde_json::json!({
1724 "arch_name": "mimo_v2",
1725 "hidden_size": HS,
1726 "intermediate_size": DENSE_INTER,
1727 "num_layers": 4,
1728 "num_attention_heads": NH,
1729 "num_kv_heads": KVH[0],
1730 "head_dim": HD,
1731 "vocab_size": VOCAB,
1732 "layer_types": ["FullAttention", "SlidingAttention", "SlidingAttention", "FullAttention"],
1733 "rms_norm_eps": 1e-6,
1734 "rope_theta": 10_000_000.0,
1735 "rope_local_base_freq": 10_000.0,
1736 "partial_rotary_factor": 0.5,
1737 "sliding_window": 3,
1738 "tie_word_embeddings": true,
1739 "max_position_embeddings": 256,
1740 "linear_conv_kernel_dim": 0,
1741 "linear_num_key_heads": 0,
1742 "linear_num_value_heads": 0,
1743 "kv_heads_per_layer": KVH,
1744 "v_head_dim": VD,
1745 "moe": {
1746 "num_experts": NE,
1747 "top_k": TOPK,
1748 "moe_intermediate_size": INTER,
1749 "norm_topk_prob": true,
1750 "router_sigmoid": true
1751 }
1752 }))
1753 .expect("arch")
1754 }
1755
1756 fn write_model(tag: &str) -> (std::path::PathBuf, Arc<CmfModel>, HashMap<String, Vec<u8>>) {
1759 let mut rng = Rng(0x5EED_0000 ^ tag.len() as u64);
1760 let mut specs = vec![
1761 f32_spec(
1762 "model.embed_tokens.weight".into(),
1763 &[VOCAB, HS],
1764 &mut rng,
1765 2.0,
1766 0.0,
1767 ),
1768 f32_spec("model.norm.weight".into(), &[HS], &mut rng, 0.2, 1.0),
1769 ];
1770 let mut experts = HashMap::new();
1771 for (li, &kv) in KVH.iter().enumerate() {
1772 let p = format!("model.layers.{li}.");
1773 specs.push(f32_spec(
1774 format!("{p}input_layernorm.weight"),
1775 &[HS],
1776 &mut rng,
1777 0.2,
1778 1.0,
1779 ));
1780 specs.push(f32_spec(
1781 format!("{p}post_attention_layernorm.weight"),
1782 &[HS],
1783 &mut rng,
1784 0.2,
1785 1.0,
1786 ));
1787 for (n, shape) in [
1788 ("q_proj", [NH * HD, HS]),
1789 ("k_proj", [kv * HD, HS]),
1790 ("v_proj", [kv * VD, HS]),
1791 ("o_proj", [HS, NH * VD]),
1792 ] {
1793 let mut spec = f32_spec(
1794 format!("{p}self_attn.{n}.weight"),
1795 &shape,
1796 &mut rng,
1797 0.2,
1798 0.0,
1799 );
1800 if tag == "attn-graph" {
1801 let scale = 0.2f32 / 127.0;
1804 let mut data: Vec<u8> = spec.data.chunks_exact(4)
1805 .map(|v| (f32::from_le_bytes(v.try_into().unwrap()) / scale)
1806 .round().clamp(-127.0, 127.0) as i8 as u8).collect();
1807 for _ in 0..shape[0] { data.extend_from_slice(&f32_to_f16(scale).to_le_bytes()); }
1808 for _ in 0..shape[1] { data.extend_from_slice(&f32_to_f16(1.0).to_le_bytes()); }
1809 spec.dtype = TensorDtype::Q8_2f;
1810 spec.data = data;
1811 }
1812 specs.push(spec);
1813 }
1814 if li == 1 || li == 2 {
1815 specs.push(f32_spec(
1816 format!("{p}self_attn.sinks"),
1817 &[NH],
1818 &mut rng,
1819 2.0,
1820 0.0,
1821 ));
1822 }
1823 if li == 0 {
1824 for (n, shape) in [
1825 ("gate_proj", [DENSE_INTER, HS]),
1826 ("up_proj", [DENSE_INTER, HS]),
1827 ("down_proj", [HS, DENSE_INTER]),
1828 ] {
1829 specs.push(f32_spec(
1830 format!("{p}mlp.{n}.weight"),
1831 &shape,
1832 &mut rng,
1833 0.2,
1834 0.0,
1835 ));
1836 }
1837 continue;
1838 }
1839 specs.push(f32_spec(
1840 format!("{p}mlp.gate.weight"),
1841 &[NE, HS],
1842 &mut rng,
1843 0.4,
1844 0.0,
1845 ));
1846 specs.push(f32_spec(
1847 format!("{p}mlp.expert_bias"),
1848 &[NE],
1849 &mut rng,
1850 0.2,
1851 0.0,
1852 ));
1853 for e in 0..NE {
1854 for (n, rows, cols) in [
1855 ("gate_proj", INTER, HS),
1856 ("up_proj", INTER, HS),
1857 ("down_proj", HS, INTER),
1858 ] {
1859 let name = format!("{p}mlp.experts.{e}.{n}.weight");
1860 let data = q4tp_bytes(rows, cols, &mut rng);
1861 experts.insert(name.clone(), data.clone());
1862 specs.push(TensorSpec {
1863 name,
1864 dtype: TensorDtype::Q4TiledP,
1865 shape: vec![rows, cols],
1866 data,
1867 });
1868 }
1869 }
1870 }
1871 let header = CmfHeader {
1872 format: "cmf".into(),
1873 version: CMF_VERSION,
1874 arch: arch(),
1875 quant_type: QuantType::F32,
1876 provenance: None,
1877 tokenizer_config: None,
1878 section_hashes: None,
1879 skills: Vec::new(),
1880 shard: None,
1881 calibration: None,
1882 routing: None,
1883 genome: None,
1884 lineage: Vec::new(),
1885 router: None,
1886 segments: Vec::new(),
1887 };
1888 let dir = std::env::temp_dir().join(format!("cmf-mimo-bank-{}-{tag}", std::process::id()));
1889 let _ = std::fs::remove_dir_all(&dir);
1890 std::fs::create_dir_all(&dir).unwrap();
1891 let path = dir.join("m.cmf");
1892 CmfModel::write(&path, &header, &specs, None, None).unwrap();
1893 (dir, Arc::new(CmfModel::open(&path).unwrap()), experts)
1894 }
1895
1896 fn moe_layers(p: &Pipeline) -> Vec<(usize, &MoeFfn)> {
1897 p.weights
1898 .layers
1899 .iter()
1900 .enumerate()
1901 .filter_map(|(li, lw)| match &lw.ffn {
1902 FfnKind::Moe(m) => Some((li, m)),
1903 _ => None,
1904 })
1905 .collect()
1906 }
1907
1908 fn bank(p: &Pipeline) -> Slot {
1909 Slot::decide_with(
1910 &moe_layers(p),
1911 p.num_layers,
1912 false,
1913 Some(MoeMode::Dynamic),
1914 Some(SLOTS),
1915 )
1916 }
1917
1918 fn rel(a: &[f32], b: &[f32]) -> f32 {
1920 let d = a
1921 .iter()
1922 .zip(b)
1923 .map(|(x, y)| (x - y).abs())
1924 .fold(0f32, f32::max);
1925 d / b.iter().map(|v| v.abs()).fold(1e-30f32, f32::max)
1926 }
1927
1928 fn exact_moe(
1930 experts: &HashMap<String, Vec<u8>>,
1931 li: usize,
1932 x: &[f32],
1933 picks: &[usize],
1934 w: &[f32],
1935 ) -> Vec<f32> {
1936 let deq = |n: &str, rows: usize, cols: usize| {
1937 let mut v = vec![0f32; rows * cols];
1938 dequant_q4tp(&experts[n], rows, cols, &mut v);
1939 v
1940 };
1941 let mut out = vec![0f64; HS];
1942 for &e in picks {
1943 let p = format!("model.layers.{li}.mlp.experts.{e}.");
1944 let g = deq(&format!("{p}gate_proj.weight"), INTER, HS);
1945 let u = deq(&format!("{p}up_proj.weight"), INTER, HS);
1946 let d = deq(&format!("{p}down_proj.weight"), HS, INTER);
1947 let mut act = vec![0f64; INTER];
1948 for r in 0..INTER {
1949 let (mut gv, mut uv) = (0f64, 0f64);
1950 for c in 0..HS {
1951 gv += g[r * HS + c] as f64 * x[c] as f64;
1952 uv += u[r * HS + c] as f64 * x[c] as f64;
1953 }
1954 act[r] = gv / (1.0 + (-gv).exp()) * uv;
1955 }
1956 for r in 0..HS {
1957 let mut acc = 0f64;
1958 for c in 0..INTER {
1959 acc += d[r * INTER + c] as f64 * act[c];
1960 }
1961 out[r] += w[e] as f64 * acc;
1962 }
1963 }
1964 out.into_iter().map(|v| v as f32).collect()
1965 }
1966
1967 fn bank_ready() -> bool {
1968 crate::gpu::enabled()
1969 && crate::gpu::wgpu_active()
1970 && crate::gpu_wgpu::dsv4_global_moe_supported()
1971 && crate::gpu_wgpu::dsv4_vram_budget().is_some_and(|b| b != u64::MAX)
1974 }
1975
1976 #[test]
1977 fn unified_memory_keeps_the_expert_bank_off() {
1978 let _g = serial();
1979 if !crate::gpu::enabled() || !crate::gpu::wgpu_active()
1980 || crate::gpu_wgpu::dsv4_vram_budget() != Some(u64::MAX)
1981 {
1982 return;
1983 }
1984 let (dir, model, _) = write_model("unified-placement");
1985 let p = Pipeline::from_model(&model, SamplerConfig::default()).unwrap();
1986 assert!(!bank(&p).is_on(), "UMA must retain host-paged expert placement");
1987 drop(p);
1988 drop(model);
1989 std::fs::remove_dir_all(dir).unwrap();
1990 }
1991
1992 fn serial() -> std::sync::MutexGuard<'static, ()> {
1993 static GPU: std::sync::Mutex<()> = std::sync::Mutex::new(());
1994 GPU.lock().unwrap_or_else(|e| e.into_inner())
1995 }
1996
1997 #[test]
2001 fn bank_layer_equals_exact_expert_sum() {
2002 layer_check("layer", false);
2003 }
2004
2005 #[test]
2008 fn generic_bank_layer_equals_exact_expert_sum() {
2009 layer_check("layer-generic", true);
2010 }
2011
2012 fn layer_check(tag: &str, generic: bool) {
2013 let _g = serial();
2014 if !bank_ready() {
2015 eprintln!("skip: no wgpu adapter with an expert bank");
2016 return;
2017 }
2018 let (dir, model, experts) = write_model(tag);
2019 let p = Pipeline::from_model(&model, SamplerConfig::default()).expect("load");
2020 let mut slot = bank(&p);
2021 assert!(
2022 slot.is_on(),
2023 "the bank must come up on this adapter: {}",
2024 last_decision()
2025 );
2026 if let Slot::On(d) = &mut slot {
2027 d.fast = generic.then_some(false);
2028 }
2029 let s0 = stats();
2030 let mut rng = Rng(77);
2031 let strict = !crate::qtensor::a8w8_enabled();
2032 let (mut worst_dyn, mut worst_host, mut calls) = (0f32, 0f32, 0usize);
2033 for round in 0..16 {
2034 for (li, m) in moe_layers(&p) {
2035 let x: Vec<f32> = (0..HS)
2038 .map(|i| {
2039 ((i * 7 + li * 3) as f32 * 0.37).sin() + 0.35 * rng.f() * (round % 3) as f32
2040 })
2041 .collect();
2042 let r = crate::pipeline::moe_ffn_route(m, &x, None, None);
2043 assert_eq!(r.idx.len(), TOPK);
2044 let mix: Vec<f32> = (0..NE)
2045 .map(|e| {
2046 if r.idx.contains(&e) {
2047 r.p[e] / r.wsum
2048 } else {
2049 0.0
2050 }
2051 })
2052 .collect();
2053 let want = exact_moe(&experts, li, &x, &r.idx, &mix);
2054 let jobs: Vec<_> = r.idx.iter().map(|&e| (&m.experts[e], mix[e])).collect();
2055 let host = crate::gpu::cpu_scope(|| {
2056 crate::pipeline::moe_cold_experts_cpu(&jobs, &x, None)
2057 });
2058 let got = slot
2059 .forward(li, m, &x, &r, None)
2060 .expect("the bank served the layer");
2061 worst_dyn = worst_dyn.max(rel(&got, &want));
2062 worst_host = worst_host.max(rel(&host, &want));
2063 calls += 1;
2064 }
2065 }
2066 let s1 = stats();
2067 let (hits, fills, cold) = (s1.hits - s0.hits, s1.fills - s0.fills, s1.cold - s0.cold);
2068 let kernels = match &slot {
2069 Slot::On(d) => d.fast,
2070 _ => None,
2071 };
2072 assert_eq!(
2073 kernels,
2074 Some(!generic),
2075 "expected the {} kernels",
2076 if generic { "generic" } else { "bank" }
2077 );
2078 eprintln!(
2079 "bank layer check ({tag}): {calls} calls, {} picks: hits {hits} fills {fills} cold {cold}; \
2080 max rel |bank−exact| {worst_dyn:.2e}, |host−exact| {worst_host:.2e} (a8w8 {})",
2081 s1.picks - s0.picks,
2082 !strict
2083 );
2084 assert!(
2085 hits > 0 && fills > 0 && cold > 0,
2086 "hits {hits} fills {fills} cold {cold}"
2087 );
2088 let bound = if strict { 1e-5 } else { worst_host.max(1e-5) };
2094 assert!(
2095 worst_dyn <= bound,
2096 "bank vs exact {worst_dyn:.3e} > {bound:e}"
2097 );
2098 drop(p);
2099 let _ = std::fs::remove_dir_all(&dir);
2100 }
2101
2102 #[test]
2103 fn cold_batch_rows_equal_single_token_kernels() {
2104 let (dir, model, _) = write_model("cold-rows");
2105 let p = Pipeline::from_model(&model, SamplerConfig::default()).unwrap();
2106 let (_, m) = moe_layers(&p)[0];
2107 let xs: Vec<f32> = (0..4 * HS).map(|i| (i as f32 * 0.17).sin()).collect();
2108 let jobs = vec![
2109 vec![(&m.experts[0], 0.3), (&m.experts[1], 0.7)],
2110 vec![],
2111 vec![(&m.experts[1], 0.2), (&m.experts[0], 0.8)],
2112 vec![(&m.experts[0], 1.0)],
2113 ];
2114 crate::gpu::cpu_scope(|| {
2115 let batch = crate::pipeline::moe_cold_experts_rows_cpu(&jobs, &xs, HS, None);
2116 for (r, jobs) in jobs.iter().enumerate() {
2117 let one =
2118 crate::pipeline::moe_cold_experts_cpu(jobs, &xs[r * HS..(r + 1) * HS], None);
2119 assert_eq!(batch[r * HS..(r + 1) * HS], one, "cold row {r}");
2120 }
2121 });
2122 drop(p);
2123 std::fs::remove_dir_all(dir).unwrap();
2124 }
2125
2126 #[test]
2127 fn dynamic_attention_graph_batches_preserve_layer_keys_and_rewind() {
2128 let _g = serial();
2129 if !bank_ready() {
2130 return;
2131 }
2132 let (dir, model, _) = write_model("attn-graph");
2133 let mut batch = Pipeline::from_model(&model, SamplerConfig::default()).unwrap();
2134 let mut single = Pipeline::from_model(&model, SamplerConfig::default()).unwrap();
2135 batch.mimo_moe = bank(&batch);
2136 single.mimo_moe = bank(&single);
2137 let crate::pipeline::AttnKind::Full { wq, .. } = &batch.weights.layers[2].attn else {
2140 unreachable!()
2141 };
2142 let (owner, idx, _, _) = wq.graph_weight().unwrap();
2143 for b in 1..=4 {
2144 let xs: Vec<f32> = (0..b * HS).map(|i| (i as f32 * 0.071).cos()).collect();
2145 let mut actual = vec![0.0; b * NH * HD];
2146 let mut expected = actual.clone();
2147 assert!(crate::gpu::q82_short_rows(
2148 owner,
2149 idx,
2150 &xs,
2151 b,
2152 NH * HD,
2153 HS,
2154 &mut actual
2155 ));
2156 assert!(crate::gpu::mimo_q8_short_scope(false, || {
2157 crate::gpu::q82_short_rows(owner, idx, &xs, b, NH * HD, HS, &mut expected)
2158 }));
2159 assert_eq!(actual, expected, "q82 short/wide panel b={b}");
2160 }
2161 batch.kv_cache.max_seq_len = 512;
2165 single.kv_cache.max_seq_len = 512;
2166 for li in [2, 3] {
2167 let mut pos = 0;
2168 for b in (0..180).map(|i| i % 4 + 1) {
2169 let positions: Vec<_> = (pos..pos + b).collect();
2170 let xs: Vec<f32> = (0..b * HS)
2171 .map(|i| ((i + pos * HS) as f32 * 0.017).sin())
2172 .collect();
2173 let mut ys = xs.clone();
2174 assert!(
2175 matches!(
2176 batch.mimo_graph_layer_rows(li, &mut ys, &positions),
2177 crate::gpu::BatchGraphOutcome::Completed
2178 ),
2179 "batch admission li={li}, b={b}"
2180 );
2181 let mut want = xs;
2182 for (row, &p) in want.chunks_exact_mut(HS).zip(&positions) {
2183 let outcome = crate::gpu::mimo_q8_short_scope(false, || {
2184 crate::gpu::mimo_attention_scratch_scope(false, || {
2185 single.mimo_graph_layer_rows(li, row, &[p])
2186 })
2187 });
2188 assert!(matches!(outcome, crate::gpu::BatchGraphOutcome::Completed));
2189 }
2190 assert!(
2191 rel(&ys, &want) < 2e-5,
2192 "li={li}, b={b}: {}",
2193 rel(&ys, &want)
2194 );
2195 pos += b;
2196 assert_eq!(
2197 crate::gpu::graph_kv_stored(batch.test_graph_kv_id(), li),
2198 Some(pos)
2199 );
2200 assert_eq!(
2201 crate::gpu::graph_kv_stored(batch.test_graph_kv_id(), 0),
2202 None
2203 );
2204 if b > 1 {
2206 pos -= 1;
2207 assert!(crate::gpu::graph_kv_set_stored(
2208 batch.test_graph_kv_id(),
2209 li,
2210 pos
2211 ));
2212 assert!(crate::gpu::graph_kv_set_stored(
2213 single.test_graph_kv_id(),
2214 li,
2215 pos
2216 ));
2217 }
2218 }
2219 assert!(pos > 300);
2220 }
2221 drop(batch);
2222 drop(single);
2223 drop(model);
2224 std::fs::remove_dir_all(dir).unwrap();
2225 }
2226
2227 #[test]
2228 fn hybrid_bank_epoch_advances_at_dynamic_boundary() {
2229 let _g = serial();
2230 if !bank_ready() {
2231 return;
2232 }
2233 let (dir, model, _) = write_model("hybrid-epoch");
2234 let p = Pipeline::from_model(&model, SamplerConfig::default()).unwrap();
2235 let mut slot = bank(&p);
2236 let Slot::On(d) = &mut slot else {
2237 panic!("bank unavailable")
2238 };
2239 d.dyn_from = 2;
2242 d.placement.mode = MoeMode::Hybrid;
2243 d.placement.prefix_layers = 1;
2244 let bank = d.bank.clone();
2245 let initial = bank.lock().unwrap().tok;
2246 let x: Vec<f32> = (0..HS).map(|i| (i as f32 * 0.13).sin()).collect();
2247 for li in [2, 3] {
2248 let FfnKind::Moe(m) = &p.weights.layers[li].ffn else {
2249 unreachable!()
2250 };
2251 let route = crate::pipeline::moe_ffn_route(m, &x, None, None);
2252 assert!(slot.forward(li, m, &x, &route, None).is_some());
2253 assert_eq!(bank.lock().unwrap().tok, initial + 1, "decode layer {li}");
2254 }
2255 let xs = [x.as_slice(), x.as_slice()].concat();
2256 for li in [2, 3] {
2257 let FfnKind::Moe(m) = &p.weights.layers[li].ffn else {
2258 unreachable!()
2259 };
2260 let routes: Vec<_> = xs.chunks_exact(HS)
2261 .map(|row| crate::pipeline::moe_ffn_route(m, row, None, None))
2262 .collect();
2263 assert!(slot.forward_rows(li, m, &xs, &routes, None).is_some());
2264 assert_eq!(bank.lock().unwrap().tok, initial + 2, "verify layer {li}");
2265 }
2266 drop(p);
2267 std::fs::remove_dir_all(dir).unwrap();
2268 }
2269
2270 #[test]
2271 fn bank_batch_frame_equals_single_token_frames() {
2272 let _g = serial();
2273 if !bank_ready() {
2274 return;
2275 }
2276 let (dir, model, _) = write_model("batch-frames");
2277 let p = Pipeline::from_model(&model, SamplerConfig::default()).unwrap();
2278 let mut slot = bank(&p);
2279 assert_eq!(slot.graph_prefix_end(), Some(1));
2280 let Slot::On(d) = &mut slot else {
2281 panic!("bank unavailable")
2282 };
2283 let bank_arc = d.bank.clone();
2284 {
2285 let mut b = bank_arc.lock().unwrap();
2286 b.next_token();
2287 b.resolve(1, &[0, 1, 2, 3], &d.ids[1]).unwrap();
2288 }
2289 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
2290 loop {
2291 let mut b = bank_arc.lock().unwrap();
2292 b.drain();
2293 if b.pending() == 0 {
2294 break;
2295 }
2296 assert!(std::time::Instant::now() < deadline, "bank fills timed out");
2297 drop(b);
2298 std::thread::sleep(std::time::Duration::from_millis(5));
2299 }
2300 let mut b = bank_arc.lock().unwrap();
2301 let remap = b.resolve(1, &[0, 1, 2, 3], &d.ids[1]).unwrap();
2302 assert!((0..4).all(|e| remap[e] != u32::MAX));
2303 for rows in 1..=4 {
2304 let xs: Vec<f32> = (0..rows * HS).map(|i| (i as f32 * 0.13).cos()).collect();
2305 let sel: Vec<u32> = (0..rows * TOPK)
2306 .map(|i| if i % 5 == 0 { u32::MAX } else { remap[i % 4] })
2307 .collect();
2308 let wt: Vec<f32> = (0..sel.len())
2309 .map(|i| 0.1 + 0.03 * (i % 4) as f32)
2310 .collect();
2311 let mut batch = vec![0.0; xs.len()];
2312 assert!(crate::gpu_wgpu::mimo_bank::mimo_bank_rows(
2313 &model, &xs, &sel, &wt, INTER, rows, &mut batch
2314 ));
2315 for row in 0..rows {
2316 let mut one = vec![0.0; HS];
2317 assert!(crate::gpu_wgpu::mimo_bank::mimo_bank_frame(
2318 &model,
2319 &xs[row * HS..(row + 1) * HS],
2320 &sel[row * TOPK..(row + 1) * TOPK],
2321 &wt[row * TOPK..(row + 1) * TOPK],
2322 INTER,
2323 &mut one
2324 ));
2325 assert_eq!(batch[row * HS..(row + 1) * HS], one, "GPU row {row}/{rows}");
2326 }
2327 }
2328 drop(b);
2329 drop(p);
2330 std::fs::remove_dir_all(dir).unwrap();
2331 }
2332
2333 #[test]
2336 fn bank_decode_equals_host_decode() {
2337 let _g = serial();
2338 if !bank_ready() {
2339 eprintln!("skip: no wgpu adapter with an expert bank");
2340 return;
2341 }
2342 let (dir, model, _) = write_model("decode");
2343 let run = |bank_on: bool| -> (Vec<Vec<f32>>, Vec<u32>) {
2344 let mut p = Pipeline::from_model(&model, SamplerConfig::default()).expect("load");
2345 p.mimo_moe = if bank_on { bank(&p) } else { Slot::Off };
2346 assert_eq!(p.mimo_moe.is_on(), bank_on, "{}", last_decision());
2347 let n = p.num_layers;
2348 let mut ids: Vec<u32> = vec![3, 17, 42, 5, 9, 33, 21, 8, 60, 1, 12];
2349 let prompt = ids.len();
2350 let mut all = Vec::new();
2351 for pos in 0..prompt + 12 {
2352 let id = ids[pos];
2353 let step = |p: &mut Pipeline| {
2354 let emb = p.embed_id(id);
2355 let h = p.forward_span(&emb, pos, 0, n - 1, None).unwrap();
2356 p.logits_from_hidden(&h)
2357 };
2358 let lg = if bank_on {
2359 step(&mut p)
2360 } else {
2361 crate::gpu::cpu_scope(|| step(&mut p))
2362 };
2363 if pos + 1 >= prompt {
2364 let next = lg
2365 .iter()
2366 .enumerate()
2367 .max_by(|a, b| a.1.total_cmp(b.1))
2368 .map(|(i, _)| i as u32)
2369 .unwrap();
2370 ids.push(next);
2371 }
2372 all.push(lg);
2373 }
2374 (all, ids)
2375 };
2376 let s0 = stats();
2377 let (host, host_ids) = run(false);
2378 let s1 = stats();
2379 assert_eq!(s1.calls, s0.calls, "the host run must not touch the bank");
2380 let (dynm, dyn_ids) = run(true);
2381 let s2 = stats();
2382 let worst = host
2383 .iter()
2384 .zip(&dynm)
2385 .map(|(h, d)| rel(d, h))
2386 .fold(0f32, f32::max);
2387 let strict = !crate::qtensor::a8w8_enabled();
2388 eprintln!(
2389 "bank decode check: {} steps, bank calls {} hits {} fills {} cold {}; logits max rel \
2390 {worst:.2e}; greedy {:?} vs host {:?}",
2391 host.len(),
2392 s2.calls - s1.calls,
2393 s2.hits - s1.hits,
2394 s2.fills - s1.fills,
2395 s2.cold - s1.cold,
2396 &dyn_ids[11..],
2397 &host_ids[11..],
2398 );
2399 assert_eq!(
2400 s2.calls - s1.calls,
2401 (host.len() * 3) as u64,
2402 "every MoE call took the bank"
2403 );
2404 assert!(
2405 s2.hits > s1.hits && s2.cold > s1.cold,
2406 "both resident and cold picks ran"
2407 );
2408 if !strict {
2409 eprintln!("bank decode check: A8W8 host arm — bounds need CMF_SDOT=0, not asserted");
2413 let _ = std::fs::remove_dir_all(&dir);
2414 return;
2415 }
2416 assert!(worst < 1e-4, "logits bank vs host {worst:.3e} ≥ 1e-4");
2417 let am = |v: &[f32]| {
2420 v.iter()
2421 .enumerate()
2422 .max_by(|a, b| a.1.total_cmp(b.1))
2423 .unwrap()
2424 .0
2425 };
2426 let (a_host, a_dyn): (Vec<usize>, Vec<usize>) =
2427 host.iter().zip(&dynm).map(|(h, d)| (am(h), am(d))).unzip();
2428 assert_eq!(a_dyn, a_host, "per-step argmax differs");
2429 assert_eq!(dyn_ids, host_ids, "greedy tokens differ");
2430 let _ = std::fs::remove_dir_all(&dir);
2431 }
2432}