Skip to main content

ferrox_core/weight_matrix/
repack_cache.rs

1//! A process-wide, byte-budgeted cache of interleaved ("repacked")
2//! weight bytes, keyed by the identity of the mapping the bytes came
3//! from.
4//!
5//! A repack rewrites a whole matrix into the row-interleaved layout the
6//! `x4` / `x8` GEMV kernels read, so it costs a pass over every weight
7//! byte. Done once per matrix that is the load-time cost llama.cpp pays
8//! for its `repack` backend. Done once per CALL it is a full copy of the
9//! matrix on every token, and that is what shipped: `apply_cpu_q8`, the
10//! int-dot matvec the dense FFN gate/up and the MoE experts take, passed
11//! a hand-written `/* uncacheable */ None` where every other matvec
12//! passed `data.map_id()`. Profiled at one thread on TinyLlama Q8_0,
13//! roughly 85% of a decode token was `pack_q8_0_matrix_x4` and the
14//! `Arc` copy behind it, against under 10% in the GEMV it fed (#128).
15//!
16//! The `None` was a second, hand-restated copy of a decision
17//! [`WeightBytes::map_id`] already makes: `Owned` and `Shared` bytes
18//! answer `None` there, because their address is not an identity, and
19//! `Mapped` bytes answer `Some`. Two places deciding one thing, with
20//! nothing making them agree, is this repo's dominant bug shape, so the
21//! typed lookups below take the [`WeightBytes`] and ask it themselves.
22//! A call site can no longer say "uncacheable" on its own authority.
23//!
24//! # The budget
25//!
26//! Caching a packing RETAINS a second copy of a matrix that is already
27//! mapped. Measured on TinyLlama-1.1B Q8_0, retaining the dense FFN
28//! gate/up packings cost **+527 MB of peak footprint** (685 MB to 1213
29//! MB), and that scales with gate/up bytes: an 8B checkpoint pays
30//! several GB, and a resident MoE pays it once per expert that has ever
31//! been routed to. Unbounded, it grows with the number of distinct
32//! matrices the process touches, which for an MoE is unbounded in
33//! practice.
34//!
35//! So there is ONE cache, not one per format, holding
36//! [`budget_bytes`] at most, evicting least-recently-used entries to
37//! stay under it. A matrix that does not fit is packed and returned
38//! uncached, so pressure degrades to recomputation and never to a wrong
39//! answer -- the same degradation
40//! [`crate::expert_store::ExpertStore::acquire`] makes, for the same
41//! reason.
42//!
43//! Five caches would be five budgets over one pool of RAM, which is the
44//! defect the budget exists to close, so the format is part of the key
45//! instead.
46//!
47//! The budget itself is DERIVED, in
48//! [`crate::host_memory::derived_copy_budget`], from a live probe of the
49//! host minus what `expert_store` has already committed. It can be zero,
50//! and zero means nothing is ever retained: exactly the behaviour before
51//! this cache existed.
52
53use std::collections::HashMap;
54use std::sync::{Arc, Mutex, MutexGuard, OnceLock};
55
56use super::WeightBytes;
57
58/// Identity of the memory mapping a repacked buffer was built from.
59///
60/// The repack caches key on a weight's **address**, and an address is
61/// only a stable identity for as long as the mapping that published it
62/// is alive. Unmap one file and map another and the kernel will hand
63/// the same address straight back -- a textbook ABA. The cache then
64/// serves one matrix another matrix's interleaved bytes, which panicked
65/// with an out-of-range slice when the two shapes differed and was
66/// SILENT, i.e. wrong output, when they matched.
67///
68/// Holding a [`std::sync::Weak`] is what closes it, and it closes both
69/// halves at once:
70///
71/// * while the `Weak` lives, the `Arc`'s control block cannot be
72///   recycled, so [`Self::id`] is a unique name for exactly one mapping
73///   for as long as the cache entry exists; and
74/// * `upgrade()` succeeding proves the mapping itself is still alive,
75///   which is what makes the address it published still mean what it
76///   meant when the entry was written.
77///
78/// A dead `Weak` is therefore a *stale entry*, not a hit, and is
79/// repacked and replaced. The `Weak` holds no mapping open, so nothing
80/// here keeps a file resident.
81#[derive(Clone)]
82pub struct MapId {
83    map: std::sync::Weak<memmap2::Mmap>,
84    id: usize,
85    offset: usize,
86}
87
88impl MapId {
89    /// The identity of `range.start` inside `mmap`. Only
90    /// [`WeightBytes::map_id`] builds one, and only for `Mapped` bytes.
91    pub(super) fn of(mmap: &Arc<memmap2::Mmap>, offset: usize) -> Self {
92        MapId {
93            map: Arc::downgrade(mmap),
94            id: Arc::as_ptr(mmap) as usize,
95            offset,
96        }
97    }
98
99    /// True when `other` names the same, still-live mapping.
100    fn matches(&self, other: &MapId) -> bool {
101        self.id == other.id
102            && self.offset == other.offset
103            && self
104                .map
105                .upgrade()
106                .is_some_and(|m| Arc::as_ptr(&m) as usize == other.id)
107    }
108
109    /// The cache key this identity contributes to, for `format` at
110    /// `rows x cols`.
111    fn key(&self, format: Format, rows: usize, cols: usize) -> RepackKey {
112        (format, self.id, self.offset, rows, cols)
113    }
114}
115
116/// Which interleaved layout a cached packing is in.
117///
118/// Part of the KEY rather than the identity of a separate cache: one
119/// budget over one map is the whole point, and five maps would be five
120/// budgets spending the same RAM.
121///
122/// Carrying it in the key is defensive rather than load-bearing, and
123/// saying so is the honest version: one mapping offset is one tensor,
124/// a tensor has one quant kind, and each kind reaches exactly one
125/// packer, so no call site today can ask for two formats at one
126/// address. The key names the packing anyway, so that invariant is not
127/// something a future format has to rediscover. A test cannot
128/// distinguish it for the same reason it cannot happen, so there is no
129/// test claiming to.
130#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
131enum Format {
132    /// `block_q4_Kx8`
133    Q4Kx8,
134    /// `block_q5_Kx8`
135    Q5Kx8,
136    /// `block_q6_Kx8`
137    Q6Kx8,
138    /// `block_q8_0x4`
139    Q8_0x4,
140    /// `block_q4_0x4`
141    Q4_0x4,
142}
143
144/// `(format, mapping id, byte offset, rows, cols)`.
145///
146/// `cols` is in the key because two tensors of equal row count and
147/// unequal width are different matrices with different repacked lengths,
148/// and the old `(address, rows)` key called them the same one.
149type RepackKey = (Format, usize, usize, usize, usize);
150
151/// Interleaved bytes, beside the mapping identity that makes the key
152/// meaningful ([`MapId`]) and the recency stamp eviction orders by.
153struct Entry {
154    id: MapId,
155    packed: Arc<[u8]>,
156    /// Monotonic; smallest is least recently used. A stamp per touch
157    /// rather than an LRU list, which is what
158    /// [`crate::expert_store`] does and for the same reason: an O(n)
159    /// scan is cheap at the few-hundred entries a checkpoint produces,
160    /// and a list is another structure to keep in agreement.
161    last_used: u64,
162}
163
164/// The one cache. See the module docs for why it is one and not five.
165#[derive(Default)]
166struct Cache {
167    entries: HashMap<RepackKey, Entry>,
168    /// Sum of `entries[..].packed.len()`, maintained on every insert and
169    /// every eviction so the budget check is O(1).
170    resident_bytes: usize,
171    clock: u64,
172}
173
174impl Cache {
175    /// Drops one entry and un-accounts its bytes.
176    ///
177    /// The only way an entry leaves the map. A `remove` that forgot to
178    /// subtract would leak budget until the cache stopped caching
179    /// anything, which is exactly the kind of silent divergence this
180    /// repo keeps paying for, so there is one of these and everything
181    /// calls it.
182    fn evict(&mut self, key: &RepackKey) {
183        if let Some(entry) = self.entries.remove(key) {
184            self.resident_bytes = self.resident_bytes.saturating_sub(entry.packed.len());
185        }
186    }
187
188    /// The least recently used key, or `None` when the map is empty.
189    fn lru(&self) -> Option<RepackKey> {
190        self.entries
191            .iter()
192            .min_by_key(|(_, e)| e.last_used)
193            .map(|(k, _)| *k)
194    }
195
196    /// Serves `key` if it holds a live packing for `id`, dropping a
197    /// stale entry rather than returning it.
198    ///
199    /// A dead `Weak` means the mapping that published this address is
200    /// gone and the address has been handed to somebody else, so the
201    /// entry is a textbook ABA and must not be served.
202    fn take_hit(&mut self, key: &RepackKey, id: &MapId) -> Option<Arc<[u8]>> {
203        match self.entries.get_mut(key) {
204            Some(entry) if entry.id.matches(id) => {
205                self.clock += 1;
206                entry.last_used = self.clock;
207                Some(Arc::clone(&entry.packed))
208            }
209            Some(_) => {
210                self.evict(key);
211                None
212            }
213            None => None,
214        }
215    }
216
217    /// Retains `packed` under `key` if the budget can hold it, evicting
218    /// least-recently-used entries to make room.
219    ///
220    /// Returns without inserting when one packing alone exceeds the
221    /// budget -- including when the budget is zero, which is how "never
222    /// retain anything" is expressed. The caller already holds the
223    /// packing, so declining costs a recomputation next time and
224    /// nothing else.
225    fn insert_within_budget(&mut self, key: RepackKey, id: MapId, packed: Arc<[u8]>) {
226        let budget = budget_bytes();
227        let size = packed.len();
228        if size > budget {
229            return;
230        }
231        while self.resident_bytes + size > budget {
232            let Some(victim) = self.lru() else { break };
233            self.evict(&victim);
234        }
235        // The loop can only exit early when the map is empty, and an
236        // empty map holds zero bytes, so this cannot fail after it --
237        // but assert rather than assume, because the budget is the
238        // property the tests pin.
239        if self.resident_bytes + size > budget {
240            return;
241        }
242        self.clock += 1;
243        self.resident_bytes += size;
244        self.entries.insert(
245            key,
246            Entry {
247                id,
248                packed,
249                last_used: self.clock,
250            },
251        );
252    }
253}
254
255/// The process-wide cache, built on first use.
256fn cache() -> &'static Mutex<Cache> {
257    static CACHE: OnceLock<Mutex<Cache>> = OnceLock::new();
258    CACHE.get_or_init(|| Mutex::new(Cache::default()))
259}
260
261/// The one way any of this module takes the cache lock.
262///
263/// A poisoned lock is recovered from rather than propagated. The map
264/// holds no invariant a panic can leave half-built: entries are
265/// inserted whole, and every read re-checks the identity before trusting
266/// the bytes. Propagating the poison instead would let one panic
267/// anywhere in the process turn EVERY later matvec into a panic, which
268/// is a much worse failure than serving a correct cached packing.
269fn lock() -> MutexGuard<'static, Cache> {
270    cache().lock().unwrap_or_else(|e| e.into_inner())
271}
272
273/// Bytes this cache may retain, decided once for the process.
274///
275/// `FERROX_REPACK_CACHE_BYTES` overrides it, and `0` is a legal value
276/// meaning "never retain anything" -- the behaviour before this cache
277/// existed, and the reason a memory-constrained host is expressible
278/// rather than merely given a smaller number.
279///
280/// Otherwise it is DERIVED by
281/// [`crate::host_memory::derived_copy_budget`] from what the host says
282/// is available, less the standard fit headroom, less what
283/// `expert_store` has already committed. That subtraction is the whole
284/// relationship between this budget and the expert one: they are not
285/// two independent numbers, they are one pool spent in a fixed order.
286fn budget_bytes() -> usize {
287    #[cfg(test)]
288    {
289        if let Some(bytes) = tests::budget_override() {
290            return bytes;
291        }
292    }
293    static BYTES: OnceLock<usize> = OnceLock::new();
294    *BYTES.get_or_init(|| {
295        if let Some(explicit) = std::env::var("FERROX_REPACK_CACHE_BYTES")
296            .ok()
297            .and_then(|v| v.trim().parse::<u64>().ok())
298        {
299            return usize::try_from(explicit).unwrap_or(usize::MAX);
300        }
301        let derived = crate::host_memory::derived_copy_budget(
302            crate::host_memory::available_bytes(),
303            crate::host_memory::FIT_HEADROOM_BYTES,
304            crate::expert_store::committed_expert_bytes(),
305        );
306        usize::try_from(derived).unwrap_or(usize::MAX)
307    })
308}
309
310/// The one lookup every format's repack shares.
311///
312/// `id` is `None` for bytes whose address may be recycled under us
313/// (owned buffers, and an expert store's leases -- see
314/// [`WeightBytes::map_id`]), and those always repack. Private: the only
315/// way for a matvec to reach this is through a typed lookup below, which
316/// derives `id` from the [`WeightBytes`] rather than accepting one.
317fn get_or_repack(
318    format: Format,
319    id: Option<MapId>,
320    rows: usize,
321    cols: usize,
322    repack: impl FnOnce() -> Vec<u8>,
323) -> Arc<[u8]> {
324    let Some(id) = id else {
325        return Arc::from(repack().into_boxed_slice());
326    };
327    let key = id.key(format, rows, cols);
328    if let Some(hit) = lock().take_hit(&key, &id) {
329        return hit;
330    }
331    let arc: Arc<[u8]> = Arc::from(repack().into_boxed_slice());
332    let mut cache = lock();
333    // Another thread may have won the race; prefer the existing entry,
334    // but only if it is one this caller would have accepted above.
335    match cache.take_hit(&key, &id) {
336        Some(hit) => hit,
337        None => {
338            cache.insert_within_budget(key, id, Arc::clone(&arc));
339            arc
340        }
341    }
342}
343
344/// Whether the packing of `data` (as `format` at `rows x cols`) is
345/// currently held under a live identity. Tests only.
346#[cfg(test)]
347fn is_cached(format: Format, data: &WeightBytes, rows: usize, cols: usize) -> bool {
348    let Some(id) = data.map_id() else {
349        return false;
350    };
351    lock()
352        .entries
353        .get(&id.key(format, rows, cols))
354        .is_some_and(|e| e.id.matches(&id))
355}
356
357/// Bytes the cache currently holds. Diagnostics and tests.
358#[cfg(test)]
359fn resident_bytes() -> usize {
360    lock().resident_bytes
361}
362
363/// Empties the cache. Tests only: the cache is process-wide, so a test
364/// that asserts about the budget has to start from a known footprint.
365#[cfg(test)]
366fn clear() {
367    let mut cache = lock();
368    cache.entries.clear();
369    cache.resident_bytes = 0;
370}
371
372pub(super) fn get_or_repack_q4k(data: &WeightBytes, rows: usize, cols: usize) -> Arc<[u8]> {
373    get_or_repack(Format::Q4Kx8, data.map_id(), rows, cols, || {
374        ferrox_quant::pack_q4_k_matrix_x8(
375            data.as_slice(),
376            rows,
377            cols,
378            ferrox_quant::q4_kx8_interleave(),
379        )
380    })
381}
382
383pub(super) fn get_or_repack_q5k(data: &WeightBytes, rows: usize, cols: usize) -> Arc<[u8]> {
384    get_or_repack(Format::Q5Kx8, data.map_id(), rows, cols, || {
385        ferrox_quant::pack_q5_k_matrix_x8(
386            data.as_slice(),
387            rows,
388            cols,
389            ferrox_quant::q5_kx8_interleave(),
390        )
391    })
392}
393
394pub(super) fn get_or_repack_q6k(data: &WeightBytes, rows: usize, cols: usize) -> Arc<[u8]> {
395    get_or_repack(Format::Q6Kx8, data.map_id(), rows, cols, || {
396        ferrox_quant::pack_q6_k_matrix_x8(
397            data.as_slice(),
398            rows,
399            cols,
400            ferrox_quant::q6_kx8_interleave(),
401        )
402    })
403}
404
405pub(super) fn get_or_repack_q8x4(data: &WeightBytes, rows: usize, cols: usize) -> Arc<[u8]> {
406    get_or_repack(Format::Q8_0x4, data.map_id(), rows, cols, || {
407        ferrox_quant::pack_q8_0_matrix_x4(
408            data.as_slice(),
409            rows,
410            cols,
411            ferrox_quant::q8_0x4_interleave(),
412        )
413    })
414}
415
416pub(super) fn get_or_repack_q4_0x4(data: &WeightBytes, rows: usize, cols: usize) -> Arc<[u8]> {
417    get_or_repack(Format::Q4_0x4, data.map_id(), rows, cols, || {
418        ferrox_quant::pack_q4_0_matrix_x4(
419            data.as_slice(),
420            rows,
421            cols,
422            ferrox_quant::q4_0x4_interleave(),
423        )
424    })
425}
426
427/// Whether the `Q8_0x4` packing of `data` is held in the cache. What
428/// the `apply_cpu_q8` test below asks after one call.
429#[cfg(test)]
430pub(super) fn q8x4_is_cached(data: &WeightBytes, rows: usize, cols: usize) -> bool {
431    is_cached(Format::Q8_0x4, data, rows, cols)
432}
433
434#[cfg(test)]
435mod tests {
436    use super::super::tests::{f16_le, ForceIntDot};
437    use super::super::{QuantKind, WeightMatrix};
438    use super::*;
439    use std::sync::atomic::{AtomicUsize, Ordering};
440
441    /// `usize::MAX` means "no override": a real budget of `usize::MAX`
442    /// is not reachable, since it is a quarter of a byte count that
443    /// came out of a memory probe.
444    static BUDGET_OVERRIDE: AtomicUsize = AtomicUsize::new(usize::MAX);
445
446    /// The budget [`super::budget_bytes`] should report, if a test has
447    /// pinned one.
448    pub(super) fn budget_override() -> Option<usize> {
449        match BUDGET_OVERRIDE.load(Ordering::Acquire) {
450            usize::MAX => None,
451            bytes => Some(bytes),
452        }
453    }
454
455    /// Pins the cache budget, and empties the cache, for the lifetime of
456    /// the guard.
457    ///
458    /// Both halves are necessary and both are here rather than at the
459    /// call sites: the cache and the budget are process-wide, so a test
460    /// that asserts about either has to own both, and two tests holding
461    /// different budgets at once would see each other's. The mutex is
462    /// what serializes them, the same shape as
463    /// `weight_matrix::tests::ForceIntDot`.
464    pub(super) struct ForceBudget {
465        _lock: std::sync::MutexGuard<'static, ()>,
466    }
467
468    impl ForceBudget {
469        fn new(bytes: usize) -> Self {
470            static LOCK: Mutex<()> = Mutex::new(());
471            let lock = LOCK.lock().unwrap_or_else(|e| e.into_inner());
472            BUDGET_OVERRIDE.store(bytes, Ordering::Release);
473            clear();
474            ForceBudget { _lock: lock }
475        }
476
477        /// Enough for any fixture here: the budget is not what the test
478        /// is about.
479        fn generous() -> Self {
480            Self::new(1 << 20)
481        }
482    }
483
484    impl Drop for ForceBudget {
485        fn drop(&mut self) {
486            clear();
487            BUDGET_OVERRIDE.store(usize::MAX, Ordering::Release);
488        }
489    }
490
491    // -----------------------------------------------------------------
492    // Repack cache identity (see `MapId`)
493    //
494    // The bug these cover: the caches used to key on `(address, rows)`
495    // and gate on an `address_is_stable() -> bool`. Drop one mmap, make
496    // another, and the kernel hands the same address back, so the cache
497    // served the previous matrix's interleaved bytes -- an out-of-range
498    // panic when the shapes differed, silent wrong output when they
499    // matched.
500    //
501    // Address reuse is the OS's decision and cannot be demanded from a
502    // test, so these do not wait for it. They fabricate exactly what the
503    // cache would SEE in that moment -- a key that collides while the
504    // mapping behind it is gone, or while the width differs -- and
505    // assert the cache refuses to serve it.
506    // -----------------------------------------------------------------
507
508    /// Writes `bytes` to a temp file and maps it. The caller holds the
509    /// `Arc`, so when the mapping dies is explicit, which is the whole
510    /// subject of these tests.
511    fn mapped(tag: &str, bytes: &[u8]) -> (Arc<memmap2::Mmap>, WeightBytes) {
512        let path = std::env::temp_dir().join(format!(
513            "ferrox_repack_{tag}_{}_{:?}.bin",
514            std::process::id(),
515            std::thread::current().id()
516        ));
517        std::fs::write(&path, bytes).expect("write fixture");
518        let file = std::fs::File::open(&path).expect("open fixture");
519        // SAFETY: the file was written and closed above, is named for
520        // this process and thread, and nothing mutates it while mapped.
521        let mmap = Arc::new(unsafe { memmap2::Mmap::map(&file).expect("map fixture") });
522        let _ = std::fs::remove_file(&path);
523        let view = WeightBytes::Mapped {
524            mmap: Arc::clone(&mmap),
525            range: 0..bytes.len(),
526        };
527        (mmap, view)
528    }
529
530    /// Q8_0 bytes with finite scales, `rows * cols/32` blocks.
531    fn q8_0_matrix_bytes(rows: usize, cols: usize, seed: u32) -> Vec<u8> {
532        let mut state = seed | 1;
533        let mut next = move || {
534            state = state.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
535            (state >> 24) as u8
536        };
537        let mut data = Vec::with_capacity(rows * (cols / 32) * 34);
538        for _ in 0..rows * (cols / 32) {
539            data.extend_from_slice(&f16_le(0.02 + f32::from(next()) * 0.0004));
540            for _ in 0..32 {
541                data.push(next());
542            }
543        }
544        data
545    }
546
547    /// A `MapId` is only an identity while its mapping is alive. This is
548    /// the check the old boolean could not express, and it is the one
549    /// thing standing between the cache and an ABA.
550    #[test]
551    fn map_id_stops_matching_once_its_mapping_is_dropped() {
552        let (mmap, view) = mapped("live", &q8_0_matrix_bytes(4, 32, 7));
553        let id = view.map_id().expect("Mapped bytes must have an identity");
554        let held = id.clone();
555        assert!(
556            held.matches(&id),
557            "a live mapping must match its own identity"
558        );
559
560        // Everything that could witness the mapping is gone: this is
561        // precisely the moment the address becomes reusable.
562        drop(view);
563        drop(mmap);
564        assert!(
565            !held.matches(&id),
566            "an identity whose mapping is dead must not match, or the \
567             cache will trust an address the kernel has already reissued"
568        );
569    }
570
571    /// A cache entry left behind by a dead mapping must be replaced, not
572    /// served. Fabricates the entry rather than waiting on the OS to
573    /// reissue an address; the entry is byte-for-byte what the old code
574    /// would have left there.
575    #[test]
576    fn stale_repack_entry_is_replaced_not_served() {
577        let _budget = ForceBudget::generous();
578        let (rows, cols) = (8usize, 64usize);
579        let bytes = q8_0_matrix_bytes(rows, cols, 11);
580        let (_mmap, view) = mapped("stale", &bytes);
581        let id = view.map_id().expect("Mapped bytes must have an identity");
582
583        // Some other matrix's packing, parked at the key this live
584        // matrix will look up, under an identity that can never upgrade.
585        let poison = vec![0xABu8; 16];
586        {
587            let mut cache = lock();
588            cache.insert_within_budget(
589                id.key(Format::Q8_0x4, rows, cols),
590                MapId {
591                    map: std::sync::Weak::new(),
592                    id: id.id,
593                    offset: id.offset,
594                },
595                Arc::from(poison.clone().into_boxed_slice()),
596            );
597        }
598
599        let got = get_or_repack_q8x4(&view, rows, cols);
600        let want = ferrox_quant::pack_q8_0_matrix_x4(
601            view.as_slice(),
602            rows,
603            cols,
604            ferrox_quant::q8_0x4_interleave(),
605        );
606        assert_ne!(&got[..], &poison[..], "served a dead mapping's bytes");
607        assert_eq!(&got[..], &want[..], "stale entry was not repacked");
608
609        // And the dead entry is gone rather than pinning a control block.
610        let cache = lock();
611        let entry = cache
612            .entries
613            .get(&id.key(Format::Q8_0x4, rows, cols))
614            .expect("the live packing should now be cached");
615        assert!(
616            entry.id.matches(&id),
617            "the replacement entry must carry the LIVE identity"
618        );
619    }
620
621    /// Two widths at one address are two matrices. The old key was
622    /// `(address, rows)`, so a 576x576 and a 576x1536 collided and the
623    /// second was served the first's shorter buffer.
624    ///
625    /// Drives the primitive directly: the collision needs two byte
626    /// buffers under ONE identity, which is exactly what the typed
627    /// lookups exist to make impossible for production code.
628    #[test]
629    fn repack_key_separates_two_widths_at_one_address() {
630        let _budget = ForceBudget::generous();
631        let rows = 8usize;
632        let narrow = q8_0_matrix_bytes(rows, 32, 3);
633        let wide = q8_0_matrix_bytes(rows, 64, 5);
634        let (_mmap, view) = mapped("widths", &narrow);
635        let id = view.map_id().expect("Mapped bytes must have an identity");
636
637        let il = ferrox_quant::q8_0x4_interleave();
638        let a = get_or_repack(Format::Q8_0x4, Some(id.clone()), rows, 32, || {
639            ferrox_quant::pack_q8_0_matrix_x4(&narrow, rows, 32, il)
640        });
641        let b = get_or_repack(Format::Q8_0x4, Some(id.clone()), rows, 64, || {
642            ferrox_quant::pack_q8_0_matrix_x4(&wide, rows, 64, il)
643        });
644        assert_eq!(
645            &a[..],
646            &ferrox_quant::pack_q8_0_matrix_x4(&narrow, rows, 32, il)[..]
647        );
648        assert_eq!(
649            &b[..],
650            &ferrox_quant::pack_q8_0_matrix_x4(&wide, rows, 64, il)[..],
651            "the wider matrix was served the narrower one's packing"
652        );
653        assert!(b.len() > a.len(), "widths must not share a cache entry");
654    }
655
656    /// Owned buffers and expert-store leases are never cacheable. The
657    /// lease is the interesting one: its allocation stays alive and keeps
658    /// its address while its CONTENTS are replaced by another expert's,
659    /// so no liveness check could rescue it.
660    #[test]
661    fn map_id_is_none_for_owned_and_shared_bytes() {
662        let owned = WeightBytes::Owned(q8_0_matrix_bytes(4, 32, 9));
663        assert!(owned.map_id().is_none(), "an owned Vec's address is reused");
664
665        let buf = Arc::new(q8_0_matrix_bytes(4, 32, 13));
666        let leased = WeightBytes::Shared {
667            buf,
668            range: 0..34 * 4,
669        };
670        assert!(
671            leased.map_id().is_none(),
672            "an expert lease keeps its address across a content swap"
673        );
674    }
675
676    /// A mapped matrix is packed once, and a second lookup is a hit that
677    /// hands back the SAME allocation. An owned matrix is never cached.
678    /// Both halves are the contract the matvecs rely on.
679    #[test]
680    fn a_mapped_matrix_is_packed_once_and_an_owned_one_never_cached() {
681        let _budget = ForceBudget::generous();
682        let (rows, cols) = (8usize, 64usize);
683        let bytes = q8_0_matrix_bytes(rows, cols, 17);
684        let (_mmap, view) = mapped("once", &bytes);
685        assert!(!q8x4_is_cached(&view, rows, cols));
686        let first = get_or_repack_q8x4(&view, rows, cols);
687        assert!(q8x4_is_cached(&view, rows, cols));
688        let second = get_or_repack_q8x4(&view, rows, cols);
689        assert!(
690            Arc::ptr_eq(&first, &second),
691            "a second lookup of a live mapping must be a cache hit"
692        );
693
694        let owned = WeightBytes::Owned(bytes);
695        let a = get_or_repack_q8x4(&owned, rows, cols);
696        let b = get_or_repack_q8x4(&owned, rows, cols);
697        assert!(!q8x4_is_cached(&owned, rows, cols));
698        assert!(
699            !Arc::ptr_eq(&a, &b),
700            "owned bytes have no identity and must repack every time"
701        );
702    }
703
704    /// The #128 regression, at the call site that carried it.
705    ///
706    /// `apply_cpu_q8` is the int-dot matvec the dense FFN gate/up and
707    /// every MoE expert take, and it repacked its matrix on EVERY call:
708    /// it passed a hand-written `None` identity where the other matvecs
709    /// passed `data.map_id()`. Measured at one thread on TinyLlama Q8_0,
710    /// that repack was ~85% of a decode token. The typed lookup now
711    /// derives the identity itself, and this asserts the packing of a
712    /// mapped matrix is in the cache after one call through that path.
713    ///
714    /// Sabotage: make `get_or_repack_q8x4` pass `None` instead of
715    /// `data.map_id()` and this goes red.
716    ///
717    /// Two preconditions, both of which have to hold before there is
718    /// anything to assert. The budget must be non-zero, or nothing is
719    /// retained by design. And `apply_cpu_q8` is a MATVEC, so it only
720    /// exists on a host that takes the matvec half of the int-dot tier
721    /// (#152 turned that half off on x86, where it measured 4x to 8.8x
722    /// slower than the AVX2 f32 dot). Where it does not, the guard skips
723    /// rather than asserting a `None` is a `Some`.
724    #[test]
725    fn apply_cpu_q8_caches_the_packing_of_a_mapped_matrix() {
726        let _force = ForceIntDot::new(true);
727        let _budget = ForceBudget::generous();
728        if !super::super::cpu_int_dot_for(super::super::IntDotShape::Matvec) {
729            return;
730        }
731        let (rows, cols) = (8usize, 64usize);
732        let bytes = q8_0_matrix_bytes(rows, cols, 23);
733        let (_mmap, view) = mapped("apply_q8", &bytes);
734        let m = WeightMatrix::Quantized {
735            data: view,
736            rows,
737            cols,
738            kind: QuantKind::Q8_0,
739        };
740        let WeightMatrix::Quantized { data, .. } = &m else {
741            unreachable!()
742        };
743        assert!(!q8x4_is_cached(data, rows, cols), "fresh mapping");
744
745        let x: Vec<f32> = (0..cols).map(|i| (i as f32) * 0.01 - 0.3).collect();
746        let act = ferrox_quant::quantize_activations_q8(&x);
747        let out = m
748            .apply_cpu_q8(&act)
749            .expect("Q8_0 with int-dot on takes the interleaved path");
750        assert_eq!(out.len(), rows);
751        assert!(
752            q8x4_is_cached(data, rows, cols),
753            "apply_cpu_q8 repacked a mapped matrix without caching it: \
754             that is a full copy of the matrix per token (#128)"
755        );
756    }
757
758    // -----------------------------------------------------------------
759    // The budget
760    //
761    // The cache retains a SECOND copy of a matrix that is already
762    // mapped. Measured at +527 MB of peak footprint on TinyLlama-1.1B
763    // Q8_0 for the dense FFN gate/up packings alone, and a resident MoE
764    // pays that once per expert ever routed to. These tests are the
765    // bound on that.
766    // -----------------------------------------------------------------
767
768    /// The interleave the Q8_0 fixtures pack with, spelled once.
769    fn il() -> usize {
770        ferrox_quant::q8_0x4_interleave()
771    }
772
773    /// Ten distinct matrices under a budget that holds three of them.
774    ///
775    /// Three separate properties, because a cache can fail each one on
776    /// its own:
777    ///
778    /// 1. it never exceeds the budget, at any step;
779    /// 2. it still answers correctly for what it dropped;
780    /// 3. it EVICTS rather than stops caching. A cache that filled up
781    ///    and then refused every later matrix would satisfy (1) and (2)
782    ///    and be useless: decode walks every layer, so the first three
783    ///    matrices would be cached forever and every other matrix would
784    ///    repack per token, which is #128 again for all but three of
785    ///    them.
786    ///
787    /// Sabotage: insert unconditionally and (1) goes red; delete the
788    /// eviction loop and (3) goes red. Both were run.
789    #[test]
790    fn the_cache_never_exceeds_its_budget() {
791        let (rows, cols) = (8usize, 64usize);
792        let one_packing =
793            ferrox_quant::pack_q8_0_matrix_x4(&q8_0_matrix_bytes(rows, cols, 1), rows, cols, il())
794                .len();
795        // Room for three packings, and not a byte more.
796        let budget = one_packing * 3;
797        let _guard = ForceBudget::new(budget);
798
799        let mut held = Vec::new();
800        for seed in 0..10u32 {
801            let bytes = q8_0_matrix_bytes(rows, cols, seed + 1);
802            let (mmap, view) = mapped(&format!("budget{seed}"), &bytes);
803            let got = get_or_repack_q8x4(&view, rows, cols);
804            assert_eq!(
805                &got[..],
806                &ferrox_quant::pack_q8_0_matrix_x4(&bytes, rows, cols, il())[..],
807                "an evicting cache must still answer correctly"
808            );
809            assert!(
810                resident_bytes() <= budget,
811                "cache grew past its budget at matrix {seed}: {} > {budget}",
812                resident_bytes()
813            );
814            // Hold the mappings so no address is reused mid-test, which
815            // would make an eviction indistinguishable from an ABA drop.
816            held.push((mmap, view));
817        }
818        assert!(resident_bytes() <= budget);
819        assert!(
820            resident_bytes() >= one_packing,
821            "a budget that fits three packings must be holding some"
822        );
823
824        // (3): the LAST matrix is resident and the FIRST is not, which
825        // only an evicting cache can manage under this budget.
826        let (_, last) = held.last().expect("ten matrices were packed");
827        assert!(
828            q8x4_is_cached(last, rows, cols),
829            "the most recent matrix must be cached: a cache that stops \
830             caching once full leaves every later matrix repacking per \
831             token, which is the #128 defect for all but the first few"
832        );
833        let (_, first) = &held[0];
834        assert!(
835            !q8x4_is_cached(first, rows, cols),
836            "ten packings into a three-packing budget must have evicted \
837             the least recently used one"
838        );
839    }
840
841    /// The batched GEMM path answers the same with the cache holding its
842    /// packing and with the cache disabled.
843    ///
844    /// This is where #152 and #158 have to compose. #152 turns the
845    /// batch half of the int-dot tier ON for x86, so a prefill there now
846    /// repacks matrices that nothing repacked before; #158 bounds what
847    /// those packings may retain. They meet at `get_or_repack_*`, which
848    /// is the ONE budgeted lookup either half reaches -- the batch path
849    /// opens no cache of its own, so there is one pool, not two.
850    ///
851    /// What must hold across that meeting is that the budget decides
852    /// only where the interleaved bytes LIVE, never what they are. A
853    /// miss under a tight budget returns a fresh packing and the GEMM
854    /// must produce bit-identical output, or a memory-constrained host
855    /// would silently answer differently from a roomy one.
856    ///
857    /// So the generous side runs the GEMM TWICE: once cold, which
858    /// misses and inserts, and once warm, which is served the retained
859    /// packing. Comparing warm against cold is what puts the hit path
860    /// under test; comparing either against the zero-budget run is what
861    /// puts the budget under test. A first draft compared one cold run
862    /// against one zero-budget run and survived zeroing the miss path,
863    /// because that corrupts both sides identically -- it asserted that
864    /// two equally wrong answers agreed.
865    ///
866    /// Sabotage: return zeroed bytes from `Cache::take_hit`, and this
867    /// goes red where the miss-path version did not.
868    #[test]
869    fn the_batch_path_answers_the_same_with_the_cache_full_and_disabled() {
870        let _force = ForceIntDot::new(true);
871        if !super::super::cpu_int_dot_for(super::super::IntDotShape::BatchGemm) {
872            return;
873        }
874        // Two row-groups plus a tail, and a batch that straddles the
875        // 4-wide activation quad, so the GEMM takes both its full-tile
876        // and partial-tile paths under each budget.
877        let (rows, cols, batch) = (19usize, 64usize, 6usize);
878        let bytes = q8_0_matrix_bytes(rows, cols, 71);
879        let (_mmap, view) = mapped("batch_budget", &bytes);
880        let m = WeightMatrix::Quantized {
881            data: view,
882            rows,
883            cols,
884            kind: QuantKind::Q8_0,
885        };
886        let x: Vec<f32> = (0..batch * cols)
887            .map(|i| ((i as f32) * 0.013 - 0.7).sin() * 1.4)
888            .collect();
889
890        let cached = {
891            let _budget = ForceBudget::generous();
892            let cold = m.apply_batch(&x, batch);
893            assert!(
894                resident_bytes() > 0,
895                "a generous budget retained nothing, so the second call \
896                 below would miss too and the hit path would go untested"
897            );
898            let warm = m.apply_batch(&x, batch);
899            assert_eq!(
900                cold.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
901                warm.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
902                "the retained packing served a different answer than the \
903                 one that built it"
904            );
905            warm
906        };
907        let uncached = {
908            let _budget = ForceBudget::new(0);
909            let out = m.apply_batch(&x, batch);
910            assert_eq!(resident_bytes(), 0, "a zero budget retained something");
911            out
912        };
913
914        assert_eq!(
915            cached.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
916            uncached.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
917            "the repack budget changed the answer, not just where the \
918             interleaved bytes live"
919        );
920    }
921
922    /// A zero budget retains nothing, which is the behaviour before the
923    /// cache existed: every call repacks, into its own allocation, and
924    /// the answer is unchanged.
925    ///
926    /// This is what makes the memory-constrained host expressible rather
927    /// than merely given a smaller number.
928    ///
929    /// Sabotage: make `insert_within_budget` skip its `size > budget`
930    /// return and this goes red.
931    #[test]
932    fn a_zero_budget_retains_nothing_and_matches_the_pre_cache_behaviour() {
933        let _guard = ForceBudget::new(0);
934        let (rows, cols) = (8usize, 64usize);
935        let bytes = q8_0_matrix_bytes(rows, cols, 29);
936        let (_mmap, view) = mapped("zero_budget", &bytes);
937
938        let first = get_or_repack_q8x4(&view, rows, cols);
939        let second = get_or_repack_q8x4(&view, rows, cols);
940        assert_eq!(resident_bytes(), 0, "a zero budget retained something");
941        assert!(!q8x4_is_cached(&view, rows, cols));
942        assert!(
943            !Arc::ptr_eq(&first, &second),
944            "a zero budget must repack every call, as the engine did \
945             before this cache existed"
946        );
947        let want = ferrox_quant::pack_q8_0_matrix_x4(&bytes, rows, cols, il());
948        assert_eq!(&first[..], &want[..]);
949        assert_eq!(&second[..], &want[..], "same bytes, different allocation");
950    }
951
952    /// Every format spends the SAME budget. Five caches would be five
953    /// budgets over one pool of RAM, which is the defect
954    /// `expert_store`'s single-holder rule exists to prevent.
955    ///
956    /// Sabotage: ignore the budget in `insert_within_budget`, or delete
957    /// its eviction loop, and this goes red. Collapsing `Format` out of
958    /// the key does NOT turn it red, and the doc on `Format` says why:
959    /// the two fixtures are two mappings, so their keys differ with or
960    /// without it.
961    #[test]
962    fn every_format_spends_one_budget() {
963        let (rows, cols) = (8usize, 64usize);
964        let bytes = q8_0_matrix_bytes(rows, cols, 31);
965        let q8_len = ferrox_quant::pack_q8_0_matrix_x4(&bytes, rows, cols, il()).len();
966        let _guard = ForceBudget::new(q8_len);
967
968        let (_mmap, view) = mapped("one_budget_q8", &bytes);
969        let _ = get_or_repack_q8x4(&view, rows, cols);
970        assert!(q8x4_is_cached(&view, rows, cols), "the budget holds one");
971
972        // A Q4_0 packing of a DIFFERENT mapping, into a budget with room
973        // for exactly one entry.
974        let q4_bytes = vec![7u8; rows * (cols / 32) * 18];
975        let (_mmap4, view4) = mapped("one_budget_q4", &q4_bytes);
976        let _ = get_or_repack_q4_0x4(&view4, rows, cols);
977
978        assert!(
979            resident_bytes() <= q8_len,
980            "the two formats spent one budget, not two"
981        );
982        assert!(
983            !q8x4_is_cached(&view, rows, cols),
984            "the Q4_0 packing must have displaced the Q8_0 one"
985        );
986    }
987}