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, None))
107    }
108
109    /// [`Device::map_file`] for the bytes `offset..offset + len` of the file only: a page
110    /// multiple `offset`, and `len` rounded up to whole pages (within the file). The
111    /// buffer's bytes are that range; nothing outside it is mapped, wired or counted.
112    pub fn map_file_range(
113        &self,
114        path: &std::path::Path,
115        offset: usize,
116        len: usize,
117    ) -> Result<Buffer> {
118        autoreleasepool(|_| self.map_file_now(path, Some((offset, len))))
119    }
120
121    fn map_file_now(
122        &self,
123        path: &std::path::Path,
124        range: Option<(usize, usize)>,
125    ) -> Result<Buffer> {
126        use std::os::fd::AsRawFd;
127        let io = |e: std::io::Error| GpuError::Io(format!("{}: {e}", path.display()));
128        let file = std::fs::File::open(path).map_err(io)?;
129        let file_len = usize::try_from(file.metadata().map_err(io)?.len())
130            .map_err(|_| GpuError::Io(format!("{}: too large", path.display())))?;
131        let (offset, len) = range.unwrap_or((0, file_len));
132        if !offset.is_multiple_of(crate::memory::page()) {
133            return Err(GpuError::InvalidArgument(format!(
134                "{}: a mapping must start at a page boundary, not byte {offset}",
135                path.display()
136            )));
137        }
138        if offset.checked_add(len).is_none_or(|end| end > file_len) {
139            return Err(GpuError::InvalidArgument(format!(
140                "{}: bytes {offset}..{} are past the end ({file_len} bytes)",
141                path.display(),
142                offset.saturating_add(len)
143            )));
144        }
145        if len == 0 {
146            return self.new_buffer(0);
147        }
148        let map_len = crate::memory::mapping_size(len);
149        // SAFETY: a fresh read-only mapping of an open file descriptor; the result is
150        // checked below. The descriptor may be closed afterwards (the mapping keeps the file).
151        let ptr = unsafe {
152            libc::mmap(
153                std::ptr::null_mut(),
154                map_len,
155                libc::PROT_READ,
156                libc::MAP_SHARED,
157                file.as_raw_fd(),
158                offset as libc::off_t,
159            )
160        };
161        if ptr == libc::MAP_FAILED {
162            return Err(io(std::io::Error::last_os_error()));
163        }
164        let start = NonNull::new(ptr.cast::<u8>()).expect("mmap returned a non-null address");
165        let unmap = block2::RcBlock::new(move |p: NonNull<std::ffi::c_void>, n: usize| {
166            // SAFETY: Metal calls this once, with the mapping created above, after the
167            // buffer is released (so neither the CPU nor the GPU uses it any more).
168            unsafe { libc::munmap(p.as_ptr(), n) };
169        });
170        let options = objc2_metal::MTLResourceOptions::StorageModeShared
171            | objc2_metal::MTLResourceOptions::HazardTrackingModeUntracked;
172        // SAFETY: the pointer is page-aligned (from `mmap`), valid for `map_len` bytes, and
173        // owned by the buffer from now on: the deallocator unmaps it exactly once.
174        let raw = unsafe {
175            self.raw()
176                .newBufferWithBytesNoCopy_length_options_deallocator(
177                    start.cast(),
178                    map_len,
179                    options,
180                    Some(&unmap),
181                )
182        };
183        let Some(raw) = raw else {
184            // SAFETY: Metal did not take the mapping, so it is still ours to release.
185            unsafe { libc::munmap(ptr, map_len) };
186            return Err(GpuError::OutOfMemory(len));
187        };
188        // Over the memory limit: refused before anything is wired; dropping `raw`
189        // unmaps the file.
190        self.pool().track(&raw)?;
191        Ok(Buffer {
192            raw: Some(raw),
193            ptr: start,
194            len,
195            size: 0,
196            pool: self.pool().clone(),
197            in_flight: Arc::default(),
198            previous: None,
199            mapped: true,
200        })
201    }
202
203    /// A buffer holding a copy of `bytes`.
204    pub fn buffer_from(&self, bytes: &[u8]) -> Result<Buffer> {
205        let mut buffer = self.new_buffer(bytes.len())?;
206        buffer.contents_mut()?.copy_from_slice(bytes);
207        Ok(buffer)
208    }
209}
210
211impl Buffer {
212    /// Size in bytes.
213    pub fn len(&self) -> usize {
214        self.len
215    }
216
217    /// Whether the buffer holds no bytes.
218    pub fn is_empty(&self) -> bool {
219        self.len == 0
220    }
221
222    /// Whether submitted GPU work still uses (reads or writes) the buffer.
223    pub fn in_use(&self) -> bool {
224        self.in_flight.users.load(Ordering::Acquire) != 0
225    }
226
227    /// Waits until the previous owner's GPU work on reused memory has completed (it is
228    /// committed, so it does): blocks until its completion releases the last use.
229    fn wait_for_previous(&self) {
230        if let Some(previous) = &self.previous {
231            previous.wait_idle();
232        }
233    }
234
235    /// Whether the previous owner's GPU work on reused memory is still pending.
236    pub(crate) fn previous_busy(&self) -> bool {
237        self.previous.as_ref().is_some_and(|p| !p.idle())
238    }
239
240    /// The bytes, if no submitted GPU work writes the buffer (concurrent GPU reads are fine).
241    /// For reused memory ([`Device::new_gpu_buffer`]) whose previous owner's GPU work is
242    /// still running, it first blocks until that work has completed.
243    pub fn contents(&self) -> Result<&[u8]> {
244        self.wait_for_previous();
245        if self.in_flight.writers.load(Ordering::Acquire) != 0 {
246            return Err(GpuError::BufferInUse);
247        }
248        if self.raw.is_none() {
249            return Ok(&[]);
250        }
251        // SAFETY: `ptr` is `raw.contents()`, at least `len` bytes (the allocation is at least the
252        // rounded size) of CPU-visible shared storage that lives as long as `raw`, which this
253        // borrow keeps alive. No submitted GPU work writes the buffer (checked above), so any
254        // concurrent GPU access only reads; new GPU writes need `&mut self`, which this shared
255        // borrow excludes.
256        Ok(unsafe { std::slice::from_raw_parts(self.ptr.as_ptr(), self.len) })
257    }
258
259    /// The bytes, mutably, if no submitted GPU work uses (reads or writes) the buffer and
260    /// it is not a read-only mapped file.
261    pub fn contents_mut(&mut self) -> Result<&mut [u8]> {
262        if self.mapped {
263            return Err(GpuError::ReadOnly);
264        }
265        if self.in_use() {
266            return Err(GpuError::BufferInUse);
267        }
268        self.wait_for_previous();
269        self.previous = None;
270        if self.raw.is_none() {
271            return Ok(&mut []);
272        }
273        // SAFETY: as in `contents`; additionally `&mut self` is exclusive, and `Buffer` is not
274        // `Clone`, so no other CPU reference to these bytes exists.
275        Ok(unsafe { std::slice::from_raw_parts_mut(self.ptr.as_ptr(), self.len) })
276    }
277
278    /// The bytes' address and length, for a CPU write that submitted GPU work is ordered
279    /// after by other means (an [`Event`](super::Event) it waits for), so the in-flight
280    /// guards that [`contents_mut`](Self::contents_mut) checks do not apply. Blocks first
281    /// until the previous owner's GPU work on reused memory has completed. The caller
282    /// writes through it only while no GPU work accesses the bytes and no other CPU
283    /// reference to them exists.
284    pub fn unsynchronized_ptr(&self) -> Result<(NonNull<u8>, usize)> {
285        if self.mapped {
286            return Err(GpuError::ReadOnly);
287        }
288        self.wait_for_previous();
289        Ok((self.ptr, if self.raw.is_none() { 0 } else { self.len }))
290    }
291
292    /// The Metal buffer to bind: the device's placeholder for empty buffers.
293    pub(crate) fn raw(&self) -> Option<&ProtocolObject<dyn MTLBuffer>> {
294        self.raw
295            .as_deref()
296            .or_else(|| self.pool.placeholder().map(|r| &**r))
297    }
298
299    pub(crate) fn retained(&self) -> Option<Raw> {
300        self.raw
301            .clone()
302            .or_else(|| self.pool.placeholder().cloned())
303    }
304
305    /// Whether the buffer is a read-only mapped file.
306    pub fn is_mapped(&self) -> bool {
307        self.mapped
308    }
309
310    pub(crate) fn guard(&self) -> Arc<Guard> {
311        self.in_flight.clone()
312    }
313
314    /// GPU uses of the buffer encoded in command buffers not submitted yet, all and the
315    /// writing ones.
316    pub(crate) fn unsubmitted(&self) -> (usize, usize) {
317        self.in_flight.unsubmitted()
318    }
319
320    /// The fences GPU work using the buffer waits for: its own pending uses' and, for reused
321    /// memory, the previous owner's.
322    pub(crate) fn pending_fences(&self, write: bool) -> Vec<Fence> {
323        let mut fences = self.in_flight.pending_fences(write);
324        if let Some(previous) = &self.previous {
325            fences.extend(previous.pending_fences(true));
326        }
327        fences
328    }
329
330    /// Identity for hazard tracking (unique per live buffer, including empty ones).
331    pub(crate) fn id(&self) -> usize {
332        Arc::as_ptr(&self.in_flight) as usize
333    }
334}
335
336/// Counts submitted GPU uses of a buffer: all uses, the writing ones and those not yet
337/// committed; and the fences of the ordered command buffers using it. While uses are
338/// pending it stays with the memory, also when the memory is cached and reused.
339#[derive(Default)]
340pub(crate) struct Guard {
341    /// The sequence number ([`commit_sequence`]) of the latest command buffer committed with
342    /// a use.
343    last_commit: AtomicU64,
344    users: AtomicUsize,
345    writers: AtomicUsize,
346    /// Uses encoded in command buffers not committed yet: their fences cannot order other
347    /// work yet.
348    uncommitted: AtomicUsize,
349    /// Uses encoded in command buffers not submitted yet, all and the writing ones: work
350    /// in other command buffers cannot be ordered after them (their fences are not recorded
351    /// yet).
352    unsubmitted: AtomicUsize,
353    unsubmitted_writers: AtomicUsize,
354    fences: Mutex<Fences>,
355    /// Signalled (under `fences`) when the last pending use is released.
356    idle: Condvar,
357}
358
359impl std::fmt::Debug for Guard {
360    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
361        f.debug_struct("Guard")
362            .field("users", &self.users)
363            .field("writers", &self.writers)
364            .finish_non_exhaustive()
365    }
366}
367
368/// The fences of ordered command buffers whose uses of a buffer are pending.
369#[derive(Debug, Default)]
370struct Fences {
371    /// The last writer's.
372    writer: Option<Fence>,
373    /// Every pending use's (a command buffer's fence leaves when it completes, so a buffer
374    /// that is never idle, such as weights read by every pipelined step, keeps only the
375    /// fences of work still in flight).
376    users: Vec<Fence>,
377}
378
379/// Command buffers committed so far (all devices and queues).
380static COMMITS: AtomicU64 = AtomicU64::new(0);
381
382/// The next command buffer's sequence number.
383pub(crate) fn commit_sequence() -> u64 {
384    COMMITS.fetch_add(1, Ordering::AcqRel) + 1
385}
386
387impl Guard {
388    /// A use's command buffer was committed as number `sequence`.
389    pub(crate) fn committed_as(&self, sequence: u64) {
390        self.last_commit.fetch_max(sequence, Ordering::AcqRel);
391    }
392
393    /// Whether a use is in the latest committed command buffer: work taking the memory then
394    /// waits for that command buffer, so the next one cannot overlap it.
395    pub(crate) fn in_latest_commit(&self) -> bool {
396        self.last_commit.load(Ordering::Acquire) >= COMMITS.load(Ordering::Acquire)
397    }
398
399    /// The fences to wait for before using the buffer: the pending writer's for a read, and
400    /// every pending user's for a write.
401    pub(crate) fn pending_fences(&self, write: bool) -> Vec<Fence> {
402        if self.users.load(Ordering::Acquire) == 0 {
403            return Vec::new();
404        }
405        let fences = self.fences.lock().unwrap_or_else(PoisonError::into_inner);
406        if write {
407            fences.users.clone()
408        } else if self.writers.load(Ordering::Acquire) != 0 {
409            fences.writer.iter().cloned().collect()
410        } else {
411            Vec::new()
412        }
413    }
414
415    /// Records the fence of a command buffer that uses the buffer (before it is committed).
416    pub(crate) fn record(&self, fence: &Fence, write: bool) {
417        let mut fences = self.fences.lock().unwrap_or_else(PoisonError::into_inner);
418        if write {
419            fences.writer = Some(fence.clone());
420        }
421        fences.users.push(fence.clone());
422    }
423
424    /// Whether no submitted GPU work uses the buffer.
425    pub(crate) fn idle(&self) -> bool {
426        self.users.load(Ordering::Acquire) == 0
427    }
428
429    /// Blocks until no submitted GPU work uses the buffer. Only for committed uses: those
430    /// are released by their command buffer's completion.
431    pub(crate) fn wait_idle(&self) {
432        if self.idle() {
433            return;
434        }
435        let mut fences = self.fences.lock().unwrap_or_else(PoisonError::into_inner);
436        // `release` takes this lock after the last use leaves, before notifying: no lost
437        // wake-up.
438        while !self.idle() {
439            fences = self
440                .idle
441                .wait(fences)
442                .unwrap_or_else(PoisonError::into_inner);
443        }
444    }
445
446    /// Uses encoded in command buffers not submitted yet, all and the writing ones.
447    pub(crate) fn unsubmitted(&self) -> (usize, usize) {
448        (
449            self.unsubmitted.load(Ordering::Acquire),
450            self.unsubmitted_writers.load(Ordering::Acquire),
451        )
452    }
453
454    /// Whether every pending use is committed (so its fence orders later work after it).
455    pub(crate) fn committed_uses(&self) -> bool {
456        self.uncommitted.load(Ordering::Acquire) == 0
457    }
458
459    /// A use's command buffer was committed (or abandoned).
460    pub(crate) fn committed(&self) {
461        self.uncommitted.fetch_sub(1, Ordering::AcqRel);
462    }
463
464    /// A use's command buffer was submitted to the GPU (or abandoned).
465    pub(crate) fn submitted(&self, write: bool) {
466        if write {
467            self.unsubmitted_writers.fetch_sub(1, Ordering::AcqRel);
468        }
469        self.unsubmitted.fetch_sub(1, Ordering::AcqRel);
470    }
471
472    pub(crate) fn acquire(&self, write: bool) {
473        self.uncommitted.fetch_add(1, Ordering::AcqRel);
474        self.unsubmitted.fetch_add(1, Ordering::AcqRel);
475        self.users.fetch_add(1, Ordering::AcqRel);
476        if write {
477            self.unsubmitted_writers.fetch_add(1, Ordering::AcqRel);
478            self.writers.fetch_add(1, Ordering::AcqRel);
479        }
480    }
481
482    /// A use's command buffer completed (or was abandoned); `fence` is its fence, if it
483    /// recorded one: nothing has to wait for that work any more.
484    pub(crate) fn release(&self, write: bool, fence: Option<&Fence>) {
485        if let Some(fence) = fence {
486            let mut fences = self.fences.lock().unwrap_or_else(PoisonError::into_inner);
487            let same = |f: &Fence| Retained::as_ptr(f) == Retained::as_ptr(fence);
488            if let Some(i) = fences.users.iter().position(same) {
489                fences.users.swap_remove(i);
490            }
491            if fences.writer.as_ref().is_some_and(same) {
492                fences.writer = None;
493            }
494        }
495        if write {
496            self.writers.fetch_sub(1, Ordering::AcqRel);
497        }
498        if self.users.fetch_sub(1, Ordering::AcqRel) == 1 {
499            // Idle: forget the fences, unless a new use began meanwhile (checked under the
500            // lock that `record` takes, so a new user's fence is never dropped).
501            let mut fences = self.fences.lock().unwrap_or_else(PoisonError::into_inner);
502            if self.users.load(Ordering::Acquire) == 0 {
503                *fences = Fences::default();
504                self.idle.notify_all();
505            }
506            drop(fences);
507        }
508    }
509}
510
511impl Drop for Buffer {
512    /// Returns the memory to the allocator's cache at once, with the hazard state of GPU
513    /// work that still uses it (only GPU work reuses it before that work completes). Runs
514    /// in an autorelease pool: a Metal buffer's deallocation autoreleases objects.
515    fn drop(&mut self) {
516        let Some(raw) = self.raw.take() else {
517            return;
518        };
519        autoreleasepool(|_| {
520            if self.mapped {
521                // Metal unmaps the file once the last reference (ours or a command
522                // buffer's) is released.
523                self.pool.untrack(&raw);
524                drop(raw);
525                return;
526            }
527            // Its own GPU work runs after the previous owner's: while that is pending, it
528            // orders reuse; otherwise the previous owner's (if pending) does.
529            let busy = if self.in_use() {
530                Some(self.in_flight.clone())
531            } else {
532                self.previous.take().filter(|p| !p.idle())
533            };
534            self.pool.free(raw, self.size, busy);
535        });
536    }
537}
538
539impl std::fmt::Debug for Buffer {
540    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
541        f.debug_struct("Buffer")
542            .field("len", &self.len)
543            .field("in_use", &self.in_use())
544            .finish()
545    }
546}
547
548#[cfg(test)]
549mod tests {
550    use objc2_metal::MTLDevice;
551
552    use super::*;
553
554    /// A buffer that is never idle (as weights read by every pipelined step) keeps only
555    /// the fences of uses still pending, not one per command buffer since it was idle.
556    #[test]
557    fn completed_fences_leave_buffers_that_stay_in_use() {
558        let Some(device) = Device::system_default() else {
559            return;
560        };
561        let new_fence = || device.raw().newFence().expect("a fence");
562        let guard = Guard::default();
563        // A long-running use keeps the buffer busy throughout.
564        let long = new_fence();
565        guard.acquire(false);
566        guard.record(&long, false);
567        guard.committed();
568        guard.submitted(false);
569        for _ in 0..1000 {
570            let fence = new_fence();
571            guard.acquire(true);
572            guard.record(&fence, true);
573            guard.committed();
574            guard.submitted(true);
575            guard.release(true, Some(&fence));
576        }
577        let fences = guard.fences.lock().unwrap();
578        assert_eq!(fences.users.len(), 1);
579        assert!(fences.writer.is_none());
580        drop(fences);
581        guard.release(false, Some(&long));
582        assert!(guard.idle());
583    }
584}