1use std::collections::{HashMap, HashSet};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum ExpertRefreshPolicy {
18 EveryForward,
20 EveryDecodeSteps(usize),
22 EveryDenoiseSteps(usize),
25}
26
27impl Default for ExpertRefreshPolicy {
28 fn default() -> Self {
29 Self::EveryDenoiseSteps(1)
30 }
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum MoEExecMode {
36 Reuse,
38 Refresh,
40}
41
42#[derive(Debug, Clone, Copy, PartialEq)]
46pub struct HysteresisConfig {
47 pub margin: f32,
51 pub min_dwell: u64,
54}
55
56impl Default for HysteresisConfig {
57 fn default() -> Self {
58 Self {
59 margin: 0.0,
60 min_dwell: 0,
61 }
62 }
63}
64
65impl HysteresisConfig {
66 pub fn is_active(&self) -> bool {
68 self.margin > 0.0 || self.min_dwell > 0
69 }
70}
71
72#[derive(Debug, Clone)]
74pub struct ExpertPoolConfig {
75 pub num_experts: usize,
76 pub gpu_budget: usize,
78 pub refresh: ExpertRefreshPolicy,
79 pub hysteresis: HysteresisConfig,
81}
82
83impl ExpertPoolConfig {
84 pub fn new(num_experts: usize, gpu_budget: usize, refresh: ExpertRefreshPolicy) -> Self {
85 Self {
86 num_experts,
87 gpu_budget: gpu_budget.min(num_experts),
88 refresh,
89 hysteresis: HysteresisConfig::default(),
90 }
91 }
92
93 pub fn with_hysteresis(mut self, hysteresis: HysteresisConfig) -> Self {
95 self.hysteresis = hysteresis;
96 self
97 }
98
99 pub fn all_resident(num_experts: usize) -> Self {
101 Self::new(num_experts, num_experts, ExpertRefreshPolicy::EveryForward)
102 }
103}
104
105#[derive(Debug, Clone, PartialEq, Eq)]
107pub struct ExpertRefreshResult {
108 pub target_gpu: Vec<usize>,
109 pub promotions: usize,
110 pub demotions: usize,
111}
112
113#[derive(Debug, Clone, Default, PartialEq, Eq)]
115pub struct ExpertPoolStats {
116 pub refreshes: u64,
117 pub promotions: u64,
118 pub demotions: u64,
119}
120
121#[derive(Debug, Clone)]
123pub struct ExpertPool {
124 num_experts: usize,
125 gpu_budget: usize,
126 refresh: ExpertRefreshPolicy,
127 resident: HashSet<usize>,
128 steps_since_refresh: usize,
130 stats: ExpertPoolStats,
131 hysteresis: HysteresisConfig,
133 refresh_clock: u64,
136 resident_since: HashMap<usize, u64>,
138}
139
140impl ExpertPool {
141 pub fn new(config: ExpertPoolConfig) -> Self {
142 let gpu_budget = config.gpu_budget.min(config.num_experts);
143 let mut resident = HashSet::new();
144 let mut resident_since = HashMap::new();
145 for e in 0..gpu_budget {
146 resident.insert(e);
147 resident_since.insert(e, 0);
148 }
149 Self {
150 num_experts: config.num_experts,
151 gpu_budget,
152 refresh: config.refresh,
153 resident,
154 steps_since_refresh: 0,
155 stats: ExpertPoolStats::default(),
156 hysteresis: config.hysteresis,
157 refresh_clock: 0,
158 resident_since,
159 }
160 }
161
162 pub fn num_experts(&self) -> usize {
163 self.num_experts
164 }
165
166 pub fn gpu_budget(&self) -> usize {
167 self.gpu_budget
168 }
169
170 pub fn refresh_policy(&self) -> ExpertRefreshPolicy {
171 self.refresh
172 }
173
174 pub fn stats(&self) -> &ExpertPoolStats {
175 &self.stats
176 }
177
178 pub fn reset_step_stats(&mut self) {
180 self.stats = ExpertPoolStats::default();
181 }
182
183 pub fn resident_gpu_experts(&self) -> impl Iterator<Item = usize> + '_ {
184 self.resident.iter().copied()
185 }
186
187 pub fn resident_mask(&self) -> Vec<bool> {
189 (0..self.num_experts)
190 .map(|e| self.resident.contains(&e))
191 .collect()
192 }
193
194 pub fn is_gpu_resident(&self, expert: usize) -> bool {
195 self.resident.contains(&expert)
196 }
197
198 pub fn offload_enabled(&self) -> bool {
200 self.gpu_budget < self.num_experts
201 }
202
203 pub fn should_refresh(
205 &self,
206 mode: MoEExecMode,
207 denoise_step: usize,
208 is_prefill_block: bool,
209 ) -> bool {
210 if !self.offload_enabled() {
211 return false;
212 }
213 match mode {
214 MoEExecMode::Refresh => true,
215 MoEExecMode::Reuse => {
216 if is_prefill_block {
217 return true;
218 }
219 match self.refresh {
220 ExpertRefreshPolicy::EveryForward => true,
221 ExpertRefreshPolicy::EveryDecodeSteps(n)
222 | ExpertRefreshPolicy::EveryDenoiseSteps(n) => {
223 let interval = n.max(1);
224 denoise_step.is_multiple_of(interval)
225 }
226 }
227 }
228 }
229 }
230
231 pub fn on_forward_step(
233 &mut self,
234 mode: MoEExecMode,
235 denoise_step: usize,
236 is_prefill_block: bool,
237 ) -> bool {
238 let refresh = self.should_refresh(mode, denoise_step, is_prefill_block);
239 if refresh {
240 self.steps_since_refresh = 0;
241 } else {
242 self.steps_since_refresh = self.steps_since_refresh.saturating_add(1);
243 }
244 refresh
245 }
246
247 pub fn count_hits(expert_idx: &[u32], num_experts: usize) -> Vec<u64> {
249 let mut counts = vec![0u64; num_experts];
250 for &e in expert_idx {
251 let e = e as usize;
252 if e < num_experts {
253 counts[e] += 1;
254 }
255 }
256 counts
257 }
258
259 pub fn target_gpu_from_counts(counts: &[u64], gpu_budget: usize) -> Vec<usize> {
261 let mut ranked: Vec<(u64, usize)> = counts
262 .iter()
263 .enumerate()
264 .filter(|&(_, c)| *c > 0)
265 .map(|(e, &c)| (c, e))
266 .collect();
267 ranked.sort_by(|a, b| b.0.cmp(&a.0).then_with(|| a.1.cmp(&b.1)));
268 ranked
269 .into_iter()
270 .take(gpu_budget)
271 .map(|(_, e)| e)
272 .collect()
273 }
274
275 pub fn refresh_from_indices(&mut self, expert_idx: &[u32]) -> ExpertRefreshResult {
281 let counts = Self::count_hits(expert_idx, self.num_experts);
282 self.refresh_from_counts(&counts)
283 }
284
285 pub fn refresh_from_counts(&mut self, counts: &[u64]) -> ExpertRefreshResult {
288 if self.hysteresis.is_active() {
289 self.apply_hysteresis_placement(counts)
290 } else {
291 let target_order = Self::target_gpu_from_counts(counts, self.gpu_budget);
292 self.apply_target_placement(&target_order)
293 }
294 }
295
296 pub fn resident_since(&self, expert: usize) -> u64 {
298 self.resident_since.get(&expert).copied().unwrap_or(0)
299 }
300
301 pub fn hysteresis(&self) -> HysteresisConfig {
302 self.hysteresis
303 }
304
305 fn touch_resident_since(&mut self, e: usize) {
308 self.resident_since.insert(e, self.refresh_clock);
309 }
310
311 pub fn apply_target_placement(&mut self, target_order: &[usize]) -> ExpertRefreshResult {
313 self.refresh_clock += 1;
314 let clock = self.refresh_clock;
315 let target_set: HashSet<usize> = target_order.iter().copied().collect();
316
317 let to_promote: Vec<usize> = target_order
318 .iter()
319 .copied()
320 .filter(|e| !self.resident.contains(e))
321 .collect();
322 let can_demote: Vec<usize> = self
323 .resident
324 .iter()
325 .copied()
326 .filter(|e| !target_set.contains(e))
327 .collect();
328 let to_demote: Vec<usize> = can_demote.iter().copied().take(to_promote.len()).collect();
329
330 let mut new_resident = target_set;
331 for e in can_demote.iter().skip(to_promote.len()) {
332 new_resident.insert(*e);
333 }
334
335 for &e in &to_promote {
338 self.resident_since.insert(e, clock);
339 }
340 self.resident_since.retain(|e, _| new_resident.contains(e));
341
342 let promotions = to_promote.len();
343 let demotions = to_demote.len();
344 self.resident = new_resident;
345 self.stats.refreshes += 1;
346 self.stats.promotions += promotions as u64;
347 self.stats.demotions += demotions as u64;
348
349 ExpertRefreshResult {
350 target_gpu: target_order.to_vec(),
351 promotions,
352 demotions,
353 }
354 }
355
356 fn apply_hysteresis_placement(&mut self, counts: &[u64]) -> ExpertRefreshResult {
363 self.refresh_clock += 1;
364 let clock = self.refresh_clock;
365 let budget = self.gpu_budget;
366 let margin = self.hysteresis.margin.max(0.0) as f64;
367 let dwell = self.hysteresis.min_dwell;
368 let hits = |e: usize| counts.get(e).copied().unwrap_or(0);
369
370 let desired = Self::target_gpu_from_counts(counts, budget);
371 let desired_set: HashSet<usize> = desired.iter().copied().collect();
372
373 let to_add: Vec<usize> = desired
375 .iter()
376 .copied()
377 .filter(|e| !self.resident.contains(e))
378 .collect();
379
380 let mut evictable: Vec<usize> = self
383 .resident
384 .iter()
385 .copied()
386 .filter(|e| !desired_set.contains(e))
387 .collect();
388 evictable.sort_by(|&a, &b| {
389 hits(a)
390 .cmp(&hits(b))
391 .then_with(|| self.resident_since(a).cmp(&self.resident_since(b)))
392 .then_with(|| a.cmp(&b))
393 });
394
395 let mut promotions = 0usize;
396 let mut demotions = 0usize;
397 let mut ev_idx = 0usize;
398
399 for c in to_add {
400 let c_hits = hits(c) as f64;
401 if self.resident.len() < budget {
402 self.resident.insert(c);
404 self.touch_resident_since(c);
405 promotions += 1;
406 continue;
407 }
408 let mut victim: Option<usize> = None;
410 while ev_idx < evictable.len() {
411 let i = evictable[ev_idx];
412 let held = clock.saturating_sub(self.resident_since(i));
413 if held < dwell {
414 ev_idx += 1;
416 continue;
417 }
418 if c_hits >= hits(i) as f64 * (1.0 + margin) {
419 victim = Some(i);
420 ev_idx += 1;
421 }
422 break;
424 }
425 match victim {
426 Some(i) => {
427 self.resident.remove(&i);
428 self.resident_since.remove(&i);
429 self.resident.insert(c);
430 self.touch_resident_since(c);
431 promotions += 1;
432 demotions += 1;
433 }
434 None => break, }
436 }
437
438 self.stats.refreshes += 1;
439 self.stats.promotions += promotions as u64;
440 self.stats.demotions += demotions as u64;
441
442 ExpertRefreshResult {
443 target_gpu: desired,
444 promotions,
445 demotions,
446 }
447 }
448}
449
450pub fn per_layer_resident_masks(pools: &[ExpertPool]) -> Vec<Vec<bool>> {
452 pools.iter().map(|p| p.resident_mask()).collect()
453}
454
455pub fn merged_resident_mask(pools: &[ExpertPool]) -> Vec<bool> {
457 let Some(first) = pools.first() else {
458 return Vec::new();
459 };
460 let n = first.num_experts();
461 (0..n)
462 .map(|e| pools.iter().any(|p| p.is_gpu_resident(e)))
463 .collect()
464}
465
466pub fn gpu_expert_budget_from_vram(
467 free_bytes: usize,
468 reserve_bytes: usize,
469 expert_param_bytes: usize,
470 num_moe_layers: usize,
471 max_gpu_experts_per_layer: usize,
472 num_experts: usize,
473) -> usize {
474 if expert_param_bytes == 0 || num_moe_layers == 0 {
475 return max_gpu_experts_per_layer.min(num_experts);
476 }
477 let usable = free_bytes.saturating_sub(reserve_bytes);
478 let per_layer = usable / (expert_param_bytes.saturating_mul(num_moe_layers));
479 per_layer.min(max_gpu_experts_per_layer).min(num_experts)
480}
481
482#[cfg(test)]
483mod tests {
484 use super::*;
485
486 #[test]
487 fn per_layer_masks_differ_from_merged_union() {
488 let mut p0 = ExpertPool::new(ExpertPoolConfig::new(
489 4,
490 2,
491 ExpertRefreshPolicy::EveryForward,
492 ));
493 let mut p1 = ExpertPool::new(ExpertPoolConfig::new(
494 4,
495 2,
496 ExpertRefreshPolicy::EveryForward,
497 ));
498 p0.refresh_from_indices(&[0, 1]);
499 p1.refresh_from_indices(&[2, 3]);
500 let pools = [p0, p1];
501 let merged = merged_resident_mask(&pools);
502 assert_eq!(merged, vec![true, true, true, true]);
503 let per = per_layer_resident_masks(&pools);
504 assert_eq!(per[0], vec![true, true, false, false]);
505 assert_eq!(per[1], vec![false, false, true, true]);
506 }
507
508 #[test]
509 fn count_hits_matches_bincount() {
510 let idx = [1u32, 0, 1, 2, 1];
511 let c = ExpertPool::count_hits(&idx, 4);
512 assert_eq!(c, [1, 3, 1, 0]);
513 }
514
515 #[test]
516 fn target_gpu_picks_top_by_count() {
517 let counts = [10, 50, 30, 0, 50];
518 let t = ExpertPool::target_gpu_from_counts(&counts, 3);
519 assert_eq!(t, vec![1, 4, 2]); }
521
522 #[test]
523 fn paired_swap_limits_demotions() {
524 let mut pool = ExpertPool::new(ExpertPoolConfig::new(
525 8,
526 2,
527 ExpertRefreshPolicy::EveryForward,
528 ));
529 pool.resident = [0, 1].into_iter().collect();
530 let r = pool.apply_target_placement(&[6, 7]);
531 assert_eq!(r.promotions, 2);
532 assert_eq!(r.demotions, 2);
533 assert_eq!(pool.resident, [6, 7].into_iter().collect::<HashSet<_>>());
534 }
535
536 #[test]
537 fn paired_swap_keeps_extra_residents() {
538 let mut pool = ExpertPool::new(ExpertPoolConfig::new(
539 8,
540 4,
541 ExpertRefreshPolicy::EveryForward,
542 ));
543 pool.resident = [0, 1, 2, 3].into_iter().collect();
544 let r = pool.apply_target_placement(&[2, 3, 4, 5]);
547 assert_eq!(r.promotions, 2);
548 assert_eq!(r.demotions, 2);
549 assert_eq!(pool.resident.len(), 4);
550 for e in [2, 3, 4, 5] {
551 assert!(pool.is_gpu_resident(e));
552 }
553 assert!(!pool.is_gpu_resident(0));
554 }
555
556 #[test]
557 fn jump_steps_refresh_schedule() {
558 let pool = ExpertPool::new(ExpertPoolConfig::new(
559 256,
560 64,
561 ExpertRefreshPolicy::EveryDenoiseSteps(3),
562 ));
563 assert!(pool.should_refresh(MoEExecMode::Reuse, 0, false));
564 assert!(!pool.should_refresh(MoEExecMode::Reuse, 1, false));
565 assert!(!pool.should_refresh(MoEExecMode::Reuse, 2, false));
566 assert!(pool.should_refresh(MoEExecMode::Reuse, 3, false));
567 assert!(pool.should_refresh(MoEExecMode::Reuse, 0, true)); }
569
570 #[test]
571 fn hysteresis_margin_protects_incumbent() {
572 let cfg = ExpertPoolConfig::new(4, 2, ExpertRefreshPolicy::EveryForward).with_hysteresis(
573 HysteresisConfig {
574 margin: 0.5,
575 min_dwell: 0,
576 },
577 );
578 let mut pool = ExpertPool::new(cfg); let r = pool.refresh_from_counts(&[10, 10, 12, 0]);
582 assert_eq!(r.promotions, 0);
583 assert_eq!(r.demotions, 0);
584 assert!(!pool.is_gpu_resident(2));
585 assert!(pool.is_gpu_resident(1));
586
587 let r = pool.refresh_from_counts(&[10, 10, 20, 0]);
589 assert_eq!(r.promotions, 1);
590 assert_eq!(r.demotions, 1);
591 assert!(pool.is_gpu_resident(2));
592 assert!(!pool.is_gpu_resident(1));
593 }
594
595 #[test]
596 fn dwell_delays_eviction() {
597 let cfg = ExpertPoolConfig::new(4, 2, ExpertRefreshPolicy::EveryForward).with_hysteresis(
598 HysteresisConfig {
599 margin: 0.0,
600 min_dwell: 3,
601 },
602 );
603 let mut pool = ExpertPool::new(cfg); for _ in 0..2 {
607 let r = pool.refresh_from_counts(&[0, 0, 100, 0]);
608 assert_eq!(r.promotions, 0, "dwell must protect fresh incumbents");
609 assert!(!pool.is_gpu_resident(2));
610 }
611 let r = pool.refresh_from_counts(&[0, 0, 100, 0]);
613 assert_eq!(r.promotions, 1);
614 assert!(pool.is_gpu_resident(2));
615 }
616
617 #[test]
618 fn hysteresis_disabled_matches_plain_topk() {
619 let counts = [3u64, 9, 1, 7, 0, 5];
620 let mut plain = ExpertPool::new(ExpertPoolConfig::new(
622 6,
623 3,
624 ExpertRefreshPolicy::EveryForward,
625 ));
626 plain.refresh_from_counts(&counts);
627 let mut hyst = ExpertPool::new(
629 ExpertPoolConfig::new(6, 3, ExpertRefreshPolicy::EveryForward)
630 .with_hysteresis(HysteresisConfig::default()),
631 );
632 hyst.refresh_from_counts(&counts);
633 assert_eq!(plain.resident_mask(), hyst.resident_mask());
634 assert_eq!(
636 plain.resident_mask(),
637 vec![false, true, false, true, false, true]
638 );
639 }
640
641 #[test]
642 fn vram_budget_formula() {
643 let b = gpu_expert_budget_from_vram(
644 40 * 1024 * 1024 * 1024,
645 2 * 1024 * 1024 * 1024,
646 50 * 1024 * 1024,
647 20,
648 128,
649 256,
650 );
651 assert!(b > 0 && b <= 128);
652 }
653}