1use 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#[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#[derive(Clone)]
72struct PlannedRead {
73 transfer: RecordRead,
74 kind: u8,
76 need_index: usize,
77}
78
79#[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
88pub 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 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
196pub(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
205fn 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 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 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 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 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 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 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 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 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 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 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 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 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 _set: Retained<ProtocolObject<dyn MTLResidencySet>>,
467 base: usize,
468 gpu_base: u64,
469 stride: usize,
470 slot_stride: usize,
472 slot_rec: Vec<u32>,
474 slot_used: Vec<u64>,
476 rec_slot: Vec<u32>,
478 slot_kind: Vec<u8>,
481 low_file: Option<File>,
482 low_stride: usize,
483 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 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 member_pos: Vec<u32>,
584 members: Vec<u32>,
585 last_used: Vec<u64>,
587 dirty: bool,
589 pub budget: usize,
590}
591
592unsafe 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 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 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 pub fn addr(&mut self, ctx: &MetalContext, rid: usize) -> Result<u64> {
652 Ok(self.buf(ctx, rid)?.gpuAddress())
653 }
654
655 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 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 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;