Skip to main content

oxicuda_driver/
lib.rs

1//! # OxiCUDA Driver
2//!
3//! **Dynamic, safe Rust bindings for the NVIDIA CUDA Driver API.**
4//!
5//! `oxicuda-driver` provides a zero-SDK-dependency wrapper around the CUDA
6//! Driver API.  Unlike traditional CUDA crate approaches that require the
7//! CUDA Toolkit (or at least its headers and link stubs) to be present at
8//! **build time**, this crate loads the driver shared library entirely at
9//! **runtime** via [`libloading`](https://crates.io/crates/libloading).
10//!
11//! ## Zero build-time dependency
12//!
13//! No `cuda.h`, no `libcuda.so` symlink, no `nvcc` — the crate compiles on
14//! any Rust toolchain.  The actual GPU driver is discovered and loaded the
15//! first time you call [`try_driver()`] or [`init()`].
16//!
17//! ## Runtime library loading
18//!
19//! | Platform | Library searched             |
20//! |----------|-----------------------------|
21//! | Linux    | `libcuda.so`, `libcuda.so.1` |
22//! | Windows  | `nvcuda.dll`                 |
23//! | macOS    | *(returns `UnsupportedPlatform` — NVIDIA dropped macOS support)* |
24//!
25//! ## Key types
26//!
27//! | Type          | Description                                    |
28//! |---------------|------------------------------------------------|
29//! | [`Device`]    | A CUDA-capable GPU discovered on the system    |
30//! | [`Context`]   | Owns a CUDA context bound to a device          |
31//! | [`Stream`]    | Asynchronous command queue within a context     |
32//! | [`Event`]     | Timing / synchronisation marker on a stream    |
33//! | [`Module`]    | Loaded PTX or cubin containing kernel code     |
34//! | [`Function`]  | A single kernel entry point inside a module    |
35//! | [`CudaError`] | Strongly-typed driver error code               |
36//!
37//! ## Quick start
38//!
39//! ```rust,no_run
40//! use oxicuda_driver::prelude::*;
41//!
42//! // Initialise the CUDA driver (loads libcuda at runtime).
43//! init()?;
44//!
45//! // Pick the best available GPU and create a context.
46//! let dev = Device::get(0)?;
47//! let _ctx = Context::new(&dev)?;
48//!
49//! // Load a PTX module and look up a kernel.
50//! let module = Module::from_ptx("ptx_source")?;
51//! let kernel = module.get_function("vector_add")?;
52//! # Ok::<(), oxicuda_driver::CudaError>(())
53//! ```
54
55#![warn(missing_docs)]
56#![warn(clippy::all)]
57#![allow(clippy::module_name_repetitions)]
58#![allow(clippy::missing_safety_doc)]
59#![allow(clippy::too_many_arguments)]
60#![allow(clippy::macro_metavars_in_unsafe)]
61
62// ---------------------------------------------------------------------------
63// Module declarations
64// ---------------------------------------------------------------------------
65
66pub mod context;
67pub mod context_config;
68pub mod cooperative_launch;
69pub mod cupti_stubs;
70pub mod debug;
71pub mod device;
72pub mod error;
73pub mod event;
74pub mod fabric_handle;
75pub mod ffi;
76pub mod function_attr;
77pub mod graph;
78pub mod link;
79pub mod loader;
80pub mod memory_info;
81pub mod module;
82pub mod multi_gpu;
83pub mod nvlink_topology;
84pub mod occupancy;
85pub mod occupancy_ext;
86pub mod occupancy_register_count;
87pub mod primary_context;
88pub mod profiler;
89pub mod stream;
90pub mod stream_ordered_alloc;
91pub mod stream_ordered_model;
92pub mod tma;
93
94// ---------------------------------------------------------------------------
95// Re-exports — error handling
96// ---------------------------------------------------------------------------
97
98pub use error::{CudaError, CudaResult, DriverLoadError, check};
99
100// ---------------------------------------------------------------------------
101// Re-exports — FFI types and constants
102// ---------------------------------------------------------------------------
103
104pub use ffi::{
105    CU_TRSF_DISABLE_TRILINEAR_OPTIMIZATION, CU_TRSF_NORMALIZED_COORDINATES,
106    CU_TRSF_READ_AS_INTEGER, CU_TRSF_SRGB, CUDA_ARRAY_DESCRIPTOR, CUDA_ARRAY3D_CUBEMAP,
107    CUDA_ARRAY3D_DESCRIPTOR, CUDA_ARRAY3D_LAYERED, CUDA_ARRAY3D_SURFACE_LDST,
108    CUDA_ARRAY3D_TEXTURE_GATHER, CUDA_MEMCPY2D, CUDA_RESOURCE_DESC, CUDA_RESOURCE_VIEW_DESC,
109    CUDA_TEXTURE_DESC, CUaddress_mode, CUarray, CUarray_format, CUcontext, CUdevice,
110    CUdevice_attribute, CUdeviceptr, CUevent, CUfilter_mode, CUfunction, CUfunction_attribute,
111    CUjit_option, CUjitInputType, CUkernel, CUlibrary, CUlimit, CUlinkState, CUmemAccessDesc,
112    CUmemAccessFlags, CUmemAllocationHandleType, CUmemAllocationProp, CUmemAllocationType,
113    CUmemGenericAllocationHandle, CUmemLocation, CUmemLocationType, CUmemPoolProps, CUmemoryPool,
114    CUmemorytype, CUmipmappedArray, CUmodule, CUmulticastObject, CUpointer_attribute,
115    CUresourceViewFormat, CUresourcetype, CUstream, CUsurfObject, CUsurfref, CUtexObject, CUtexref,
116    CuLaunchAttribute, CuLaunchAttributeClusterDim, CuLaunchAttributeId, CuLaunchAttributeValue,
117    CuLaunchConfig, CudaResourceDescArray, CudaResourceDescLinear, CudaResourceDescMipmap,
118    CudaResourceDescPitch2d, CudaResourceDescRes,
119};
120
121// ---------------------------------------------------------------------------
122// Re-exports — high-level safe wrappers
123// ---------------------------------------------------------------------------
124
125pub use context::Context;
126pub use context_config::{CacheConfig, SharedMemConfig};
127pub use cooperative_launch::{
128    CooperativeLaunchConfig, CooperativeLaunchSupport, DeviceLaunchConfig,
129    MultiDeviceCooperativeLaunchConfig, cooperative_launch, cooperative_launch_multi_device,
130};
131pub use cupti_stubs::{
132    ActivityRecord, ActivitySession, CuptiActivityError, CuptiActivityKind, cupti_available,
133};
134pub use debug::{DebugLevel, DebugSession, KernelDebugger, MemoryChecker, NanInfChecker};
135pub use device::{Device, DeviceInfo, best_device, can_access_peer, driver_version, list_devices};
136pub use event::Event;
137pub use fabric_handle::{
138    FABRIC_HANDLE_BYTES, FabricAccess, FabricAllocationProps, FabricHandle, FabricImport,
139    FabricImporter, FabricMemory,
140};
141pub use graph::{Graph, GraphExec, GraphNode, MemcpyDirection, StreamCapture};
142pub use link::{
143    FallbackStrategy, LinkInputType, LinkedModule, Linker, LinkerOptions, OptimizationLevel,
144};
145pub use loader::try_driver;
146pub use module::{Function, JitDiagnostic, JitLog, JitOptions, JitSeverity, Module};
147pub use multi_gpu::DevicePool;
148pub use nvlink_topology::{GpuTopology, NvLinkVersion, TopologyTree, TopologyType};
149pub use occupancy_register_count::{OccupancyFromPtx, PtxKernel, PtxRegisterUsage};
150pub use primary_context::PrimaryContext;
151pub use profiler::ProfilerGuard;
152pub use stream::Stream;
153pub use stream_ordered_alloc::{
154    StreamAllocation, StreamMemoryPool, StreamOrderedAllocConfig, stream_alloc, stream_free,
155};
156pub use stream_ordered_model::StreamOrderId;
157
158// ---------------------------------------------------------------------------
159// Driver initialisation
160// ---------------------------------------------------------------------------
161
162/// Initialise the CUDA driver API.
163///
164/// This must be called before any other driver function.  It is safe to call
165/// multiple times; subsequent calls are no-ops inside the driver itself.
166///
167/// Internally this loads the shared library (if not already cached) and
168/// invokes `cuInit(0)`.
169///
170/// # Errors
171///
172/// Returns [`CudaError::NotInitialized`] if the CUDA driver library cannot be
173/// loaded, or another [`CudaError`] variant if `cuInit` reports a failure.
174pub fn init() -> CudaResult<()> {
175    let driver = loader::try_driver()?;
176    error::check(unsafe { (driver.cu_init)(0) })
177}
178
179// ---------------------------------------------------------------------------
180// Prelude — convenient glob import
181// ---------------------------------------------------------------------------
182
183/// Convenient glob import for common OxiCUDA Driver types.
184///
185/// ```rust
186/// use oxicuda_driver::prelude::*;
187/// ```
188pub mod prelude {
189    pub use crate::{
190        ActivitySession, CacheConfig, Context, CooperativeLaunchConfig, CooperativeLaunchSupport,
191        CudaError, CudaResult, DebugLevel, DebugSession, Device, DeviceLaunchConfig, DevicePool,
192        Event, FabricHandle, FabricImporter, FabricMemory, FallbackStrategy, Function, GpuTopology,
193        Graph, GraphExec, GraphNode, KernelDebugger, LinkInputType, LinkedModule, Linker,
194        LinkerOptions, MemcpyDirection, Module, MultiDeviceCooperativeLaunchConfig, NvLinkVersion,
195        OccupancyFromPtx, OptimizationLevel, PrimaryContext, ProfilerGuard, SharedMemConfig,
196        Stream, StreamAllocation, StreamCapture, StreamMemoryPool, StreamOrderedAllocConfig,
197        TopologyTree, TopologyType, can_access_peer, cooperative_launch,
198        cooperative_launch_multi_device, cupti_available, driver_version, init, stream_alloc,
199        stream_free, try_driver,
200    };
201}
202
203// ---------------------------------------------------------------------------
204// Compile-time feature flags
205// ---------------------------------------------------------------------------
206
207/// Compile-time feature availability.
208pub mod features {
209    /// Whether GPU tests are enabled (`--features gpu-tests`).
210    pub const HAS_GPU_TESTS: bool = cfg!(feature = "gpu-tests");
211}
212
213// ---------------------------------------------------------------------------
214// CPU-only tests for driver infrastructure
215// ---------------------------------------------------------------------------
216
217#[cfg(test)]
218mod driver_infra_tests {
219    // -----------------------------------------------------------------------
220    // Task 2 — Multi-threaded context migration (F3)
221    //
222    // Verifies the thread-safety of the context-stack data structure model
223    // using pure Rust primitives.  No GPU is required.
224    // -----------------------------------------------------------------------
225
226    /// Simulate 4 threads each pushing and popping a "context ID" to/from a
227    /// thread-local stack, then verifying all results are collected correctly.
228    ///
229    /// This exercises the logical structure of context push/pop across threads
230    /// (corresponding to `cuCtxPushCurrent` / `cuCtxPopCurrent`) without
231    /// needing a real CUDA driver.
232    #[test]
233    fn context_push_pop_thread_safety() {
234        use std::sync::{Arc, Mutex};
235        use std::thread;
236
237        let results: Arc<Mutex<Vec<(u32, u32)>>> = Arc::new(Mutex::new(vec![]));
238        let mut handles = vec![];
239
240        for thread_id in 0..4u32 {
241            let results_clone = Arc::clone(&results);
242            let handle = thread::spawn(move || {
243                // Each thread simulates pushing two context IDs onto its
244                // private stack and then reading the top (most-recently-pushed)
245                // context.
246                let ctx_id = thread_id * 100;
247                let stack: Vec<u32> = vec![ctx_id, ctx_id + 1];
248                // Pop semantics: the top of the stack is the last element.
249                let top = stack.last().copied().unwrap_or(0);
250                let mut r = results_clone.lock().expect("results lock failed");
251                r.push((thread_id, top));
252            });
253            handles.push(handle);
254        }
255
256        for h in handles {
257            h.join().expect("thread panicked");
258        }
259
260        let results = results.lock().expect("final lock failed");
261        assert_eq!(results.len(), 4, "all 4 threads must contribute a result");
262
263        // Every thread should have seen `ctx_id + 1` as the top of its stack.
264        for &(thread_id, top) in results.iter() {
265            let expected_top = thread_id * 100 + 1;
266            assert_eq!(
267                top, expected_top,
268                "thread {thread_id}: expected top {expected_top}, got {top}"
269            );
270        }
271    }
272
273    // -----------------------------------------------------------------------
274    // Task 3 — Scope-exit / Drop resource release under OOM (F10)
275    //
276    // Verifies that Drop impls run correctly even when further allocations
277    // fail (simulated OOM), and that Rust's LIFO drop order is preserved.
278    // -----------------------------------------------------------------------
279
280    /// `Drop` is invoked for every resource that was successfully constructed,
281    /// even when a subsequent allocation would fail (simulated OOM).
282    #[test]
283    fn drop_counter_tracks_resource_release() {
284        use std::sync::Arc;
285        use std::sync::atomic::{AtomicUsize, Ordering};
286
287        struct FakeResource {
288            dropped: Arc<AtomicUsize>,
289        }
290
291        impl Drop for FakeResource {
292            fn drop(&mut self) {
293                self.dropped.fetch_add(1, Ordering::SeqCst);
294            }
295        }
296
297        let counter = Arc::new(AtomicUsize::new(0));
298
299        {
300            let _r1 = FakeResource {
301                dropped: Arc::clone(&counter),
302            };
303            let _r2 = FakeResource {
304                dropped: Arc::clone(&counter),
305            };
306            // Simulate OOM by not creating r3 — neither r1 nor r2 is dropped yet.
307            assert_eq!(
308                counter.load(Ordering::SeqCst),
309                0,
310                "resources must not be dropped before scope exit"
311            );
312        }
313
314        // After the block ends, both r1 and r2 must have been dropped.
315        assert_eq!(
316            counter.load(Ordering::SeqCst),
317            2,
318            "both resources must be dropped at scope exit"
319        );
320    }
321
322    /// Rust drops local variables in **reverse declaration order** (LIFO).
323    /// This test verifies that invariant for RAII guard types.
324    #[test]
325    fn drop_order_is_lifo() {
326        use std::sync::{Arc, Mutex};
327
328        let order: Arc<Mutex<Vec<u32>>> = Arc::new(Mutex::new(vec![]));
329
330        struct Ordered {
331            id: u32,
332            order: Arc<Mutex<Vec<u32>>>,
333        }
334
335        impl Drop for Ordered {
336            fn drop(&mut self) {
337                self.order.lock().expect("order lock failed").push(self.id);
338            }
339        }
340
341        {
342            let _a = Ordered {
343                id: 1,
344                order: Arc::clone(&order),
345            };
346            let _b = Ordered {
347                id: 2,
348                order: Arc::clone(&order),
349            };
350            let _c = Ordered {
351                id: 3,
352                order: Arc::clone(&order),
353            };
354        }
355
356        let observed = order.lock().expect("final order lock failed");
357        assert_eq!(
358            *observed,
359            vec![3, 2, 1],
360            "CUDA RAII guards must be released in LIFO order"
361        );
362    }
363
364    // -----------------------------------------------------------------------
365    // Task 4 — Driver version negotiation (NVIDIA Driver 525 / 535 / 550 / 560)
366    //
367    // `cuDriverGetVersion` returns the CUDA version as `major * 1000 + minor`.
368    // These tests verify the parsing logic and the version-gating conditions
369    // used throughout OxiCUDA without requiring a real driver.
370    // -----------------------------------------------------------------------
371
372    /// NVIDIA Driver 525 ships with CUDA 12.0.  Verify the parse of 12000.
373    #[test]
374    fn driver_version_parsing_cuda_12_0() {
375        // cuDriverGetVersion returns 12000 for CUDA 12.0 (driver 525).
376        let cuda_version: i32 = 12000;
377        let major = cuda_version / 1000;
378        let minor = cuda_version % 1000;
379        assert_eq!(major, 12, "major version mismatch");
380        assert_eq!(minor, 0, "minor version mismatch");
381    }
382
383    /// NVIDIA Driver 535 ships with CUDA 12.2.  Verify the parse of 12020.
384    #[test]
385    fn driver_version_parsing_cuda_12_2() {
386        let cuda_version: i32 = 12020;
387        let major = cuda_version / 1000;
388        let minor = cuda_version % 1000;
389        assert_eq!(major, 12);
390        assert_eq!(minor, 20);
391    }
392
393    /// NVIDIA Driver 550 ships with CUDA 12.4.  Verify the parse of 12040.
394    #[test]
395    fn driver_version_parsing_cuda_12_4() {
396        let cuda_version: i32 = 12040;
397        let major = cuda_version / 1000;
398        let minor = cuda_version % 1000;
399        assert_eq!(major, 12);
400        assert_eq!(minor, 40);
401    }
402
403    /// NVIDIA Driver 560 ships with CUDA 12.6.  Verify the parse of 12060.
404    #[test]
405    fn driver_version_parsing_cuda_12_6() {
406        let cuda_version: i32 = 12060;
407        let major = cuda_version / 1000;
408        let minor = cuda_version % 1000;
409        assert_eq!(major, 12);
410        assert_eq!(minor, 60);
411    }
412
413    /// OxiCUDA requires CUDA 11.2+ (`cuMemAllocAsync` availability).
414    /// Verify that the set of supported versions all meet the minimum and
415    /// that older versions are correctly rejected.
416    #[test]
417    fn driver_version_minimum_requirement() {
418        // cuMemAllocAsync was introduced in CUDA 11.2 (version integer 11020).
419        let min_required: i32 = 11020;
420
421        let supported: [i32; 5] = [11020, 11040, 12000, 12060, 12080];
422        for v in supported {
423            assert!(
424                v >= min_required,
425                "CUDA version {v} should be supported (>= {min_required})"
426            );
427        }
428
429        let too_old: [i32; 2] = [10020, 11010];
430        for v in too_old {
431            assert!(
432                v < min_required,
433                "CUDA version {v} should NOT be supported (< {min_required})"
434            );
435        }
436    }
437
438    /// CUDA 12.8 (version 12080) introduces `cuMemcpyBatchAsync`.
439    /// Verify the feature-gating arithmetic.
440    #[test]
441    fn driver_cuda_12_8_features_available() {
442        // 12.8 → 12080
443        let cuda_128: i32 = 12080;
444        assert!(
445            cuda_128 >= 12080,
446            "CUDA 12.8 must support cuMemcpyBatchAsync"
447        );
448
449        // 12.0 does not have it.
450        let cuda_120: i32 = 12000;
451        assert!(
452            cuda_120 < 12080,
453            "CUDA 12.0 must NOT support cuMemcpyBatchAsync"
454        );
455    }
456
457    /// Verify the complete NVIDIA-driver-version → CUDA-version mapping used
458    /// in OxiCUDA's version negotiation table.
459    #[test]
460    fn driver_nvidia_to_cuda_version_mapping() {
461        // (nvidia_driver, expected_cuda_version_int)
462        let mapping: [(u32, i32); 4] = [
463            (525, 12000), // Driver 525  → CUDA 12.0
464            (535, 12020), // Driver 535  → CUDA 12.2
465            (550, 12040), // Driver 550  → CUDA 12.4
466            (560, 12060), // Driver 560  → CUDA 12.6
467        ];
468
469        for (nvidia_driver, cuda_version) in mapping {
470            let major = cuda_version / 1000;
471            let minor = cuda_version % 1000;
472            // Sanity: all are CUDA 12.x
473            assert_eq!(major, 12, "driver {nvidia_driver}: expected CUDA 12.x");
474            // Minor must be a multiple of 10 (CUDA minor encoding)
475            assert_eq!(
476                minor % 10,
477                0,
478                "driver {nvidia_driver}: minor {minor} is not a multiple of 10"
479            );
480            // CUDA 12.8+ features require version >= 12080
481            let has_12_8_features = cuda_version >= 12080;
482            assert!(
483                !has_12_8_features,
484                "driver {nvidia_driver} (CUDA {major}.{:02}) should NOT have 12.8+ features",
485                minor / 10
486            );
487        }
488    }
489}