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}