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}