Skip to main content

nuvec_gpu/metal/
buffer.rs

1//! Shared-memory buffers.
2
3use std::ptr::NonNull;
4use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
5use std::sync::{Arc, Condvar, Mutex, PoisonError};
6
7use objc2::rc::{Retained, autoreleasepool};
8use objc2::runtime::ProtocolObject;
9use objc2_metal::{MTLBuffer, MTLDevice};
10
11use super::Device;
12use super::allocator::{Pool, Raw};
13use super::command::Fence;
14use crate::{GpuError, Result};
15
16/// GPU memory in unified (shared) storage: the CPU reads and writes it in place.
17///
18/// Buffers come from the device's caching allocator and return to it when dropped. Hazard
19/// tracking is disabled at the Metal level (as in MLX); nuvec orders accesses itself. CPU
20/// access is refused while submitted GPU work uses the buffer, and GPU writes require
21/// `&mut Buffer`, so the CPU and the GPU never race on it.
22pub struct Buffer {
23    /// `None` for zero-length buffers, which own no Metal memory.
24    raw: Option<Raw>,
25    /// `raw.contents()`, fixed for the buffer's lifetime (cached: each lookup is an
26    /// Objective-C message).
27    ptr: NonNull<u8>,
28    len: usize,
29    /// Accounted (rounded) size in the pool.
30    size: usize,
31    pool: Arc<Pool>,
32    /// Submitted-but-unfinished GPU uses of this buffer.
33    in_flight: Arc<Guard>,
34    /// For memory reused while the previous owner's committed GPU work still used it: that
35    /// work's uses. GPU work on this buffer waits for their fences; CPU access waits until
36    /// they complete.
37    previous: Option<Arc<Guard>>,
38    /// Memory of a mapped file (not the allocator's): released with the Metal buffer.
39    mapped: bool,
40}
41
42// SAFETY: Metal buffer objects may be retained, released and bound from any thread. The only
43// non-thread-safe part is the memory behind `contents()`, and access to it is synchronised by
44// Rust borrows (`contents_mut` needs `&mut self`; GPU writes need `&mut Buffer`) plus the
45// `in_flight` guard, which refuses CPU reads while GPU writes are pending and CPU writes while
46// any GPU use is pending.
47unsafe impl Send for Buffer {}
48// SAFETY: as above; `&Buffer` only allows shared reads of memory no GPU work is writing.
49unsafe impl Sync for Buffer {}
50
51impl Device {
52    /// A buffer of `len` bytes from the caching allocator. Its contents are unspecified
53    /// (a reused buffer holds stale bytes); fails with [`GpuError::OutOfMemory`] beyond the
54    /// memory limit.
55    pub fn new_buffer(&self, len: usize) -> Result<Buffer> {
56        self.alloc_buffer(len, false)
57    }
58
59    /// A buffer of `len` bytes for GPU work to write: as [`new_buffer`](Self::new_buffer),
60    /// but it may be memory that committed GPU work still uses (as MLX reuses memory). GPU
61    /// work encoded with it is ordered after those uses; the CPU cannot access it until
62    /// they complete ([`Buffer::in_use`]).
63    pub fn new_gpu_buffer(&self, len: usize) -> Result<Buffer> {
64        self.alloc_buffer(len, true)
65    }
66
67    /// Runs in an autorelease pool: Metal may autorelease objects while allocating or
68    /// wiring.
69    fn alloc_buffer(&self, len: usize, gpu: bool) -> Result<Buffer> {
70        let pool = self.pool().clone();
71        let (raw, size, busy, ptr) = autoreleasepool(|_| {
72            Ok::<_, GpuError>(match pool.alloc(self.raw(), len, gpu)? {
73                Some((raw, size, busy)) => {
74                    let ptr = raw.contents().cast();
75                    (Some(raw), size, busy, ptr)
76                }
77                None => (None, 0, None, NonNull::dangling()),
78            })
79        })?;
80        Ok(Buffer {
81            raw,
82            ptr,
83            len,
84            size,
85            pool,
86            in_flight: Arc::default(),
87            previous: busy,
88            mapped: false,
89        })
90    }
91
92    /// The file at `path`, memory-mapped read-only and wrapped without a copy
93    /// (`newBufferWithBytesNoCopy`): its pages are the page cache's, read from disk when
94    /// first touched by the CPU or the GPU. The buffer cannot be written (CPU writes and GPU
95    /// writes are refused). The mapping is released when the buffer and every GPU use of
96    /// it are gone.
97    ///
98    /// A shared read-only mapping is used because a private (copy-on-write) one costs
99    /// about 4× more to fault in on first use.
100    ///
101    /// The file must not be modified (written or truncated, by this or another process)
102    /// while it is mapped: the bytes [`Buffer::contents`] returns would change underneath
103    /// a `&[u8]` (undefined behaviour), and access past a truncated end raises `SIGBUS`.
104    /// Map only files nothing writes while they are in use, such as model weights.
105    pub fn map_file(&self, path: &std::path::Path) -> Result<Buffer> {
106        autoreleasepool(|_| self.map_file_now(path))
107    }
108
109    fn map_file_now(&self, path: &std::path::Path) -> Result<Buffer> {
110        use std::os::fd::AsRawFd;
111        let io = |e: std::io::Error| GpuError::Io(format!("{}: {e}", path.display()));
112        let file = std::fs::File::open(path).map_err(io)?;
113        let len = usize::try_from(file.metadata().map_err(io)?.len())
114            .map_err(|_| GpuError::Io(format!("{}: too large", path.display())))?;
115        if len == 0 {
116            return self.new_buffer(0);
117        }
118        let map_len = crate::memory::mapping_size(len);
119        // SAFETY: a fresh read-only mapping of an open file descriptor; the result is
120        // checked below. The descriptor may be closed afterwards (the mapping keeps the file).
121        let ptr = unsafe {
122            libc::mmap(
123                std::ptr::null_mut(),
124                map_len,
125                libc::PROT_READ,
126                libc::MAP_SHARED,
127                file.as_raw_fd(),
128                0,
129            )
130        };
131        if ptr == libc::MAP_FAILED {
132            return Err(io(std::io::Error::last_os_error()));
133        }
134        let start = NonNull::new(ptr.cast::<u8>()).expect("mmap returned a non-null address");
135        let unmap = block2::RcBlock::new(move |p: NonNull<std::ffi::c_void>, n: usize| {
136            // SAFETY: Metal calls this once, with the mapping created above, after the
137            // buffer is released (so neither the CPU nor the GPU uses it any more).
138            unsafe { libc::munmap(p.as_ptr(), n) };
139        });
140        let options = objc2_metal::MTLResourceOptions::StorageModeShared
141            | objc2_metal::MTLResourceOptions::HazardTrackingModeUntracked;
142        // SAFETY: the pointer is page-aligned (from `mmap`), valid for `map_len` bytes, and
143        // owned by the buffer from now on: the deallocator unmaps it exactly once.
144        let raw = unsafe {
145            self.raw()
146                .newBufferWithBytesNoCopy_length_options_deallocator(
147                    start.cast(),
148                    map_len,
149                    options,
150                    Some(&unmap),
151                )
152        };
153        let Some(raw) = raw else {
154            // SAFETY: Metal did not take the mapping, so it is still ours to release.
155            unsafe { libc::munmap(ptr, map_len) };
156            return Err(GpuError::OutOfMemory(len));
157        };
158        // Over the memory limit: refused before anything is wired; dropping `raw`
159        // unmaps the file.
160        self.pool().track(&raw)?;
161        Ok(Buffer {
162            raw: Some(raw),
163            ptr: start,
164            len,
165            size: 0,
166            pool: self.pool().clone(),
167            in_flight: Arc::default(),
168            previous: None,
169            mapped: true,
170        })
171    }
172
173    /// A buffer holding a copy of `bytes`.
174    pub fn buffer_from(&self, bytes: &[u8]) -> Result<Buffer> {
175        let mut buffer = self.new_buffer(bytes.len())?;
176        buffer.contents_mut()?.copy_from_slice(bytes);
177        Ok(buffer)
178    }
179}
180
181impl Buffer {
182    /// Size in bytes.
183    pub fn len(&self) -> usize {
184        self.len
185    }
186
187    /// Whether the buffer holds no bytes.
188    pub fn is_empty(&self) -> bool {
189        self.len == 0
190    }
191
192    /// Whether submitted GPU work still uses (reads or writes) the buffer.
193    pub fn in_use(&self) -> bool {
194        self.in_flight.users.load(Ordering::Acquire) != 0
195    }
196
197    /// Waits until the previous owner's GPU work on reused memory has completed (it is
198    /// committed, so it does): blocks until its completion releases the last use.
199    fn wait_for_previous(&self) {
200        if let Some(previous) = &self.previous {
201            previous.wait_idle();
202        }
203    }
204
205    /// Whether the previous owner's GPU work on reused memory is still pending.
206    pub(crate) fn previous_busy(&self) -> bool {
207        self.previous.as_ref().is_some_and(|p| !p.idle())
208    }
209
210    /// The bytes, if no submitted GPU work writes the buffer (concurrent GPU reads are fine).
211    /// For reused memory ([`Device::new_gpu_buffer`]) whose previous owner's GPU work is
212    /// still running, it first blocks until that work has completed.
213    pub fn contents(&self) -> Result<&[u8]> {
214        self.wait_for_previous();
215        if self.in_flight.writers.load(Ordering::Acquire) != 0 {
216            return Err(GpuError::BufferInUse);
217        }
218        if self.raw.is_none() {
219            return Ok(&[]);
220        }
221        // SAFETY: `ptr` is `raw.contents()`, at least `len` bytes (the allocation is at least the
222        // rounded size) of CPU-visible shared storage that lives as long as `raw`, which this
223        // borrow keeps alive. No submitted GPU work writes the buffer (checked above), so any
224        // concurrent GPU access only reads; new GPU writes need `&mut self`, which this shared
225        // borrow excludes.
226        Ok(unsafe { std::slice::from_raw_parts(self.ptr.as_ptr(), self.len) })
227    }
228
229    /// The bytes, mutably, if no submitted GPU work uses (reads or writes) the buffer and
230    /// it is not a read-only mapped file.
231    pub fn contents_mut(&mut self) -> Result<&mut [u8]> {
232        if self.mapped {
233            return Err(GpuError::ReadOnly);
234        }
235        if self.in_use() {
236            return Err(GpuError::BufferInUse);
237        }
238        self.wait_for_previous();
239        self.previous = None;
240        if self.raw.is_none() {
241            return Ok(&mut []);
242        }
243        // SAFETY: as in `contents`; additionally `&mut self` is exclusive, and `Buffer` is not
244        // `Clone`, so no other CPU reference to these bytes exists.
245        Ok(unsafe { std::slice::from_raw_parts_mut(self.ptr.as_ptr(), self.len) })
246    }
247
248    /// The Metal buffer to bind: the device's placeholder for empty buffers.
249    pub(crate) fn raw(&self) -> Option<&ProtocolObject<dyn MTLBuffer>> {
250        self.raw
251            .as_deref()
252            .or_else(|| self.pool.placeholder().map(|r| &**r))
253    }
254
255    pub(crate) fn retained(&self) -> Option<Raw> {
256        self.raw
257            .clone()
258            .or_else(|| self.pool.placeholder().cloned())
259    }
260
261    /// Whether the buffer is a read-only mapped file.
262    pub fn is_mapped(&self) -> bool {
263        self.mapped
264    }
265
266    pub(crate) fn guard(&self) -> Arc<Guard> {
267        self.in_flight.clone()
268    }
269
270    /// GPU uses of the buffer encoded in command buffers not submitted yet, all and the
271    /// writing ones.
272    pub(crate) fn unsubmitted(&self) -> (usize, usize) {
273        self.in_flight.unsubmitted()
274    }
275
276    /// The fences GPU work using the buffer waits for: its own pending uses' and, for reused
277    /// memory, the previous owner's.
278    pub(crate) fn pending_fences(&self, write: bool) -> Vec<Fence> {
279        let mut fences = self.in_flight.pending_fences(write);
280        if let Some(previous) = &self.previous {
281            fences.extend(previous.pending_fences(true));
282        }
283        fences
284    }
285
286    /// Identity for hazard tracking (unique per live buffer, including empty ones).
287    pub(crate) fn id(&self) -> usize {
288        Arc::as_ptr(&self.in_flight) as usize
289    }
290}
291
292/// Counts submitted GPU uses of a buffer: all uses, the writing ones and those not yet
293/// committed; and the fences of the ordered command buffers using it. While uses are
294/// pending it stays with the memory, also when the memory is cached and reused.
295#[derive(Default)]
296pub(crate) struct Guard {
297    /// The sequence number ([`commit_sequence`]) of the latest command buffer committed with
298    /// a use.
299    last_commit: AtomicU64,
300    users: AtomicUsize,
301    writers: AtomicUsize,
302    /// Uses encoded in command buffers not committed yet: their fences cannot order other
303    /// work yet.
304    uncommitted: AtomicUsize,
305    /// Uses encoded in command buffers not submitted yet, all and the writing ones: work
306    /// in other command buffers cannot be ordered after them (their fences are not recorded
307    /// yet).
308    unsubmitted: AtomicUsize,
309    unsubmitted_writers: AtomicUsize,
310    fences: Mutex<Fences>,
311    /// Signalled (under `fences`) when the last pending use is released.
312    idle: Condvar,
313}
314
315impl std::fmt::Debug for Guard {
316    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
317        f.debug_struct("Guard")
318            .field("users", &self.users)
319            .field("writers", &self.writers)
320            .finish_non_exhaustive()
321    }
322}
323
324/// The fences of ordered command buffers whose uses of a buffer are pending.
325#[derive(Debug, Default)]
326struct Fences {
327    /// The last writer's.
328    writer: Option<Fence>,
329    /// Every pending use's (a command buffer's fence leaves when it completes, so a buffer
330    /// that is never idle, such as weights read by every pipelined step, keeps only the
331    /// fences of work still in flight).
332    users: Vec<Fence>,
333}
334
335/// Command buffers committed so far (all devices and queues).
336static COMMITS: AtomicU64 = AtomicU64::new(0);
337
338/// The next command buffer's sequence number.
339pub(crate) fn commit_sequence() -> u64 {
340    COMMITS.fetch_add(1, Ordering::AcqRel) + 1
341}
342
343impl Guard {
344    /// A use's command buffer was committed as number `sequence`.
345    pub(crate) fn committed_as(&self, sequence: u64) {
346        self.last_commit.fetch_max(sequence, Ordering::AcqRel);
347    }
348
349    /// Whether a use is in the latest committed command buffer: work taking the memory then
350    /// waits for that command buffer, so the next one cannot overlap it.
351    pub(crate) fn in_latest_commit(&self) -> bool {
352        self.last_commit.load(Ordering::Acquire) >= COMMITS.load(Ordering::Acquire)
353    }
354
355    /// The fences to wait for before using the buffer: the pending writer's for a read, and
356    /// every pending user's for a write.
357    pub(crate) fn pending_fences(&self, write: bool) -> Vec<Fence> {
358        if self.users.load(Ordering::Acquire) == 0 {
359            return Vec::new();
360        }
361        let fences = self.fences.lock().unwrap_or_else(PoisonError::into_inner);
362        if write {
363            fences.users.clone()
364        } else if self.writers.load(Ordering::Acquire) != 0 {
365            fences.writer.iter().cloned().collect()
366        } else {
367            Vec::new()
368        }
369    }
370
371    /// Records the fence of a command buffer that uses the buffer (before it is committed).
372    pub(crate) fn record(&self, fence: &Fence, write: bool) {
373        let mut fences = self.fences.lock().unwrap_or_else(PoisonError::into_inner);
374        if write {
375            fences.writer = Some(fence.clone());
376        }
377        fences.users.push(fence.clone());
378    }
379
380    /// Whether no submitted GPU work uses the buffer.
381    pub(crate) fn idle(&self) -> bool {
382        self.users.load(Ordering::Acquire) == 0
383    }
384
385    /// Blocks until no submitted GPU work uses the buffer. Only for committed uses: those
386    /// are released by their command buffer's completion.
387    pub(crate) fn wait_idle(&self) {
388        if self.idle() {
389            return;
390        }
391        let mut fences = self.fences.lock().unwrap_or_else(PoisonError::into_inner);
392        // `release` takes this lock after the last use leaves, before notifying: no lost
393        // wake-up.
394        while !self.idle() {
395            fences = self
396                .idle
397                .wait(fences)
398                .unwrap_or_else(PoisonError::into_inner);
399        }
400    }
401
402    /// Uses encoded in command buffers not submitted yet, all and the writing ones.
403    pub(crate) fn unsubmitted(&self) -> (usize, usize) {
404        (
405            self.unsubmitted.load(Ordering::Acquire),
406            self.unsubmitted_writers.load(Ordering::Acquire),
407        )
408    }
409
410    /// Whether every pending use is committed (so its fence orders later work after it).
411    pub(crate) fn committed_uses(&self) -> bool {
412        self.uncommitted.load(Ordering::Acquire) == 0
413    }
414
415    /// A use's command buffer was committed (or abandoned).
416    pub(crate) fn committed(&self) {
417        self.uncommitted.fetch_sub(1, Ordering::AcqRel);
418    }
419
420    /// A use's command buffer was submitted to the GPU (or abandoned).
421    pub(crate) fn submitted(&self, write: bool) {
422        if write {
423            self.unsubmitted_writers.fetch_sub(1, Ordering::AcqRel);
424        }
425        self.unsubmitted.fetch_sub(1, Ordering::AcqRel);
426    }
427
428    pub(crate) fn acquire(&self, write: bool) {
429        self.uncommitted.fetch_add(1, Ordering::AcqRel);
430        self.unsubmitted.fetch_add(1, Ordering::AcqRel);
431        self.users.fetch_add(1, Ordering::AcqRel);
432        if write {
433            self.unsubmitted_writers.fetch_add(1, Ordering::AcqRel);
434            self.writers.fetch_add(1, Ordering::AcqRel);
435        }
436    }
437
438    /// A use's command buffer completed (or was abandoned); `fence` is its fence, if it
439    /// recorded one: nothing has to wait for that work any more.
440    pub(crate) fn release(&self, write: bool, fence: Option<&Fence>) {
441        if let Some(fence) = fence {
442            let mut fences = self.fences.lock().unwrap_or_else(PoisonError::into_inner);
443            let same = |f: &Fence| Retained::as_ptr(f) == Retained::as_ptr(fence);
444            if let Some(i) = fences.users.iter().position(same) {
445                fences.users.swap_remove(i);
446            }
447            if fences.writer.as_ref().is_some_and(same) {
448                fences.writer = None;
449            }
450        }
451        if write {
452            self.writers.fetch_sub(1, Ordering::AcqRel);
453        }
454        if self.users.fetch_sub(1, Ordering::AcqRel) == 1 {
455            // Idle: forget the fences, unless a new use began meanwhile (checked under the
456            // lock that `record` takes, so a new user's fence is never dropped).
457            let mut fences = self.fences.lock().unwrap_or_else(PoisonError::into_inner);
458            if self.users.load(Ordering::Acquire) == 0 {
459                *fences = Fences::default();
460                self.idle.notify_all();
461            }
462            drop(fences);
463        }
464    }
465}
466
467impl Drop for Buffer {
468    /// Returns the memory to the allocator's cache at once, with the hazard state of GPU
469    /// work that still uses it (only GPU work reuses it before that work completes). Runs
470    /// in an autorelease pool: a Metal buffer's deallocation autoreleases objects.
471    fn drop(&mut self) {
472        let Some(raw) = self.raw.take() else {
473            return;
474        };
475        autoreleasepool(|_| {
476            if self.mapped {
477                // Metal unmaps the file once the last reference (ours or a command
478                // buffer's) is released.
479                self.pool.untrack(&raw);
480                drop(raw);
481                return;
482            }
483            // Its own GPU work runs after the previous owner's: while that is pending, it
484            // orders reuse; otherwise the previous owner's (if pending) does.
485            let busy = if self.in_use() {
486                Some(self.in_flight.clone())
487            } else {
488                self.previous.take().filter(|p| !p.idle())
489            };
490            self.pool.free(raw, self.size, busy);
491        });
492    }
493}
494
495impl std::fmt::Debug for Buffer {
496    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
497        f.debug_struct("Buffer")
498            .field("len", &self.len)
499            .field("in_use", &self.in_use())
500            .finish()
501    }
502}
503
504#[cfg(test)]
505mod tests {
506    use objc2_metal::MTLDevice;
507
508    use super::*;
509
510    /// A buffer that is never idle (as weights read by every pipelined step) keeps only
511    /// the fences of uses still pending, not one per command buffer since it was idle.
512    #[test]
513    fn completed_fences_leave_buffers_that_stay_in_use() {
514        let Some(device) = Device::system_default() else {
515            return;
516        };
517        let new_fence = || device.raw().newFence().expect("a fence");
518        let guard = Guard::default();
519        // A long-running use keeps the buffer busy throughout.
520        let long = new_fence();
521        guard.acquire(false);
522        guard.record(&long, false);
523        guard.committed();
524        guard.submitted(false);
525        for _ in 0..1000 {
526            let fence = new_fence();
527            guard.acquire(true);
528            guard.record(&fence, true);
529            guard.committed();
530            guard.submitted(true);
531            guard.release(true, Some(&fence));
532        }
533        let fences = guard.fences.lock().unwrap();
534        assert_eq!(fences.users.len(), 1);
535        assert!(fences.writer.is_none());
536        drop(fences);
537        guard.release(false, Some(&long));
538        assert!(guard.idle());
539    }
540}