use anyhow::{Context, Result, bail};
use std::collections::{BTreeMap, HashMap};
use std::fs::File;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct BankIx(usize);
#[derive(Debug, Clone)]
pub struct BankLocation {
pub path: PathBuf,
pub offset: u64,
pub num_experts: usize,
pub bytes_per_expert: usize,
}
impl BankLocation {
fn expert_offset(&self, id: usize) -> u64 {
self.offset + (id as u64) * (self.bytes_per_expert as u64)
}
pub fn total_bytes(&self) -> u64 {
(self.num_experts as u64) * (self.bytes_per_expert as u64)
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct PagerStats {
pub hits: u64,
pub misses: u64,
pub bytes_read: u64,
pub bytes_served: u64,
pub evictions: u64,
}
impl PagerStats {
pub fn hit_rate(&self) -> f64 {
let n = self.hits + self.misses;
if n == 0 {
return 0.0;
}
self.hits as f64 / n as f64
}
}
struct Entry {
bytes: Arc<Vec<u8>>,
used: u64,
}
struct Inner {
resident: HashMap<(BankIx, usize), Entry>,
lru: BTreeMap<u64, (BankIx, usize)>,
clock: u64,
bytes: u64,
files: HashMap<PathBuf, Arc<File>>,
stats: PagerStats,
}
pub struct ExpertPager {
locs: Vec<BankLocation>,
by_name: HashMap<usize, HashMap<String, BankIx>>,
budget_bytes: u64,
inner: Mutex<Inner>,
}
impl ExpertPager {
pub fn new(budget_bytes: u64) -> Self {
Self {
locs: Vec::new(),
by_name: HashMap::new(),
budget_bytes,
inner: Mutex::new(Inner {
resident: HashMap::new(),
lru: BTreeMap::new(),
clock: 0,
bytes: 0,
files: HashMap::new(),
stats: PagerStats::default(),
}),
}
}
pub fn register(
&mut self,
layer: usize,
bank: impl Into<String>,
loc: BankLocation,
) -> Result<BankIx> {
if loc.num_experts == 0 {
bail!("expert bank has no experts");
}
if loc.bytes_per_expert == 0 {
bail!("expert bank has zero-length experts");
}
let bank = bank.into();
if let Ok(meta) = std::fs::metadata(&loc.path) {
let end = loc.offset + loc.total_bytes();
if end > meta.len() {
bail!(
"bank {bank} of layer {layer} runs to byte {end} but {} is only {} bytes",
loc.path.display(),
meta.len()
);
}
}
if let Some(&ix) = self.by_name.get(&layer).and_then(|m| m.get(&bank)) {
self.locs[ix.0] = loc;
self.forget(ix);
return Ok(ix);
}
let ix = BankIx(self.locs.len());
self.locs.push(loc);
self.by_name.entry(layer).or_default().insert(bank, ix);
Ok(ix)
}
pub fn bank_ix(&self, layer: usize, bank: &str) -> Option<BankIx> {
self.by_name.get(&layer)?.get(bank).copied()
}
pub fn location(&self, ix: BankIx) -> &BankLocation {
&self.locs[ix.0]
}
fn forget(&mut self, ix: BankIx) {
let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
let doomed: Vec<(BankIx, usize)> = inner
.resident
.keys()
.filter(|(b, _)| *b == ix)
.copied()
.collect();
for k in doomed {
if let Some(e) = inner.resident.remove(&k) {
inner.bytes -= e.bytes.len() as u64;
inner.lru.remove(&e.used);
}
}
}
pub fn bank_count(&self) -> usize {
self.locs.len()
}
pub fn total_bank_bytes(&self) -> u64 {
self.locs.iter().map(|l| l.total_bytes()).sum()
}
pub fn stats(&self) -> PagerStats {
self.inner.lock().unwrap_or_else(|e| e.into_inner()).stats
}
pub fn resident_bytes(&self) -> u64 {
self.inner.lock().unwrap_or_else(|e| e.into_inner()).bytes
}
pub fn expert(&self, layer: usize, bank: &str, id: usize) -> Result<Arc<Vec<u8>>> {
let ix = self
.bank_ix(layer, bank)
.with_context(|| format!("no expert bank registered for layer {layer} `{bank}`"))?;
self.expert_at(ix, id)
}
pub fn expert_at(&self, ix: BankIx, id: usize) -> Result<Arc<Vec<u8>>> {
let loc = self
.locs
.get(ix.0)
.with_context(|| format!("bank handle {ix:?} is not registered"))?;
if id >= loc.num_experts {
bail!(
"expert {id} out of range for bank {ix:?} ({} experts)",
loc.num_experts
);
}
let ck = (ix, id);
let file = {
let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
if let Some(bytes) = inner.touch(&ck) {
return Ok(bytes);
}
inner.open(&loc.path)?
};
let mut buf = vec![0u8; loc.bytes_per_expert];
read_exact_at(&file, &mut buf, loc.expert_offset(id)).with_context(|| {
format!(
"reading expert {id} of bank {ix:?} from {}",
loc.path.display()
)
})?;
let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
inner.stats.misses += 1;
inner.stats.bytes_read += buf.len() as u64;
if let Some(bytes) = inner.touch(&ck) {
return Ok(bytes);
}
let bytes = Arc::new(buf);
inner.admit(ck, bytes.clone(), self.budget_bytes);
Ok(bytes)
}
pub fn gather(&self, layer: usize, bank: &str, ids: &[usize]) -> Result<Vec<u8>> {
let ix = self
.bank_ix(layer, bank)
.with_context(|| format!("no expert bank registered for layer {layer} `{bank}`"))?;
self.gather_at(ix, ids)
}
pub fn gather_at(&self, ix: BankIx, ids: &[usize]) -> Result<Vec<u8>> {
let per = self
.locs
.get(ix.0)
.with_context(|| format!("bank handle {ix:?} is not registered"))?
.bytes_per_expert;
let mut out = Vec::with_capacity(ids.len() * per);
for &id in ids {
out.extend_from_slice(&self.expert_at(ix, id)?);
}
Ok(out)
}
pub fn prefetch(&self, layer: usize, bank: &str, ids: &[usize]) {
for &id in ids {
let _ = self.expert(layer, bank, id);
}
}
pub fn clear(&self) {
let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
inner.resident.clear();
inner.lru.clear();
inner.bytes = 0;
}
}
impl Inner {
fn touch(&mut self, key: &(BankIx, usize)) -> Option<Arc<Vec<u8>>> {
let old = self.resident.get(key)?.used;
self.lru.remove(&old);
self.clock += 1;
let now = self.clock;
self.lru.insert(now, *key);
let e = self.resident.get_mut(key)?;
e.used = now;
let bytes = e.bytes.clone();
self.stats.hits += 1;
self.stats.bytes_served += bytes.len() as u64;
Some(bytes)
}
fn open(&mut self, path: &Path) -> Result<Arc<File>> {
if let Some(f) = self.files.get(path) {
return Ok(f.clone());
}
let f = Arc::new(
File::open(path).with_context(|| format!("opening checkpoint {}", path.display()))?,
);
self.files.insert(path.to_path_buf(), f.clone());
Ok(f)
}
fn admit(&mut self, key: (BankIx, usize), bytes: Arc<Vec<u8>>, budget: u64) {
let len = bytes.len() as u64;
if len > budget {
return;
}
while self.bytes + len > budget {
let Some((&oldest, _)) = self.lru.iter().next() else {
break;
};
let Some(victim) = self.lru.remove(&oldest) else {
break;
};
if let Some(e) = self.resident.remove(&victim) {
self.bytes -= e.bytes.len() as u64;
self.stats.evictions += 1;
}
}
self.clock += 1;
let now = self.clock;
self.lru.insert(now, key);
self.bytes += len;
self.resident.insert(key, Entry { bytes, used: now });
}
}
fn read_exact_at(f: &File, buf: &mut [u8], offset: u64) -> std::io::Result<()> {
#[cfg(unix)]
{
use std::os::unix::fs::FileExt;
f.read_exact_at(buf, offset)
}
#[cfg(windows)]
{
use std::os::windows::fs::FileExt;
let mut done = 0usize;
while done < buf.len() {
let n = f.seek_read(&mut buf[done..], offset + done as u64)?;
if n == 0 {
return Err(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
"short read paging an expert",
));
}
done += n;
}
Ok(())
}
#[cfg(not(any(unix, windows)))]
{
use std::io::{Read, Seek, SeekFrom};
let mut f = f.try_clone()?;
f.seek(SeekFrom::Start(offset))?;
f.read_exact(buf)
}
}
#[cfg(feature = "gguf")]
pub fn register_gguf_expert_banks(
pager: &mut ExpertPager,
path: impl AsRef<Path>,
bank_suffixes: &[&str],
) -> Result<usize> {
let path = path.as_ref();
let g = rlx_gguf::GgufFile::header_from_path(path)
.with_context(|| format!("reading GGUF header of {}", path.display()))?;
let base = g.data_offset();
let mut n = 0usize;
for t in g.tensors.values() {
let Some(rest) = t.name.strip_prefix("blk.") else {
continue;
};
let Some((idx, tail)) = rest.split_once('.') else {
continue;
};
let Ok(layer) = idx.parse::<usize>() else {
continue;
};
let Some(&suffix) = bank_suffixes.iter().find(|s| tail == **s) else {
continue;
};
let num_experts = *t.shape.last().unwrap_or(&0);
if num_experts == 0 {
bail!("{}: expert bank with an empty shape", t.name);
}
let total = rlx_gguf::bytes_for_public(t.dtype, t.n_elements()).ok_or_else(|| {
anyhow::anyhow!("{}: no byte size known for dtype {:?}", t.name, t.dtype)
})?;
if !total.is_multiple_of(num_experts) {
bail!(
"{}: {total} bytes do not divide into {num_experts} experts; the expert slices are not block-aligned and cannot be paged independently",
t.name
);
}
let bank = suffix.strip_suffix(".weight").unwrap_or(suffix);
pager.register(
layer,
bank,
BankLocation {
path: path.to_path_buf(),
offset: base + t.offset,
num_experts,
bytes_per_expert: total / num_experts,
},
)?;
n += 1;
}
Ok(n)
}
pub const GGUF_MOE_BANKS: [&str; 3] = [
"ffn_gate_exps.weight",
"ffn_up_exps.weight",
"ffn_down_exps.weight",
];
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
fn checkpoint(dir: &str, banks: usize, experts: usize, per: usize) -> PathBuf {
let d = std::env::temp_dir().join(format!("rlx-pager-{dir}"));
let _ = std::fs::create_dir_all(&d);
let p = d.join("ckpt.bin");
let mut f = File::create(&p).unwrap();
for b in 0..banks {
for i in 0..experts {
f.write_all(&vec![(b * 100 + i) as u8; per]).unwrap();
}
}
f.flush().unwrap();
p
}
fn loc(path: &Path, bank: usize, experts: usize, per: usize) -> BankLocation {
BankLocation {
path: path.to_path_buf(),
offset: (bank * experts * per) as u64,
num_experts: experts,
bytes_per_expert: per,
}
}
#[test]
fn reads_the_right_expert_window() {
let p = checkpoint("windows", 3, 8, 64);
let mut pager = ExpertPager::new(1 << 20);
for b in 0..3 {
pager
.register(b, format!("bank{b}"), loc(&p, b, 8, 64))
.unwrap();
}
for b in 0..3 {
for i in 0..8 {
let got = pager.expert(b, &format!("bank{b}"), i).unwrap();
assert_eq!(got.len(), 64);
assert!(
got.iter().all(|&x| x == (b * 100 + i) as u8),
"layer {b} expert {i} read the wrong window: {:?}",
&got[..8]
);
}
}
}
#[test]
fn gather_concatenates_in_fired_order() {
let p = checkpoint("gather", 1, 16, 32);
let mut pager = ExpertPager::new(1 << 20);
pager.register(0, "exps", loc(&p, 0, 16, 32)).unwrap();
let ids = [7usize, 2, 15, 2];
let g = pager.gather(0, "exps", &ids).unwrap();
assert_eq!(g.len(), ids.len() * 32);
for (slot, &id) in ids.iter().enumerate() {
let w = &g[slot * 32..(slot + 1) * 32];
assert!(
w.iter().all(|&x| x == id as u8),
"slot {slot} should hold expert {id}, holds {:?}",
&w[..4]
);
}
}
#[test]
fn the_byte_budget_is_never_exceeded() {
let p = checkpoint("budget", 1, 64, 1024);
let budget = 8 * 1024u64;
let mut pager = ExpertPager::new(budget);
pager.register(0, "exps", loc(&p, 0, 64, 1024)).unwrap();
for i in 0..64 {
pager.expert(0, "exps", i).unwrap();
assert!(
pager.resident_bytes() <= budget,
"after {i} experts the cache holds {} bytes against a {budget} budget",
pager.resident_bytes()
);
}
assert!(pager.stats().evictions > 0, "nothing was ever evicted");
}
#[test]
fn eviction_is_least_recently_used() {
let p = checkpoint("lru", 1, 8, 100);
let mut pager = ExpertPager::new(300);
pager.register(0, "exps", loc(&p, 0, 8, 100)).unwrap();
for i in 0..3 {
pager.expert(0, "exps", i).unwrap();
}
pager.expert(0, "exps", 0).unwrap();
pager.expert(0, "exps", 2).unwrap();
let before = pager.stats();
pager.expert(0, "exps", 3).unwrap(); assert_eq!(pager.stats().evictions, before.evictions + 1);
let mid = pager.stats();
pager.expert(0, "exps", 0).unwrap();
pager.expert(0, "exps", 2).unwrap();
assert_eq!(
pager.stats().hits,
mid.hits + 2,
"the recently-used experts were evicted instead of the stale one"
);
let mid = pager.stats();
pager.expert(0, "exps", 1).unwrap();
assert_eq!(
pager.stats().misses,
mid.misses + 1,
"expert 1 was the least-recently-used and should have been evicted"
);
}
#[test]
fn a_refired_expert_is_served_from_ram() {
let p = checkpoint("reuse", 1, 8, 256);
let mut pager = ExpertPager::new(1 << 20);
pager.register(0, "exps", loc(&p, 0, 8, 256)).unwrap();
pager.gather(0, "exps", &[1, 3, 5]).unwrap();
let after_first = pager.stats();
assert_eq!(after_first.misses, 3);
assert_eq!(after_first.bytes_read, 3 * 256);
pager.gather(0, "exps", &[1, 3, 5]).unwrap();
let s = pager.stats();
assert_eq!(s.misses, 3, "the second token re-read from disk");
assert_eq!(s.bytes_read, 3 * 256, "no new bytes should have been read");
assert_eq!(s.hits, 3);
assert!((s.hit_rate() - 0.5).abs() < 1e-9, "{}", s.hit_rate());
}
#[test]
fn a_zero_budget_pages_everything() {
let p = checkpoint("nocache", 1, 4, 128);
let mut pager = ExpertPager::new(0);
pager.register(0, "exps", loc(&p, 0, 4, 128)).unwrap();
for _ in 0..3 {
pager.gather(0, "exps", &[0, 1]).unwrap();
}
let s = pager.stats();
assert_eq!(s.hits, 0);
assert_eq!(s.misses, 6);
assert_eq!(pager.resident_bytes(), 0);
}
#[test]
fn a_bank_that_overruns_the_file_is_rejected() {
let p = checkpoint("overrun", 1, 4, 64);
let mut pager = ExpertPager::new(1 << 20);
let err = pager
.register(
0,
"exps",
BankLocation {
path: p.clone(),
offset: 0,
num_experts: 4,
bytes_per_expert: 1024, },
)
.expect_err("an overrunning bank must be rejected");
assert!(err.to_string().contains("bytes"), "{err}");
}
#[test]
fn the_cache_lock_is_free_while_a_read_is_in_flight() {
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
const BIG: usize = 64 << 20;
let p = checkpoint("lockfree", 1, 1, BIG);
let mut pager = ExpertPager::new(0);
pager.register(0, "exps", loc(&p, 0, 1, BIG)).unwrap();
let pager = Arc::new(pager);
pager.expert(0, "exps", 0).unwrap();
let done = Arc::new(AtomicBool::new(false));
let spins = Arc::new(AtomicUsize::new(0));
let spinner = {
let (pager, done, spins) = (pager.clone(), done.clone(), spins.clone());
std::thread::spawn(move || {
while !done.load(Ordering::Relaxed) {
let _ = pager.stats();
spins.fetch_add(1, Ordering::Relaxed);
}
})
};
for _ in 0..4 {
pager.expert(0, "exps", 0).unwrap();
}
done.store(true, Ordering::Relaxed);
spinner.join().unwrap();
let n = spins.load(Ordering::Relaxed);
assert!(
n > 1_000,
"the spinner took the lock only {n} times across four 64 MB reads — \
it is being blocked for the duration of each read, so the cache \
mutex is held across the IO"
);
let _ = std::fs::remove_dir_all(p.parent().unwrap());
}
#[test]
fn a_racing_double_read_admits_one_copy() {
let per = 1 << 20;
let p = checkpoint("race", 1, 2, per);
let mut pager = ExpertPager::new(8 * per as u64);
pager.register(0, "exps", loc(&p, 0, 2, per)).unwrap();
let pager = Arc::new(pager);
let barrier = Arc::new(std::sync::Barrier::new(4));
let hs: Vec<_> = (0..4)
.map(|_| {
let pager = pager.clone();
let barrier = barrier.clone();
std::thread::spawn(move || {
barrier.wait();
pager.expert(0, "exps", 1).unwrap()
})
})
.collect();
let got: Vec<_> = hs.into_iter().map(|h| h.join().unwrap()).collect();
for g in &got {
assert!(g.iter().all(|&x| x == 1u8), "a racing read got wrong bytes");
}
assert_eq!(
pager.resident_bytes(),
per as u64,
"the same expert is cached more than once"
);
let _ = std::fs::remove_dir_all(p.parent().unwrap());
}
#[test]
fn a_handle_resolves_to_the_same_bytes_as_a_name() {
let p = checkpoint("handles", 2, 6, 48);
let mut pager = ExpertPager::new(1 << 20);
let ix0 = pager.register(0, "bank0", loc(&p, 0, 6, 48)).unwrap();
let ix1 = pager.register(1, "bank1", loc(&p, 1, 6, 48)).unwrap();
assert_ne!(ix0, ix1, "distinct banks must get distinct handles");
assert_eq!(pager.bank_ix(0, "bank0"), Some(ix0));
assert_eq!(pager.bank_ix(1, "bank1"), Some(ix1));
assert_eq!(pager.bank_ix(0, "bank1"), None, "handles are per layer");
for id in 0..6 {
assert_eq!(
pager.expert(0, "bank0", id).unwrap(),
pager.expert_at(ix0, id).unwrap()
);
}
assert_eq!(
pager.gather(1, "bank1", &[3, 0, 5]).unwrap(),
pager.gather_at(ix1, &[3, 0, 5]).unwrap()
);
}
#[test]
fn re_registering_a_bank_invalidates_its_cached_experts() {
let p = checkpoint("reregister", 2, 4, 64);
let mut pager = ExpertPager::new(1 << 20);
let ix = pager.register(0, "exps", loc(&p, 0, 4, 64)).unwrap();
let before = pager.expert_at(ix, 0).unwrap();
assert!(before.iter().all(|&x| x == 0u8));
assert!(pager.resident_bytes() > 0);
let again = pager.register(0, "exps", loc(&p, 1, 4, 64)).unwrap();
assert_eq!(again, ix, "the handle should survive re-registration");
assert_eq!(
pager.resident_bytes(),
0,
"experts cached from the old location are still held"
);
let after = pager.expert_at(ix, 0).unwrap();
assert!(
after.iter().all(|&x| x == 100u8),
"re-registered bank still serves the old file's bytes: {:?}",
&after[..4]
);
}
#[test]
fn an_out_of_range_expert_is_an_error_not_a_bad_read() {
let p = checkpoint("range", 1, 4, 64);
let mut pager = ExpertPager::new(1 << 20);
pager.register(0, "exps", loc(&p, 0, 4, 64)).unwrap();
let err = pager
.expert(0, "exps", 4)
.expect_err("expert 4 of 4 does not exist");
assert!(err.to_string().contains("out of range"), "{err}");
}
}