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}