Skip to main content

ferrox_core/
expert_store.rs

1//! A bounded, lease-protected byte cache for routed-expert weights --
2//! the storage foundation for running MoE checkpoints whose experts do
3//! not all fit in RAM at once (stream cold experts from SSD, keep hot
4//! ones resident under one global byte budget).
5//!
6//! Status: wired into both decode paths as an opt-in
7//! (`Decoder::from_gguf_with_expert_cache` for GGUF,
8//! `load_kimi_checkpoint_with_expert_cache` for Kimi safetensors, both
9//! behind the server's `FERROX_EXPERT_CACHE_BYTES` or
10//! `FERROX_SSD_STREAMING=1` which defaults the cache to 2 GiB), each
11//! proven
12//! bit-identical to its resident path on the committed fixtures at
13//! both generous and smaller-than-one-expert budgets. Without the
14//! opt-in, experts still load resident (mmap) exactly as before.
15//! [`ExpertStore::prefetch`] warms keys for ds4-style SSD streaming
16//! overlap (caller supplies the hotlist). Never yet exercised against
17//! a real large checkpoint. Reference clone: `.scratch/ds4` (gitignored).
18//!
19//! Design:
20//!
21//! - [`ExpertSource`] abstracts where bytes come from (a file with
22//!   positional reads, a shard set, an in-memory test source). The
23//!   store never caches a partial expert: one `(layer, expert)` key
24//!   maps to one complete byte buffer (gate+up+down and any auxiliary
25//!   tensors, concatenated by the source in a layout the consumer
26//!   defines).
27//! - [`ExpertStore::acquire`] returns an [`ExpertLease`] -- a cheap
28//!   `Arc` handle that *pins* the entry: eviction skips any entry with
29//!   an outstanding lease, so a slot can never be reused while
30//!   CPU (or, later, GPU) work still reads it. This is the
31//!   slot-reuse-corruption guard, enforced structurally (an `Arc` with
32//!   `strong_count > 1` is simply not freeable), not by convention.
33//! - When an expert doesn't fit even after evicting every unleased
34//!   entry (e.g. a cache configured smaller than one decode step's
35//!   expert union), `acquire` still succeeds: the bytes are read and
36//!   returned as an *uncached* pass-through lease. Capacity pressure
37//!   degrades to more I/O, never to a wrong answer or a deadlock.
38//! - Reads happen outside the store lock, so concurrent misses on
39//!   different experts overlap their I/O. Two concurrent misses on the
40//!   *same* expert may both read it; the first to insert wins and the
41//!   loser's buffer becomes that caller's private pass-through copy --
42//!   duplicated work under a rare race, never wrong bytes.
43
44use std::collections::HashMap;
45use std::io;
46use std::sync::atomic::{AtomicU64, Ordering};
47use std::sync::{Arc, Mutex};
48
49/// Stable identity of one routed expert within a checkpoint.
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
51pub struct ExpertKey {
52    pub layer: u32,
53    pub expert: u32,
54}
55
56/// Where expert bytes come from. Implementations must be cheap to call
57/// concurrently (positional reads, not a shared seek cursor).
58pub trait ExpertSource: Send + Sync {
59    /// The exact byte length of this expert, or `None` if the key does
60    /// not exist. Used for budget accounting *before* reading.
61    fn expert_len(&self, key: ExpertKey) -> Option<usize>;
62
63    /// Reads this expert's complete bytes.
64    fn read_expert(&self, key: ExpertKey) -> io::Result<Vec<u8>>;
65}
66
67/// A pinned handle to one expert's bytes. While any lease for an entry
68/// is alive, the store cannot evict or reuse that entry's memory.
69pub struct ExpertLease {
70    data: Arc<Vec<u8>>,
71}
72
73impl ExpertLease {
74    pub fn bytes(&self) -> &[u8] {
75        &self.data
76    }
77
78    /// The shared buffer behind this lease, for building
79    /// `WeightBytes::Shared` sub-range views (one per matrix packed
80    /// into the expert's combined bytes). Every clone extends the
81    /// entry's pin: the store cannot evict while any of these views is
82    /// alive.
83    pub fn shared_buf(&self) -> Arc<Vec<u8>> {
84        Arc::clone(&self.data)
85    }
86}
87
88/// Monotonic counters, readable at any time without taking the store
89/// lock. `resident_bytes` is a gauge (current cache footprint).
90#[derive(Debug, Clone, Copy, Default)]
91pub struct ExpertStoreStats {
92    pub hits: u64,
93    pub misses: u64,
94    pub evictions: u64,
95    /// Acquires that could not be cached (entry larger than what the
96    /// budget could free) and were served as uncached pass-throughs.
97    pub pass_throughs: u64,
98    pub bytes_read: u64,
99    pub resident_bytes: u64,
100}
101
102struct Entry {
103    data: Arc<Vec<u8>>,
104    /// Monotonic recency stamp; smallest = least recently used. A
105    /// stamp per touch is simpler and cheaper than reshuffling a
106    /// dedicated LRU list under the same lock, at the cost of an O(n)
107    /// scan on eviction -- fine for the hundreds-to-low-thousands of
108    /// entries a real expert cache holds.
109    last_used: u64,
110}
111
112struct Inner {
113    entries: HashMap<ExpertKey, Entry>,
114    resident_bytes: usize,
115    clock: u64,
116}
117
118/// The bounded cache. See the module docs for the design contract.
119pub struct ExpertStore<S: ExpertSource> {
120    source: S,
121    budget_bytes: usize,
122    inner: Mutex<Inner>,
123    hits: AtomicU64,
124    misses: AtomicU64,
125    evictions: AtomicU64,
126    pass_throughs: AtomicU64,
127    bytes_read: AtomicU64,
128}
129
130impl<S: ExpertSource> ExpertStore<S> {
131    pub fn new(source: S, budget_bytes: usize) -> Self {
132        ExpertStore {
133            source,
134            budget_bytes,
135            inner: Mutex::new(Inner {
136                entries: HashMap::new(),
137                resident_bytes: 0,
138                clock: 0,
139            }),
140            hits: AtomicU64::new(0),
141            misses: AtomicU64::new(0),
142            evictions: AtomicU64::new(0),
143            pass_throughs: AtomicU64::new(0),
144            bytes_read: AtomicU64::new(0),
145        }
146    }
147
148    pub fn budget_bytes(&self) -> usize {
149        self.budget_bytes
150    }
151
152    pub fn stats(&self) -> ExpertStoreStats {
153        let resident = self
154            .inner
155            .lock()
156            .unwrap_or_else(|p| p.into_inner())
157            .resident_bytes as u64;
158        ExpertStoreStats {
159            hits: self.hits.load(Ordering::Relaxed),
160            misses: self.misses.load(Ordering::Relaxed),
161            evictions: self.evictions.load(Ordering::Relaxed),
162            pass_throughs: self.pass_throughs.load(Ordering::Relaxed),
163            bytes_read: self.bytes_read.load(Ordering::Relaxed),
164            resident_bytes: resident,
165        }
166    }
167
168    /// Best-effort warm of `keys` into the cache (ds4-style SSD
169    /// streaming prefetch): acquires each key and drops the lease so
170    /// the entry stays resident under the budget until eviction.
171    /// Concurrent acquires on the same key may duplicate I/O; never
172    /// returns wrong bytes. Failures on individual keys are skipped.
173    pub fn prefetch(&self, keys: &[ExpertKey]) {
174        for &key in keys {
175            let _ = self.acquire(key);
176        }
177    }
178
179    /// Returns a pinned lease on `key`'s bytes, reading them from the
180    /// source on a miss. Never blocks waiting for other leases to be
181    /// released: if the entry cannot fit in the budget right now, the
182    /// bytes are returned uncached (see module docs).
183    pub fn acquire(&self, key: ExpertKey) -> io::Result<ExpertLease> {
184        // Fast path: cache hit under the lock.
185        {
186            let mut inner = self.inner.lock().unwrap_or_else(|p| p.into_inner());
187            inner.clock += 1;
188            let clock = inner.clock;
189            if let Some(entry) = inner.entries.get_mut(&key) {
190                entry.last_used = clock;
191                self.hits.fetch_add(1, Ordering::Relaxed);
192                return Ok(ExpertLease {
193                    data: Arc::clone(&entry.data),
194                });
195            }
196        }
197
198        // Miss: read outside the lock so concurrent misses overlap I/O.
199        self.misses.fetch_add(1, Ordering::Relaxed);
200        let data = self.source.read_expert(key)?;
201        self.bytes_read
202            .fetch_add(data.len() as u64, Ordering::Relaxed);
203        let size = data.len();
204        let data = Arc::new(data);
205
206        let mut inner = self.inner.lock().unwrap_or_else(|p| p.into_inner());
207        // Someone else may have inserted this key while we read -- use
208        // theirs (keeps exactly one cached copy), our read becomes the
209        // rare-race duplicate described in the module docs. Already
210        // counted as a miss above (each acquire increments exactly one
211        // of hits/misses), so no hit is recorded here.
212        if let Some(entry) = inner.entries.get_mut(&key) {
213            return Ok(ExpertLease {
214                data: Arc::clone(&entry.data),
215            });
216        }
217
218        if size > self.budget_bytes {
219            self.pass_throughs.fetch_add(1, Ordering::Relaxed);
220            return Ok(ExpertLease { data });
221        }
222
223        // Evict least-recently-used *unleased* entries until it fits.
224        while inner.resident_bytes + size > self.budget_bytes {
225            let victim = inner
226                .entries
227                .iter()
228                .filter(|(_, e)| Arc::strong_count(&e.data) == 1)
229                .min_by_key(|(_, e)| e.last_used)
230                .map(|(k, _)| *k);
231            match victim {
232                Some(k) => {
233                    let e = inner.entries.remove(&k).expect("victim key just found");
234                    inner.resident_bytes -= e.data.len();
235                    self.evictions.fetch_add(1, Ordering::Relaxed);
236                }
237                None => {
238                    // Every resident entry is pinned by a live lease --
239                    // serve uncached rather than waiting (a wait here
240                    // could deadlock against our own caller's leases).
241                    self.pass_throughs.fetch_add(1, Ordering::Relaxed);
242                    return Ok(ExpertLease { data });
243                }
244            }
245        }
246
247        inner.clock += 1;
248        let clock = inner.clock;
249        inner.resident_bytes += size;
250        inner.entries.insert(
251            key,
252            Entry {
253                data: Arc::clone(&data),
254                last_used: clock,
255            },
256        );
257        Ok(ExpertLease { data })
258    }
259}
260
261/// A file-backed [`ExpertSource`]: each expert is one contiguous
262/// `(offset, len)` byte range in a single file, read with positional
263/// reads (`pread`-style on unix, so concurrent misses never contend on
264/// a shared seek cursor). This is the portable buffered-I/O default
265/// the storage design starts from; `O_DIRECT`/`F_NOCACHE`/`io_uring`
266/// variants are future work behind the same trait and must produce
267/// identical bytes.
268pub struct FileRangeSource {
269    file: std::fs::File,
270    ranges: HashMap<ExpertKey, (u64, usize)>,
271    /// Non-unix fallback only: serializes seek+read pairs.
272    #[cfg(not(unix))]
273    seek_lock: Mutex<()>,
274}
275
276impl FileRangeSource {
277    pub fn new(file: std::fs::File, ranges: HashMap<ExpertKey, (u64, usize)>) -> Self {
278        FileRangeSource {
279            file,
280            ranges,
281            #[cfg(not(unix))]
282            seek_lock: Mutex::new(()),
283        }
284    }
285}
286
287impl ExpertSource for FileRangeSource {
288    fn expert_len(&self, key: ExpertKey) -> Option<usize> {
289        self.ranges.get(&key).map(|&(_, len)| len)
290    }
291
292    fn read_expert(&self, key: ExpertKey) -> io::Result<Vec<u8>> {
293        let &(offset, len) = self
294            .ranges
295            .get(&key)
296            .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, format!("{key:?}")))?;
297        let mut buf = vec![0u8; len];
298        #[cfg(unix)]
299        {
300            use std::os::unix::fs::FileExt;
301            self.file.read_exact_at(&mut buf, offset)?;
302        }
303        #[cfg(not(unix))]
304        {
305            use std::io::{Read, Seek, SeekFrom};
306            let _guard = self.seek_lock.lock().unwrap_or_else(|p| p.into_inner());
307            let mut f = &self.file;
308            f.seek(SeekFrom::Start(offset))?;
309            f.read_exact(&mut buf)?;
310        }
311        Ok(buf)
312    }
313}
314
315#[cfg(test)]
316mod tests {
317    use super::*;
318
319    /// Deterministic per-key content so any slot-reuse/wrong-bytes bug
320    /// fails loudly: byte `i` of expert `(l, e)` is a function of all
321    /// three.
322    struct PatternSource {
323        len: usize,
324        n_layers: u32,
325        n_experts: u32,
326    }
327
328    fn expected_bytes(key: ExpertKey, len: usize) -> Vec<u8> {
329        (0..len)
330            .map(|i| (key.layer as usize * 31 + key.expert as usize * 7 + i * 13) as u8)
331            .collect()
332    }
333
334    impl ExpertSource for PatternSource {
335        fn expert_len(&self, key: ExpertKey) -> Option<usize> {
336            (key.layer < self.n_layers && key.expert < self.n_experts).then_some(self.len)
337        }
338        fn read_expert(&self, key: ExpertKey) -> io::Result<Vec<u8>> {
339            if self.expert_len(key).is_none() {
340                return Err(io::Error::new(io::ErrorKind::NotFound, "no such expert"));
341            }
342            Ok(expected_bytes(key, self.len))
343        }
344    }
345
346    fn key(layer: u32, expert: u32) -> ExpertKey {
347        ExpertKey { layer, expert }
348    }
349
350    fn store(len: usize, budget: usize) -> ExpertStore<PatternSource> {
351        ExpertStore::new(
352            PatternSource {
353                len,
354                n_layers: 8,
355                n_experts: 8,
356            },
357            budget,
358        )
359    }
360
361    #[test]
362    fn hits_misses_and_lru_eviction_order() {
363        let s = store(100, 250); // fits 2 experts
364        assert_eq!(
365            s.acquire(key(0, 0)).unwrap().bytes(),
366            expected_bytes(key(0, 0), 100)
367        );
368        assert_eq!(
369            s.acquire(key(0, 1)).unwrap().bytes(),
370            expected_bytes(key(0, 1), 100)
371        );
372        // Touch (0,0) so (0,1) is now least recently used.
373        s.acquire(key(0, 0)).unwrap();
374        // Third expert evicts (0,1), not (0,0).
375        s.acquire(key(0, 2)).unwrap();
376        let before = s.stats();
377        s.acquire(key(0, 0)).unwrap(); // must still be a hit
378        let after = s.stats();
379        assert_eq!(after.hits, before.hits + 1);
380        assert_eq!(after.misses, before.misses);
381        assert_eq!(after.evictions, 1);
382        assert!(after.resident_bytes <= 250);
383        // (0,1) was the eviction victim: acquiring it again is a miss.
384        s.acquire(key(0, 1)).unwrap();
385        assert_eq!(s.stats().misses, after.misses + 1);
386    }
387
388    #[test]
389    fn a_live_lease_pins_its_entry_against_eviction() {
390        let s = store(100, 250); // fits 2 experts
391        let pinned = s.acquire(key(1, 0)).unwrap();
392        // Fill and churn the cache well past the budget: the pinned
393        // entry must never be evicted or corrupted while the lease
394        // lives...
395        for e in 1..6 {
396            s.acquire(key(1, e)).unwrap();
397        }
398        assert_eq!(pinned.bytes(), expected_bytes(key(1, 0), 100));
399        // ...and it is still resident (a re-acquire is a hit).
400        let hits_before = s.stats().hits;
401        let again = s.acquire(key(1, 0)).unwrap();
402        assert_eq!(s.stats().hits, hits_before + 1);
403        assert_eq!(again.bytes(), expected_bytes(key(1, 0), 100));
404
405        // Once every lease is dropped, pressure may evict it like any
406        // other entry.
407        drop(pinned);
408        drop(again);
409        for e in 1..6 {
410            s.acquire(key(2, e)).unwrap();
411        }
412        let misses_before = s.stats().misses;
413        s.acquire(key(1, 0)).unwrap();
414        assert_eq!(
415            s.stats().misses,
416            misses_before + 1,
417            "unpinned entry was evictable"
418        );
419    }
420
421    /// The plan's "cache smaller than one token's expert union" case:
422    /// a budget that can't hold even one expert still serves correct
423    /// bytes on every acquire, as uncached pass-throughs.
424    #[test]
425    fn budget_smaller_than_one_expert_degrades_to_pass_through_not_failure() {
426        let s = store(100, 50);
427        for e in 0..4 {
428            let lease = s.acquire(key(0, e)).unwrap();
429            assert_eq!(lease.bytes(), expected_bytes(key(0, e), 100));
430        }
431        let st = s.stats();
432        assert_eq!(st.pass_throughs, 4);
433        assert_eq!(st.resident_bytes, 0);
434        assert_eq!(st.evictions, 0);
435    }
436
437    /// If every resident entry is pinned, a new acquire must not
438    /// deadlock or evict a pinned entry -- it passes through.
439    #[test]
440    fn fully_pinned_cache_serves_new_experts_uncached() {
441        let s = store(100, 200);
442        let _a = s.acquire(key(0, 0)).unwrap();
443        let _b = s.acquire(key(0, 1)).unwrap();
444        let c = s.acquire(key(0, 2)).unwrap();
445        assert_eq!(c.bytes(), expected_bytes(key(0, 2), 100));
446        assert_eq!(s.stats().pass_throughs, 1);
447        assert_eq!(s.stats().evictions, 0);
448        // The two pinned entries are still hits.
449        let hits_before = s.stats().hits;
450        s.acquire(key(0, 0)).unwrap();
451        s.acquire(key(0, 1)).unwrap();
452        assert_eq!(s.stats().hits, hits_before + 2);
453    }
454
455    #[test]
456    fn missing_expert_is_a_clean_error() {
457        let s = store(100, 1000);
458        assert!(s.acquire(key(99, 0)).is_err());
459    }
460
461    /// Concurrent stress under a deliberately tiny budget: many
462    /// threads acquiring random keys while evictions churn constantly.
463    /// Every lease's bytes must match that key's expected pattern --
464    /// the direct test for slot-reuse corruption.
465    #[test]
466    fn concurrent_acquires_under_eviction_pressure_never_yield_wrong_bytes() {
467        let s = Arc::new(store(64, 200)); // fits 3 of 16 hot keys
468        let mut handles = Vec::new();
469        for t in 0..8u32 {
470            let s = Arc::clone(&s);
471            handles.push(std::thread::spawn(move || {
472                let mut state = t.wrapping_mul(2654435761).wrapping_add(12345);
473                for _ in 0..500 {
474                    state = state.wrapping_mul(1664525).wrapping_add(1013904223);
475                    let k = key((state >> 8) % 4, (state >> 16) % 4);
476                    let lease = s.acquire(k).expect("in-range key must read");
477                    assert_eq!(
478                        lease.bytes(),
479                        expected_bytes(k, 64),
480                        "wrong bytes for {k:?} -- slot reuse corruption"
481                    );
482                }
483            }));
484        }
485        for h in handles {
486            h.join().unwrap();
487        }
488        let st = s.stats();
489        assert_eq!(
490            st.hits + st.misses,
491            8 * 500,
492            "every acquire is a hit or a miss"
493        );
494        assert!(st.resident_bytes <= 200, "budget held under concurrency");
495    }
496
497    /// The bridge to the weight types: a quantized `WeightMatrix` built
498    /// over a lease's shared buffer must (a) compute exactly what the
499    /// same bytes compute as an owned buffer, and (b) keep the cache
500    /// entry pinned for as long as the matrix lives, even after the
501    /// original lease is dropped.
502    #[test]
503    fn weight_matrix_over_a_lease_computes_identically_and_extends_the_pin() {
504        use crate::weight_matrix::{QuantKind, WeightBytes, WeightMatrix};
505
506        // A source whose "expert" payload is real Q8_0 rows.
507        struct QuantSource {
508            rows: Vec<u8>,
509        }
510        impl ExpertSource for QuantSource {
511            fn expert_len(&self, _k: ExpertKey) -> Option<usize> {
512                Some(self.rows.len())
513            }
514            fn read_expert(&self, _k: ExpertKey) -> io::Result<Vec<u8>> {
515                Ok(self.rows.clone())
516            }
517        }
518
519        let cols = 64;
520        let rows = 2;
521        let values: Vec<f32> = (0..rows * cols)
522            .map(|i| ((i as f32) * 0.11).sin())
523            .collect();
524        let mut packed = Vec::new();
525        for r in 0..rows {
526            packed.extend(ferrox_quant::quantize_q8_0(
527                &values[r * cols..(r + 1) * cols],
528            ));
529        }
530        let store = ExpertStore::new(
531            QuantSource {
532                rows: packed.clone(),
533            },
534            packed.len(),
535        );
536
537        let lease = store.acquire(key(0, 0)).unwrap();
538        let matrix = WeightMatrix::Quantized {
539            data: WeightBytes::Shared {
540                buf: lease.shared_buf(),
541                range: 0..packed.len(),
542            },
543            rows,
544            cols,
545            kind: QuantKind::Q8_0,
546        };
547        drop(lease); // the matrix alone must keep the entry pinned
548
549        let owned = WeightMatrix::Quantized {
550            data: WeightBytes::Owned(packed),
551            rows,
552            cols,
553            kind: QuantKind::Q8_0,
554        };
555        let x: Vec<f32> = (0..cols).map(|i| ((i as f32) * 0.07).cos()).collect();
556        assert_eq!(
557            matrix.apply(&x),
558            owned.apply(&x),
559            "lease-backed == owned, bit for bit"
560        );
561
562        // Entry is still resident (pinned by the matrix): re-acquire is
563        // a hit even though the budget is exactly one expert.
564        let hits_before = store.stats().hits;
565        store.acquire(key(0, 0)).unwrap();
566        assert_eq!(store.stats().hits, hits_before + 1);
567    }
568
569    /// End-to-end through a real on-disk file: positional-range reads
570    /// through the bounded store return exactly the bytes written at
571    /// each expert's offset, including under concurrent access.
572    #[test]
573    fn file_range_source_reads_correct_ranges_through_the_store() {
574        let dir = std::env::temp_dir();
575        let path = dir.join(format!(
576            "ferrox_expert_store_test_{}.bin",
577            std::process::id()
578        ));
579        let mut file_bytes = Vec::new();
580        let mut ranges = HashMap::new();
581        for l in 0..3u32 {
582            for e in 0..4u32 {
583                let content = expected_bytes(key(l, e), 96);
584                ranges.insert(key(l, e), (file_bytes.len() as u64, content.len()));
585                file_bytes.extend_from_slice(&content);
586            }
587        }
588        std::fs::write(&path, &file_bytes).unwrap();
589        let source = FileRangeSource::new(std::fs::File::open(&path).unwrap(), ranges);
590        let store = Arc::new(ExpertStore::new(source, 300)); // fits 3 of 12
591
592        let mut handles = Vec::new();
593        for t in 0..4u32 {
594            let store = Arc::clone(&store);
595            handles.push(std::thread::spawn(move || {
596                for i in 0..200u32 {
597                    let k = key((t + i) % 3, i % 4);
598                    let lease = store.acquire(k).unwrap();
599                    assert_eq!(lease.bytes(), expected_bytes(k, 96));
600                }
601            }));
602        }
603        for h in handles {
604            h.join().unwrap();
605        }
606        std::fs::remove_file(&path).ok();
607    }
608}