Skip to main content

memra_engine/
moe_cache.rs

1//! EDGE-1 §B: SLRU GPU expert-residency cache (MOE-SLRU-PLAN §B).
2//!
3//! Stage-1 `moe_ffn` re-stages EVERY routed expert EVERY token over PCIe into one scratch slot.
4//! The same ~15-20% of experts recur (the "hot expert" mass), so an SLRU residency cache makes
5//! the steady-state re-stage count -> ~0. The cache holds N fixed-address GPU slots (never
6//! re-allocated, never fragmented), a `BlockId -> slot` residency table, an SLRU eviction policy
7//! (probation + protected segments; the second-miss "ghost" admission filter was measured a net
8//! loss in both regimes and removed 2026-07-08 — first-miss admit is the policy) so a one-off cold
9//! expert can never evict a genuinely hot one.
10//!
11//! THE bit-identity property (MOE-SLRU-PLAN §B.3): a cache HIT and a MISS feed `qmatvec_view` the
12//! *same* GGUF block bytes — the only difference is whether the `memcpy_htod` ran. So the cache-hit
13//! weight path is byte-for-byte identical to stage-every-token; the §D.2 gate pins this.
14//!
15//! Gated behind `MEMRA_MOE_CACHE` (default off => current stage-every-token behavior).
16
17use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
18use std::sync::Arc;
19use cudarc::driver::{CudaEvent, CudaSlice, CudaStream, HostSlice, SyncOnDrop};
20use crate::Engine;
21use crate::model::{ExpertKeepalive, ExpertSource};
22use crate::spill_pread::{PreadPool, PreadStats, ReadTicket, SpillIoMode};
23
24/// Which projection of an expert (gate/up/down are three distinct GGUF blocks per expert).
25pub const PROJ_GATE: u8 = 0;
26pub const PROJ_UP: u8 = 1;
27pub const PROJ_DOWN: u8 = 2;
28
29/// Residency key: expert `ex` of layer `layer` projection `proj` is a distinct block.
30#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
31pub struct BlockId {
32    pub layer: u16,
33    pub proj: u8,
34    pub ex: u16,
35}
36impl BlockId {
37    #[inline]
38    pub fn new(layer: u16, proj: u8, ex: u16) -> Self { BlockId { layer, proj, ex } }
39}
40
41/// Where a dispatched block landed (always a retained resident slot since the first-miss-admit
42/// policy, 2026-07-08 — the transient staging tier went with the ghost filter).
43#[derive(Clone, Copy, Debug)]
44pub enum DispatchSlot {
45        Resident(usize),
46}
47
48/// One fixed-address size class with an independent SLRU. Separating queues by capacity prevents a
49/// small mixed-layout block from consuming the scarce slots that can hold a larger block.
50struct SlotClass {
51    capacity: usize,
52    probation: VecDeque<usize>,
53    protected: VecDeque<usize>,
54    free: Vec<usize>,
55    protected_cap: usize,
56}
57
58/// SLRU GPU expert-residency cache. Slots remain fixed-address for the cache lifetime. Uniform
59/// models use one class; mixed-layout models may preallocate several exact-capacity classes.
60pub struct MoeSlotCache {
61    slots: Vec<CudaSlice<u8>>, // fixed GPU buffers; capacities live in `classes`
62    slot_class: Vec<usize>,    // slot index -> size-class index
63    classes: Vec<SlotClass>,
64    occupant: Vec<Option<BlockId>>, // slots[s] currently holds occupant[s]  (the residency bitmask)
65    table: HashMap<BlockId, usize>, // BlockId -> slot index (O(1) residency lookup)
66    /// Exponentially aged online access scores for the optional mixed-layout LFU victim policy.
67    /// Scores survive eviction and perf-counter resets; an opt-in decode-epoch decay prevents a
68    /// batched prompt from permanently outweighing recent token-to-token reuse.
69    frequencies: HashMap<BlockId, f32>,
70    /// Copy-stream prefetches that have reserved a slot but are not visible in `table` until the
71    /// consumer inserts an explicit compute-stream wait for `ready`. Pending slots are absent from
72    /// both SLRU queues, so neither synchronous admission nor another prefetch can evict them.
73
74    pending: HashMap<BlockId, PendingBlock>,
75    /// Source owners whose copy completed submission but not yet DMA completion. They are reaped
76    /// only after the recorded copy-stream event reports complete.
77    inflight_sources: Vec<(Arc<CudaEvent>, ExpertKeepalive)>,
78    /// Owners for copies whose completion could not be proved. Kept until a whole-stream drain;
79    /// leaked with the GPU slots if teardown cannot establish safety.
80    quarantined_sources: Vec<ExpertKeepalive>,
81    /// Unique owners used by demand/fallback H2D on the compute stream. `stage_expert` receives a
82    /// raw byte slice, so cudarc cannot attach its own source-lifetime event. Retain each backing
83    /// allocation once until cache teardown instead of paying one CUDA event per miss.
84    compute_sources: HashMap<KeepaliveKey, ExpertKeepalive>,
85    /// Opt-in positioned-read backends. Pinned buffers remain owned here until their explicit
86    /// compute-stream completion events fire.
87        pread: Option<PreadPool>,
88    /// Known-next reads submitted to disk workers but not yet consumed by dispatch. They own pinned
89    /// buffers, not GPU slots; all CUDA submission remains on the caller thread.
90    worker_reads: HashMap<BlockId, WorkerRead>,
91    pread_requested: bool,
92    pread_fallbacks: u64,
93    /// Retained so an event-creation failure after copy submission can be drained again during
94    /// teardown. A slot touched by an unprovable copy is quarantined outside every cache queue.
95    copy_stream: Arc<CudaStream>,
96    copy_stream_unknown: bool,
97    compute_stream: Arc<CudaStream>,
98        compute_stream_unknown: bool,
99
100    n: usize,
101    max_block_bytes: usize,
102    size_aware: bool,
103    frequency_evict: bool,
104    frequency_decay: Option<f32>,
105    /// Relative LFU value of a NextN/MTP access. The MTP block is keyed at `u16::MAX` and is
106    /// latency-critical during speculative decode, but contributes only one layer of observations
107    /// versus the full trunk. Keep the neutral default; local fixed-residency profiling may raise
108    /// it after an exact throughput sweep.
109    mtp_frequency_weight: f32,
110    last_forward_layer: Option<u16>,
111    last_forward_t: usize,
112    /// Stable-residency mode for heterogeneous CPU/GPU expert execution. Once frozen, callers may
113    /// still read resident slots, but must stage cache misses through transient scratch instead of
114    /// changing which experts execute on each backend.
115    frozen: bool,
116
117    // --- LAUNCH-STRUCTURE STAGE 3 (2026-07-05): device-side expert-pointer indirection ---
118    /// Resident-block count per LAYER (all 3 projections summed). When a layer reaches
119    /// 3*n_expert every routed block of that layer is cache-resident at a fixed address, so the
120    /// whole layer can dispatch via the DEVICE pointer table with ZERO host routing (no router
121    /// DtoH, no per-layer stream sync — the round-trip stall the decode profile measured at
122    /// ~36us x 40 layers/token). Maintained by admit/evict.
123    per_layer: HashMap<u16, u32>,
124    /// Per-layer device pointer row [3, n_expert] of slot base addresses (u64), uploaded lazily
125    /// when the layer first reads as fully resident. Slots are fixed-address for the cache's
126    /// lifetime, so a row stays valid until an eviction touches that layer (which drops the row
127    /// -> re-upload on next full residency).
128    dev_rows: HashMap<u16, CudaSlice<u64>>,
129    /// Layers whose one-shot prewarm was already attempted (success or not) — spill rigs whose
130    /// free slots can't hold a full layer must not re-scan 3*n_expert blocks every token.
131    prewarm_tried: HashSet<u16>,
132
133    // --- §D.4 instrumentation ---
134    pub hits: u64,
135    pub misses: u64,
136    pub staged_bytes: u64,    // total H2D bytes the cache caused (admit + first-miss transient)
137}
138
139struct PendingBlock {
140    slot: usize,
141    ready: Arc<CudaEvent>,
142        keepalive: Option<ExpertKeepalive>,
143}
144
145#[derive(Clone, Copy)]
146struct WorkerRead {
147    ticket: ReadTicket,
148    len: usize,
149}
150
151#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
152enum KeepaliveKey {
153    Pinned(usize),
154    Buffer(usize),
155    Mmap(usize),
156}
157
158impl KeepaliveKey {
159    fn from_owner(owner: &ExpertKeepalive) -> Self {
160        match owner {
161            ExpertKeepalive::Pinned(value) => Self::Pinned(Arc::as_ptr(value) as usize),
162            ExpertKeepalive::Buffer(value) => Self::Buffer(Arc::as_ptr(value) as usize),
163            ExpertKeepalive::Mmap(value) => Self::Mmap(Arc::as_ptr(value) as usize),
164        }
165    }
166}
167
168/// Exact-length view over one CUDA-pinned pool allocation. cudarc's raw `&[u8]` HostSlice waits
169/// for the whole stream before returning, while passing `PinnedHostSlice` would copy its full
170/// capacity. This wrapper submits exactly the expert prefix; the caller records and retains the
171/// completion event before the backing allocation can be reused.
172struct ExactPinnedPrefix<'a>(&'a [u8]);
173
174impl HostSlice<u8> for ExactPinnedPrefix<'_> {
175    fn len(&self) -> usize { self.0.len() }
176
177    unsafe fn stream_synced_slice<'a>(
178                &'a self,
179        _stream: &'a CudaStream,
180    ) -> (&'a [u8], SyncOnDrop<'a>) {
181        // SAFETY: the pread staging helpers record an explicit event immediately after the async
182        // memcpy and PreadPool retains both allocation and event until it completes.
183        (self.0, SyncOnDrop::Record(None))
184    }
185
186
187
188    unsafe fn stream_synced_mut_slice<'a>(
189        &'a mut self,
190        _stream: &'a CudaStream,
191    ) -> (&'a mut [u8], SyncOnDrop<'a>) {
192        panic!("ExactPinnedPrefix is a source-only HostSlice")
193    }
194}
195
196fn stage_on_copy_stream(
197    e: &Engine,
198    host_bytes: &[u8],
199    slot: &mut CudaSlice<u8>,
200) -> Result<Arc<CudaEvent>, (Box<dyn std::error::Error>, bool)> {
201    // Protect all earlier compute-stream users of a reused slot before the copy stream overwrites it.
202    let prior = match e.stream().record_event(None) {
203        Ok(prior) => prior,
204        Err(err) => return Err((err.into(), true)),
205    };
206    if let Err(err) = e.copy_stream.wait(&prior) {
207        return Err((err.into(), true));
208    }
209    match e.stage_expert_async(host_bytes, slot, 0) {
210        Ok(ready) => Ok(Arc::new(ready)),
211        Err(err) => {
212            // The H2D may have been submitted before event creation failed. Never release either the
213            // destination slot or pinned source until the copy stream has drained.
214            match e.copy_stream.synchronize() {
215                Ok(()) => Err((err, true)),
216                Err(sync_err) => Err((std::io::Error::other(format!(
217                    "copy-stream H2D setup failed ({err}); stream drain also failed ({sync_err})"
218                )).into(), false)),
219            }
220        }
221    }
222}
223
224fn stage_pread_on_compute_stream(
225    e: &Engine,
226    host_bytes: &[u8],
227    slot: &mut CudaSlice<u8>,
228) -> Result<Arc<CudaEvent>, Box<dyn std::error::Error>> {
229    let ready = Arc::new(e.ctx().new_event(None)?);
230    let source = ExactPinnedPrefix(host_bytes);
231    let mut dst = slot.slice_mut(0..host_bytes.len());
232    e.stream().memcpy_htod(&source, &mut dst)?;
233    ready.record(&e.stream())?;
234        Ok(ready)
235}
236
237fn stage_pread_prefetch_on_copy_stream(
238    e: &Engine,
239    host_bytes: &[u8],
240    slot: &mut CudaSlice<u8>,
241) -> Result<Arc<CudaEvent>, (Box<dyn std::error::Error>, bool)> {
242    let ready = match e.ctx().new_event(None) {
243        Ok(ready) => Arc::new(ready),
244        Err(err) => return Err((err.into(), true)),
245    };
246    let source = ExactPinnedPrefix(host_bytes);
247    let mut dst = slot.slice_mut(0..host_bytes.len());
248    let submitted = e
249        .copy_stream
250        .memcpy_htod(&source, &mut dst)
251        .and_then(|()| ready.record(&e.copy_stream));
252    match submitted {
253        Ok(()) => Ok(ready),
254        Err(err) => match e.copy_stream.synchronize() {
255            Ok(()) => Err((err.into(), true)),
256            Err(sync_err) => Err((
257                std::io::Error::other(format!(
258                "pread copy-stream H2D setup failed ({err}); stream drain also failed ({sync_err})"
259            ))
260                .into(),
261                false,
262            )),
263        },
264    }
265}
266
267/// Allocate the same fraction of every exact block-size class under one byte budget. This avoids
268/// biasing residency toward either low-bit or high-bit tiers while eliminating max-slot padding.
269fn size_class_plan(block_bytes: &[usize], budget_bytes: usize) -> Vec<(usize, usize)> {
270    let mut counts: BTreeMap<usize, usize> = BTreeMap::new();
271    for &bytes in block_bytes.iter().filter(|&&bytes| bytes > 0) {
272        *counts.entry(bytes).or_insert(0) += 1;
273    }
274    if counts.is_empty() || budget_bytes == 0 {
275        return Vec::new();
276    }
277    let total_bytes: u128 = counts
278        .iter()
279        .map(|(&bytes, &count)| (bytes as u128 + 8) * count as u128)
280        .sum();
281    let budget = budget_bytes as u128;
282    let mut plan: Vec<(usize, usize, u128)> = counts
283        .iter()
284        .map(|(&bytes, &count)| {
285            let scaled = count as u128 * budget;
286            (
287                bytes,
288                (scaled / total_bytes).min(count as u128) as usize,
289                scaled % total_bytes,
290            )
291        })
292        .collect();
293    let mut used: u128 = plan
294        .iter()
295        .map(|(bytes, count, _)| (*bytes as u128 + 8) * *count as u128)
296        .sum();
297
298    // Hamilton-style remainder pass keeps class proportions close after flooring. There are only
299    // a handful of layout classes, so one additional slot per class covers all rounding loss.
300    let mut order: Vec<usize> = (0..plan.len()).collect();
301    order.sort_by(|&a, &b| plan[b].2.cmp(&plan[a].2).then(a.cmp(&b)));
302    for index in order {
303        let (bytes, count, _) = plan[index];
304        let available = counts[&bytes];
305        let required = bytes as u128 + 8;
306        if count < available && used + required <= budget {
307            plan[index].1 += 1;
308            used += required;
309        }
310    }
311    plan.into_iter()
312        .filter_map(|(bytes, count, _)| (count > 0).then_some((bytes, count)))
313        .collect()
314}
315
316impl MoeSlotCache {
317    /// Build the cache sizing N from free VRAM (MOE-SLRU-PLAN §B.4): probe free VRAM AFTER residents
318    /// are loaded; N is shared across ALL layers so it must hold the WHOLE-MODEL hot set, not one
319    /// layer's. The 35B-A3B keeps its 256 experts HOST-resident, so the GPU has ~20+ GB free at
320    /// decode — empirically a 256-slot cache thrashes (~2-7% hit) while a few-thousand-slot cache
321    /// reaches ~85%+ steady-state. So the DEFAULT auto-sizes N to fill `MEMRA_MOE_VRAM_FRAC` (default
322    /// 0.85) of free VRAM, clamped to [256, ~hot-set]. `MEMRA_MOE_SLOTS` forces an exact N.
323    pub fn new(e: &Engine, max_block_bytes: usize) -> Result<Self, Box<dyn std::error::Error>> {
324        let (free, _total) = e.ctx().mem_get_info()?;
325        // Keep two blocks of slack after the machine-specific hard ceiling. The default remains
326        // 80%; tightly provisioned spill rigs may raise it only after an OOM-gated local sweep.
327        let hard_frac = cache_hard_vram_frac();
328        let hard_bytes =
329            ((free as f64 * hard_frac) as usize).saturating_sub(2 * (max_block_bytes + 8));
330        let forced_slots = std::env::var("MEMRA_MOE_SLOTS")
331            .ok()
332            .and_then(|s| s.parse::<usize>().ok());
333        let requested_bytes = if let Some(n) = forced_slots {
334            n.saturating_mul(max_block_bytes + 8)
335        } else {
336            // auto: fill MEMRA_MOE_VRAM_FRAC of free VRAM with slots (default 85%).
337            // DEFAULT 0.85 (2026-07-06 local sweep: 0.40=25.0, 0.60=28.0, 0.85=28.5 tok/s on the
338            // spill-regime 35B — hit-rate 87.8% -> 99.2%, PCIe 55 -> 3.8 MB/tok; the 0.80
339            // hard-headroom cap below still bounds the true allocation, so 0.85 requests the max).
340            // Rigs co-running other GPU work should set MEMRA_MOE_VRAM_FRAC lower.
341            let frac = std::env::var("MEMRA_MOE_VRAM_FRAC")                .ok()
342                .and_then(|s| s.parse::<f64>().ok())
343                .unwrap_or(0.85);
344            (free as f64 * frac) as usize
345        };
346        let budget_bytes = requested_bytes.min(hard_bytes);
347        let layout = e.moe_cache_layout().unwrap_or_default();
348        let size_aware = forced_slots.is_none()
349            && std::env::var("MEMRA_MOE_SIZE_AWARE").as_deref() == Ok("1")
350            && !layout.is_empty();
351        let frequency_evict = std::env::var("MEMRA_MOE_LFU").as_deref() == Ok("1");
352        let frequency_decay = if frequency_evict {
353            cache_lfu_decay()
354        } else {
355            None
356        };
357        let mtp_frequency_weight = cache_lfu_mtp_weight();
358        let mut class_plan = if size_aware {
359            size_class_plan(&layout, budget_bytes)
360        } else {
361            Vec::new()
362        };
363        if class_plan.iter().map(|(_, count)| count).sum::<usize>() < 8 {
364            let n = (budget_bytes / (max_block_bytes + 8)).max(8);
365            class_plan = vec![(max_block_bytes, n)];
366        }
367        let n: usize = class_plan.iter().map(|(_, count)| count).sum();
368
369        let mut slots = Vec::with_capacity(n);
370        let mut slot_class = Vec::with_capacity(n);
371        let mut classes = Vec::with_capacity(class_plan.len());
372        let mut occupant = Vec::with_capacity(n);
373        for (class_index, &(capacity, count)) in class_plan.iter().enumerate() {
374            let start = slots.len();
375            for _ in 0..count {
376                // +8 tail pad: wide expert dots may issue an aligned read past the final block.
377                slots.push(e.alloc_u8(capacity + 8)?);
378                slot_class.push(class_index);
379                occupant.push(None);
380            }
381            let free_slots = (start..start + count).rev().collect();
382            classes.push(SlotClass {
383                capacity,
384                probation: VecDeque::new(),
385                protected: VecDeque::new(),
386                free: free_slots,
387                protected_cap: ((count as f64 * 0.8) as usize).max(1),
388            });
389        }
390        if size_aware {
391            let allocated: usize = class_plan
392                .iter()
393                .map(|(bytes, count)| (bytes + 8) * count)
394                .sum();
395            eprintln!(
396                "[moe-cache] size-aware fixed slots: {n} slots in {} classes, {:.2} GB / {:.2} GB budget",
397                class_plan.len(),
398                allocated as f64 / 1e9,
399                budget_bytes as f64 / 1e9
400            );
401        }
402        let pread_mode = crate::spill_pread::configured_mode();
403        let pread_requested = pread_mode != SpillIoMode::Mmap;
404        let pread = if pread_requested {
405
406            match PreadPool::try_new(e, max_block_bytes, pread_mode) {
407                Ok(pool) => Some(pool),
408                Err(err) => {
409                    eprintln!("[spill-pread] pinned-buffer initialization failed ({err}); using mmap");
410                    None
411                }
412            }
413        } else { None };
414
415
416        Ok(MoeSlotCache {
417            slots,
418            slot_class,
419            classes,
420            occupant,
421            table: HashMap::with_capacity(n * 2),
422            frequencies: HashMap::with_capacity(layout.len().max(n * 2)),
423            pending: HashMap::new(),
424            inflight_sources: Vec::new(),
425            quarantined_sources: Vec::new(),
426 compute_sources: HashMap::new(),
427            pread, worker_reads: HashMap::new(), pread_requested, pread_fallbacks: 0,
428            copy_stream: e.copy_stream.clone(), copy_stream_unknown: false,
429                        compute_stream: e.stream().clone(),
430            compute_stream_unknown: false,
431            n,
432            max_block_bytes,
433            size_aware,
434            frequency_evict,
435            frequency_decay,
436            mtp_frequency_weight,
437            last_forward_layer: None,
438            last_forward_t: 0,
439            frozen: false,
440            per_layer: HashMap::new(),
441            dev_rows: HashMap::new(),
442            prewarm_tried: HashSet::new(),
443            hits: 0, misses: 0, staged_bytes: 0,
444        })
445    }
446
447    #[inline]
448    pub fn n_slots(&self) -> usize {         self.n
449    }
450    #[inline]
451    pub fn is_frozen(&self) -> bool {
452        self.frozen
453    }
454    pub fn freeze(&mut self) {
455        if !self.frozen {
456            self.frozen = true;
457            let (_, complete, one_projection, two_projections, stranded_blocks) =
458                self.expert_residency_shape();
459            eprintln!(
460                "[moe-cache] residency frozen: {} slots, {} resident blocks; \
461                 {complete} complete experts, {one_projection} one-projection fragments, \
462                 {two_projections} two-projection fragments ({stranded_blocks} stranded blocks)",
463                self.n,
464                self.table.len()
465            );
466            let mut mtp_masks = HashMap::<u16, u8>::new();
467            for id in self.table.keys().filter(|id| id.layer == u16::MAX) {
468                *mtp_masks.entry(id.ex).or_insert(0) |= 1u8 << id.proj;
469            }
470            if !mtp_masks.is_empty() {
471                let complete = mtp_masks.values().filter(|&&mask| mask == 0b111).count();
472                eprintln!(
473                    "[moe-cache] frozen MTP residency: {} blocks, {complete} complete experts",
474                    mtp_masks.values().map(|mask| mask.count_ones() as usize).sum::<usize>()
475                );
476            }
477        }
478    }
479
480    pub(crate) fn expert_residency_shape(&self) -> (usize, usize, usize, usize, usize) {
481        let mut masks = HashMap::<(u16, u16), u8>::new();
482        for id in self.table.keys() {
483            *masks.entry((id.layer, id.ex)).or_insert(0) |= 1u8 << id.proj;
484        }
485        let complete = masks.values().filter(|&&mask| mask == 0b111).count();
486        let one_projection = masks
487            .values()
488            .filter(|&&mask| mask.count_ones() == 1)
489            .count();
490        let two_projections = masks
491            .values()
492            .filter(|&&mask| mask.count_ones() == 2)
493            .count();
494        let stranded_blocks = one_projection + 2 * two_projections;
495        (
496            masks.len(),
497            complete,
498            one_projection,
499            two_projections,
500            stranded_blocks,
501        )
502    }
503
504    #[inline]
505    pub fn max_block_bytes(&self) -> usize {
506        self.max_block_bytes
507    }
508
509
510    /// O(1) residency check (the ktransformers `generate_gpu_experts_masks` analog).
511    #[inline]
512    pub fn resident(&self, id: BlockId) -> Option<usize> { self.table.get(&id).copied() }
513
514    #[inline]
515    fn frequency_increment(&self, id: BlockId) -> f32 {
516        if id.layer == u16::MAX { self.mtp_frequency_weight } else { 1.0 }
517    }
518
519    /// Record a routed block that a fused all-hit path consumed without going through dispatch.
520    /// Warmup-only callers use this to make the LFU profile reflect actual grouped GPU traffic;
521    /// frozen serving skips it because residency can no longer change.
522    pub(crate) fn note_profile_hit(&mut self, id: BlockId) {
523        if self.frozen || !self.table.contains_key(&id) {
524            return;
525        }
526        let increment = self.frequency_increment(id);
527        *self.frequencies.entry(id).or_insert(0.0) += increment;
528    }
529
530    /// HIT promotion (SLRU): on a probation hit promote to protected; on a protected hit bump to MRU.
531    ///
532    /// O(1) EARLY-OUT (STAGING-ELISION stage, 2026-07-04): while FREE slots remain, `admit` pops
533    /// `free` and `evict_one` is unreachable — recency order is dead state until the cache fills.
534    /// The promotion below is a linear scan of two VecDeques (O(n_slots) PER HIT; at ~46k slots x
535    /// ~850 hits/token that was ~40M host ops/token — measured as the fast-admit A/B regression
536    /// 48.5 -> 46.0 tok/s on the 35B g7e decode). Skip it until eviction is possible. On 96GB
537    /// (slots >= whole-model block count) every HIT stays an O(1) table lookup forever; on spill
538        /// rigs this only defers SLRU ordering to when eviction pressure actually exists.
539    /// Bookkeeping-only: the dispatched bytes are identical either way (the D.2 gate pins it).
540    fn on_hit(&mut self, slot: usize) {
541        let class_index = self.slot_class[slot];
542        let class = &mut self.classes[class_index];
543        if !class.free.is_empty() {
544            return;
545        }
546        if let Some(pos) = class.probation.iter().position(|&x| x == slot) {
547            class.probation.remove(pos);
548            self.push_protected(slot);
549        } else if let Some(pos) = class.protected.iter().position(|&x| x == slot) {
550            class.protected.remove(pos);
551            class.protected.push_back(slot); // MRU
552        } else {
553            // not in either segment (shouldn't happen for a resident slot) — treat as protected MRU
554            self.push_protected(slot);
555
556        }
557    }
558
559
560    /// Push a slot to protected MRU; if protected exceeds its cap, demote its LRU front to probation.
561    fn push_protected(&mut self, slot: usize) {
562        let class = &mut self.classes[self.slot_class[slot]];
563        class.protected.push_back(slot);
564        while class.protected.len() > class.protected_cap {
565            if let Some(demoted) = class.protected.pop_front() {
566                class.probation.push_back(demoted);
567            } else {
568                break;
569            }
570        }
571    }
572
573    fn remove_occupant(&mut self, slot: usize) {
574        if let Some(old) = self.occupant[slot].take() {
575            self.table.remove(&old);
576            self.on_block_evicted(old.layer);
577        }
578    }
579
580    /// Lowest cumulative-frequency resident in one class; ties keep ordinary LRU order. A cold
581    /// admission therefore becomes the sacrificial slot on the next miss instead of displacing a
582    /// prompt-proven hot expert. `keep` protects the expert whose kernels are currently queued.
583    fn frequency_victim_in_class(&mut self, class_index: usize, keep: &[BlockId]) -> Option<usize> {
584        let class = &self.classes[class_index];
585        let probation_len = class.probation.len();
586        let candidate = class
587            .probation
588            .iter()
589            .chain(class.protected.iter())
590            .enumerate()
591            .filter_map(|(position, &slot)| {
592                let id = self.occupant[slot]?;
593                (!keep.contains(&id)).then_some((
594                    self.frequencies.get(&id).copied().unwrap_or(0.0),
595                    position,
596                    slot,
597                ))
598            })
599            .min_by(|a, b| a.0.total_cmp(&b.0).then(a.1.cmp(&b.1)));
600        let (_, position, slot) = candidate?;
601        if position < probation_len {
602            self.classes[class_index].probation.remove(position);
603        } else {
604            self.classes[class_index]
605                .protected
606                .remove(position - probation_len);
607        }
608        Some(slot)
609    }
610
611    /// Pick the LRU victim from the smallest class that can hold `required` bytes.
612    fn evict_one(&mut self, required: usize) -> Option<usize> {
613        for class_index in 0..self.classes.len() {
614            if self.classes[class_index].capacity < required {
615                continue;
616            }
617            let slot = if self.frequency_evict {
618                self.frequency_victim_in_class(class_index, &[])
619            } else {
620                self.classes[class_index]
621                    .probation
622                    .pop_front()
623                    .or_else(|| self.classes[class_index].protected.pop_front())
624            };
625            if let Some(slot) = slot {
626                self.remove_occupant(slot);
627                return Some(slot);
628            }
629        }
630        None
631    }
632
633    /// Pick a resident victim that is not needed by the expert currently being computed. Pending
634    /// slots never enter the SLRU queues, so they are excluded automatically. Returns `None` rather
635    /// than evicting a protected block; the caller then leaves this block to the synchronous path.
636    fn evict_one_excluding(&mut self, required: usize, keep: &[BlockId]) -> Option<usize> {
637        let take = |q: &mut VecDeque<usize>, occupant: &[Option<BlockId>]| {
638            q.iter()
639                .position(|&s| occupant[s].is_some_and(|id| !keep.contains(&id)))
640                .and_then(|pos| q.remove(pos))
641        };
642        for class_index in 0..self.classes.len() {
643            if self.classes[class_index].capacity < required {
644                continue;
645            }
646            let slot = if self.frequency_evict {
647                self.frequency_victim_in_class(class_index, keep)
648            } else {
649                take(&mut self.classes[class_index].probation, &self.occupant)
650                    .or_else(|| take(&mut self.classes[class_index].protected, &self.occupant))
651            };
652            if let Some(slot) = slot {
653                self.remove_occupant(slot);
654                return Some(slot);
655            }
656        }
657        None
658    }
659
660    /// STAGE 3 bookkeeping: a resident block of `layer` was evicted — the layer is no longer fully
661    /// resident, so its device pointer row (if uploaded) must be invalidated. NOTE: the row's device
662    /// buffer is dropped here, which is safe because the fully-resident fast path is only taken when
663    /// `dev_rows` contains the layer at DISPATCH time and all launches consuming the row were
664    /// enqueued BEFORE this eviction's staging memcpy on the same stream (single-stream ordering).
665    fn on_block_evicted(&mut self, layer: u16) {
666        if let Some(c) = self.per_layer.get_mut(&layer) { *c -= 1; }
667                self.dev_rows.remove(&layer);
668    }
669
670    fn reserve_slot(&mut self, required: usize) -> Option<usize> {
671        for class in &mut self.classes {
672            if class.capacity >= required {
673                if let Some(slot) = class.free.pop() {
674                    return Some(slot);
675                }
676            }
677        }
678        self.evict_one(required)
679    }
680
681    fn release_reserved_slot(&mut self, slot: usize) {
682        debug_assert!(self.occupant[slot].is_none());
683        self.classes[self.slot_class[slot]].free.push(slot);
684    }
685
686    fn publish(&mut self, id: BlockId, slot: usize) {
687        self.occupant[slot] = Some(id);
688        self.table.insert(id, slot);
689        self.classes[self.slot_class[slot]]
690            .probation
691            .push_back(slot);
692        *self.per_layer.entry(id.layer).or_insert(0) += 1;
693    }
694
695
696
697    fn reap_copy_sources(&mut self) {
698        self.inflight_sources.retain(|(ready, _)| !ready.is_complete());
699    }
700
701    fn retain_compute_source(&mut self, owner: Option<ExpertKeepalive>) {
702        if let Some(owner) = owner {
703            let key = KeepaliveKey::from_owner(&owner);
704            self.compute_sources.entry(key).or_insert(owner);
705        }
706    }
707
708    /// Admit a block: evict a victim, stage `host_bytes` into its slot, register residency, place in
709    /// probation (new admissions enter probation — they earn promotion on a later hit).
710    fn admit(&mut self, id: BlockId, host_bytes: &[u8], e: &Engine)
711             -> Result<usize, Box<dyn std::error::Error>> {
712        let slot = self.reserve_slot(host_bytes.len()).ok_or_else(|| {
713            std::io::Error::other(format!(
714                "no MoE cache slot can hold {} bytes (max class {})",
715                host_bytes.len(),
716                self.classes.last().map(|class| class.capacity).unwrap_or(0)
717            ))
718        })?;
719        // Pending copy-stream admissions are not in either SLRU queue, so `evict_one` cannot return
720        // an in-flight slot. This synchronous copy and its consumer remain ordered on gpu.stream.
721        if let Err(err) = e.stage_expert(host_bytes, &mut self.slots[slot], 0) {
722            return match e.stream().synchronize() {
723                Ok(()) => {
724                    self.release_reserved_slot(slot);
725                    Err(err)
726                }
727                Err(sync_err) => {
728                    // Keep the slot outside free/table/SLRU. Drop retries the stream drain and
729                    // leaks every slot if CUDA never provides a completion proof.
730                    self.compute_stream_unknown = true;
731                    Err(std::io::Error::other(format!(
732                        "compute-stream H2D setup failed ({err}); stream drain also failed ({sync_err})"
733                    )).into())
734                }
735            };
736        }
737        self.staged_bytes += host_bytes.len() as u64;
738        self.publish(id, slot);
739        Ok(slot)
740    }
741
742    fn note_pread_fallback(&mut self, reason: &dyn std::fmt::Display) {
743        self.pread_fallbacks += 1;
744        if let Some(pool) = self.pread.as_mut() {
745            pool.note_fallback();
746        }
747        if self.pread_fallbacks <= 3 {
748            eprintln!("[spill-pread] falling back to mmap: {reason}");
749        }
750    }
751
752    /// Start one MoE forward's worker-I/O scope. Any ticket left by an earlier error/early return is
753    /// no longer a valid lookahead target; cancel it before this scope submits its own known-next
754    /// reads. In-flight CPU reads keep their buffers until completion restores them safely.
755    pub(crate) fn begin_worker_scope(&mut self) {
756        if self.worker_reads.is_empty() { return; }
757                let tickets: Vec<_> = self
758            .worker_reads
759            .drain()
760            .map(|(_, read)| read.ticket)
761            .collect();
762        if let Some(pool) = self.pread.as_mut().filter(|pool| pool.is_worker()) {
763            for ticket in tickets {
764 let _ = pool.cancel_worker(ticket); }
765                }
766    }
767
768    /// Age cumulative LFU at decode-token boundaries. A batched prompt may touch one block many
769    /// times before decode begins; treating those touches as permanent future-use votes poisons a
770    /// spill cache. The first T=1 sweep starts a fresh frequency epoch while preserving populated
771    /// GPU slots. Later decode sweeps exponentially age history so recent cross-token reuse can
772    /// displace stale prompt-specific experts.
773    ///
774    /// MoE layers are visited in ascending order and the cache is model-global, so
775    /// `layer <= previous_layer` marks a new model forward. This changes victim selection only;
776    /// every hit and miss still feeds identical expert bytes to the same GPU kernel.
777    pub(crate) fn begin_forward_epoch(&mut self, layer: u16, t: usize) {
778        let Some(decay) = self.frequency_decay else {
779            self.last_forward_layer = Some(layer);
780            self.last_forward_t = t;
781            return;
782        };
783        let new_sweep = self
784            .last_forward_layer
785            .is_some_and(|previous| layer <= previous);
786        if new_sweep && t == 1 {
787            if self.last_forward_t != 1 {
788                self.frequencies.clear();
789            } else {
790                self.frequencies.retain(|_, score| {
791                    *score *= decay;
792                    *score >= 1.0e-3
793                });
794            }
795        }
796        self.last_forward_layer = Some(layer);
797        self.last_forward_t = t;
798    }
799
800    /// Turn already-submitted disk reads into copy-stream GPU admissions at a host-routing
801    /// boundary. The caller must invoke this only after the router's DtoH synchronization has
802    /// completed all earlier-layer compute, and `keep` must contain every block selected in the
803    /// current layer. A reserved victim is therefore neither in use nor about to be used, so its
804    /// H2D can start immediately while the CPU workers finish later reads. Consumers still insert
805    /// an explicit compute-stream wait through the ordinary `pending` dispatch path.
806    pub(crate) fn promote_worker_reads_at_safe_boundary(
807        &mut self,
808        order: &[BlockId],
809        keep: &[BlockId],
810        e: &Engine,
811    ) -> Result<usize, Box<dyn std::error::Error>> {
812        if !crate::spill_pread::copy_h2d_enabled() {
813            return Ok(0);
814        }
815        let mut promoted = 0usize;
816        for &id in order {
817            if self.table.contains_key(&id) || self.pending.contains_key(&id) {
818                if let Some(read) = self.worker_reads.remove(&id) {
819                    if let Some(pool) = self.pread.as_mut() {
820                        let _ = pool.cancel_worker(read.ticket);
821                    }
822                }
823                continue;
824            }
825            let Some(read) = self.worker_reads.get(&id).copied() else {
826                continue;
827            };
828            let Some(slot) = self.reserve_prefetch_slot(read.len, keep) else {
829                continue;
830            };
831            self.worker_reads.remove(&id);
832
833            let index = match self.pread.as_mut().unwrap().wait_worker(read.ticket) {
834                Ok(index) => index,
835                Err(err) => {
836                    let _ = self.pread.as_mut().unwrap().cancel_worker(read.ticket);
837                    self.release_reserved_slot(slot);
838                    self.note_pread_fallback(err.as_ref());
839                    continue;
840                }
841            };
842            let ready = {
843                let bytes = match self.pread.as_ref().unwrap().bytes(index, read.len) {
844                    Ok(bytes) => bytes,
845                    Err(err) => {
846                        self.pread.as_mut().unwrap().abort_read(index);
847                        self.release_reserved_slot(slot);
848                        self.note_pread_fallback(err.as_ref());
849                        continue;
850                    }
851                };
852                stage_pread_prefetch_on_copy_stream(e, bytes, &mut self.slots[slot])
853            };
854            let ready = match ready {
855                Ok(ready) => ready,
856                Err((err, reusable)) => {
857                    if reusable {
858                        self.pread.as_mut().unwrap().abort_read(index);
859                        self.release_reserved_slot(slot);
860                        self.note_pread_fallback(err.as_ref());
861                        continue;
862                    }
863                    self.pread.as_mut().unwrap().mark_unknown_h2d(index);
864                    self.copy_stream_unknown = true;
865                    return Err(err);
866                }
867            };
868            self.pread.as_mut().unwrap().mark_h2d(index, ready.clone());
869            self.occupant[slot] = Some(id);
870            self.pending.insert(
871                id,
872                PendingBlock {
873                    slot,
874                    ready,
875                    keepalive: None,
876                },
877            );
878            self.staged_bytes += read.len as u64;
879            promoted += 1;
880        }
881        Ok(promoted)
882    }
883
884    fn dispatch_disk(
885        &mut self,
886        id: BlockId,
887        file: &Arc<std::fs::File>,
888        offset: u64,
889        len: usize,
890        fallback: &[u8],
891        e: &Engine,
892    ) -> Result<DispatchSlot, Box<dyn std::error::Error>> {
893        if self.pread.is_none() {
894            if self.pread_requested {
895                self.note_pread_fallback(&"pinned-buffer backend unavailable");
896            }
897            return Ok(DispatchSlot::Resident(self.admit(id, fallback, e)?));
898        }
899
900        let pending = self.worker_reads.remove(&id);
901                let pool = self.pread.as_mut().unwrap();
902        let read = if pool.is_worker() {
903            let ticket = match pending {
904                Some(read) => Ok(Some(read.ticket)),
905                None => pool.submit_worker(file.clone(), offset, len),
906            };
907            match ticket {
908
909                Ok(Some(ticket)) => match pool.wait_worker(ticket) {
910                    Ok(index) => Ok(index),
911                    Err(err) => {
912                        // Read errors normally release in wait_worker. A worker/channel failure may
913                        // return earlier; cancel defensively so the next scope cannot lose the slot.
914                        let _ = pool.cancel_worker(ticket);
915                        Err(err)
916                    }
917                },
918                Ok(None) => Err(std::io::Error::other("worker read ring is busy").into()),
919                Err(err) => Err(err),
920            }
921        } else {
922            debug_assert!(pending.is_none());
923            pool.read(file.as_ref(), offset, len)
924        };
925        let index = match read {
926            Ok(index) => index,
927            Err(err) => {
928                self.note_pread_fallback(err.as_ref());
929                return Ok(DispatchSlot::Resident(self.admit(id, fallback, e)?));
930            }
931        };
932
933
934        // The blocking read happens before eviction, so an I/O failure leaves cache residency
935        // untouched and can safely use the mmap oracle.
936        let slot = self.reserve_slot(len).ok_or_else(|| {
937            std::io::Error::other(format!(
938                "no MoE cache slot can hold {len} bytes (max class {})",
939                self.classes.last().map(|class| class.capacity).unwrap_or(0)
940            ))
941        })?;
942        let ready = {
943            let bytes = match self.pread.as_ref().unwrap().bytes(index, len) {
944                Ok(bytes) => bytes,
945                Err(err) => {
946                    self.pread.as_mut().unwrap().abort_read(index);
947                    self.release_reserved_slot(slot);
948                    self.note_pread_fallback(err.as_ref());
949                    return Ok(DispatchSlot::Resident(self.admit(id, fallback, e)?));
950                }
951            };
952            stage_pread_on_compute_stream(e, bytes, &mut self.slots[slot])
953        };
954        let ready = match ready {
955            Ok(ready) => ready,
956            Err(err) => {
957                // A memcpy or event-record failure can occur after submission. Synchronize the
958                // retained compute stream before either source or destination is reused. If CUDA
959                // cannot prove completion, quarantine both and fail instead of risking UAF.
960                match e.stream().synchronize() {
961                    Ok(()) => {
962                        self.pread.as_mut().unwrap().abort_read(index);
963                        self.release_reserved_slot(slot);
964                        self.note_pread_fallback(err.as_ref());
965                        return Ok(DispatchSlot::Resident(self.admit(id, fallback, e)?));
966                    }
967                    Err(sync_err) => {
968                        self.pread.as_mut().unwrap().mark_unknown_h2d(index);
969                        return Err(std::io::Error::other(format!(
970                            "pread H2D setup failed ({err}); CUDA stream drain also failed ({sync_err})"
971                        )).into());
972                    }
973                }
974            }
975        };
976        self.pread.as_mut().unwrap().mark_h2d(index, ready);
977        // Copy and dependent GEMM share the compute stream, so stream order is the consumer fence.
978        // Publish only after both memcpy submission and explicit completion-event recording.
979        self.staged_bytes += len as u64;
980        self.publish(id, slot);
981        Ok(DispatchSlot::Resident(slot))
982    }
983
984    /// The dispatch decision for one (BlockId, host_bytes). Returns where the block landed; resolve
985    /// the device buffer with `buf()`. On the bit-identity-critical path the buffer holds EXACTLY
986    /// `host_bytes` either way (a HIT skipped the copy; the prior stage wrote the same bytes).
987    ///
988    /// Policy (MOE-SLRU-PLAN §B.2, first-miss admit since 2026-07-06):
989    /// - HIT  (table[id] = s): promote, return s. ZERO PCIe.
990    /// - MISS: admit (stage into a retained slot, evicting an SLRU victim when full).
991    pub fn dispatch(&mut self, id: BlockId, host_bytes: &[u8], e: &Engine)
992                    -> Result<DispatchSlot, Box<dyn std::error::Error>> {
993        self.dispatch_source(id, ExpertSource::Memory { bytes: host_bytes, keepalive: None }, e)
994    }
995
996    pub(crate) fn dispatch_source(&mut self, id: BlockId, source: ExpertSource<'_>, e: &Engine)
997                                  -> Result<DispatchSlot, Box<dyn std::error::Error>> {
998        self.reap_copy_sources();
999        let increment = self.frequency_increment(id);
1000        *self.frequencies.entry(id).or_insert(0.0) += increment;
1001        if let Some(s) = self.table.get(&id).copied() {
1002            self.hits += 1;
1003            self.on_hit(s);
1004            return Ok(DispatchSlot::Resident(s));
1005        }
1006        if let Some(pending) = self.pending.remove(&id) {
1007            if let Err(err) = e.compute_wait(pending.ready.as_ref()) {
1008                self.pending.insert(id, pending);
1009                return Err(err);
1010            }
1011            self.misses += 1;
1012            let slot = pending.slot;
1013            if let Some(keepalive) = pending.keepalive {
1014                self.inflight_sources.push((pending.ready, keepalive));
1015            }
1016            self.publish(id, slot);
1017            return Ok(DispatchSlot::Resident(slot));
1018        }
1019        self.misses += 1;
1020        // FIRST-MISS ADMIT (the only policy since 2026-07-08; the second-miss "ghost" filter and
1021        // its seams MEMRA_MOE_GHOST / MEMRA_MOE_FAST_ADMIT are gone). Measured record: while FREE
1022        // slots remain, admission evicts nothing — filtering only delayed residency (96GB: 83.7%
1023        // steady hit-rate instead of ~100%, 74 MB/token avoidable PCIe; 2026-07-04). In the SPILL
1024        // regime (cache permanently full, local 35B) the filter made every cold block pay TWO H2D
1025        // copies — ~6% of token PCIe, measured ABOVE its eviction-protection benefit (24.2 -> 25.0
1026        // tok/s with it off, 2026-07-06). First-miss admit evicts an SLRU victim when full; the
1027        // SLRU probation segment still protects the protected set. Bit-identity unchanged: the
1028        // slot holds byte-for-byte the same GGUF block (D.2 gate).
1029        match source {
1030            ExpertSource::Memory { bytes, keepalive } => {
1031                // Retain before H2D submission so even the setup-error path cannot release a
1032                // pinned/mapped source while CUDA may still be reading it.
1033                self.retain_compute_source(keepalive);
1034                let slot = self.admit(id, bytes, e)?;
1035                Ok(DispatchSlot::Resident(slot))
1036            }
1037            ExpertSource::Disk { file, offset, len, fallback, keepalive } => {
1038                // The owner is only needed when dispatch_disk falls back to mmap, but retaining
1039                // the usually shared mmap Arc once keeps every fallback branch simple and safe.
1040                self.retain_compute_source(Some(keepalive));
1041                self.dispatch_disk(id, file, offset, len, fallback, e)
1042            }
1043        }
1044    }
1045
1046    /// Deterministically stage a known-future block on the copy stream. The slot is reserved but is
1047    /// not considered resident until `dispatch` inserts a compute-stream wait for the returned copy
1048    /// event. Before overwriting a reused slot, the copy stream waits for all compute work already
1049    /// queued at this call site; the caller issues prefetch before the current expert's kernels, so
1050    /// the transfer can overlap those kernels without racing any earlier consumer of the victim.
1051    ///
1052    /// `keep` is the current expert's gate/up/down ids. If no safe victim exists, return `false` and
1053    /// let the normal synchronous miss path handle the block.
1054    pub fn prefetch(&mut self, id: BlockId, host_bytes: &[u8], keep: &[BlockId], e: &Engine)
1055                    -> Result<bool, Box<dyn std::error::Error>> {
1056        self.prefetch_source(
1057            id,
1058            ExpertSource::Memory { bytes: host_bytes, keepalive: None },
1059            keep,
1060            e,
1061                )
1062    }
1063
1064    fn reserve_prefetch_slot(&mut self, required: usize, keep: &[BlockId]) -> Option<usize> {
1065        for class in &mut self.classes {
1066            if class.capacity >= required {
1067                if let Some(slot) = class.free.pop() {
1068                    return Some(slot);
1069                }
1070            }
1071        }
1072        self.evict_one_excluding(required, keep)
1073    }
1074
1075    fn prefetch_bytes(
1076&mut self, id: BlockId, host_bytes: &[u8],
1077                      keepalive: Option<ExpertKeepalive>, keep: &[BlockId], e: &Engine)
1078                      -> Result<bool, Box<dyn std::error::Error>> {
1079        let Some(slot) = self.reserve_prefetch_slot(host_bytes.len(), keep) else { return Ok(false) };
1080        let ready = match stage_on_copy_stream(e, host_bytes, &mut self.slots[slot]) {
1081            Ok(ready) => ready,
1082            Err((err, reusable)) => {
1083                if reusable {
1084                    self.release_reserved_slot(slot);
1085                } else {
1086                    // The slot is absent from free/table/SLRU and cannot be reused. Drop retries a
1087                    // whole copy-stream drain and leaks all slots if CUDA still cannot prove safety.
1088                    self.copy_stream_unknown = true;
1089                    if let Some(keepalive) = keepalive {
1090                        self.quarantined_sources.push(keepalive);
1091                    }
1092                    eprintln!("[moe-cache] quarantining slot {slot} after unprovable copy completion");
1093                }
1094                return Err(err);
1095            }
1096        };
1097        self.occupant[slot] = Some(id);
1098        self.pending.insert(id, PendingBlock { slot, ready, keepalive });
1099        self.staged_bytes += host_bytes.len() as u64;
1100        Ok(true)
1101    }
1102
1103    pub(crate) fn prefetch_source(
1104        &mut self,
1105        id: BlockId,
1106        source: ExpertSource<'_>,
1107        keep: &[BlockId],
1108        e: &Engine,
1109    ) -> Result<bool, Box<dyn std::error::Error>> {
1110        self.reap_copy_sources();
1111        if self.table.contains_key(&id) || self.pending.contains_key(&id)
1112            || self.worker_reads.contains_key(&id) {
1113            return Ok(false);
1114        }
1115        match source {
1116            ExpertSource::Memory { bytes, keepalive } => {
1117                self.prefetch_bytes(id, bytes, keepalive, keep, e)
1118            }
1119            ExpertSource::Disk { file, offset, len, fallback, keepalive } => {
1120                if self.pread.as_ref().is_some_and(PreadPool::is_worker) {
1121                    match self.pread.as_mut().unwrap()
1122                        .submit_worker_speculative(file.clone(), offset, len) {
1123                        Ok(Some(ticket)) => {
1124                            self.worker_reads.insert(id, WorkerRead { ticket, len });
1125                            Ok(true)
1126                        }
1127                        Ok(None) => Ok(false),
1128                        Err(err) => {
1129                            self.note_pread_fallback(err.as_ref());
1130                            Ok(false)
1131                        }
1132                    }
1133                } else if self.pread.is_some() {
1134                    // Blocking `pread` remains demand-only so it cannot delay current compute.
1135                    Ok(false)
1136                } else {
1137                    self.prefetch_bytes(id, fallback, Some(keepalive), keep, e)
1138                }
1139            }
1140        }
1141    }
1142
1143    /// Pre-warm: force-admit a block (used by the §D.2 bit-identity gate to make all blocks resident).
1144    pub fn force_admit(&mut self, id: BlockId, host_bytes: &[u8], e: &Engine)
1145                       -> Result<usize, Box<dyn std::error::Error>> {
1146        if let Some(s) = self.table.get(&id).copied() { return Ok(s); }
1147        self.admit(id, host_bytes, e)
1148    }
1149
1150    /// STAGE 3 one-shot PREWARM: force-admit every block of `layer` while FREE slots can hold it
1151    /// (never evicts — a spill rig whose cache can't fit the layer just skips; organic residency
1152    /// still applies). Runs at most once per layer (success or not). The H2D copies are the SAME
1153    /// stage_expert bytes the miss path would issue — bit-identity unchanged; this only front-loads
1154    /// them so the device-dispatch fast path fires from token 0 instead of after the SLRU fill.
1155    /// Frozen residency as (layer, proj, ex) triples in slot order, for the freeze-profile
1156    /// sidecar. Slot order keeps the restage admit sequence close to the original placement.
1157    pub fn export_residency(&self) -> Vec<(u16, u8, u16)> {
1158        self.occupant
1159            .iter()
1160            .flatten()
1161            .map(|id| (id.layer, id.proj, id.ex))
1162            .collect()
1163    }
1164
1165    /// Admit one specific block from a saved freeze profile, reading through the layer's
1166    /// established expert source (the same recipe as `prewarm_layer`, but id-targeted so a
1167    /// persisted residency set restages without a profiling warmup). Returns false for ids
1168    /// that no longer resolve (changed plan, pruned expert) — the caller counts and reports.
1169    pub fn restage_block(&mut self, id: BlockId, m: &crate::hybrid::MoeWeights, e: &Engine)
1170                         -> Result<bool, Box<dyn std::error::Error>> {
1171        if self.table.contains_key(&id) {
1172            return Ok(true);
1173        }
1174        let exps = match id.proj {
1175            PROJ_GATE => &m.gate_exps,
1176            PROJ_UP => &m.up_exps,
1177            PROJ_DOWN => &m.down_exps,
1178            _ => return Ok(false),
1179        };
1180        if id.ex as usize >= exps.n_expert {
1181            return Ok(false);
1182        }
1183        if m.active_experts.as_ref().is_some_and(|active| !active[id.ex as usize]) {
1184            return Ok(false);
1185        }
1186        if exps.expert_layout(id.ex as usize).len == 0 {
1187            return Ok(false);
1188        }
1189        match exps.expert_source(id.ex as usize) {
1190            ExpertSource::Memory { bytes, keepalive } => {
1191                self.retain_compute_source(keepalive);
1192                self.admit(id, bytes, e)?;
1193            }
1194            ExpertSource::Disk { fallback, keepalive, .. } => {
1195                self.retain_compute_source(Some(keepalive));
1196                self.admit(id, fallback, e)?;
1197            }
1198        }
1199        Ok(true)
1200    }
1201
1202    pub fn prewarm_layer(&mut self, layer: u16, m: &crate::hybrid::MoeWeights, e: &Engine)
1203                         -> Result<(), Box<dyn std::error::Error>> {
1204        if !self.prewarm_tried.insert(layer) { return Ok(()); }
1205        let n_expert = m.gate_exps.n_expert;
1206        if self.pread.is_some() && (0..n_expert).any(|ex| {
1207            matches!(m.gate_exps.expert_source(ex), ExpertSource::Disk { .. })
1208                || matches!(m.up_exps.expert_source(ex), ExpertSource::Disk { .. })
1209                || matches!(m.down_exps.expert_source(ex), ExpertSource::Disk { .. })
1210        }) {
1211            // Prewarm is a whole-layer scan. It must not silently turn explicit demand I/O back
1212            // into an mmap walk; organic misses will populate the cache through dispatch_source.
1213            return Ok(());
1214                }
1215        let resident = self.per_layer.get(&layer).copied().unwrap_or(0) as usize;
1216        let missing = 3 * n_expert - resident;
1217        if self.size_aware {
1218            return Ok(());
1219        } // heterogeneous prewarm needs a per-class fit proof
1220        if self
1221            .classes
1222            .iter()
1223            .map(|class| class.free.len())
1224            .sum::<usize>()
1225            < missing
1226        {
1227            return Ok(()); // won't evict for a prewarm
1228        }
1229        for ex in 0..n_expert {
1230            for (proj, exps) in [
1231                (PROJ_GATE, &m.gate_exps),
1232 (PROJ_UP, &m.up_exps),
1233                                 (PROJ_DOWN, &m.down_exps)] {
1234                let id = BlockId::new(layer, proj, ex as u16);
1235                if self.table.contains_key(&id) { continue; }
1236                match exps.expert_source(ex) {
1237                    ExpertSource::Memory { bytes, keepalive } => {
1238                        self.retain_compute_source(keepalive);
1239                        self.admit(id, bytes, e)?;
1240                    }
1241                    ExpertSource::Disk { fallback, keepalive, .. } => {
1242                        self.retain_compute_source(Some(keepalive));
1243                        self.admit(id, fallback, e)?;
1244                    }
1245                }
1246            }
1247        }
1248        Ok(())
1249    }
1250
1251    /// STAGE 3: device pointer row for a FULLY-RESIDENT layer. Returns the [3, n_expert] u64 slot
1252    /// base-address table (proj-major: gate row, up row, down row) if EVERY block of `layer` is
1253    /// cache-resident, else None (caller falls back to host routing). The row is built+uploaded on
1254    /// first full residency and reused until an eviction touches the layer. `n_expert` is the
1255    /// layer's expert count (the full-residency threshold is 3*n_expert blocks).
1256    pub fn layer_dev_row(&mut self, layer: u16, n_expert: usize, e: &Engine)
1257                         -> Result<Option<&CudaSlice<u64>>, Box<dyn std::error::Error>> {
1258        if self.per_layer.get(&layer).copied().unwrap_or(0) as usize != 3 * n_expert {
1259            return Ok(None);
1260        }
1261        if !self.dev_rows.contains_key(&layer) {
1262            use cudarc::driver::DevicePtr;
1263            let mut host = vec![0u64; 3 * n_expert];
1264            for proj in 0..3u8 {
1265                for ex in 0..n_expert {
1266                    let Some(&s) = self.table.get(&BlockId::new(layer, proj, ex as u16)) else {
1267                        // count said fully resident but a block is missing — inconsistent; bail safe.
1268                        return Ok(None);
1269                    };
1270                    let __s_ev = e.stream();
1271                    let (p, _ev) = self.slots[s].device_ptr(&__s_ev);
1272                    host[proj as usize * n_expert + ex] = p as u64;
1273                }
1274            }
1275            let row = e.stream().clone_htod(&host)?;
1276            self.dev_rows.insert(layer, row);
1277        }
1278        Ok(self.dev_rows.get(&layer))
1279    }
1280
1281    /// Resolve a `DispatchSlot` to the device buffer to feed `qmatvec_view`.
1282    #[inline]
1283    pub fn buf(&self, d: DispatchSlot) -> &CudaSlice<u8> {
1284        match d {
1285            DispatchSlot::Resident(s) => &self.slots[s],
1286        }
1287    }
1288
1289    /// Read-only access to a slot's device buffer (the `qmatvec_view` source on a HIT).
1290    #[inline]
1291    pub fn slot(&self, s: usize) -> &CudaSlice<u8> { &self.slots[s] }
1292
1293    /// Hit rate over this cache's lifetime (for the §D.4 print).
1294    pub fn hit_rate(&self) -> f64 {
1295        let tot = self.hits + self.misses;
1296        if tot == 0 { 0.0 } else { self.hits as f64 / tot as f64 }
1297    }
1298
1299    /// Reset the per-window perf counters (lets the run print steady-state vs warmup separately).
1300    pub fn reset_counters(&mut self) {
1301        self.hits = 0; self.misses = 0; self.staged_bytes = 0;
1302    }
1303
1304    pub(crate) fn pread_stats(&self) -> Option<PreadStats> {
1305        if !self.pread_requested { return None; }
1306        let mut stats = self.pread.as_ref().map(PreadPool::stats).unwrap_or_default();
1307        stats.fallbacks = self.pread_fallbacks;
1308        Some(stats)
1309        }
1310}
1311
1312fn cache_lfu_decay() -> Option<f32> {
1313    let raw = std::env::var("MEMRA_MOE_LFU_DECAY").ok()?;
1314    match parse_cache_lfu_decay(Some(&raw)) {
1315        Ok(value) => value,
1316        Err(reason) => {
1317            eprintln!(
1318                "[moe-cache] invalid MEMRA_MOE_LFU_DECAY={raw:?} ({reason}); disabling LFU decay"
1319            );
1320            None
1321        }
1322    }
1323}
1324
1325fn cache_lfu_mtp_weight() -> f32 {
1326    const DEFAULT: f32 = 1.0;
1327    let raw = std::env::var("MEMRA_MOE_LFU_MTP_WEIGHT").ok();
1328    match parse_cache_lfu_mtp_weight(raw.as_deref()) {
1329        Ok(value) => value,
1330        Err(reason) => {
1331            eprintln!(
1332                "[moe-cache] invalid MEMRA_MOE_LFU_MTP_WEIGHT={:?} ({reason}); using {DEFAULT}",
1333                raw.as_deref().unwrap_or("")
1334            );
1335            DEFAULT
1336        }
1337    }
1338}
1339
1340fn parse_cache_lfu_mtp_weight(raw: Option<&str>) -> Result<f32, &'static str> {
1341    let value = raw
1342        .unwrap_or("1")
1343        .parse::<f32>()
1344        .map_err(|_| "expected a number")?;
1345    if value.is_finite() && (0.25..=64.0).contains(&value) {
1346        Ok(value)
1347    } else {
1348        Err("expected a finite multiplier from 0.25 through 64")
1349    }
1350}
1351
1352fn parse_cache_lfu_decay(raw: Option<&str>) -> Result<Option<f32>, &'static str> {
1353    let Some(raw) = raw else { return Ok(None) };
1354    let value = raw.parse::<f32>().map_err(|_| "expected a number")?;
1355    if value.is_finite() && value > 0.0 && value <= 1.0 {
1356        Ok(Some(value))
1357    } else {
1358        Err("expected a finite fraction greater than 0 and at most 1")
1359    }
1360}
1361
1362fn cache_hard_vram_frac() -> f64 {
1363    const DEFAULT: f64 = 0.80;
1364    let raw = std::env::var("MEMRA_MOE_HARD_VRAM_FRAC").ok();
1365    match parse_cache_hard_vram_frac(raw.as_deref()) {
1366        Ok(value) => value,
1367        Err(reason) => {
1368            eprintln!(
1369                "[moe-cache] invalid MEMRA_MOE_HARD_VRAM_FRAC={:?} ({reason}); using {DEFAULT}",
1370                raw.as_deref().unwrap_or("")
1371            );
1372            DEFAULT
1373        }
1374    }
1375}
1376
1377fn parse_cache_hard_vram_frac(raw: Option<&str>) -> Result<f64, &'static str> {
1378    let value = raw
1379        .unwrap_or("0.80")
1380        .parse::<f64>()
1381        .map_err(|_| "expected a number")?;
1382    if value.is_finite() && (0.10..=0.95).contains(&value) {
1383        Ok(value)
1384    } else {
1385        Err("expected a finite fraction from 0.10 through 0.95")
1386    }
1387}
1388
1389#[cfg(test)]
1390mod vram_fraction_tests {
1391    use super::{
1392        parse_cache_hard_vram_frac, parse_cache_lfu_decay, parse_cache_lfu_mtp_weight,
1393        size_class_plan,
1394    };
1395
1396    #[test]
1397    fn hard_vram_fraction_defaults_and_rejects_unsafe_values() {
1398        assert_eq!(parse_cache_hard_vram_frac(None), Ok(0.80));
1399        assert_eq!(parse_cache_hard_vram_frac(Some("0.82")), Ok(0.82));
1400        assert!(parse_cache_hard_vram_frac(Some("NaN")).is_err());
1401        assert_eq!(parse_cache_hard_vram_frac(Some("0.95")), Ok(0.95));
1402        assert!(parse_cache_hard_vram_frac(Some("0.96")).is_err());
1403        assert!(parse_cache_hard_vram_frac(Some("1.0")).is_err());
1404        assert!(parse_cache_hard_vram_frac(Some("bad")).is_err());
1405    }
1406
1407    #[test]
1408    fn lfu_decay_is_opt_in_and_bounded() {
1409        assert_eq!(parse_cache_lfu_decay(None), Ok(None));
1410        assert_eq!(parse_cache_lfu_decay(Some("0.8")), Ok(Some(0.8)));
1411        assert_eq!(parse_cache_lfu_decay(Some("1")), Ok(Some(1.0)));
1412        for value in ["0", "-0.1", "1.1", "NaN", "bad"] {
1413            assert!(
1414                parse_cache_lfu_decay(Some(value)).is_err(),
1415                "accepted {value}"
1416            );
1417        }
1418    }
1419
1420    #[test]
1421    fn lfu_mtp_weight_defaults_and_is_bounded() {
1422        assert_eq!(parse_cache_lfu_mtp_weight(None), Ok(1.0));
1423        assert_eq!(parse_cache_lfu_mtp_weight(Some("4")), Ok(4.0));
1424        for value in ["0", "0.1", "65", "NaN", "bad"] {
1425            assert!(
1426                parse_cache_lfu_mtp_weight(Some(value)).is_err(),
1427                "accepted {value}"
1428            );
1429        }
1430    }
1431
1432    #[test]
1433    fn size_class_plan_preserves_classes_and_never_exceeds_budget() {
1434        let blocks = [100usize, 100, 100, 200, 200, 400];
1435        let budget = (108 * 2) + 208 + 408;
1436        let plan = size_class_plan(&blocks, budget);
1437        assert!(plan.iter().all(|(_, count)| *count > 0));
1438        assert!(
1439            plan.iter()
1440                .map(|(bytes, count)| (bytes + 8) * count)
1441                .sum::<usize>()
1442                <= budget
1443        );
1444        assert!(plan.iter().all(|(bytes, count)| {
1445            *count <= blocks.iter().filter(|block| **block == *bytes).count()
1446        }));
1447    }
1448
1449    #[test]
1450    fn size_class_plan_returns_full_inventory_when_it_fits() {
1451        let blocks = [100usize, 100, 200, 400];
1452        let budget: usize = blocks.iter().map(|bytes| bytes + 8).sum();
1453        assert_eq!(
1454            size_class_plan(&blocks, budget),
1455            vec![(100, 2), (200, 1), (400, 1)]
1456        );
1457    }
1458
1459    #[test]
1460    fn size_class_plan_does_not_overflow_on_pathological_sizes() {
1461        let plan = size_class_plan(&[usize::MAX, usize::MAX], usize::MAX);
1462        assert!(plan.is_empty());
1463    }
1464
1465}
1466
1467impl Drop for MoeSlotCache {
1468    fn drop(&mut self) {
1469        // Event tracking is intentionally disabled in Engine. Drain explicit copy-stream handoffs
1470        // before either the destination slots or pinned read buffers begin field destruction.
1471        let mut safe_to_drop_slots = true;
1472        if self.compute_stream_unknown || !self.compute_sources.is_empty() {
1473            if let Err(err) = self.compute_stream.synchronize() {
1474                safe_to_drop_slots = false;
1475                eprintln!("[moe-cache] unknown compute-stream drain failed ({err}); leaking GPU slots for safety");
1476                for (_, keepalive) in self.compute_sources.drain() {
1477                    std::mem::forget(keepalive);
1478                }
1479            } else {
1480                self.compute_stream_unknown = false;
1481                self.compute_sources.clear();
1482            }
1483        }
1484        let need_copy_drain = self.copy_stream_unknown || !self.pending.is_empty()
1485            || !self.inflight_sources.is_empty() || !self.quarantined_sources.is_empty();
1486        if need_copy_drain {
1487            if let Err(err) = self.copy_stream.synchronize() {
1488                safe_to_drop_slots = false;
1489                eprintln!("[moe-cache] unknown copy-stream drain failed ({err}); leaking GPU slots for safety");
1490                for (_, keepalive) in self.inflight_sources.drain(..) {
1491                    std::mem::forget(keepalive);
1492                }
1493                for keepalive in self.quarantined_sources.drain(..) {
1494                    std::mem::forget(keepalive);
1495                }
1496                for (_, pending) in self.pending.drain() {
1497                    if let Some(keepalive) = pending.keepalive {
1498                        std::mem::forget(keepalive);
1499                    }
1500                }
1501            } else {
1502                self.copy_stream_unknown = false;
1503                self.inflight_sources.clear();
1504                self.quarantined_sources.clear();
1505                self.pending.clear();
1506            }
1507        }
1508        if let Some(pool) = self.pread.as_mut() {
1509            safe_to_drop_slots &= pool.drain();
1510        } else if self.pread_requested && self.pread_fallbacks != 0 {
1511            eprintln!(
1512                "[spill-pread] backend unavailable; mmap_fallbacks={}",
1513                self.pread_fallbacks
1514            );
1515        }
1516        if !safe_to_drop_slots {
1517            for slot in self.slots.drain(..) {
1518                std::mem::forget(slot);
1519            }
1520        }
1521    }
1522}