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, Mutex, PoisonError};
6
7use objc2::rc::Retained;
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    fn alloc_buffer(&self, len: usize, gpu: bool) -> Result<Buffer> {
68        let pool = self.pool().clone();
69        let (raw, size, busy) = match pool.alloc(self.raw(), len, gpu)? {
70            Some((raw, size, busy)) => (Some(raw), size, busy),
71            None => (None, 0, None),
72        };
73        let ptr = raw
74            .as_ref()
75            .map_or(NonNull::dangling(), |r| r.contents().cast());
76        Ok(Buffer {
77            raw,
78            ptr,
79            len,
80            size,
81            pool,
82            in_flight: Arc::default(),
83            previous: busy,
84            mapped: false,
85        })
86    }
87
88    /// The file at `path`, memory-mapped read-only and wrapped without a copy
89    /// (`newBufferWithBytesNoCopy`): its pages are the page cache's, read from disk when
90    /// first touched by the CPU or the GPU. The buffer cannot be written (CPU writes and GPU
91    /// writes are refused). The mapping is released when the buffer and every GPU use of
92    /// it are gone.
93    ///
94    /// A shared read-only mapping is used because a private (copy-on-write) one costs
95    /// about 4× more to fault in on first use.
96    pub fn map_file(&self, path: &std::path::Path) -> Result<Buffer> {
97        use std::os::fd::AsRawFd;
98        let io = |e: std::io::Error| GpuError::Io(format!("{}: {e}", path.display()));
99        let file = std::fs::File::open(path).map_err(io)?;
100        let len = usize::try_from(file.metadata().map_err(io)?.len())
101            .map_err(|_| GpuError::Io(format!("{}: too large", path.display())))?;
102        if len == 0 {
103            return self.new_buffer(0);
104        }
105        // SAFETY: `sysconf` has no preconditions.
106        let page = usize::try_from(unsafe { libc::sysconf(libc::_SC_PAGESIZE) }).unwrap_or(16384);
107        let map_len = len.div_ceil(page) * page;
108        // SAFETY: a fresh read-only mapping of an open file descriptor; the result is
109        // checked below. The descriptor may be closed afterwards (the mapping keeps the file).
110        let ptr = unsafe {
111            libc::mmap(
112                std::ptr::null_mut(),
113                map_len,
114                libc::PROT_READ,
115                libc::MAP_SHARED,
116                file.as_raw_fd(),
117                0,
118            )
119        };
120        if ptr == libc::MAP_FAILED {
121            return Err(io(std::io::Error::last_os_error()));
122        }
123        let start = NonNull::new(ptr.cast::<u8>()).expect("mmap returned a non-null address");
124        let unmap = block2::RcBlock::new(move |p: NonNull<std::ffi::c_void>, n: usize| {
125            // SAFETY: Metal calls this once, with the mapping created above, after the
126            // buffer is released (so neither the CPU nor the GPU uses it any more).
127            unsafe { libc::munmap(p.as_ptr(), n) };
128        });
129        let options = objc2_metal::MTLResourceOptions::StorageModeShared
130            | objc2_metal::MTLResourceOptions::HazardTrackingModeUntracked;
131        // SAFETY: the pointer is page-aligned (from `mmap`), valid for `map_len` bytes, and
132        // owned by the buffer from now on: the deallocator unmaps it exactly once.
133        let raw = unsafe {
134            self.raw()
135                .newBufferWithBytesNoCopy_length_options_deallocator(
136                    start.cast(),
137                    map_len,
138                    options,
139                    Some(&unmap),
140                )
141        };
142        let Some(raw) = raw else {
143            // SAFETY: Metal did not take the mapping, so it is still ours to release.
144            unsafe { libc::munmap(ptr, map_len) };
145            return Err(GpuError::OutOfMemory(len));
146        };
147        self.pool().track(&raw);
148        Ok(Buffer {
149            raw: Some(raw),
150            ptr: start,
151            len,
152            size: 0,
153            pool: self.pool().clone(),
154            in_flight: Arc::default(),
155            previous: None,
156            mapped: true,
157        })
158    }
159
160    /// A buffer holding a copy of `bytes`.
161    pub fn buffer_from(&self, bytes: &[u8]) -> Result<Buffer> {
162        let mut buffer = self.new_buffer(bytes.len())?;
163        buffer.contents_mut()?.copy_from_slice(bytes);
164        Ok(buffer)
165    }
166}
167
168impl Buffer {
169    /// Size in bytes.
170    pub fn len(&self) -> usize {
171        self.len
172    }
173
174    /// Whether the buffer holds no bytes.
175    pub fn is_empty(&self) -> bool {
176        self.len == 0
177    }
178
179    /// Whether submitted GPU work still uses (reads or writes) the buffer.
180    pub fn in_use(&self) -> bool {
181        self.in_flight.users.load(Ordering::Acquire) != 0
182    }
183
184    /// Waits until the previous owner's GPU work on reused memory has completed (it is
185    /// committed, so it does).
186    fn wait_for_previous(&self) {
187        if let Some(previous) = &self.previous {
188            while !previous.idle() {
189                std::thread::yield_now();
190            }
191        }
192    }
193
194    /// The bytes, if no submitted GPU work writes the buffer (concurrent GPU reads are fine).
195    pub fn contents(&self) -> Result<&[u8]> {
196        self.wait_for_previous();
197        if self.in_flight.writers.load(Ordering::Acquire) != 0 {
198            return Err(GpuError::BufferInUse);
199        }
200        if self.raw.is_none() {
201            return Ok(&[]);
202        }
203        // SAFETY: `ptr` is `raw.contents()`, at least `len` bytes (the allocation is at least the
204        // rounded size) of CPU-visible shared storage that lives as long as `raw`, which this
205        // borrow keeps alive. No submitted GPU work writes the buffer (checked above), so any
206        // concurrent GPU access only reads; new GPU writes need `&mut self`, which this shared
207        // borrow excludes.
208        Ok(unsafe { std::slice::from_raw_parts(self.ptr.as_ptr(), self.len) })
209    }
210
211    /// The bytes, mutably, if no submitted GPU work uses (reads or writes) the buffer and
212    /// it is not a read-only mapped file.
213    pub fn contents_mut(&mut self) -> Result<&mut [u8]> {
214        if self.mapped {
215            return Err(GpuError::ReadOnly);
216        }
217        if self.in_use() {
218            return Err(GpuError::BufferInUse);
219        }
220        self.wait_for_previous();
221        self.previous = None;
222        if self.raw.is_none() {
223            return Ok(&mut []);
224        }
225        // SAFETY: as in `contents`; additionally `&mut self` is exclusive, and `Buffer` is not
226        // `Clone`, so no other CPU reference to these bytes exists.
227        Ok(unsafe { std::slice::from_raw_parts_mut(self.ptr.as_ptr(), self.len) })
228    }
229
230    /// The Metal buffer to bind: the device's placeholder for empty buffers.
231    pub(crate) fn raw(&self) -> Option<&ProtocolObject<dyn MTLBuffer>> {
232        self.raw
233            .as_deref()
234            .or_else(|| self.pool.placeholder().map(|r| &**r))
235    }
236
237    pub(crate) fn retained(&self) -> Option<Raw> {
238        self.raw
239            .clone()
240            .or_else(|| self.pool.placeholder().cloned())
241    }
242
243    /// Whether the buffer is a read-only mapped file.
244    pub fn is_mapped(&self) -> bool {
245        self.mapped
246    }
247
248    pub(crate) fn guard(&self) -> Arc<Guard> {
249        self.in_flight.clone()
250    }
251
252    /// The fences GPU work using the buffer waits for: its own pending uses' and, for reused
253    /// memory, the previous owner's.
254    pub(crate) fn pending_fences(&self, write: bool) -> Vec<Fence> {
255        let mut fences = self.in_flight.pending_fences(write);
256        if let Some(previous) = &self.previous {
257            fences.extend(previous.pending_fences(true));
258        }
259        fences
260    }
261
262    /// Identity for hazard tracking (unique per live buffer, including empty ones).
263    pub(crate) fn id(&self) -> usize {
264        Arc::as_ptr(&self.in_flight) as usize
265    }
266}
267
268/// Counts submitted GPU uses of a buffer: all uses, the writing ones and those not yet
269/// committed; and the fences of the ordered command buffers using it. While uses are
270/// pending it stays with the memory, also when the memory is cached and reused.
271#[derive(Default)]
272pub(crate) struct Guard {
273    users: AtomicUsize,
274    writers: AtomicUsize,
275    /// Uses encoded in command buffers not committed yet: their fences cannot order other
276    /// work yet.
277    uncommitted: AtomicUsize,
278    fences: Mutex<Fences>,
279}
280
281impl std::fmt::Debug for Guard {
282    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
283        f.debug_struct("Guard")
284            .field("users", &self.users)
285            .field("writers", &self.writers)
286            .finish_non_exhaustive()
287    }
288}
289
290/// The fences of ordered command buffers whose uses of a buffer are pending.
291#[derive(Debug, Default)]
292struct Fences {
293    /// The last writer's.
294    writer: Option<Fence>,
295    /// Every pending use's (a command buffer's fence leaves when it completes, so a buffer
296    /// that is never idle, such as weights read by every pipelined step, keeps only the
297    /// fences of work still in flight).
298    users: Vec<Fence>,
299}
300
301impl Guard {
302    /// The fences to wait for before using the buffer: the pending writer's for a read, and
303    /// every pending user's for a write.
304    pub(crate) fn pending_fences(&self, write: bool) -> Vec<Fence> {
305        if self.users.load(Ordering::Acquire) == 0 {
306            return Vec::new();
307        }
308        let fences = self.fences.lock().unwrap_or_else(PoisonError::into_inner);
309        if write {
310            fences.users.clone()
311        } else if self.writers.load(Ordering::Acquire) != 0 {
312            fences.writer.iter().cloned().collect()
313        } else {
314            Vec::new()
315        }
316    }
317
318    /// Records the fence of a command buffer that uses the buffer (before it is committed).
319    pub(crate) fn record(&self, fence: &Fence, write: bool) {
320        let mut fences = self.fences.lock().unwrap_or_else(PoisonError::into_inner);
321        if write {
322            fences.writer = Some(fence.clone());
323        }
324        fences.users.push(fence.clone());
325    }
326
327    /// Whether no submitted GPU work uses the buffer.
328    pub(crate) fn idle(&self) -> bool {
329        self.users.load(Ordering::Acquire) == 0
330    }
331
332    /// Whether every pending use is committed (so its fence orders later work after it).
333    pub(crate) fn committed_uses(&self) -> bool {
334        self.uncommitted.load(Ordering::Acquire) == 0
335    }
336
337    /// A use's command buffer was committed (or abandoned).
338    pub(crate) fn committed(&self) {
339        self.uncommitted.fetch_sub(1, Ordering::AcqRel);
340    }
341
342    pub(crate) fn acquire(&self, write: bool) {
343        self.uncommitted.fetch_add(1, Ordering::AcqRel);
344        self.users.fetch_add(1, Ordering::AcqRel);
345        if write {
346            self.writers.fetch_add(1, Ordering::AcqRel);
347        }
348    }
349
350    /// A use's command buffer completed (or was abandoned); `fence` is its fence, if it
351    /// recorded one: nothing has to wait for that work any more.
352    pub(crate) fn release(&self, write: bool, fence: Option<&Fence>) {
353        if let Some(fence) = fence {
354            let mut fences = self.fences.lock().unwrap_or_else(PoisonError::into_inner);
355            let same = |f: &Fence| Retained::as_ptr(f) == Retained::as_ptr(fence);
356            if let Some(i) = fences.users.iter().position(same) {
357                fences.users.swap_remove(i);
358            }
359            if fences.writer.as_ref().is_some_and(same) {
360                fences.writer = None;
361            }
362        }
363        if write {
364            self.writers.fetch_sub(1, Ordering::AcqRel);
365        }
366        if self.users.fetch_sub(1, Ordering::AcqRel) == 1 {
367            // Idle: forget the fences, unless a new use began meanwhile (checked under the
368            // lock that `record` takes, so a new user's fence is never dropped).
369            let mut fences = self.fences.lock().unwrap_or_else(PoisonError::into_inner);
370            if self.users.load(Ordering::Acquire) == 0 {
371                *fences = Fences::default();
372            }
373            drop(fences);
374        }
375    }
376}
377
378impl Drop for Buffer {
379    /// Returns the memory to the allocator's cache at once, with the hazard state of GPU
380    /// work that still uses it (only GPU work reuses it before that work completes).
381    fn drop(&mut self) {
382        if let Some(raw) = self.raw.take() {
383            if self.mapped {
384                // Metal unmaps the file once the last reference (ours or a command
385                // buffer's) is released.
386                self.pool.untrack(&raw);
387                drop(raw);
388                return;
389            }
390            // Its own GPU work runs after the previous owner's: while that is pending, it
391            // orders reuse; otherwise the previous owner's (if pending) does.
392            let busy = if self.in_use() {
393                Some(self.in_flight.clone())
394            } else {
395                self.previous.take().filter(|p| !p.idle())
396            };
397            self.pool.free(raw, self.size, busy);
398        }
399    }
400}
401
402impl std::fmt::Debug for Buffer {
403    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
404        f.debug_struct("Buffer")
405            .field("len", &self.len)
406            .field("in_use", &self.in_use())
407            .finish()
408    }
409}
410
411#[cfg(test)]
412mod tests {
413    use objc2_metal::MTLDevice;
414
415    use super::*;
416
417    /// A buffer that is never idle (as weights read by every pipelined step) keeps only
418    /// the fences of uses still pending, not one per command buffer since it was idle.
419    #[test]
420    fn completed_fences_leave_buffers_that_stay_in_use() {
421        let Some(device) = Device::system_default() else {
422            return;
423        };
424        let new_fence = || device.raw().newFence().expect("a fence");
425        let guard = Guard::default();
426        // A long-running use keeps the buffer busy throughout.
427        let long = new_fence();
428        guard.acquire(false);
429        guard.record(&long, false);
430        guard.committed();
431        for _ in 0..1000 {
432            let fence = new_fence();
433            guard.acquire(true);
434            guard.record(&fence, true);
435            guard.committed();
436            guard.release(true, Some(&fence));
437        }
438        let fences = guard.fences.lock().unwrap();
439        assert_eq!(fences.users.len(), 1);
440        assert!(fences.writer.is_none());
441        drop(fences);
442        guard.release(false, Some(&long));
443        assert!(guard.idle());
444    }
445}