spark_runtime/weights/gguf/
expert_lru_evict.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2// provenance-id: 526f6e616c6420522e205374657369616b
3
4//! [`ExpertLru`]'s victim selection: strict LRU, or a random slot among the
5//! oldest few percent (the cure for the cyclic replay of a working set a
6//! little larger than the cache). Split from `expert_lru.rs` (500-LoC cap).
7
8use anyhow::{Result, bail};
9
10use super::expert_lru::{ExpertLru, NONE};
11
12impl ExpertLru {
13    /// Move `i` to the LRU end without unmapping it (a prediction that was
14    /// not used: the next victim, but still a hit if it is asked for).
15    pub(super) fn demote(&mut self, i: u32) {
16        self.unlink(i);
17        let m = &mut self.meta[i as usize];
18        m.next = NONE;
19        m.prev = self.tail;
20        if self.tail != NONE {
21            self.meta[self.tail as usize].next = i;
22        }
23        self.tail = i;
24        if self.head == NONE {
25            self.head = i;
26        }
27    }
28
29    /// Strict LRU or a random victim among the oldest `pct`% (see `take_victim`).
30    pub fn set_evict_random_pct(&mut self, pct: usize) {
31        self.evict_pct = pct.min(100);
32    }
33
34    /// xorshift64*, seeded once: the same run picks the same victims.
35    fn rng_next(&mut self) -> u64 {
36        let mut x = self.rng;
37        x ^= x >> 12;
38        x ^= x << 25;
39        x ^= x >> 27;
40        self.rng = x;
41        x.wrapping_mul(0x2545_F491_4F6C_DD1D)
42    }
43
44    /// Take a victim among the `evict_pct`% least recently used slots that
45    /// are not pinned in this epoch and not being filled, dropping whatever
46    /// it held. Strict LRU (`evict_pct` = 0, or fewer than 100/pct slots)
47    /// takes the oldest; otherwise a random one of the oldest few percent.
48    ///
49    /// The randomness is the cure for the cyclic case: a request whose
50    /// working set is a few experts larger than the cache re-reads it in the
51    /// same order, and strict LRU evicts exactly the expert needed next at
52    /// every miss (MinHeap at 8,380 slots: 8,418 distinct experts, 5.5
53    /// misses a step on r2/r3, 09-19). A victim drawn from the oldest 5%
54    /// breaks the lockstep: 1.1 misses a step in the replay, Volvo (fits)
55    /// unchanged at 0. The policy never touches the math.
56    pub(super) fn take_victim(&mut self) -> Result<u32> {
57        let want = (self.n_slots * self.evict_pct / 100).max(1);
58        let mut cands: [u32; 8] = [NONE; 8];
59        let mut n = 0usize;
60        // reservoir-sample up to eight of the oldest `want` eligible slots
61        let mut seen = 0usize;
62        let mut i = self.tail;
63        while i != NONE && seen < want {
64            let m = &self.meta[i as usize];
65            if m.epoch != self.epoch && m.ticket.is_none() {
66                if n < cands.len() {
67                    cands[n] = i;
68                    n += 1;
69                } else {
70                    let j = (self.rng_next() % (seen as u64 + 1)) as usize;
71                    if j < cands.len() {
72                        cands[j] = i;
73                    }
74                }
75                seen += 1;
76            }
77            i = self.meta[i as usize].prev;
78        }
79        if n > 0 {
80            let pick = if n == 1 {
81                cands[0]
82            } else {
83                cands[(self.rng_next() % n as u64) as usize]
84            };
85            if let Some(k) = self.meta[pick as usize].key.take() {
86                self.map.remove(&k);
87                self.slot_changes.push((k.0, k.1, -1));
88                self.stats.evictions += 1;
89            }
90            return Ok(pick);
91        }
92        bail!(
93            "expert cache of {} slots is smaller than one token's working set ({} pinned)",
94            self.n_slots,
95            self.map.len()
96        )
97    }
98}