Skip to main content

nuvec_gpu/metal/
buffer.rs

1//! Shared-memory buffers.
2
3use std::ptr::NonNull;
4use std::sync::atomic::{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    users: AtomicUsize,
298    writers: AtomicUsize,
299    /// Uses encoded in command buffers not committed yet: their fences cannot order other
300    /// work yet.
301    uncommitted: AtomicUsize,
302    /// Uses encoded in command buffers not submitted yet, all and the writing ones: work
303    /// in other command buffers cannot be ordered after them (their fences are not recorded
304    /// yet).
305    unsubmitted: AtomicUsize,
306    unsubmitted_writers: AtomicUsize,
307    fences: Mutex<Fences>,
308    /// Signalled (under `fences`) when the last pending use is released.
309    idle: Condvar,
310}
311
312impl std::fmt::Debug for Guard {
313    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
314        f.debug_struct("Guard")
315            .field("users", &self.users)
316            .field("writers", &self.writers)
317            .finish_non_exhaustive()
318    }
319}
320
321/// The fences of ordered command buffers whose uses of a buffer are pending.
322#[derive(Debug, Default)]
323struct Fences {
324    /// The last writer's.
325    writer: Option<Fence>,
326    /// Every pending use's (a command buffer's fence leaves when it completes, so a buffer
327    /// that is never idle, such as weights read by every pipelined step, keeps only the
328    /// fences of work still in flight).
329    users: Vec<Fence>,
330}
331
332impl Guard {
333    /// The fences to wait for before using the buffer: the pending writer's for a read, and
334    /// every pending user's for a write.
335    pub(crate) fn pending_fences(&self, write: bool) -> Vec<Fence> {
336        if self.users.load(Ordering::Acquire) == 0 {
337            return Vec::new();
338        }
339        let fences = self.fences.lock().unwrap_or_else(PoisonError::into_inner);
340        if write {
341            fences.users.clone()
342        } else if self.writers.load(Ordering::Acquire) != 0 {
343            fences.writer.iter().cloned().collect()
344        } else {
345            Vec::new()
346        }
347    }
348
349    /// Records the fence of a command buffer that uses the buffer (before it is committed).
350    pub(crate) fn record(&self, fence: &Fence, write: bool) {
351        let mut fences = self.fences.lock().unwrap_or_else(PoisonError::into_inner);
352        if write {
353            fences.writer = Some(fence.clone());
354        }
355        fences.users.push(fence.clone());
356    }
357
358    /// Whether no submitted GPU work uses the buffer.
359    pub(crate) fn idle(&self) -> bool {
360        self.users.load(Ordering::Acquire) == 0
361    }
362
363    /// Blocks until no submitted GPU work uses the buffer. Only for committed uses: those
364    /// are released by their command buffer's completion.
365    pub(crate) fn wait_idle(&self) {
366        if self.idle() {
367            return;
368        }
369        let mut fences = self.fences.lock().unwrap_or_else(PoisonError::into_inner);
370        // `release` takes this lock after the last use leaves, before notifying: no lost
371        // wake-up.
372        while !self.idle() {
373            fences = self
374                .idle
375                .wait(fences)
376                .unwrap_or_else(PoisonError::into_inner);
377        }
378    }
379
380    /// Uses encoded in command buffers not submitted yet, all and the writing ones.
381    pub(crate) fn unsubmitted(&self) -> (usize, usize) {
382        (
383            self.unsubmitted.load(Ordering::Acquire),
384            self.unsubmitted_writers.load(Ordering::Acquire),
385        )
386    }
387
388    /// Whether every pending use is committed (so its fence orders later work after it).
389    pub(crate) fn committed_uses(&self) -> bool {
390        self.uncommitted.load(Ordering::Acquire) == 0
391    }
392
393    /// A use's command buffer was committed (or abandoned).
394    pub(crate) fn committed(&self) {
395        self.uncommitted.fetch_sub(1, Ordering::AcqRel);
396    }
397
398    /// A use's command buffer was submitted to the GPU (or abandoned).
399    pub(crate) fn submitted(&self, write: bool) {
400        if write {
401            self.unsubmitted_writers.fetch_sub(1, Ordering::AcqRel);
402        }
403        self.unsubmitted.fetch_sub(1, Ordering::AcqRel);
404    }
405
406    pub(crate) fn acquire(&self, write: bool) {
407        self.uncommitted.fetch_add(1, Ordering::AcqRel);
408        self.unsubmitted.fetch_add(1, Ordering::AcqRel);
409        self.users.fetch_add(1, Ordering::AcqRel);
410        if write {
411            self.unsubmitted_writers.fetch_add(1, Ordering::AcqRel);
412            self.writers.fetch_add(1, Ordering::AcqRel);
413        }
414    }
415
416    /// A use's command buffer completed (or was abandoned); `fence` is its fence, if it
417    /// recorded one: nothing has to wait for that work any more.
418    pub(crate) fn release(&self, write: bool, fence: Option<&Fence>) {
419        if let Some(fence) = fence {
420            let mut fences = self.fences.lock().unwrap_or_else(PoisonError::into_inner);
421            let same = |f: &Fence| Retained::as_ptr(f) == Retained::as_ptr(fence);
422            if let Some(i) = fences.users.iter().position(same) {
423                fences.users.swap_remove(i);
424            }
425            if fences.writer.as_ref().is_some_and(same) {
426                fences.writer = None;
427            }
428        }
429        if write {
430            self.writers.fetch_sub(1, Ordering::AcqRel);
431        }
432        if self.users.fetch_sub(1, Ordering::AcqRel) == 1 {
433            // Idle: forget the fences, unless a new use began meanwhile (checked under the
434            // lock that `record` takes, so a new user's fence is never dropped).
435            let mut fences = self.fences.lock().unwrap_or_else(PoisonError::into_inner);
436            if self.users.load(Ordering::Acquire) == 0 {
437                *fences = Fences::default();
438                self.idle.notify_all();
439            }
440            drop(fences);
441        }
442    }
443}
444
445impl Drop for Buffer {
446    /// Returns the memory to the allocator's cache at once, with the hazard state of GPU
447    /// work that still uses it (only GPU work reuses it before that work completes). Runs
448    /// in an autorelease pool: a Metal buffer's deallocation autoreleases objects.
449    fn drop(&mut self) {
450        let Some(raw) = self.raw.take() else {
451            return;
452        };
453        autoreleasepool(|_| {
454            if self.mapped {
455                // Metal unmaps the file once the last reference (ours or a command
456                // buffer's) is released.
457                self.pool.untrack(&raw);
458                drop(raw);
459                return;
460            }
461            // Its own GPU work runs after the previous owner's: while that is pending, it
462            // orders reuse; otherwise the previous owner's (if pending) does.
463            let busy = if self.in_use() {
464                Some(self.in_flight.clone())
465            } else {
466                self.previous.take().filter(|p| !p.idle())
467            };
468            self.pool.free(raw, self.size, busy);
469        });
470    }
471}
472
473impl std::fmt::Debug for Buffer {
474    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
475        f.debug_struct("Buffer")
476            .field("len", &self.len)
477            .field("in_use", &self.in_use())
478            .finish()
479    }
480}
481
482#[cfg(test)]
483mod tests {
484    use objc2_metal::MTLDevice;
485
486    use super::*;
487
488    /// A buffer that is never idle (as weights read by every pipelined step) keeps only
489    /// the fences of uses still pending, not one per command buffer since it was idle.
490    #[test]
491    fn completed_fences_leave_buffers_that_stay_in_use() {
492        let Some(device) = Device::system_default() else {
493            return;
494        };
495        let new_fence = || device.raw().newFence().expect("a fence");
496        let guard = Guard::default();
497        // A long-running use keeps the buffer busy throughout.
498        let long = new_fence();
499        guard.acquire(false);
500        guard.record(&long, false);
501        guard.committed();
502        guard.submitted(false);
503        for _ in 0..1000 {
504            let fence = new_fence();
505            guard.acquire(true);
506            guard.record(&fence, true);
507            guard.committed();
508            guard.submitted(true);
509            guard.release(true, Some(&fence));
510        }
511        let fences = guard.fences.lock().unwrap();
512        assert_eq!(fences.users.len(), 1);
513        assert!(fences.writer.is_none());
514        drop(fences);
515        guard.release(false, Some(&long));
516        assert!(guard.idle());
517    }
518}