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 };
1884 let dir = std::env::temp_dir().join(format!("cmf-mimo-bank-{}-{tag}", std::process::id()));
1885 let _ = std::fs::remove_dir_all(&dir);
1886 std::fs::create_dir_all(&dir).unwrap();
1887 let path = dir.join("m.cmf");
1888 CmfModel::write(&path, &header, &specs, None, None).unwrap();
1889 (dir, Arc::new(CmfModel::open(&path).unwrap()), experts)
1890 }
1891
1892 fn moe_layers(p: &Pipeline) -> Vec<(usize, &MoeFfn)> {
1893 p.weights
1894 .layers
1895 .iter()
1896 .enumerate()
1897 .filter_map(|(li, lw)| match &lw.ffn {
1898 FfnKind::Moe(m) => Some((li, m)),
1899 _ => None,
1900 })
1901 .collect()
1902 }
1903
1904 fn bank(p: &Pipeline) -> Slot {
1905 Slot::decide_with(
1906 &moe_layers(p),
1907 p.num_layers,
1908 false,
1909 Some(MoeMode::Dynamic),
1910 Some(SLOTS),
1911 )
1912 }
1913
1914 fn rel(a: &[f32], b: &[f32]) -> f32 {
1916 let d = a
1917 .iter()
1918 .zip(b)
1919 .map(|(x, y)| (x - y).abs())
1920 .fold(0f32, f32::max);
1921 d / b.iter().map(|v| v.abs()).fold(1e-30f32, f32::max)
1922 }
1923
1924 fn exact_moe(
1926 experts: &HashMap<String, Vec<u8>>,
1927 li: usize,
1928 x: &[f32],
1929 picks: &[usize],
1930 w: &[f32],
1931 ) -> Vec<f32> {
1932 let deq = |n: &str, rows: usize, cols: usize| {
1933 let mut v = vec![0f32; rows * cols];
1934 dequant_q4tp(&experts[n], rows, cols, &mut v);
1935 v
1936 };
1937 let mut out = vec![0f64; HS];
1938 for &e in picks {
1939 let p = format!("model.layers.{li}.mlp.experts.{e}.");
1940 let g = deq(&format!("{p}gate_proj.weight"), INTER, HS);
1941 let u = deq(&format!("{p}up_proj.weight"), INTER, HS);
1942 let d = deq(&format!("{p}down_proj.weight"), HS, INTER);
1943 let mut act = vec![0f64; INTER];
1944 for r in 0..INTER {
1945 let (mut gv, mut uv) = (0f64, 0f64);
1946 for c in 0..HS {
1947 gv += g[r * HS + c] as f64 * x[c] as f64;
1948 uv += u[r * HS + c] as f64 * x[c] as f64;
1949 }
1950 act[r] = gv / (1.0 + (-gv).exp()) * uv;
1951 }
1952 for r in 0..HS {
1953 let mut acc = 0f64;
1954 for c in 0..INTER {
1955 acc += d[r * INTER + c] as f64 * act[c];
1956 }
1957 out[r] += w[e] as f64 * acc;
1958 }
1959 }
1960 out.into_iter().map(|v| v as f32).collect()
1961 }
1962
1963 fn bank_ready() -> bool {
1964 crate::gpu::enabled()
1965 && crate::gpu::wgpu_active()
1966 && crate::gpu_wgpu::dsv4_global_moe_supported()
1967 && crate::gpu_wgpu::dsv4_vram_budget().is_some_and(|b| b != u64::MAX)
1970 }
1971
1972 #[test]
1973 fn unified_memory_keeps_the_expert_bank_off() {
1974 let _g = serial();
1975 if !crate::gpu::enabled() || !crate::gpu::wgpu_active()
1976 || crate::gpu_wgpu::dsv4_vram_budget() != Some(u64::MAX)
1977 {
1978 return;
1979 }
1980 let (dir, model, _) = write_model("unified-placement");
1981 let p = Pipeline::from_model(&model, SamplerConfig::default()).unwrap();
1982 assert!(!bank(&p).is_on(), "UMA must retain host-paged expert placement");
1983 drop(p);
1984 drop(model);
1985 std::fs::remove_dir_all(dir).unwrap();
1986 }
1987
1988 fn serial() -> std::sync::MutexGuard<'static, ()> {
1989 static GPU: std::sync::Mutex<()> = std::sync::Mutex::new(());
1990 GPU.lock().unwrap_or_else(|e| e.into_inner())
1991 }
1992
1993 #[test]
1997 fn bank_layer_equals_exact_expert_sum() {
1998 layer_check("layer", false);
1999 }
2000
2001 #[test]
2004 fn generic_bank_layer_equals_exact_expert_sum() {
2005 layer_check("layer-generic", true);
2006 }
2007
2008 fn layer_check(tag: &str, generic: bool) {
2009 let _g = serial();
2010 if !bank_ready() {
2011 eprintln!("skip: no wgpu adapter with an expert bank");
2012 return;
2013 }
2014 let (dir, model, experts) = write_model(tag);
2015 let p = Pipeline::from_model(&model, SamplerConfig::default()).expect("load");
2016 let mut slot = bank(&p);
2017 assert!(
2018 slot.is_on(),
2019 "the bank must come up on this adapter: {}",
2020 last_decision()
2021 );
2022 if let Slot::On(d) = &mut slot {
2023 d.fast = generic.then_some(false);
2024 }
2025 let s0 = stats();
2026 let mut rng = Rng(77);
2027 let strict = !crate::qtensor::a8w8_enabled();
2028 let (mut worst_dyn, mut worst_host, mut calls) = (0f32, 0f32, 0usize);
2029 for round in 0..16 {
2030 for (li, m) in moe_layers(&p) {
2031 let x: Vec<f32> = (0..HS)
2034 .map(|i| {
2035 ((i * 7 + li * 3) as f32 * 0.37).sin() + 0.35 * rng.f() * (round % 3) as f32
2036 })
2037 .collect();
2038 let r = crate::pipeline::moe_ffn_route(m, &x, None, None);
2039 assert_eq!(r.idx.len(), TOPK);
2040 let mix: Vec<f32> = (0..NE)
2041 .map(|e| {
2042 if r.idx.contains(&e) {
2043 r.p[e] / r.wsum
2044 } else {
2045 0.0
2046 }
2047 })
2048 .collect();
2049 let want = exact_moe(&experts, li, &x, &r.idx, &mix);
2050 let jobs: Vec<_> = r.idx.iter().map(|&e| (&m.experts[e], mix[e])).collect();
2051 let host = crate::gpu::cpu_scope(|| {
2052 crate::pipeline::moe_cold_experts_cpu(&jobs, &x, None)
2053 });
2054 let got = slot
2055 .forward(li, m, &x, &r, None)
2056 .expect("the bank served the layer");
2057 worst_dyn = worst_dyn.max(rel(&got, &want));
2058 worst_host = worst_host.max(rel(&host, &want));
2059 calls += 1;
2060 }
2061 }
2062 let s1 = stats();
2063 let (hits, fills, cold) = (s1.hits - s0.hits, s1.fills - s0.fills, s1.cold - s0.cold);
2064 let kernels = match &slot {
2065 Slot::On(d) => d.fast,
2066 _ => None,
2067 };
2068 assert_eq!(
2069 kernels,
2070 Some(!generic),
2071 "expected the {} kernels",
2072 if generic { "generic" } else { "bank" }
2073 );
2074 eprintln!(
2075 "bank layer check ({tag}): {calls} calls, {} picks: hits {hits} fills {fills} cold {cold}; \
2076 max rel |bank−exact| {worst_dyn:.2e}, |host−exact| {worst_host:.2e} (a8w8 {})",
2077 s1.picks - s0.picks,
2078 !strict
2079 );
2080 assert!(
2081 hits > 0 && fills > 0 && cold > 0,
2082 "hits {hits} fills {fills} cold {cold}"
2083 );
2084 let bound = if strict { 1e-5 } else { worst_host.max(1e-5) };
2090 assert!(
2091 worst_dyn <= bound,
2092 "bank vs exact {worst_dyn:.3e} > {bound:e}"
2093 );
2094 drop(p);
2095 let _ = std::fs::remove_dir_all(&dir);
2096 }
2097
2098 #[test]
2099 fn cold_batch_rows_equal_single_token_kernels() {
2100 let (dir, model, _) = write_model("cold-rows");
2101 let p = Pipeline::from_model(&model, SamplerConfig::default()).unwrap();
2102 let (_, m) = moe_layers(&p)[0];
2103 let xs: Vec<f32> = (0..4 * HS).map(|i| (i as f32 * 0.17).sin()).collect();
2104 let jobs = vec![
2105 vec![(&m.experts[0], 0.3), (&m.experts[1], 0.7)],
2106 vec![],
2107 vec![(&m.experts[1], 0.2), (&m.experts[0], 0.8)],
2108 vec![(&m.experts[0], 1.0)],
2109 ];
2110 crate::gpu::cpu_scope(|| {
2111 let batch = crate::pipeline::moe_cold_experts_rows_cpu(&jobs, &xs, HS, None);
2112 for (r, jobs) in jobs.iter().enumerate() {
2113 let one =
2114 crate::pipeline::moe_cold_experts_cpu(jobs, &xs[r * HS..(r + 1) * HS], None);
2115 assert_eq!(batch[r * HS..(r + 1) * HS], one, "cold row {r}");
2116 }
2117 });
2118 drop(p);
2119 std::fs::remove_dir_all(dir).unwrap();
2120 }
2121
2122 #[test]
2123 fn dynamic_attention_graph_batches_preserve_layer_keys_and_rewind() {
2124 let _g = serial();
2125 if !bank_ready() {
2126 return;
2127 }
2128 let (dir, model, _) = write_model("attn-graph");
2129 let mut batch = Pipeline::from_model(&model, SamplerConfig::default()).unwrap();
2130 let mut single = Pipeline::from_model(&model, SamplerConfig::default()).unwrap();
2131 batch.mimo_moe = bank(&batch);
2132 single.mimo_moe = bank(&single);
2133 let crate::pipeline::AttnKind::Full { wq, .. } = &batch.weights.layers[2].attn else {
2136 unreachable!()
2137 };
2138 let (owner, idx, _, _) = wq.graph_weight().unwrap();
2139 for b in 1..=4 {
2140 let xs: Vec<f32> = (0..b * HS).map(|i| (i as f32 * 0.071).cos()).collect();
2141 let mut actual = vec![0.0; b * NH * HD];
2142 let mut expected = actual.clone();
2143 assert!(crate::gpu::q82_short_rows(
2144 owner,
2145 idx,
2146 &xs,
2147 b,
2148 NH * HD,
2149 HS,
2150 &mut actual
2151 ));
2152 assert!(crate::gpu::mimo_q8_short_scope(false, || {
2153 crate::gpu::q82_short_rows(owner, idx, &xs, b, NH * HD, HS, &mut expected)
2154 }));
2155 assert_eq!(actual, expected, "q82 short/wide panel b={b}");
2156 }
2157 batch.kv_cache.max_seq_len = 512;
2161 single.kv_cache.max_seq_len = 512;
2162 for li in [2, 3] {
2163 let mut pos = 0;
2164 for b in (0..180).map(|i| i % 4 + 1) {
2165 let positions: Vec<_> = (pos..pos + b).collect();
2166 let xs: Vec<f32> = (0..b * HS)
2167 .map(|i| ((i + pos * HS) as f32 * 0.017).sin())
2168 .collect();
2169 let mut ys = xs.clone();
2170 assert!(
2171 matches!(
2172 batch.mimo_graph_layer_rows(li, &mut ys, &positions),
2173 crate::gpu::BatchGraphOutcome::Completed
2174 ),
2175 "batch admission li={li}, b={b}"
2176 );
2177 let mut want = xs;
2178 for (row, &p) in want.chunks_exact_mut(HS).zip(&positions) {
2179 let outcome = crate::gpu::mimo_q8_short_scope(false, || {
2180 crate::gpu::mimo_attention_scratch_scope(false, || {
2181 single.mimo_graph_layer_rows(li, row, &[p])
2182 })
2183 });
2184 assert!(matches!(outcome, crate::gpu::BatchGraphOutcome::Completed));
2185 }
2186 assert!(
2187 rel(&ys, &want) < 2e-5,
2188 "li={li}, b={b}: {}",
2189 rel(&ys, &want)
2190 );
2191 pos += b;
2192 assert_eq!(
2193 crate::gpu::graph_kv_stored(batch.test_graph_kv_id(), li),
2194 Some(pos)
2195 );
2196 assert_eq!(
2197 crate::gpu::graph_kv_stored(batch.test_graph_kv_id(), 0),
2198 None
2199 );
2200 if b > 1 {
2202 pos -= 1;
2203 assert!(crate::gpu::graph_kv_set_stored(
2204 batch.test_graph_kv_id(),
2205 li,
2206 pos
2207 ));
2208 assert!(crate::gpu::graph_kv_set_stored(
2209 single.test_graph_kv_id(),
2210 li,
2211 pos
2212 ));
2213 }
2214 }
2215 assert!(pos > 300);
2216 }
2217 drop(batch);
2218 drop(single);
2219 drop(model);
2220 std::fs::remove_dir_all(dir).unwrap();
2221 }
2222
2223 #[test]
2224 fn hybrid_bank_epoch_advances_at_dynamic_boundary() {
2225 let _g = serial();
2226 if !bank_ready() {
2227 return;
2228 }
2229 let (dir, model, _) = write_model("hybrid-epoch");
2230 let p = Pipeline::from_model(&model, SamplerConfig::default()).unwrap();
2231 let mut slot = bank(&p);
2232 let Slot::On(d) = &mut slot else {
2233 panic!("bank unavailable")
2234 };
2235 d.dyn_from = 2;
2238 d.placement.mode = MoeMode::Hybrid;
2239 d.placement.prefix_layers = 1;
2240 let bank = d.bank.clone();
2241 let initial = bank.lock().unwrap().tok;
2242 let x: Vec<f32> = (0..HS).map(|i| (i as f32 * 0.13).sin()).collect();
2243 for li in [2, 3] {
2244 let FfnKind::Moe(m) = &p.weights.layers[li].ffn else {
2245 unreachable!()
2246 };
2247 let route = crate::pipeline::moe_ffn_route(m, &x, None, None);
2248 assert!(slot.forward(li, m, &x, &route, None).is_some());
2249 assert_eq!(bank.lock().unwrap().tok, initial + 1, "decode layer {li}");
2250 }
2251 let xs = [x.as_slice(), x.as_slice()].concat();
2252 for li in [2, 3] {
2253 let FfnKind::Moe(m) = &p.weights.layers[li].ffn else {
2254 unreachable!()
2255 };
2256 let routes: Vec<_> = xs.chunks_exact(HS)
2257 .map(|row| crate::pipeline::moe_ffn_route(m, row, None, None))
2258 .collect();
2259 assert!(slot.forward_rows(li, m, &xs, &routes, None).is_some());
2260 assert_eq!(bank.lock().unwrap().tok, initial + 2, "verify layer {li}");
2261 }
2262 drop(p);
2263 std::fs::remove_dir_all(dir).unwrap();
2264 }
2265
2266 #[test]
2267 fn bank_batch_frame_equals_single_token_frames() {
2268 let _g = serial();
2269 if !bank_ready() {
2270 return;
2271 }
2272 let (dir, model, _) = write_model("batch-frames");
2273 let p = Pipeline::from_model(&model, SamplerConfig::default()).unwrap();
2274 let mut slot = bank(&p);
2275 assert_eq!(slot.graph_prefix_end(), Some(1));
2276 let Slot::On(d) = &mut slot else {
2277 panic!("bank unavailable")
2278 };
2279 let bank_arc = d.bank.clone();
2280 {
2281 let mut b = bank_arc.lock().unwrap();
2282 b.next_token();
2283 b.resolve(1, &[0, 1, 2, 3], &d.ids[1]).unwrap();
2284 }
2285 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
2286 loop {
2287 let mut b = bank_arc.lock().unwrap();
2288 b.drain();
2289 if b.pending() == 0 {
2290 break;
2291 }
2292 assert!(std::time::Instant::now() < deadline, "bank fills timed out");
2293 drop(b);
2294 std::thread::sleep(std::time::Duration::from_millis(5));
2295 }
2296 let mut b = bank_arc.lock().unwrap();
2297 let remap = b.resolve(1, &[0, 1, 2, 3], &d.ids[1]).unwrap();
2298 assert!((0..4).all(|e| remap[e] != u32::MAX));
2299 for rows in 1..=4 {
2300 let xs: Vec<f32> = (0..rows * HS).map(|i| (i as f32 * 0.13).cos()).collect();
2301 let sel: Vec<u32> = (0..rows * TOPK)
2302 .map(|i| if i % 5 == 0 { u32::MAX } else { remap[i % 4] })
2303 .collect();
2304 let wt: Vec<f32> = (0..sel.len())
2305 .map(|i| 0.1 + 0.03 * (i % 4) as f32)
2306 .collect();
2307 let mut batch = vec![0.0; xs.len()];
2308 assert!(crate::gpu_wgpu::mimo_bank::mimo_bank_rows(
2309 &model, &xs, &sel, &wt, INTER, rows, &mut batch
2310 ));
2311 for row in 0..rows {
2312 let mut one = vec![0.0; HS];
2313 assert!(crate::gpu_wgpu::mimo_bank::mimo_bank_frame(
2314 &model,
2315 &xs[row * HS..(row + 1) * HS],
2316 &sel[row * TOPK..(row + 1) * TOPK],
2317 &wt[row * TOPK..(row + 1) * TOPK],
2318 INTER,
2319 &mut one
2320 ));
2321 assert_eq!(batch[row * HS..(row + 1) * HS], one, "GPU row {row}/{rows}");
2322 }
2323 }
2324 drop(b);
2325 drop(p);
2326 std::fs::remove_dir_all(dir).unwrap();
2327 }
2328
2329 #[test]
2332 fn bank_decode_equals_host_decode() {
2333 let _g = serial();
2334 if !bank_ready() {
2335 eprintln!("skip: no wgpu adapter with an expert bank");
2336 return;
2337 }
2338 let (dir, model, _) = write_model("decode");
2339 let run = |bank_on: bool| -> (Vec<Vec<f32>>, Vec<u32>) {
2340 let mut p = Pipeline::from_model(&model, SamplerConfig::default()).expect("load");
2341 p.mimo_moe = if bank_on { bank(&p) } else { Slot::Off };
2342 assert_eq!(p.mimo_moe.is_on(), bank_on, "{}", last_decision());
2343 let n = p.num_layers;
2344 let mut ids: Vec<u32> = vec![3, 17, 42, 5, 9, 33, 21, 8, 60, 1, 12];
2345 let prompt = ids.len();
2346 let mut all = Vec::new();
2347 for pos in 0..prompt + 12 {
2348 let id = ids[pos];
2349 let step = |p: &mut Pipeline| {
2350 let emb = p.embed_id(id);
2351 let h = p.forward_span(&emb, pos, 0, n - 1, None).unwrap();
2352 p.logits_from_hidden(&h)
2353 };
2354 let lg = if bank_on {
2355 step(&mut p)
2356 } else {
2357 crate::gpu::cpu_scope(|| step(&mut p))
2358 };
2359 if pos + 1 >= prompt {
2360 let next = lg
2361 .iter()
2362 .enumerate()
2363 .max_by(|a, b| a.1.total_cmp(b.1))
2364 .map(|(i, _)| i as u32)
2365 .unwrap();
2366 ids.push(next);
2367 }
2368 all.push(lg);
2369 }
2370 (all, ids)
2371 };
2372 let s0 = stats();
2373 let (host, host_ids) = run(false);
2374 let s1 = stats();
2375 assert_eq!(s1.calls, s0.calls, "the host run must not touch the bank");
2376 let (dynm, dyn_ids) = run(true);
2377 let s2 = stats();
2378 let worst = host
2379 .iter()
2380 .zip(&dynm)
2381 .map(|(h, d)| rel(d, h))
2382 .fold(0f32, f32::max);
2383 let strict = !crate::qtensor::a8w8_enabled();
2384 eprintln!(
2385 "bank decode check: {} steps, bank calls {} hits {} fills {} cold {}; logits max rel \
2386 {worst:.2e}; greedy {:?} vs host {:?}",
2387 host.len(),
2388 s2.calls - s1.calls,
2389 s2.hits - s1.hits,
2390 s2.fills - s1.fills,
2391 s2.cold - s1.cold,
2392 &dyn_ids[11..],
2393 &host_ids[11..],
2394 );
2395 assert_eq!(
2396 s2.calls - s1.calls,
2397 (host.len() * 3) as u64,
2398 "every MoE call took the bank"
2399 );
2400 assert!(
2401 s2.hits > s1.hits && s2.cold > s1.cold,
2402 "both resident and cold picks ran"
2403 );
2404 if !strict {
2405 eprintln!("bank decode check: A8W8 host arm — bounds need CMF_SDOT=0, not asserted");
2409 let _ = std::fs::remove_dir_all(&dir);
2410 return;
2411 }
2412 assert!(worst < 1e-4, "logits bank vs host {worst:.3e} ≥ 1e-4");
2413 let am = |v: &[f32]| {
2416 v.iter()
2417 .enumerate()
2418 .max_by(|a, b| a.1.total_cmp(b.1))
2419 .unwrap()
2420 .0
2421 };
2422 let (a_host, a_dyn): (Vec<usize>, Vec<usize>) =
2423 host.iter().zip(&dynm).map(|(h, d)| (am(h), am(d))).unzip();
2424 assert_eq!(a_dyn, a_host, "per-step argmax differs");
2425 assert_eq!(dyn_ids, host_ids, "greedy tokens differ");
2426 let _ = std::fs::remove_dir_all(&dir);
2427 }
2428}