Skip to main content

cherenkov/qwen4_exp/gpu/
residency.rs

1//! The expert pool: which records the GPU may touch, LRU managed here,
2//! with two backings behind one interface.
3//!
4//! `Set`: a Metal residency set over the packed expert file's mapping.
5//! Every record is its own zero-copy sub-buffer (wrapped on first use);
6//! the set holds up to a budget of them and the GPU reaches a record
7//! through its GPU address. A record leaving the set keeps its pages in
8//! the page cache until the OS needs the memory, so a miss on a recently
9//! evicted record is served from RAM (an unwired second tier with no
10//! copies); a cold miss reads the file through the cache before the
11//! record is added. Records added while a command buffer waits on an
12//! event are resident by the time the GPU is released (measured: 1 to
13//! 4 ms for 150 records, no faults).
14//!
15//! `Copy`: one wired Metal buffer of record slots, filled by uncached
16//! parallel reads. No second tier, but no page-cache churn either, which
17//! matters when the machine is short of memory.
18
19use super::activity::reads::{ReadSource, ReadTicket, ReadTracker};
20use crate::metal::MetalContext;
21use crate::units::BYTES_PER_KIB;
22use anyhow::{Context, Result};
23use objc2::rc::Retained;
24use objc2::runtime::ProtocolObject;
25use objc2_metal::{
26    MTLBuffer, MTLCommandQueue, MTLDevice, MTLResidencySet, MTLResidencySetDescriptor,
27};
28use std::ffi::c_void;
29use std::fs::File;
30use std::sync::Arc;
31use std::sync::atomic::{AtomicBool, Ordering};
32
33/// Per-record completion retained by the deadline policy until its slot
34/// is safe to reuse. All record IO uses the measured pread path.
35#[derive(Clone)]
36pub struct Landed(Arc<AtomicBool>);
37
38impl Landed {
39    pub(super) fn new(done: bool) -> Self {
40        Self(Arc::new(AtomicBool::new(done)))
41    }
42    pub fn done(&self) -> bool {
43        self.0.load(Ordering::Acquire)
44    }
45
46    pub fn wait(&self) -> Result<()> {
47        let t = std::time::Instant::now();
48
49        while !self.done() {
50            std::hint::spin_loop();
51            anyhow::ensure!(
52                t.elapsed().as_secs() < 30,
53                "a record read did not land within 30 s"
54            );
55        }
56
57        Ok(())
58    }
59}
60
61type Buf = Retained<ProtocolObject<dyn MTLBuffer>>;
62
63pub enum Pool {
64    Set(Residency),
65    Copy(CopyPool),
66}
67
68/// One record read. A zero destination means a page-cache read for a
69/// mapped residency-set record; otherwise it is a CPU address in the copy
70/// pool. `need_index` connects completion to the caller's acquired records.
71#[derive(Clone)]
72struct PlannedRead {
73    transfer: RecordRead,
74    /// 0 = base 4-bit store, 1 = 3-bit, 2 = 2-bit.
75    kind: u8,
76    need_index: usize,
77}
78
79/// A destination and its measurement ticket travel together through batching.
80#[derive(Clone)]
81pub(super) struct RecordRead {
82    pub destination: usize,
83    pub file_offset: usize,
84    pub bytes: usize,
85    pub ticket: Option<ReadTicket>,
86}
87
88/// Reads required between pool acquisition and residency completion.
89/// Built by the service thread, then moved or borrowed by reader threads.
90pub struct ReadPlan {
91    items: Vec<PlannedRead>,
92    low_file: Option<File>,
93}
94
95impl ReadPlan {
96    pub(super) fn read_requests(&self) -> impl Iterator<Item = (usize, usize)> + '_ {
97        self.items
98            .iter()
99            .map(|read| (read.need_index, read.transfer.bytes))
100    }
101
102    pub fn is_empty(&self) -> bool {
103        self.items.is_empty()
104    }
105
106    pub fn len(&self) -> usize {
107        self.items.len()
108    }
109
110    /// Run the reads: copies go to their slots past the cache
111    /// (`nocache`), cache reads pull pages in through `cached`.
112    /// Run every read on its own detached thread; returns one flag per
113    /// entry of the `need` list the plan was built for (`need_len`), set
114    /// once that record is in memory (records that needed no read start
115    /// set). The caller keeps the flags of reads it stops waiting for and
116    /// checks them before the slot can be reused.
117    pub fn run_tracked(&self, cached: &File, nocache: &File, need_len: usize) -> Vec<Landed> {
118        let flags: Vec<Landed> = (0..need_len).map(|_| Landed::new(true)).collect();
119
120        for read in &self.items {
121            let flag = flags[read.need_index].clone();
122
123            flag.0.store(false, Ordering::Release);
124
125            let file = self
126                .file(read, cached, nocache)
127                .try_clone()
128                .expect("dup fd");
129            let read = read.clone();
130
131            std::thread::spawn(move || {
132                read.transfer.run(&file);
133                flag.0.store(true, Ordering::Release);
134            });
135        }
136
137        flags
138    }
139
140    pub fn run(&self, cached: &File, nocache: &File) {
141        std::thread::scope(|s| {
142            for read in &self.items {
143                let file = self.file(read, cached, nocache);
144
145                s.spawn(move || read.transfer.run(file));
146            }
147        });
148    }
149
150    fn file<'a>(&'a self, read: &PlannedRead, cached: &'a File, nocache: &'a File) -> &'a File {
151        if read.transfer.destination == 0 {
152            return cached;
153        }
154
155        if read.kind == 0 {
156            return nocache;
157        }
158
159        self.low_file.as_ref().expect("low-bit store")
160    }
161
162    pub(super) fn observe(&mut self, tracker: &ReadTracker, layer: usize, source: ReadSource) {
163        for read in &mut self.items {
164            read.transfer.ticket =
165                Some(tracker.ticket(layer, read.kind, source, read.transfer.bytes));
166        }
167    }
168
169    pub(super) fn tickets(&self, need: &[usize]) -> Vec<(usize, ReadTicket)> {
170        self.items
171            .iter()
172            .filter_map(|read| {
173                read.transfer
174                    .ticket
175                    .as_ref()
176                    .map(|ticket| (need[read.need_index], ticket.clone()))
177            })
178            .collect()
179    }
180}
181
182impl RecordRead {
183    fn run(&self, file: &File) {
184        let read = || read_record(file, self.destination, self.file_offset, self.bytes);
185
186        if let Some(ticket) = &self.ticket {
187            ticket.measure(read);
188
189            return;
190        }
191
192        read();
193    }
194}
195
196/// Fill independent slots in parallel, one complete record per read.
197pub(super) fn fetch_into_slots(file: &File, reads: &[RecordRead]) {
198    std::thread::scope(|s| {
199        for read in reads {
200            s.spawn(move || read.run(file));
201        }
202    });
203}
204
205/// A zero destination faults mapped pages in through bounded scratch.
206fn read_record(file: &File, destination: usize, offset: usize, bytes: usize) -> usize {
207    const PIECE: usize = 256 * BYTES_PER_KIB;
208
209    if destination != 0 {
210        // The record's slot has one writer and is not yet visible to the GPU.
211        let buffer = unsafe { std::slice::from_raw_parts_mut(destination as *mut u8, bytes) };
212
213        return read_at(file, buffer, offset);
214    }
215
216    let mut scratch = vec![0; PIECE.min(bytes)];
217    let mut done = 0;
218
219    while done < bytes {
220        let count = scratch.len().min(bytes - done);
221        let read = read_at(file, &mut scratch[..count], offset + done);
222        done += read;
223
224        if read != count {
225            break;
226        }
227    }
228
229    done
230}
231
232fn read_at(file: &File, buffer: &mut [u8], offset: usize) -> usize {
233    use std::os::unix::fs::FileExt;
234
235    let mut done = 0;
236
237    while done < buffer.len() {
238        match file.read_at(&mut buffer[done..], (offset + done) as u64) {
239            Ok(0) => break,
240            Ok(bytes) => done += bytes,
241            Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
242            Err(_) => break,
243        }
244    }
245
246    done
247}
248
249impl Pool {
250    /// `budget` records over the mapping at `base`; `copy` selects the
251    /// wired-copy backing (allocation may fail: the caller shrinks).
252    /// `slot_stride` is the pitch of a pool slot, which is the low-bit
253    /// record stride when every record is low-bit (more slots in the same
254    /// wired budget); `stride` stays the 4-bit stride for file offsets.
255    pub fn new(
256        ctx: &MetalContext,
257        base: *const u8,
258        stride: usize,
259        slot_stride: usize,
260        n_records: usize,
261        budget: usize,
262        copy: bool,
263    ) -> Result<Self> {
264        Ok(if copy {
265            Pool::Copy(CopyPool::new(ctx, stride, slot_stride, n_records, budget)?)
266        } else {
267            Pool::Set(Residency::new(ctx, base, stride, n_records, budget)?)
268        })
269    }
270
271    /// Attach a low-bit store (copy pool only): its file and record
272    /// stride. With `default_kind` non-zero every slot holds a low-bit
273    /// record, so nothing reads the 4-bit file.
274    pub fn set_low_bit_store(&mut self, file: File, low_stride: usize, default_kind: u8) {
275        if let Pool::Copy(c) = self {
276            c.low_file = Some(file);
277            c.low_stride = low_stride;
278            c.default_kind = default_kind;
279
280            if default_kind != 0 {
281                for k in c.slot_kind.iter_mut() {
282                    *k = default_kind;
283                }
284            }
285        }
286    }
287
288    /// Which store a record's slot holds (0 = 4-bit, 1 = 3-bit, 2 = 2-bit); 0 when
289    /// not resident or not a copy pool.
290    pub fn kind(&self, rid: usize) -> u8 {
291        match self {
292            Pool::Copy(c) if c.rec_slot[rid] != u32::MAX => c.slot_kind[c.rec_slot[rid] as usize],
293            _ => 0,
294        }
295    }
296
297    /// Choose the store for a record acquired this step (before
298    /// `plan_reads`); copy pool only.
299    pub fn set_kind(&mut self, rid: usize, kind: u8) {
300        if let Pool::Copy(c) = self
301            && c.rec_slot[rid] != u32::MAX
302            && (kind == 0 || c.low_file.is_some())
303        {
304            c.slot_kind[c.rec_slot[rid] as usize] = kind;
305        }
306    }
307
308    /// Store backing a prefill ring read. Its precision is also used to
309    /// select the GEMM, so compressed bytes never reach a Q4 kernel.
310    pub fn store_file<'a>(&'a self, original: &'a File, kind: u8) -> &'a File {
311        if kind == 0 {
312            original
313        } else if let Pool::Copy(c) = self {
314            c.low_file.as_ref().expect("low-bit store attached")
315        } else {
316            unreachable!("low-bit records require the copy pool")
317        }
318    }
319
320    pub fn budget(&self) -> usize {
321        match self {
322            Pool::Set(r) => r.budget,
323            Pool::Copy(c) => c.budget,
324        }
325    }
326
327    pub fn resident(&self) -> usize {
328        match self {
329            Pool::Set(r) => r.resident(),
330            Pool::Copy(c) => c.slot_rec.iter().filter(|&&r| r != u32::MAX).count(),
331        }
332    }
333
334    pub fn is_member(&self, rid: usize) -> bool {
335        match self {
336            Pool::Set(r) => r.is_member(rid),
337            Pool::Copy(c) => c.rec_slot[rid] != u32::MAX,
338        }
339    }
340
341    /// Mark records used this step and reserve room for them, evicting
342    /// least recently used records not used this step. Returns the
343    /// records that still have to be read (`plan_reads`, run the plan,
344    /// then `finish`).
345    pub fn acquire(&mut self, ctx: &MetalContext, rids: &[usize], step: u64) -> Result<Vec<usize>> {
346        match self {
347            Pool::Set(r) => r.acquire(ctx, rids, step),
348            Pool::Copy(c) => c.acquire(rids, step),
349        }
350    }
351
352    /// The reads for records returned by `acquire`, and how many of them
353    /// need none (their pages are still in memory).
354    pub fn plan_reads(&self, need: &[usize]) -> (ReadPlan, usize) {
355        match self {
356            Pool::Set(r) => {
357                let mut items = Vec::new();
358
359                for (need_index, &rid) in need.iter().enumerate() {
360                    if !r.cached(rid) {
361                        items.push(PlannedRead {
362                            transfer: RecordRead {
363                                ticket: None,
364                                destination: 0,
365                                file_offset: rid * r.stride,
366                                bytes: r.stride,
367                            },
368                            kind: 0,
369                            need_index,
370                        });
371                    }
372                }
373
374                let warm = need.len() - items.len();
375
376                (
377                    ReadPlan {
378                        items,
379                        low_file: None,
380                    },
381                    warm,
382                )
383            }
384            Pool::Copy(c) => {
385                let items: Vec<PlannedRead> = need
386                    .iter()
387                    .enumerate()
388                    .map(|(need_index, &rid)| {
389                        let slot = c.rec_slot[rid] as usize;
390                        let kind = c.slot_kind[slot];
391                        let (off, len) = if kind != 0 {
392                            (rid * c.low_stride, c.low_stride)
393                        } else {
394                            (rid * c.stride, c.stride)
395                        };
396
397                        PlannedRead {
398                            transfer: RecordRead {
399                                ticket: None,
400                                destination: c.base + slot * c.slot_stride,
401                                file_offset: off,
402                                bytes: len,
403                            },
404                            kind,
405                            need_index,
406                        }
407                    })
408                    .collect();
409                let low_file = if items.iter().any(|read| read.kind != 0) {
410                    c.low_file.as_ref().map(|f| f.try_clone().expect("dup fd"))
411                } else {
412                    None
413                };
414
415                (ReadPlan { items, low_file }, 0)
416            }
417        }
418    }
419
420    /// Logical resident budget in bytes (low-bit slots are shorter).
421    pub fn bytes(&self) -> usize {
422        match self {
423            Pool::Copy(c) => c.budget * c.slot_stride,
424            Pool::Set(r) => r.budget * r.stride,
425        }
426    }
427
428    /// Records from `acquire` whose reads are done become usable.
429    pub fn finish(&mut self, ctx: &MetalContext, rids: &[usize]) -> Result<()> {
430        match self {
431            Pool::Set(r) => r.finish(ctx, rids),
432            Pool::Copy(_) => Ok(()),
433        }
434    }
435
436    /// GPU address of a usable record.
437    pub fn addr(&mut self, ctx: &MetalContext, rid: usize) -> Result<u64> {
438        match self {
439            Pool::Set(r) => r.addr(ctx, rid),
440            Pool::Copy(c) => {
441                let slot = c.rec_slot[rid] as usize;
442                let flag = match c.slot_kind[slot] {
443                    1 => 1u64 << 63,
444                    2 => 1u64 << 62,
445                    _ => 0,
446                };
447
448                Ok((c.gpu_base + slot as u64 * c.slot_stride as u64) | flag)
449            }
450        }
451    }
452
453    /// A usable record as (buffer, byte offset) for binding.
454    pub fn buf(&mut self, ctx: &MetalContext, rid: usize) -> Result<(Buf, usize)> {
455        match self {
456            Pool::Set(r) => Ok((r.buf(ctx, rid)?, 0)),
457            Pool::Copy(c) => Ok((c.pool.clone(), c.rec_slot[rid] as usize * c.slot_stride)),
458        }
459    }
460}
461
462pub struct CopyPool {
463    pool: Buf,
464    /// This set keeps the pool resident for the queue. The GPU reaches it
465    /// through addresses rather than a binding.
466    _set: Retained<ProtocolObject<dyn MTLResidencySet>>,
467    base: usize,
468    gpu_base: u64,
469    stride: usize,
470    /// Bytes per slot (the low-bit stride when every record is low-bit).
471    slot_stride: usize,
472    /// Record id held by each slot (u32::MAX = empty).
473    slot_rec: Vec<u32>,
474    /// Step number of each slot's last use (eviction guard and LRU key).
475    slot_used: Vec<u64>,
476    /// Slot of each record id (u32::MAX = not resident).
477    rec_slot: Vec<u32>,
478    /// Store held by each slot: 0 = 4-bit record, 1 = 3-bit, 2 = 2-bit
479    /// (one low-bit store is attached at a time; `low_file`/`low_stride`).
480    slot_kind: Vec<u8>,
481    low_file: Option<File>,
482    low_stride: usize,
483    /// Kind a freshly taken slot gets (0 unless every record is low-bit).
484    default_kind: u8,
485    pub budget: usize,
486}
487
488impl CopyPool {
489    fn new(
490        ctx: &MetalContext,
491        stride: usize,
492        slot_stride: usize,
493        n_records: usize,
494        budget: usize,
495    ) -> Result<Self> {
496        let pool = ctx
497            .new_buffer(budget * slot_stride)
498            .context("allocating the expert pool")?;
499        let desc = MTLResidencySetDescriptor::new();
500        let set = ctx
501            .device
502            .newResidencySetWithDescriptor_error(&desc)
503            .map_err(|e| anyhow::anyhow!("residency set: {e}"))?;
504
505        set.addAllocation(ProtocolObject::from_ref(&*pool));
506        set.commit();
507        set.requestResidency();
508        ctx.queue.addResidencySet(&set);
509
510        Ok(CopyPool {
511            base: pool.contents().cast::<u8>().as_ptr() as usize,
512            gpu_base: pool.gpuAddress(),
513            pool,
514            _set: set,
515            stride,
516            slot_stride,
517            slot_rec: vec![u32::MAX; budget],
518            slot_used: vec![0; budget],
519            rec_slot: vec![u32::MAX; n_records],
520            slot_kind: vec![0; budget],
521            low_file: None,
522            low_stride: 0,
523            default_kind: 0,
524            budget,
525        })
526    }
527
528    fn acquire(&mut self, rids: &[usize], step: u64) -> Result<Vec<usize>> {
529        let mut need = Vec::new();
530
531        for &rid in rids {
532            let mut slot = self.rec_slot[rid];
533
534            if slot == u32::MAX {
535                // Victim: the least recently used slot not used this step.
536                let mut best = u32::MAX;
537                let mut best_used = u64::MAX;
538
539                for s in 0..self.budget {
540                    let u = self.slot_used[s];
541
542                    if u == step || u >= best_used {
543                        continue;
544                    }
545
546                    best_used = u;
547                    best = s as u32;
548
549                    if u == 0 {
550                        break;
551                    }
552                }
553
554                anyhow::ensure!(best != u32::MAX, "expert pool too small for one step");
555
556                let old = self.slot_rec[best as usize];
557
558                if old != u32::MAX {
559                    self.rec_slot[old as usize] = u32::MAX;
560                }
561
562                self.slot_rec[best as usize] = rid as u32;
563                self.slot_kind[best as usize] = self.default_kind;
564                self.rec_slot[rid] = best;
565                slot = best;
566
567                need.push(rid);
568            }
569
570            self.slot_used[slot as usize] = step;
571        }
572
573        Ok(need)
574    }
575}
576
577pub struct Residency {
578    set: Retained<ProtocolObject<dyn MTLResidencySet>>,
579    base: *const u8,
580    stride: usize,
581    bufs: Vec<Option<Buf>>,
582    /// Position in `members`, or u32::MAX when not in the set.
583    member_pos: Vec<u32>,
584    members: Vec<u32>,
585    /// Step of last use per record (LRU key; also the eviction guard).
586    last_used: Vec<u64>,
587    /// Records added to the set object since the last commit.
588    dirty: bool,
589    pub budget: usize,
590}
591
592// The set is only touched from the service thread; the mapping is
593// read-only for the process lifetime.
594unsafe impl Send for Residency {}
595
596impl Residency {
597    pub fn new(
598        ctx: &MetalContext,
599        base: *const u8,
600        stride: usize,
601        n_records: usize,
602        budget: usize,
603    ) -> Result<Self> {
604        let desc = MTLResidencySetDescriptor::new();
605
606        unsafe { desc.setInitialCapacity(budget + 256) };
607
608        let set = ctx
609            .device
610            .newResidencySetWithDescriptor_error(&desc)
611            .map_err(|e| anyhow::anyhow!("residency set: {e}"))?;
612
613        ctx.queue.addResidencySet(&set);
614
615        Ok(Residency {
616            set,
617            base,
618            stride,
619            bufs: vec![None; n_records],
620            member_pos: vec![u32::MAX; n_records],
621            members: Vec::with_capacity(budget),
622            last_used: vec![0; n_records],
623            dirty: false,
624            budget,
625        })
626    }
627
628    pub fn resident(&self) -> usize {
629        self.members.len()
630    }
631
632    pub fn is_member(&self, rid: usize) -> bool {
633        self.member_pos[rid] != u32::MAX
634    }
635
636    /// The record's sub-buffer (wrapped on first use).
637    pub fn buf(&mut self, ctx: &MetalContext, rid: usize) -> Result<Buf> {
638        if let Some(b) = &self.bufs[rid] {
639            return Ok(b.clone());
640        }
641
642        // Safety: page-aligned record inside the live read-only mapping.
643        let b =
644            unsafe { ctx.wrap_region(self.base.add(rid * self.stride).cast_mut(), self.stride)? };
645        self.bufs[rid] = Some(b.clone());
646
647        Ok(b)
648    }
649
650    /// GPU address of the record.
651    pub fn addr(&mut self, ctx: &MetalContext, rid: usize) -> Result<u64> {
652        Ok(self.buf(ctx, rid)?.gpuAddress())
653    }
654
655    /// This method reserves membership for the step's records, evicting the
656    /// least recently used members outside the step as needed. It returns
657    /// records that still need to be read and added. Call `finish` once their
658    /// pages are in memory.
659    pub fn acquire(&mut self, ctx: &MetalContext, rids: &[usize], step: u64) -> Result<Vec<usize>> {
660        let mut need = Vec::new();
661
662        for &rid in rids {
663            self.last_used[rid] = step;
664
665            if self.is_member(rid) {
666                continue;
667            }
668
669            if self.members.len() >= self.budget {
670                self.evict_one(ctx, step)?;
671            }
672
673            need.push(rid);
674        }
675
676        Ok(need)
677    }
678
679    fn evict_one(&mut self, ctx: &MetalContext, step: u64) -> Result<()> {
680        let mut best = usize::MAX;
681        let mut best_used = u64::MAX;
682
683        for (i, &m) in self.members.iter().enumerate() {
684            let u = self.last_used[m as usize];
685
686            if u != step && u < best_used {
687                best_used = u;
688                best = i;
689
690                if u == 0 {
691                    break;
692                }
693            }
694        }
695
696        anyhow::ensure!(
697            best != usize::MAX,
698            "expert residency budget too small for one step"
699        );
700
701        let victim = self.members[best] as usize;
702        let last = self.members.len() - 1;
703
704        self.members.swap(best, last);
705
706        self.member_pos[self.members[best] as usize] = best as u32;
707
708        self.members.pop();
709
710        self.member_pos[victim] = u32::MAX;
711        let b = self.buf(ctx, victim)?;
712
713        self.set.removeAllocation(ProtocolObject::from_ref(&*b));
714
715        self.dirty = true;
716
717        Ok(())
718    }
719
720    /// Whether all of the record's pages are in memory (page cache or
721    /// wired), so no read is needed before adding it.
722    pub fn cached(&self, rid: usize) -> bool {
723        let pages = self.stride / 16384;
724        let mut vec = vec![0u8; pages];
725        let r = unsafe {
726            libc::mincore(
727                self.base.add(rid * self.stride) as *mut c_void,
728                self.stride,
729                vec.as_mut_ptr() as *mut libc::c_char,
730            )
731        };
732
733        r == 0 && vec.iter().all(|v| v & 1 == 1)
734    }
735
736    /// Add records whose pages are in memory to the set and commit.
737    pub fn finish(&mut self, ctx: &MetalContext, rids: &[usize]) -> Result<()> {
738        for &rid in rids {
739            if self.is_member(rid) {
740                continue;
741            }
742
743            let b = self.buf(ctx, rid)?;
744
745            self.set.addAllocation(ProtocolObject::from_ref(&*b));
746
747            self.member_pos[rid] = self.members.len() as u32;
748
749            self.members.push(rid as u32);
750
751            self.dirty = true;
752        }
753
754        if self.dirty {
755            self.set.commit();
756            self.set.requestResidency();
757
758            self.dirty = false;
759        }
760
761        Ok(())
762    }
763}
764
765#[cfg(test)]
766#[path = "../../../tests/unit/qwen4_exp/gpu/read_io.rs"]
767mod tests;