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