oxicuda_driver/context.rs
1//! CUDA context management with RAII semantics.
2//!
3//! A CUDA **context** is the primary interface through which a CPU thread
4//! interacts with a GPU. It owns driver state such as loaded modules, allocated
5//! memory, and streams. This module provides the [`Context`] type, an RAII
6//! wrapper around `CUcontext` that automatically calls `cuCtxDestroy` on drop.
7//!
8//! # Thread safety
9//!
10//! The CUDA Driver API is thread-safe, and (since CUDA 4.0) a context may be
11//! current on multiple threads simultaneously; "current-ness" is a per-thread
12//! property set with `cuCtxSetCurrent`. Accordingly [`Context`] is both
13//! [`Send`] and [`Sync`] (auto-derived from its fields), so it can be wrapped
14//! in an [`Arc<Context>`](std::sync::Arc) and shared across threads. Each
15//! thread that issues driver calls must first make the context current on
16//! itself via [`set_current`](Context::set_current).
17//!
18//! # Examples
19//!
20//! ```no_run
21//! use oxicuda_driver::context::Context;
22//! use oxicuda_driver::device::Device;
23//!
24//! oxicuda_driver::init()?;
25//! let device = Device::get(0)?;
26//! let ctx = Context::new(&device)?;
27//! ctx.set_current()?;
28//! // ... launch kernels, allocate memory ...
29//! ctx.synchronize()?;
30//! # Ok::<(), oxicuda_driver::error::CudaError>(())
31//! ```
32
33use std::collections::HashMap;
34use std::sync::atomic::{AtomicU64, Ordering};
35use std::sync::{Mutex, MutexGuard, OnceLock};
36
37use crate::device::Device;
38use crate::error::CudaResult;
39use crate::ffi::CUcontext;
40use crate::loader::try_driver;
41
42// ---------------------------------------------------------------------------
43// Live-context registry
44// ---------------------------------------------------------------------------
45//
46// A `Context` owns a `CUcontext` created with `cuCtxCreate` and destroys it via
47// `cuCtxDestroy` on drop. Child resources (`Event`, `Module`, `GraphExec`)
48// carry raw driver handles whose validity is tied to the context that was
49// current when they were created. Safe code can drop the `Context` before the
50// child, after which the child's own `Drop` would call the driver destroy on a
51// now-stale handle — a use-after-free.
52//
53// To make this sound without changing any public signature, every `Context`
54// registers its raw address together with a unique generation here. A child
55// captures the owning `(addr, generation)` at construction and, in its `Drop`,
56// only calls the driver destroy while that owner is still registered with the
57// same generation. The shared `Mutex` serialises a child `Drop` against
58// `Context::drop`, and the generation defeats `CUcontext` address recycling.
59
60/// Monotonic source of per-context generations (never zero).
61static CTX_GENERATION: AtomicU64 = AtomicU64::new(1);
62
63/// Process-wide map of live `CUcontext` address to its generation.
64fn live_ctxs() -> &'static Mutex<HashMap<usize, u64>> {
65 static LIVE: OnceLock<Mutex<HashMap<usize, u64>>> = OnceLock::new();
66 LIVE.get_or_init(|| Mutex::new(HashMap::new()))
67}
68
69/// Lock the registry, recovering the guard if a previous holder panicked (the
70/// map is plain data, so a poisoned lock is safe to keep using).
71pub(crate) fn lock_live_ctxs() -> MutexGuard<'static, HashMap<usize, u64>> {
72 live_ctxs().lock().unwrap_or_else(|e| e.into_inner())
73}
74
75/// Identity of the CUDA context owning a driver resource, captured at the
76/// resource's construction from the thread's current context.
77pub(crate) type CtxOwner = Option<(usize, u64)>;
78
79/// Capture the current thread's context as a resource owner, if that context
80/// was created through this crate (and is therefore tracked). Returns `None`
81/// when no context is current or the current context is untracked — in which
82/// case the resource keeps the pre-existing "always destroy on drop" behaviour.
83pub(crate) fn current_ctx_owner() -> CtxOwner {
84 let driver = try_driver().ok()?;
85 let mut ctx = CUcontext::default();
86 // SAFETY: `cu_ctx_get_current` was resolved from the driver; `ctx` is a
87 // valid out-pointer.
88 let rc = unsafe { (driver.cu_ctx_get_current)(&mut ctx) };
89 if rc != 0 || ctx.is_null() {
90 return None;
91 }
92 let addr = ctx.0 as usize;
93 let map = lock_live_ctxs();
94 map.get(&addr).map(|&generation| (addr, generation))
95}
96
97/// Whether a resource owned by `owner` may still safely call its driver
98/// destroy: the owner is either untracked (`None`) or its context is still live
99/// with the same generation. Callers must hold the registry lock across the
100/// subsequent destroy so the check stays race-free.
101pub(crate) fn owner_is_live(map: &HashMap<usize, u64>, owner: CtxOwner) -> bool {
102 match owner {
103 None => true,
104 Some((addr, generation)) => map.get(&addr) == Some(&generation),
105 }
106}
107
108// ---------------------------------------------------------------------------
109// Scheduling flags
110// ---------------------------------------------------------------------------
111
112/// Context scheduling flags passed to [`Context::with_flags`].
113///
114/// These control how the CPU thread behaves while waiting for GPU operations.
115pub mod flags {
116 /// Let the driver choose the optimal scheduling policy.
117 pub const SCHED_AUTO: u32 = 0x00;
118
119 /// Actively spin (busy-wait) while waiting for GPU results. Lowest latency
120 /// but consumes a full CPU core.
121 pub const SCHED_SPIN: u32 = 0x01;
122
123 /// Yield the CPU time-slice to other threads while waiting. Good for
124 /// multi-threaded applications.
125 pub const SCHED_YIELD: u32 = 0x02;
126
127 /// Block the calling thread on a synchronisation primitive. Lowest CPU
128 /// usage but slightly higher latency.
129 pub const SCHED_BLOCKING_SYNC: u32 = 0x04;
130
131 /// Enable mapped pinned allocations in this context.
132 pub const MAP_HOST: u32 = 0x08;
133
134 /// Keep local memory allocation after launch (deprecated flag kept for
135 /// completeness).
136 pub const LMEM_RESIZE_TO_MAX: u32 = 0x10;
137}
138
139// ---------------------------------------------------------------------------
140// Scoped-context restore guard
141// ---------------------------------------------------------------------------
142
143/// RAII guard that restores a previously-current context on drop.
144///
145/// Used by [`Context::scoped`] so that the previous context is re-installed on
146/// the calling thread at scope exit regardless of how the scope is left —
147/// normal return, early error, or a panic unwinding through the closure.
148struct CurrentCtxRestore {
149 /// The context to restore (may be a null handle to detach all contexts).
150 prev: CUcontext,
151}
152
153impl Drop for CurrentCtxRestore {
154 fn drop(&mut self) {
155 if let Ok(driver) = try_driver() {
156 if let Err(e) = crate::error::check(unsafe { (driver.cu_ctx_set_current)(self.prev) }) {
157 tracing::warn!("failed to restore previous context: {e}");
158 }
159 }
160 }
161}
162
163// ---------------------------------------------------------------------------
164// Context
165// ---------------------------------------------------------------------------
166
167/// RAII wrapper for a CUDA context.
168///
169/// A context is created on a specific [`Device`] and becomes the active
170/// context for the calling thread. When the `Context` is dropped,
171/// `cuCtxDestroy_v2` is called automatically.
172///
173/// # Examples
174///
175/// ```no_run
176/// use oxicuda_driver::context::Context;
177/// use oxicuda_driver::device::Device;
178///
179/// oxicuda_driver::init()?;
180/// let dev = Device::get(0)?;
181/// let ctx = Context::new(&dev)?;
182/// println!("Context on device {}", ctx.device().ordinal());
183/// ctx.synchronize()?;
184/// // ctx is destroyed when it goes out of scope
185/// # Ok::<(), oxicuda_driver::error::CudaError>(())
186/// ```
187pub struct Context {
188 /// The raw CUDA context handle.
189 raw: CUcontext,
190 /// The device this context was created on.
191 device: Device,
192 /// Generation under which this context is registered in the live-context
193 /// registry; used to tether child resources (see the module-level
194 /// registry documentation). Unused (`0`) for borrowed, non-owning wrappers.
195 generation: u64,
196 /// Whether this wrapper owns the underlying `CUcontext`. Owning contexts
197 /// (created via [`Context::new`] / [`Context::with_flags`]) call
198 /// `cuCtxDestroy` on drop and are tracked in the live-context registry;
199 /// borrowed wrappers created via [`Context::from_raw_borrowed`] do neither.
200 owned: bool,
201}
202
203impl Context {
204 // -- Construction --------------------------------------------------------
205
206 /// Create a new context on the given device with default flags
207 /// ([`flags::SCHED_AUTO`]).
208 ///
209 /// The new context is automatically pushed onto the calling thread's
210 /// context stack and becomes the current context.
211 ///
212 /// # Errors
213 ///
214 /// Returns an error if the driver cannot create the context (e.g., device
215 /// is invalid, out of resources).
216 pub fn new(device: &Device) -> CudaResult<Self> {
217 Self::with_flags(device, flags::SCHED_AUTO)
218 }
219
220 /// Create a new context on the given device with specific scheduling flags.
221 ///
222 /// See the [`flags`] module for available values. Multiple flags can be
223 /// combined with bitwise OR.
224 ///
225 /// # Errors
226 ///
227 /// Returns an error if the driver cannot create the context.
228 ///
229 /// # Examples
230 ///
231 /// ```no_run
232 /// use oxicuda_driver::context::{Context, flags};
233 /// use oxicuda_driver::device::Device;
234 ///
235 /// oxicuda_driver::init()?;
236 /// let dev = Device::get(0)?;
237 /// let ctx = Context::with_flags(&dev, flags::SCHED_BLOCKING_SYNC)?;
238 /// # Ok::<(), oxicuda_driver::error::CudaError>(())
239 /// ```
240 pub fn with_flags(device: &Device, flags: u32) -> CudaResult<Self> {
241 let driver = try_driver()?;
242 let mut raw = CUcontext::default();
243 crate::error::check(unsafe { (driver.cu_ctx_create_v2)(&mut raw, flags, device.raw()) })?;
244 // Register this context so child resources can tether their drops to it.
245 let generation = CTX_GENERATION.fetch_add(1, Ordering::Relaxed);
246 lock_live_ctxs().insert(raw.0 as usize, generation);
247 Ok(Self {
248 raw,
249 device: *device,
250 generation,
251 owned: true,
252 })
253 }
254
255 /// Wraps an already-live `CUcontext` in a **non-owning** [`Context`].
256 ///
257 /// Unlike [`Context::new`] / [`Context::with_flags`], the returned wrapper
258 /// does *not* own the underlying context: on drop it never calls
259 /// `cuCtxDestroy`, and it is not entered into the live-context generation
260 /// registry. Its sole purpose is to hand an externally-owned context (for
261 /// example a device **primary context** retained via
262 /// [`PrimaryContext`](crate::primary_context::PrimaryContext)) to APIs that
263 /// require an `Arc<Context>` lifetime token, without transferring ownership
264 /// of the context's lifetime.
265 ///
266 /// # Safety
267 ///
268 /// The caller must guarantee that `raw` refers to a valid, live CUDA
269 /// context and that it remains live for at least as long as this `Context`
270 /// wrapper — and any resource (stream, module, …) created from it — is in
271 /// use. Because the wrapper is untracked, resources created while it is
272 /// current fall back to the driver's default "destroy on drop" behaviour,
273 /// so the borrowed context must outlive them.
274 #[must_use]
275 pub unsafe fn from_raw_borrowed(raw: CUcontext, device: Device) -> Self {
276 Self {
277 raw,
278 device,
279 generation: 0,
280 owned: false,
281 }
282 }
283
284 // -- Current context management -----------------------------------------
285
286 /// Set this context as the current context for the calling thread.
287 ///
288 /// Any previous context on this thread is detached (but not destroyed).
289 ///
290 /// # Errors
291 ///
292 /// Returns an error if the driver call fails.
293 pub fn set_current(&self) -> CudaResult<()> {
294 let driver = try_driver()?;
295 crate::error::check(unsafe { (driver.cu_ctx_set_current)(self.raw) })
296 }
297
298 /// Get the raw handle of the current context for the calling thread.
299 ///
300 /// Returns `None` if no context is bound to the current thread.
301 ///
302 /// # Errors
303 ///
304 /// Returns an error if the driver call fails.
305 pub fn current_raw() -> CudaResult<Option<CUcontext>> {
306 let driver = try_driver()?;
307 let mut ctx = CUcontext::default();
308 crate::error::check(unsafe { (driver.cu_ctx_get_current)(&mut ctx) })?;
309 if ctx.is_null() {
310 Ok(None)
311 } else {
312 Ok(Some(ctx))
313 }
314 }
315
316 /// Pop the context currently on top of the calling thread's context stack,
317 /// leaving it "floating" (owned but not bound to any thread).
318 ///
319 /// This is the standard driver-API idiom for undoing the implicit
320 /// `cuCtxCreate` push: a freshly created context is both pushed and made
321 /// current, so callers that want to manage current-ness explicitly pop it
322 /// straight away. The popped handle is discarded because the caller already
323 /// owns it via the [`Context`] wrapper.
324 ///
325 /// # Errors
326 ///
327 /// Returns an error if the driver call fails (e.g. the stack is empty).
328 pub(crate) fn pop_current() -> CudaResult<()> {
329 let driver = try_driver()?;
330 let mut popped = CUcontext::default();
331 crate::error::check(unsafe { (driver.cu_ctx_pop_current_v2)(&mut popped) })
332 }
333
334 // -- Synchronisation ----------------------------------------------------
335
336 /// Block until all pending GPU operations in this context have completed.
337 ///
338 /// This sets the context as current before synchronising to ensure the
339 /// correct context is targeted.
340 ///
341 /// # Errors
342 ///
343 /// Returns an error if any GPU operation failed or the driver call fails.
344 pub fn synchronize(&self) -> CudaResult<()> {
345 self.set_current()?;
346 let driver = try_driver()?;
347 crate::error::check(unsafe { (driver.cu_ctx_synchronize)() })
348 }
349
350 // -- Scoped execution ---------------------------------------------------
351
352 /// Execute a closure with this context set as current, then restore the
353 /// previous context.
354 ///
355 /// This is useful when temporarily switching contexts. The previous
356 /// context (if any) is restored even if the closure returns an error or
357 /// panics — restoration is performed by an RAII guard at scope exit.
358 ///
359 /// # Errors
360 ///
361 /// Propagates any error from the closure. Context-restoration errors are
362 /// logged but do not override the closure result.
363 ///
364 /// # Examples
365 ///
366 /// ```no_run
367 /// use oxicuda_driver::context::Context;
368 /// use oxicuda_driver::device::Device;
369 ///
370 /// oxicuda_driver::init()?;
371 /// let dev = Device::get(0)?;
372 /// let ctx = Context::new(&dev)?;
373 /// let result = ctx.scoped(|| {
374 /// // ctx is current here
375 /// Ok(42)
376 /// })?;
377 /// assert_eq!(result, 42);
378 /// # Ok::<(), oxicuda_driver::error::CudaError>(())
379 /// ```
380 pub fn scoped<F, R>(&self, f: F) -> CudaResult<R>
381 where
382 F: FnOnce() -> CudaResult<R>,
383 {
384 // Save the currently active context (may be None).
385 let prev = Self::current_raw()?;
386
387 // Activate this context.
388 self.set_current()?;
389
390 // Install an RAII guard that restores the previous context at scope
391 // exit — on the normal path, on an error return from the closure, and
392 // even if the closure panics (which would otherwise leave *this*
393 // context current on the thread). A null CUcontext detaches any
394 // context from the current thread, which is the correct behaviour when
395 // there was no previous context.
396 let _restore = CurrentCtxRestore {
397 prev: prev.unwrap_or_default(),
398 };
399
400 // Run the user closure. `_restore` fires when it goes out of scope.
401 f()
402 }
403
404 // -- Accessors ----------------------------------------------------------
405
406 /// Get a reference to the [`Device`] this context was created on.
407 #[inline]
408 pub fn device(&self) -> &Device {
409 &self.device
410 }
411
412 /// Get the raw `CUcontext` handle for use with FFI calls.
413 #[inline]
414 pub fn raw(&self) -> CUcontext {
415 self.raw
416 }
417
418 /// Returns `true` if this context is the current context on the calling
419 /// thread.
420 ///
421 /// # Errors
422 ///
423 /// Returns an error if the driver call fails.
424 pub fn is_current(&self) -> CudaResult<bool> {
425 match Self::current_raw()? {
426 Some(ctx) => Ok(ctx == self.raw),
427 None => Ok(false),
428 }
429 }
430}
431
432// ---------------------------------------------------------------------------
433// Drop
434// ---------------------------------------------------------------------------
435
436impl Drop for Context {
437 /// Destroy the CUDA context.
438 ///
439 /// Errors during destruction are logged via `tracing::warn` but never
440 /// propagated (destructors must not panic).
441 fn drop(&mut self) {
442 // Borrowed, non-owning wrappers (see `from_raw_borrowed`) neither own
443 // the context nor registered a generation, so there is nothing to
444 // destroy or unregister — the real owner handles teardown.
445 if !self.owned {
446 return;
447 }
448 // Hold the registry lock across the destroy so any concurrent
449 // child-resource `Drop` is serialised with it (closing the
450 // use-after-free race). Removing our entry first means children created
451 // under this context will observe it as gone and skip their own
452 // destroy (which `cuCtxDestroy` has already performed for them).
453 let mut map = lock_live_ctxs();
454 let addr = self.raw.0 as usize;
455 if map.get(&addr) == Some(&self.generation) {
456 map.remove(&addr);
457 }
458 if let Ok(driver) = try_driver() {
459 let result = unsafe { (driver.cu_ctx_destroy_v2)(self.raw) };
460 if result != 0 {
461 tracing::warn!(
462 "cuCtxDestroy_v2 failed with error code {result} during Context drop \
463 (device ordinal {})",
464 self.device.ordinal()
465 );
466 }
467 }
468 }
469}
470
471// ---------------------------------------------------------------------------
472// Trait impls
473// ---------------------------------------------------------------------------
474
475// `Context` is `Send + Sync` by auto-derivation: its fields are a `CUcontext`
476// handle (a plain driver-side identifier) and a `Device` (two integers). The
477// CUDA Driver API is thread-safe, so no manual `unsafe impl` is needed — and
478// relying on the auto-trait keeps the compiler's guard if a non-thread-safe
479// field is ever added.
480
481impl std::fmt::Debug for Context {
482 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
483 f.debug_struct("Context")
484 .field("raw", &self.raw)
485 .field("device", &self.device)
486 .finish()
487 }
488}