Skip to main content

nuvec_gpu/metal/
command.rs

1//! Command encoding with minimal barriers, and submission.
2
3use std::ptr::NonNull;
4
5use std::sync::{Arc, Mutex, PoisonError};
6
7use block2::RcBlock;
8use objc2::rc::Retained;
9use objc2::runtime::ProtocolObject;
10use objc2_metal::{
11    MTLBarrierScope, MTLBuffer, MTLCommandBuffer, MTLCommandBufferStatus, MTLCommandEncoder,
12    MTLCommandQueue, MTLComputeCommandEncoder, MTLDevice, MTLDispatchType, MTLFence, MTLSize,
13};
14
15use super::buffer::Guard;
16use super::{Buffer, Device, Pipeline};
17use crate::{GpuError, Result};
18
19/// Metal's limit on buffer argument slots per kernel.
20const MAX_ARGS: usize = 31;
21/// Metal's limit for inline bytes (`setBytes`).
22const MAX_INLINE_BYTES: usize = 4096;
23
24/// A kernel argument, bound to the slot of its position in the argument list.
25#[derive(Debug)]
26pub enum Arg<'a> {
27    /// A buffer the kernel only reads, from byte `offset` on.
28    Read(&'a Buffer, usize),
29    /// A buffer the kernel writes (and may read), from byte `offset` on.
30    Write(&'a mut Buffer, usize),
31    /// Small constant data copied into the command stream (at most 4 KiB).
32    Bytes(&'a [u8]),
33}
34
35impl<'a> Arg<'a> {
36    /// The whole buffer, read-only.
37    pub fn read(buffer: &'a Buffer) -> Self {
38        Self::Read(buffer, 0)
39    }
40
41    /// The whole buffer, written.
42    pub fn write(buffer: &'a mut Buffer) -> Self {
43        Self::Write(buffer, 0)
44    }
45
46    /// Inline constant bytes.
47    pub fn bytes(bytes: &'a [u8]) -> Self {
48        Self::Bytes(bytes)
49    }
50
51    fn buffer(&self) -> Option<(&Buffer, usize, bool)> {
52        match self {
53            Self::Read(b, offset) => Some((b, *offset, false)),
54            Self::Write(b, offset) => Some((b, *offset, true)),
55            Self::Bytes(_) => None,
56        }
57    }
58}
59
60pub(crate) type Fence = Retained<ProtocolObject<dyn MTLFence>>;
61
62/// Orders command buffers that are committed without waiting: each command buffer created
63/// on a timeline starts its work only after the previous one's work has finished. Buffers
64/// are untracked, so consecutive command buffers would otherwise overlap on the GPU (as
65/// [`Device::ordered_command_buffer`]s do when they are independent).
66///
67/// Implemented with one fence (as in MLX): a command buffer's compute pass waits for the
68/// fence and updates it at the end. Command buffers without work leave it untouched, so
69/// empty or abandoned ones need no special handling.
70pub struct Timeline {
71    fence: Fence,
72    started: bool,
73}
74
75impl std::fmt::Debug for Timeline {
76    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77        f.debug_struct("Timeline").finish_non_exhaustive()
78    }
79}
80
81/// Work being recorded for the GPU: one concurrent compute encoder, with memory barriers
82/// inserted only where a dispatch depends on an earlier one.
83pub struct CommandBuffer {
84    device: Device,
85    raw: Retained<ProtocolObject<dyn MTLCommandBuffer>>,
86    /// The timeline fence, and whether the compute pass must wait for it.
87    fence: Option<(Fence, bool)>,
88    /// For ordered command buffers: this command buffer's fence (updated at the end of its
89    /// work and recorded on the buffers it writes), and the fences already waited for.
90    own: Option<Fence>,
91    waited: Vec<usize>,
92    encoder: Option<Retained<ProtocolObject<dyn MTLComputeCommandEncoder>>>,
93    /// Buffers read and written since the last barrier.
94    reads: Vec<usize>,
95    writes: Vec<usize>,
96    barriers: usize,
97    /// In-flight guards of the buffers used, and the buffers themselves (kept alive).
98    guards: Vec<(Arc<Guard>, bool)>,
99    retained: Vec<Retained<ProtocolObject<dyn MTLBuffer>>>,
100    /// The kernels dispatched, while profiling.
101    kernels: Vec<Arc<str>>,
102}
103
104impl Device {
105    /// A new command buffer on this device's queue.
106    pub fn command_buffer(&self) -> Result<CommandBuffer> {
107        let raw = self
108            .queue()
109            .commandBuffer()
110            .ok_or_else(|| GpuError::CommandBuffer("cannot create a command buffer".into()))?;
111        Ok(CommandBuffer {
112            device: self.clone(),
113            raw,
114            fence: None,
115            own: None,
116            waited: Vec::new(),
117            encoder: None,
118            reads: Vec::new(),
119            writes: Vec::new(),
120            barriers: 0,
121            guards: Vec::new(),
122            retained: Vec::new(),
123            kernels: Vec::new(),
124        })
125    }
126}
127
128impl Device {
129    /// A new, empty timeline.
130    pub fn timeline(&self) -> Result<Timeline> {
131        let fence = self
132            .raw()
133            .newFence()
134            .ok_or_else(|| GpuError::CommandBuffer("cannot create a fence".into()))?;
135        Ok(Timeline {
136            fence,
137            started: false,
138        })
139    }
140
141    /// A new command buffer whose work reads the outputs of earlier *ordered* command
142    /// buffers (committed on this device from any thread) safely while they are in flight:
143    /// before a dispatch reads or writes a buffer with a pending GPU write, it waits for the
144    /// writing command buffer's fence. Independent command buffers overlap on the GPU, as
145    /// in MLX.
146    ///
147    /// Reads wait for the pending writer; writes wait for every pending user (writing a
148    /// buffer that earlier work still reads). Other command buffers do not wait.
149    pub fn ordered_command_buffer(&self) -> Result<CommandBuffer> {
150        let fence = self
151            .raw()
152            .newFence()
153            .ok_or_else(|| GpuError::CommandBuffer("cannot create a fence".into()))?;
154        let mut cmd = self.command_buffer()?;
155        cmd.own = Some(fence);
156        Ok(cmd)
157    }
158
159    /// A new command buffer whose work runs after the work of every command buffer created
160    /// earlier on `timeline` (commit them in creation order).
161    pub fn command_buffer_on(&self, timeline: &mut Timeline) -> Result<CommandBuffer> {
162        let mut cmd = self.command_buffer()?;
163        cmd.fence = Some((timeline.fence.clone(), timeline.started));
164        timeline.started = true;
165        Ok(cmd)
166    }
167}
168
169/// Reports a finished command buffer's GPU error, if any.
170fn status(raw: &ProtocolObject<dyn MTLCommandBuffer>) -> Result<()> {
171    if raw.status() == MTLCommandBufferStatus::Error {
172        let message = raw.error().map_or_else(
173            || "unknown error".to_owned(),
174            |e| e.localizedDescription().to_string(),
175        );
176        return Err(GpuError::CommandBuffer(message));
177    }
178    Ok(())
179}
180
181impl CommandBuffer {
182    /// Encodes `pipeline` over `threads` threads (a 1-D grid) with `args` bound to slots
183    /// `0..args.len()`. The threadgroup size is derived from the pipeline's limits.
184    pub fn dispatch(
185        &mut self,
186        pipeline: &Pipeline,
187        args: &[Arg<'_>],
188        threads: usize,
189    ) -> Result<()> {
190        if threads == 0 {
191            return validate(args);
192        }
193        let width = pipeline.execution_width().max(1);
194        let mut group = pipeline.max_threads_per_threadgroup().min(threads).max(1);
195        if group > width {
196            group -= group % width;
197        }
198        self.encode(pipeline, args, [threads, 1, 1], |encoder| {
199            encoder
200                .dispatchThreads_threadsPerThreadgroup(size([threads, 1, 1]), size([group, 1, 1]));
201        })
202    }
203
204    /// Encodes `pipeline` over a grid of `groups` threadgroups of `threads` threads each, for
205    /// kernels that cooperate within a threadgroup (reductions, tiled matrix products).
206    pub fn dispatch_groups(
207        &mut self,
208        pipeline: &Pipeline,
209        args: &[Arg<'_>],
210        groups: [usize; 3],
211        threads: [usize; 3],
212    ) -> Result<()> {
213        let per_group: usize = threads.iter().product();
214        if per_group > pipeline.max_threads_per_threadgroup() {
215            return Err(GpuError::InvalidArgument(format!(
216                "{per_group} threads per threadgroup; `{}` allows {}",
217                pipeline.name(),
218                pipeline.max_threads_per_threadgroup()
219            )));
220        }
221        if groups.contains(&0) || per_group == 0 {
222            return validate(args);
223        }
224        let extent = [0, 1, 2].map(|d| groups[d] * threads[d]);
225        self.encode(pipeline, args, extent, |encoder| {
226            encoder.dispatchThreadgroups_threadsPerThreadgroup(size(groups), size(threads));
227        })
228    }
229
230    /// Encodes `pipeline` over exactly `grid` threads in up to three dimensions, in
231    /// threadgroups of `group` threads (the edge threadgroups are partial).
232    pub fn dispatch_threads(
233        &mut self,
234        pipeline: &Pipeline,
235        args: &[Arg<'_>],
236        grid: [usize; 3],
237        group: [usize; 3],
238    ) -> Result<()> {
239        let per_group: usize = group.iter().product();
240        if per_group > pipeline.max_threads_per_threadgroup() {
241            return Err(GpuError::InvalidArgument(format!(
242                "{per_group} threads per threadgroup; `{}` allows {}",
243                pipeline.name(),
244                pipeline.max_threads_per_threadgroup()
245            )));
246        }
247        if grid.contains(&0) || per_group == 0 {
248            return validate(args);
249        }
250        self.encode(pipeline, args, grid, |encoder| {
251            encoder.dispatchThreads_threadsPerThreadgroup(size(grid), size(group));
252        })
253    }
254
255    /// Validates and binds `args`, inserts a barrier if needed, then lets `dispatch` encode
256    /// over `extent` threads (for profiling).
257    fn encode(
258        &mut self,
259        pipeline: &Pipeline,
260        args: &[Arg<'_>],
261        extent: [usize; 3],
262        dispatch: impl FnOnce(&ProtocolObject<dyn MTLComputeCommandEncoder>),
263    ) -> Result<()> {
264        if super::profile::enabled() {
265            self.kernels.push(match super::profile::shapes() {
266                true => format!("{} {extent:?}", pipeline.name()).into(),
267                false => pipeline.name.clone(),
268            });
269        }
270        validate(args)?;
271        if self.conflicts(args) {
272            self.encoder()?
273                .memoryBarrierWithScope(MTLBarrierScope::Buffers);
274            self.barriers += 1;
275            self.reads.clear();
276            self.writes.clear();
277        }
278        let encoder = self.encoder()?.clone();
279        if let Some(own) = &self.own {
280            let own = Retained::as_ptr(own) as usize;
281            for (buffer, _, write) in args.iter().filter_map(Arg::buffer) {
282                for fence in buffer.pending_fences(write) {
283                    let key = Retained::as_ptr(&fence) as usize;
284                    if key != own && !self.waited.contains(&key) {
285                        encoder.waitForFence(&fence);
286                        self.waited.push(key);
287                    }
288                }
289            }
290        }
291        encoder.setComputePipelineState(pipeline.raw());
292        for (index, arg) in args.iter().enumerate() {
293            match arg {
294                Arg::Bytes(bytes) => {
295                    let data = NonNull::new(bytes.as_ptr().cast_mut())
296                        .unwrap_or(NonNull::dangling())
297                        .cast();
298                    // SAFETY: `validate` checked `index < 31` and `bytes.len() <= 4096`;
299                    // Metal copies the bytes during this call, so the pointer need only be
300                    // valid for `bytes.len()` bytes now, which the slice guarantees.
301                    unsafe { encoder.setBytes_length_atIndex(data, bytes.len(), index) };
302                }
303                Arg::Read(..) | Arg::Write(..) => {
304                    let (buffer, offset, _) = arg.buffer().expect("buffer argument");
305                    // SAFETY: `validate` checked `index < 31` and `offset <= buffer.len()`, so
306                    // the binding stays within the buffer; the buffer is retained by this
307                    // command buffer until the GPU has finished.
308                    unsafe { encoder.setBuffer_offset_atIndex(buffer.raw(), offset, index) };
309                }
310            }
311        }
312        for (buffer, _, write) in args.iter().filter_map(Arg::buffer) {
313            (if write {
314                &mut self.writes
315            } else {
316                &mut self.reads
317            })
318            .push(buffer.id());
319            let guard = buffer.guard();
320            guard.acquire(write);
321            self.guards.push((guard, write));
322            self.retained.extend(buffer.retained());
323        }
324        dispatch(&encoder);
325        Ok(())
326    }
327
328    /// Memory barriers inserted so far.
329    pub fn barriers(&self) -> usize {
330        self.barriers
331    }
332
333    /// Ends encoding, updating the timeline fence after this command buffer's work.
334    fn close(&mut self) {
335        if let Some(encoder) = self.encoder.take() {
336            if let Some((fence, _)) = &self.fence {
337                encoder.updateFence(fence);
338            }
339            if let Some(own) = &self.own {
340                encoder.updateFence(own);
341                for (guard, write) in &self.guards {
342                    guard.record(own, *write);
343                }
344            }
345            encoder.endEncoding();
346        }
347        self.device.flights().begin();
348    }
349
350    /// Submits the work to the GPU.
351    pub fn commit(mut self) -> Submitted {
352        self.close();
353        self.raw.commit();
354        self.mark_committed();
355        Submitted {
356            device: self.device.clone(),
357            raw: self.raw.clone(),
358            unfenced: self.own.is_none(),
359            own: self.own.clone(),
360            guards: std::mem::take(&mut self.guards),
361            _retained: std::mem::take(&mut self.retained),
362            done: false,
363        }
364    }
365
366    /// Submits the work and returns at once. `on_complete` runs exactly once with the
367    /// command buffer's result: on a Metal thread when the GPU finishes, or earlier on the
368    /// thread that calls [`Pending::wait`].
369    pub fn commit_then(mut self, on_complete: impl FnOnce(Result<()>) + Send + 'static) -> Pending {
370        self.close();
371        // Without a timeline fence, nothing orders other work after these uses.
372        let guards: Vec<Arc<Guard>> = match self.own {
373            Some(_) => self.guards.iter().map(|(g, _)| g.clone()).collect(),
374            None => Vec::new(),
375        };
376        let finish: Finish = Arc::new(Mutex::new(Some(Completion {
377            on_complete: Box::new(on_complete),
378            own: self.own.clone(),
379            kernels: std::mem::take(&mut self.kernels),
380            unfenced: self.own.is_none(),
381            guards: std::mem::take(&mut self.guards),
382            _retained: std::mem::take(&mut self.retained),
383            device: self.device.clone(),
384        })));
385        let shared = finish.clone();
386        let handler = RcBlock::new(move |raw: NonNull<ProtocolObject<dyn MTLCommandBuffer>>| {
387            // SAFETY: Metal passes the completed command buffer, valid for this call.
388            complete(&shared, unsafe { raw.as_ref() });
389        });
390        // SAFETY: the block is valid, and Metal copies it before this call returns. It is
391        // called at most once, on any thread: its captures are `Send` (the buffers are
392        // only retained or released, and the guards are atomics).
393        unsafe { self.raw.addCompletedHandler(RcBlock::as_ptr(&handler)) };
394        self.raw.commit();
395        for guard in guards {
396            guard.committed();
397        }
398        Pending {
399            raw: self.raw.clone(),
400            finish,
401        }
402    }
403
404    /// The buffers' uses in this command buffer are committed: their fences now order work
405    /// committed later (only with a timeline fence; otherwise they count once complete).
406    fn mark_committed(&self) {
407        if self.own.is_some() {
408            for (guard, _) in &self.guards {
409                guard.committed();
410            }
411        }
412    }
413
414    /// Whether a dispatch with `args` depends on dispatches since the last barrier:
415    /// read-after-write, write-after-write or write-after-read.
416    fn conflicts(&self, args: &[Arg<'_>]) -> bool {
417        args.iter().filter_map(Arg::buffer).any(|(b, _, write)| {
418            let id = b.id();
419            self.writes.contains(&id) || (write && self.reads.contains(&id))
420        })
421    }
422
423    fn encoder(&mut self) -> Result<&Retained<ProtocolObject<dyn MTLComputeCommandEncoder>>> {
424        if self.encoder.is_none() {
425            let encoder = self
426                .raw
427                .computeCommandEncoderWithDispatchType(MTLDispatchType::Concurrent)
428                .ok_or_else(|| GpuError::CommandBuffer("cannot create a compute encoder".into()))?;
429            if let Some((fence, true)) = &self.fence {
430                encoder.waitForFence(fence);
431            }
432            self.encoder = Some(encoder);
433        }
434        Ok(self.encoder.as_ref().expect("just created"))
435    }
436}
437
438impl Drop for CommandBuffer {
439    /// Abandoned work is never submitted; release the buffers' guards.
440    fn drop(&mut self) {
441        if let Some(encoder) = self.encoder.take() {
442            encoder.endEncoding();
443        }
444        for (guard, write) in self.guards.drain(..) {
445            guard.committed();
446            guard.release(write, None); // abandoned before its fences were recorded
447        }
448    }
449}
450
451fn size([width, height, depth]: [usize; 3]) -> MTLSize {
452    MTLSize {
453        width,
454        height,
455        depth,
456    }
457}
458
459fn validate(args: &[Arg<'_>]) -> Result<()> {
460    if args.len() > MAX_ARGS {
461        return Err(GpuError::InvalidArgument(format!(
462            "{} arguments; Metal allows {MAX_ARGS}",
463            args.len()
464        )));
465    }
466    for (index, arg) in args.iter().enumerate() {
467        match arg {
468            Arg::Bytes(bytes) if bytes.len() > MAX_INLINE_BYTES => {
469                return Err(GpuError::InvalidArgument(format!(
470                    "argument {index}: {} inline bytes; Metal allows {MAX_INLINE_BYTES}",
471                    bytes.len()
472                )));
473            }
474            Arg::Write(b, _) if b.is_mapped() => return Err(GpuError::ReadOnly),
475            Arg::Read(..) | Arg::Write(..)
476                if arg.buffer().is_some_and(|(b, offset, _)| offset > b.len()) =>
477            {
478                let (b, offset, _) = arg.buffer().expect("buffer argument");
479                return Err(GpuError::InvalidArgument(format!(
480                    "argument {index}: offset {offset} is past the end of a {}-byte buffer",
481                    b.len()
482                )));
483            }
484            _ => {}
485        }
486    }
487    Ok(())
488}
489
490/// What remains to do when a command buffer committed with
491/// [`CommandBuffer::commit_then`] completes.
492struct Completion {
493    on_complete: Box<dyn FnOnce(Result<()>) + Send>,
494    /// The command buffer's fence, recorded on the buffers it uses.
495    own: Option<Fence>,
496    /// The kernels dispatched, while profiling.
497    kernels: Vec<Arc<str>>,
498    guards: Vec<(Arc<Guard>, bool)>,
499    /// The command buffer had no timeline fence: its uses count as committed only now.
500    unfenced: bool,
501    _retained: Vec<Retained<ProtocolObject<dyn MTLBuffer>>>,
502    device: Device,
503}
504
505// SAFETY: the retained Metal buffers are never accessed, only released (Metal objects may be
506// released from any thread); the guards are atomics, the callback is `Send`, and `Device` is
507// shared across threads by design (`Device::shared`).
508unsafe impl Send for Completion {}
509
510type Finish = Arc<Mutex<Option<Completion>>>;
511
512/// Runs the completion once, whoever gets there first (the waiter or Metal's handler).
513fn complete(finish: &Finish, raw: &ProtocolObject<dyn MTLCommandBuffer>) {
514    let Some(c) = finish.lock().unwrap_or_else(PoisonError::into_inner).take() else {
515        return;
516    };
517    let result = status(raw);
518    super::profile::record(&c.kernels, raw.GPUEndTime() - raw.GPUStartTime());
519    for (guard, write) in c.guards {
520        if c.unfenced {
521            guard.committed();
522        }
523        guard.release(write, c.own.as_ref());
524    }
525    drop(c._retained);
526    c.device.flights().end();
527    (c.on_complete)(result);
528}
529
530/// A command buffer committed with [`CommandBuffer::commit_then`]. Dropping it does not
531/// wait; [`wait`](Self::wait) blocks until the GPU has finished and runs the completion on
532/// this thread if Metal's handler has not run it yet.
533pub struct Pending {
534    raw: Retained<ProtocolObject<dyn MTLCommandBuffer>>,
535    finish: Finish,
536}
537
538impl Pending {
539    /// Blocks until the GPU has finished; the completion has run when this returns.
540    pub fn wait(&self) {
541        self.raw.waitUntilCompleted();
542        complete(&self.finish, &self.raw);
543    }
544}
545
546impl std::fmt::Debug for Pending {
547    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
548        f.debug_struct("Pending").finish_non_exhaustive()
549    }
550}
551
552/// Submitted GPU work. [`wait`](Self::wait) for it to finish; dropping it also waits.
553pub struct Submitted {
554    device: Device,
555    raw: Retained<ProtocolObject<dyn MTLCommandBuffer>>,
556    /// The command buffer had no timeline fence: its uses count as committed only once
557    /// complete.
558    unfenced: bool,
559    /// The command buffer's fence, recorded on the buffers it uses.
560    own: Option<Fence>,
561    guards: Vec<(Arc<Guard>, bool)>,
562    _retained: Vec<Retained<ProtocolObject<dyn MTLBuffer>>>,
563    done: bool,
564}
565
566impl Submitted {
567    /// Blocks until the GPU has finished, then reports any GPU error.
568    pub fn wait(mut self) -> Result<()> {
569        self.finish()
570    }
571
572    fn finish(&mut self) -> Result<()> {
573        if self.done {
574            return Ok(());
575        }
576        self.raw.waitUntilCompleted();
577        self.done = true;
578        for (guard, write) in self.guards.drain(..) {
579            if self.unfenced {
580                guard.committed();
581            }
582            guard.release(write, self.own.as_ref());
583        }
584        self.device.flights().end();
585        status(&self.raw)
586    }
587}
588
589impl Drop for Submitted {
590    fn drop(&mut self) {
591        let _ = self.finish();
592    }
593}