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}