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}