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