Skip to main content

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}