Skip to main content

gpu_async_reactor/backend/
cuda.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! CUDA: a [`MappedLine`] that streams store into with `cuStreamWriteValue64`, and [`CudaPort`],
4//! a [`CompletionPort`] over one stream (status by `cuStreamQuery`, the vendor wait by
5//! `cuStreamSynchronize`, the park on the line through the `wordwait` backend).
6//!
7//! Every driver entry point is resolved by name at run time from `libcuda.so.1` (the process's
8//! already-loaded driver when there is one), so the crate links nothing and a driver without
9//! `cuStreamWriteValue64_v2` fails only [`MappedLine::new`].
10
11use core::ffi::c_void;
12use core::ptr::NonNull;
13use core::time::Duration;
14
15use libloading::Library;
16
17use super::mapped::{Line, LineDriver, LineError};
18use crate::core::line::{CompletionLine, Slot};
19use crate::core::time::DurNs;
20use crate::core::wait::CompletionPort;
21
22/// A `CUstream` handle.
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub struct CuStream(u64);
25
26impl CuStream {
27    /// Wrap a raw `CUstream`.
28    ///
29    /// # Why
30    /// the typed stream is the only handle the reactor waits on, and the driver hands streams out as bare integers.
31    ///
32    /// # Safety
33    /// `raw` must be a live `CUstream` (or 0, the legacy default stream) of a live context for
34    /// every call made with the value: the driver dereferences it, and a bogus or destroyed handle
35    /// faults inside the driver (CWE-822).
36    ///
37    /// # No safe alternative
38    /// a handle is an integer the crate cannot validate; the typed stream is built only from it, so the constructor is unsafe.
39    #[must_use]
40    pub const unsafe fn from_raw(raw: u64) -> Self {
41        Self(raw)
42    }
43
44    /// The raw handle.
45    #[must_use]
46    pub const fn raw(self) -> u64 {
47        self.0
48    }
49}
50
51type AllocHost = unsafe extern "C" fn(*mut *mut c_void, usize) -> i32;
52type HostDevPtr = unsafe extern "C" fn(*mut u64, *mut c_void, u32) -> i32;
53type FreeHost = unsafe extern "C" fn(*mut c_void) -> i32;
54type CtxGet = unsafe extern "C" fn(*mut u64) -> i32;
55type CtxGetDevice = unsafe extern "C" fn(*mut i32) -> i32;
56type PrimaryRetain = unsafe extern "C" fn(*mut u64, i32) -> i32;
57type PrimaryRelease = unsafe extern "C" fn(i32) -> i32;
58type WriteValue64 = unsafe extern "C" fn(u64, u64, u64, u32) -> i32;
59type CtxPush = unsafe extern "C" fn(u64) -> i32;
60type CtxPop = unsafe extern "C" fn(*mut u64) -> i32;
61type CtxSync = unsafe extern "C" fn() -> i32;
62type StreamCtx = unsafe extern "C" fn(u64, *mut u64) -> i32;
63type StreamQuery = unsafe extern "C" fn(u64) -> i32;
64type StreamSync = unsafe extern "C" fn(u64) -> i32;
65
66/// `CUDA_ERROR_NOT_READY`.
67const NOT_READY: i32 = 600;
68
69/// The CUDA driver's table and the library handle that keeps it valid.
70pub(crate) struct CudaDriver {
71    alloc: AllocHost,
72    dev_ptr: HostDevPtr,
73    free: FreeHost,
74    ctx_get: CtxGet,
75    ctx_device: CtxGetDevice,
76    retain: PrimaryRetain,
77    release: PrimaryRelease,
78    write: WriteValue64,
79    push: CtxPush,
80    pop: CtxPop,
81    sync: CtxSync,
82    stream_ctx: StreamCtx,
83    query: StreamQuery,
84    stream_sync: StreamSync,
85    _lib: Library,
86}
87
88macro_rules! sym {
89    ($lib:ident, $ty:ty, $name:literal) => {
90        // WHY: resolves one driver entry point by name, to call it through a typed fn pointer.
91        // SAFETY: the type is the signature cuda.h (13.0) declares for this name: `CUresult` is a
92        // C enum read as `i32`, `CUdeviceptr` and the pointer-sized handles are `u64`; the name is
93        // NUL-free ASCII.
94        // NO SAFE ALTERNATIVE: a symbol lookup asserts a foreign signature the compiler cannot check; `libloading::Library::get` is unsafe.
95        *unsafe { $lib.get::<$ty>($name) }.ok()?
96    };
97}
98
99impl CudaDriver {
100    /// Every entry point, or `None` if the driver or any of them is missing.
101    pub(crate) fn resolve() -> Option<Self> {
102        // WHY: loads the vendor library at run time, so the crate links no vendor SDK and builds on a host without one.
103        // SAFETY: loading the CUDA driver runs its initialisers, which have no preconditions on
104        // this process; the handle is kept for as long as the table.
105        // NO SAFE ALTERNATIVE: dlopen runs foreign initialisers; `libloading::Library::new` is unsafe and there is no checked loader.
106        let lib = unsafe { Library::new("libcuda.so.1") }.ok()?;
107        Some(Self {
108            alloc: sym!(lib, AllocHost, b"cuMemAllocHost_v2"),
109            dev_ptr: sym!(lib, HostDevPtr, b"cuMemHostGetDevicePointer_v2"),
110            free: sym!(lib, FreeHost, b"cuMemFreeHost"),
111            ctx_get: sym!(lib, CtxGet, b"cuCtxGetCurrent"),
112            ctx_device: sym!(lib, CtxGetDevice, b"cuCtxGetDevice"),
113            retain: sym!(lib, PrimaryRetain, b"cuDevicePrimaryCtxRetain"),
114            release: sym!(lib, PrimaryRelease, b"cuDevicePrimaryCtxRelease_v2"),
115            write: sym!(lib, WriteValue64, b"cuStreamWriteValue64_v2"),
116            push: sym!(lib, CtxPush, b"cuCtxPushCurrent_v2"),
117            pop: sym!(lib, CtxPop, b"cuCtxPopCurrent_v2"),
118            sync: sym!(lib, CtxSync, b"cuCtxSynchronize"),
119            stream_ctx: sym!(lib, StreamCtx, b"cuStreamGetCtx"),
120            query: sym!(lib, StreamQuery, b"cuStreamQuery"),
121            stream_sync: sym!(lib, StreamSync, b"cuStreamSynchronize"),
122            _lib: lib,
123        })
124    }
125}
126
127fn ok(status: i32) -> Result<(), LineError> {
128    if status == 0 { Ok(()) } else { Err(LineError::Status(status)) }
129}
130
131/// A retained primary context: the handle and the device whose reference the line holds.
132#[derive(Clone, Copy, Debug, PartialEq, Eq)]
133pub(crate) struct PrimaryCtx {
134    ctx: u64,
135    device: i32,
136}
137
138impl CudaDriver {
139    /// Run `f` with `ctx` pushed current on the calling thread, then restore the thread's stack.
140    fn in_ctx<T>(&self, ctx: u64, f: impl FnOnce() -> T) -> Result<T, LineError> {
141        // WHY: calls a CUDA driver entry point resolved at run time (the line, stream or context operation).
142        // SAFETY: `ctx` is a primary context the calling line holds a reference to, so it is
143        // live; the push is undone by the pop, which writes only `popped`.
144        // NO SAFE ALTERNATIVE: the driver API is C: every entry point is an unsafe fn pointer, and the vendor crates' wrappers are unsafe too.
145        ok(unsafe { (self.push)(ctx) })?;
146        let r = f();
147        let mut popped = 0u64;
148        // WHY: calls a CUDA driver entry point resolved at run time (the line, stream or context operation).
149        // SAFETY: pops the context pushed above; `popped` is a live out-pointer.
150        // NO SAFE ALTERNATIVE: the driver API is C: every entry point is an unsafe fn pointer, and the vendor crates' wrappers are unsafe too.
151        ok(unsafe { (self.pop)(&raw mut popped) })?;
152        Ok(r)
153    }
154}
155
156impl LineDriver for CudaDriver {
157    type Stream = CuStream;
158    type Domain = PrimaryCtx;
159
160    fn retain_current(&self) -> Result<PrimaryCtx, LineError> {
161        let mut current = 0u64;
162        // WHY: calls a CUDA driver entry point resolved at run time (the line, stream or context operation).
163        // SAFETY: `current` is a live out-pointer, the only memory written.
164        // NO SAFE ALTERNATIVE: the driver API is C: every entry point is an unsafe fn pointer, and the vendor crates' wrappers are unsafe too.
165        ok(unsafe { (self.ctx_get)(&raw mut current) })?;
166        if current == 0 {
167            return Err(LineError::NoDomain);
168        }
169        let mut device = 0i32;
170        // WHY: calls a CUDA driver entry point resolved at run time (the line, stream or context operation).
171        // SAFETY: a context is current (checked above); `device` is a live out-pointer.
172        // NO SAFE ALTERNATIVE: the driver API is C: every entry point is an unsafe fn pointer, and the vendor crates' wrappers are unsafe too.
173        ok(unsafe { (self.ctx_device)(&raw mut device) })?;
174        let mut ctx = 0u64;
175        // WHY: calls a CUDA driver entry point resolved at run time (the line, stream or context operation).
176        // SAFETY: `device` is the current context's device; `ctx` is a live out-pointer. The
177        // reference taken here is dropped once, by `release` (or leaked with the page).
178        // NO SAFE ALTERNATIVE: the driver API is C: every entry point is an unsafe fn pointer, and the vendor crates' wrappers are unsafe too.
179        ok(unsafe { (self.retain)(&raw mut ctx, device) })?;
180        let primary = PrimaryCtx { ctx, device };
181        if ctx == 0 {
182            self.release(primary);
183            return Err(LineError::NoDomain);
184        }
185        Ok(primary)
186    }
187
188    fn release(&self, primary: PrimaryCtx) {
189        // WHY: calls a CUDA driver entry point resolved at run time (the line, stream or context operation).
190        // SAFETY: drops the one reference `retain_current` took on this device's primary context.
191        // NO SAFE ALTERNATIVE: the driver API is C: every entry point is an unsafe fn pointer, and the vendor crates' wrappers are unsafe too.
192        let _ = unsafe { (self.release)(primary.device) };
193    }
194
195    fn alloc_host(&self, primary: PrimaryCtx, bytes: usize) -> Result<NonNull<c_void>, LineError> {
196        let mut page: *mut c_void = core::ptr::null_mut();
197        // WHY: calls a CUDA driver entry point resolved at run time (the line, stream or context operation).
198        // SAFETY: `page` is a live out-pointer, the only memory the driver writes; the retained
199        // primary context is current for the call, so it owns the page.
200        // NO SAFE ALTERNATIVE: the driver API is C: every entry point is an unsafe fn pointer, and the vendor crates' wrappers are unsafe too.
201        ok(self.in_ctx(primary.ctx, || unsafe { (self.alloc)(&raw mut page, bytes) })?)?;
202        NonNull::new(page).ok_or(LineError::Unavailable)
203    }
204
205    fn device_address(&self, primary: PrimaryCtx, host: NonNull<c_void>) -> Result<u64, LineError> {
206        let mut device = 0u64;
207        // WHY: calls a CUDA driver entry point resolved at run time (the line, stream or context operation).
208        // SAFETY: `host` is a live page from `alloc_host` in this context; `device` is a live
209        // out-pointer.
210        // NO SAFE ALTERNATIVE: the driver API is C: every entry point is an unsafe fn pointer, and the vendor crates' wrappers are unsafe too.
211        ok(self.in_ctx(primary.ctx, || unsafe {
212            (self.dev_ptr)(&raw mut device, host.as_ptr(), 0)
213        })?)?;
214        Ok(device)
215    }
216
217    fn free_host(&self, primary: PrimaryCtx, host: NonNull<c_void>) {
218        // WHY: calls a CUDA driver entry point resolved at run time (the line, stream or context operation).
219        // SAFETY: `host` came from `alloc_host`, is freed once (the line's `Drop`) in its own
220        // context, which the line still retains, and every store queued into it has landed.
221        // NO SAFE ALTERNATIVE: the driver API is C: every entry point is an unsafe fn pointer, and the vendor crates' wrappers are unsafe too.
222        let _ = self.in_ctx(primary.ctx, || unsafe { (self.free)(host.as_ptr()) });
223    }
224
225    fn stream_in(&self, stream: CuStream, primary: PrimaryCtx) -> Result<bool, LineError> {
226        let mut owner = 0u64;
227        // WHY: calls a CUDA driver entry point resolved at run time (the line, stream or context operation).
228        // SAFETY: `stream` is live by `CuStream::from_raw`'s contract; `owner` is a live
229        // out-pointer, the only memory written.
230        // NO SAFE ALTERNATIVE: the driver API is C: every entry point is an unsafe fn pointer, and the vendor crates' wrappers are unsafe too.
231        ok(unsafe { (self.stream_ctx)(stream.0, &raw mut owner) })?;
232        Ok(owner == primary.ctx)
233    }
234
235    fn write_value(&self, stream: CuStream, addr: u64, value: u64) -> Result<(), LineError> {
236        // WHY: calls a CUDA driver entry point resolved at run time (the line, stream or context operation).
237        // SAFETY: `stream` is live (as above) and of the line's context (checked by the line);
238        // `addr` is a word of the line's own live device-mapped page, so `Drop` settles the
239        // context before the page is freed.
240        // NO SAFE ALTERNATIVE: the driver API is C: every entry point is an unsafe fn pointer, and the vendor crates' wrappers are unsafe too.
241        ok(unsafe { (self.write)(stream.0, addr, value, 0) })
242    }
243
244    fn settle(&self, primary: PrimaryCtx) -> bool {
245        // WHY: calls a CUDA driver entry point resolved at run time (the line, stream or context operation).
246        // SAFETY: waits on the line's retained (so live) primary context, pushed for the call.
247        // NO SAFE ALTERNATIVE: the driver API is C: every entry point is an unsafe fn pointer, and the vendor crates' wrappers are unsafe too.
248        matches!(self.in_ctx(primary.ctx, || unsafe { (self.sync)() }), Ok(0))
249    }
250}
251
252/// A page-locked, device-mapped [`CompletionLine`] that CUDA streams store into.
253pub struct MappedLine(Line<CudaDriver>);
254
255impl MappedLine {
256    /// A fresh line reading all 0, owned by the primary context of the device of the calling
257    /// thread's current context. The line holds its own reference to that primary context
258    /// (`cuDevicePrimaryCtxRetain`) until after its page is freed, so it stays valid whoever else
259    /// lets the context go. Only streams of that primary context may store into it; a stream of a
260    /// non-primary context (`cuCtxCreate`), which the line cannot keep alive, is refused with
261    /// [`LineError::ForeignStream`].
262    ///
263    /// # Errors
264    /// The driver's status, [`LineError::Unavailable`] when the driver or an entry point is
265    /// missing, [`LineError::NoDomain`] when no context is current.
266    pub fn new() -> Result<Self, LineError> {
267        Line::with_driver(CudaDriver::resolve().ok_or(LineError::Unavailable)?).map(Self)
268    }
269
270    /// The line, as the host reads it.
271    #[must_use]
272    pub fn line(&self) -> &CompletionLine {
273        self.0.line()
274    }
275
276    /// The device address of word 0; word `i` is at `+ 8 * i` (for a stream-side wait on it).
277    #[must_use]
278    pub fn device_address(&self) -> u64 {
279        self.0.device_address()
280    }
281
282    /// Queue a store of `value` into word `slot` on `stream`, behind the work queued there.
283    ///
284    /// # Errors
285    /// The driver's status, or [`LineError::ForeignStream`] for a stream of any context other
286    /// than the line's primary context.
287    pub fn enqueue_store(&self, stream: CuStream, slot: Slot, value: u64) -> Result<(), LineError> {
288        self.0.enqueue_store(stream, slot, value)
289    }
290
291    fn driver(&self) -> &CudaDriver {
292        self.0.driver()
293    }
294}
295
296/// A [`CompletionPort`] over one CUDA stream and a slot of a [`MappedLine`].
297pub struct CudaPort<'a> {
298    line: &'a MappedLine,
299    stream: CuStream,
300    slot: Slot,
301    seq: &'a mut u64,
302    wait: Option<super::wordwait::WordWait>,
303}
304
305impl<'a> CudaPort<'a> {
306    /// The port for `stream`, storing into `slot` of `line`; `seq` is that slot's last sequence
307    /// number (kept by the caller across waits). Without `wait` a park polls the word.
308    pub fn new(
309        line: &'a MappedLine,
310        stream: CuStream,
311        slot: Slot,
312        seq: &'a mut u64,
313        wait: Option<super::wordwait::WordWait>,
314    ) -> Self {
315        Self {
316            line,
317            stream,
318            slot,
319            seq,
320            wait,
321        }
322    }
323}
324
325impl CompletionPort for CudaPort<'_> {
326    type Error = crate::Error;
327
328    fn query(&mut self) -> crate::Result<bool> {
329        // WHY: calls a CUDA driver entry point resolved at run time (the line, stream or context operation).
330        // SAFETY: the stream is live by `CuStream::from_raw`'s contract.
331        // NO SAFE ALTERNATIVE: the driver API is C: every entry point is an unsafe fn pointer, and the vendor crates' wrappers are unsafe too.
332        match unsafe { (self.line.driver().query)(self.stream.0) } {
333            0 => Ok(true),
334            NOT_READY => Ok(false),
335            s => Err(crate::error!(Backend, "cuStreamQuery", s)),
336        }
337    }
338
339    async fn block(&mut self) -> crate::Result<()> {
340        // WHY: calls a CUDA driver entry point resolved at run time (the line, stream or context operation).
341        // SAFETY: as `query`.
342        // NO SAFE ALTERNATIVE: the driver API is C: every entry point is an unsafe fn pointer, and the vendor crates' wrappers are unsafe too.
343        match unsafe { (self.line.driver().stream_sync)(self.stream.0) } {
344            0 => Ok(()),
345            s => Err(crate::error!(Backend, "cuStreamSynchronize", s)),
346        }
347    }
348
349    async fn poll(&mut self) -> crate::Result<()> {
350        while !self.query()? {
351            core::hint::spin_loop();
352        }
353        Ok(())
354    }
355
356    fn arm(&mut self) -> crate::Result<()> {
357        *self.seq = self.seq.wrapping_add(1);
358        Ok(self.line.enqueue_store(self.stream, self.slot, *self.seq)?)
359    }
360
361    async fn park(&mut self, bound: DurNs) -> crate::Result<bool> {
362        let (slot, seq) = (self.slot, *self.seq);
363        let ready = |l: &CompletionLine| crate::core::line::reached(l, slot, seq);
364        Ok(match &self.wait {
365            Some(w) => w.park(self.line.line(), ready, Duration::from_nanos(bound.0)),
366            None => ready(self.line.line()),
367        })
368    }
369}