spark_runtime/weights/gguf/
expert_lru_pool.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2// provenance-id: 526f6e616c6420522e205374657369616b
3
4//! [`ExpertLru`] on the reader pool: misses and predicted experts read by
5//! the pool's threads into slots that carry a completion ticket, a real
6//! fetch waiting only on the slots it needs. Split from `expert_lru.rs`
7//! (500-LoC cap).
8//!
9//! Protocol: `fetch_many_prefetching(keys, predicted)` assigns slots for the
10//! keys' misses and enqueues their reads URGENT, then assigns slots for the
11//! predicted experts not resident and enqueues those BACKGROUND, then waits
12//! for the keys' tickets (a key already in flight from an earlier prediction
13//! is waited on too, and counts as a hit for the statistics with `waited`
14//! recorded). Every slot touched is pinned in the current epoch, so the GPU
15//! launches queued for this token can never see a slot being rewritten. At
16//! the next `begin_token`, finished tickets are cleared and predictions
17//! nobody asked for are demoted to the LRU end.
18
19use std::io::Write;
20use std::sync::Arc;
21
22use anyhow::{Context, Result, bail, ensure};
23
24use super::expert_lru::{ExpertLru, ExpertSlot};
25use super::expert_prefetch::{ReaderPool, Ticket};
26use super::expert_stream::ExpertSource;
27
28impl ExpertLru {
29    /// Read misses (and predictions) on `threads` persistent readers from
30    /// `src` instead of scoped threads per fetch.
31    pub fn set_pool(&mut self, src: Arc<dyn ExpertSource + Send + Sync>, threads: usize) {
32        self.pool = Some((src, ReaderPool::new(threads)));
33    }
34
35    pub fn has_pool(&self) -> bool {
36        self.pool.is_some()
37    }
38
39    /// Enqueue the read of `key` into slot `i` as `parts` byte ranges.
40    fn enqueue_read(&mut self, i: u32, key: (u32, u32), parts: usize, urgent: bool) -> Arc<Ticket> {
41        let (src, pool) = self.pool.as_ref().expect("reader pool");
42        let bytes = self.layout().bytes;
43        let parts = parts.clamp(1, (bytes / (1 << 20)).max(1));
44        let per = bytes.div_ceil(parts);
45        let ticket = Ticket::new(parts);
46        let p = self.slot_ptr(i);
47        for j in 0..parts {
48            let off = j * per;
49            let len = per.min(bytes - off);
50            let (src, ticket, p) = (src.clone(), ticket.clone(), p);
51            pool.submit(
52                Box::new(move || {
53                    let base = p.get();
54                    // SAFETY: slot `i` is mapped to `key` alone until its
55                    // ticket completes, and the parts of one slot are disjoint.
56                    let dst = unsafe { std::slice::from_raw_parts_mut(base.add(off), len) };
57                    ticket.finish(src.read_expert_range(key.0, key.1, off, dst));
58                }),
59                urgent,
60            );
61        }
62        self.meta[i as usize].ticket = Some(ticket.clone());
63        self.in_flight.push(i);
64        ticket
65    }
66
67    /// Slots for `keys` (misses read urgently), reads of `predicted` experts
68    /// started in the background, then the wait for `keys` only.
69    pub fn fetch_many_prefetching(
70        &mut self,
71        keys: &[(u32, u32)],
72        predicted: &[(u32, u32)],
73    ) -> Result<Vec<ExpertSlot>> {
74        ensure!(self.pool.is_some(), "fetch_many_prefetching without a pool");
75        ensure!(
76            self.staging.is_none(),
77            "device expert cache: no reader pool"
78        );
79        let threads = self.pool.as_ref().map(|p| p.1.threads()).unwrap_or(1);
80        let evictions0 = self.stats.evictions;
81        let mut out: Vec<u32> = Vec::with_capacity(keys.len());
82        let mut misses: Vec<(u32, (u32, u32))> = Vec::new();
83        let mut waits: Vec<(u32, Arc<Ticket>)> = Vec::new();
84        for &key in keys {
85            if let Some(&i) = self.map.get(&key) {
86                self.touch(i);
87                self.meta[i as usize].speculative = false;
88                if let Some(t) = self.meta[i as usize].ticket.clone()
89                    && !t.is_done()
90                {
91                    waits.push((i, t));
92                }
93                self.stats.hits += 1;
94                out.push(i);
95                continue;
96            }
97            let i = self.assign(key)?;
98            misses.push((i, key));
99            out.push(i);
100        }
101        let n_wait = waits.len();
102        // the misses first, split so all readers work on them
103        let parts = if misses.is_empty() {
104            1
105        } else {
106            (threads / misses.len()).clamp(1, 8)
107        };
108        for &(i, key) in &misses {
109            let t = self.enqueue_read(i, key, parts, true);
110            waits.push((i, t));
111        }
112        // then the predictions, whole experts in four ranges, background
113        let mut n_pred = 0usize;
114        for &key in predicted {
115            if self.map.contains_key(&key) {
116                continue;
117            }
118            let Ok(i) = self.assign(key) else { break };
119            self.meta[i as usize].speculative = true;
120            self.enqueue_read(i, key, 4, false);
121            n_pred += 1;
122        }
123        self.stats.prefetched += n_pred as u64;
124        let bytes = self.layout().bytes;
125        let mut failed: Vec<(u32, String)> = Vec::new();
126        for (i, t) in &waits {
127            if let Err(e) = t.wait() {
128                failed.push((*i, format!("{e:#}")));
129            }
130        }
131        let n_ok = misses.len()
132            - failed
133                .iter()
134                .filter(|(i, _)| misses.iter().any(|m| m.0 == *i))
135                .count();
136        self.stats.misses += n_ok as u64;
137        self.stats.bytes_read += (n_ok * bytes) as u64;
138        self.stats.waited += n_wait as u64;
139        if self.trace.is_some() {
140            let ev = self.stats.evictions - evictions0;
141            self.trace_line(keys, misses.len(), ev, n_wait);
142        }
143        if let Some((_, first)) = failed.first() {
144            let msg = format!(
145                "{} of {} expert reads failed; first: {first}",
146                failed.len(),
147                waits.len()
148            );
149            for (i, _) in &failed {
150                self.unmap(*i);
151            }
152            bail!(msg);
153        }
154        Ok(out.into_iter().map(|i| self.slot(i)).collect())
155    }
156
157    /// Start reading `predicted` experts in the background (no wait).
158    pub fn prefetch(&mut self, predicted: &[(u32, u32)]) -> Result<()> {
159        ensure!(self.pool.is_some(), "prefetch without a pool");
160        let mut n = 0usize;
161        for &key in predicted {
162            if self.map.contains_key(&key) {
163                continue;
164            }
165            let Ok(i) = self.assign(key) else { break };
166            self.meta[i as usize].speculative = true;
167            self.enqueue_read(i, key, 4, false);
168            n += 1;
169        }
170        self.stats.prefetched += n as u64;
171        Ok(())
172    }
173
174    /// Block until every read in flight has landed (tests, shutdown).
175    pub fn wait_in_flight(&self) {
176        for &i in &self.in_flight {
177            if let Some(t) = &self.meta[i as usize].ticket {
178                let _ = t.wait();
179            }
180        }
181    }
182
183    /// At a new token: drop the tickets of finished reads (unmapping the
184    /// slots of failed ones) and demote the predictions nobody asked for.
185    pub(super) fn reap(&mut self) {
186        let mut still = Vec::new();
187        for i in std::mem::take(&mut self.in_flight) {
188            let Some(t) = self.meta[i as usize].ticket.clone() else {
189                continue;
190            };
191            if !t.is_done() {
192                still.push(i);
193                continue;
194            }
195            self.meta[i as usize].ticket = None;
196            if t.error().is_some() {
197                self.unmap(i);
198                continue;
199            }
200            if self.meta[i as usize].speculative {
201                self.meta[i as usize].speculative = false;
202                self.stats.prefetch_unused += 1;
203                self.demote(i);
204            }
205        }
206        self.in_flight = still;
207    }
208
209    // ── diagnostics: the route trace ──
210
211    /// Append every `fetch_many` to `path` as one line: microseconds since
212    /// the cache was built, the layer, the key count, the misses and the
213    /// evictions of the call, then the expert ids in request order. The
214    /// exact access sequence, for replaying cache policies offline.
215    pub fn set_trace(&mut self, path: &str) -> Result<()> {
216        let f = std::fs::File::create(path).with_context(|| format!("route trace {path}"))?;
217        self.trace = Some(std::io::BufWriter::with_capacity(1 << 16, f));
218        Ok(())
219    }
220
221    pub(super) fn trace_line(
222        &mut self,
223        keys: &[(u32, u32)],
224        misses: usize,
225        evictions: u64,
226        wait: usize,
227    ) {
228        let Some(w) = self.trace.as_mut() else {
229            return;
230        };
231        let us = self.t0.elapsed().as_micros();
232        let layer = keys.first().map(|k| k.0).unwrap_or(u32::MAX);
233        let (pf, pfu) = (self.stats.prefetched, self.stats.prefetch_unused);
234        let _ = write!(
235            w,
236            "{us} L{layer} n={} miss={misses} evict={evictions} wait={wait} pf={pf} pfu={pfu}",
237            keys.len()
238        );
239        for k in keys {
240            let _ = write!(w, " {}", k.1);
241        }
242        let _ = writeln!(w);
243        // a killed serve keeps every line written so far
244        let _ = w.flush();
245    }
246}