Skip to content

Commit 1de85c8

Browse files
authored
feat(cuda): add minimal CUDA driver bindings (#10719)
## Problem The driver replacement needs a small CUDA binding layer with lazy library loading. ## Solution Add raw handles, symbols, errors, and fallible call wrappers in `vortex-cuda-driver`. ## Assessment The crate is excluded from publishing; existing consumers still use cudarc. Based on #10726 and prerequisite for #10720. Signed-off-by: Alexander Droste <alexander.droste@protonmail.com>
1 parent 9df0034 commit 1de85c8

6 files changed

Lines changed: 591 additions & 0 deletions

File tree

‎Cargo.lock‎

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ members = [
2929
"vortex-duckdb",
3030
"vortex-cuda",
3131
"vortex-cuda/cub",
32+
"vortex-cuda/driver",
3233
"vortex-cuda/ffi",
3334
"vortex-cuda/gpu-scan-cli",
3435
"vortex-cuda/macros",
@@ -348,6 +349,7 @@ vortex-zstd = { version = "0.1.0", path = "./encodings/zstd", default-features =
348349
vortex-bench = { path = "./vortex-bench", default-features = false }
349350
vortex-bench-support = { path = "./benchmarks/bench-support" }
350351
vortex-cuda = { path = "./vortex-cuda", default-features = false }
352+
vortex-cuda-driver = { path = "./vortex-cuda/driver" }
351353
vortex-cuda-macros = { path = "./vortex-cuda/macros" }
352354
vortex-duckdb = { path = "./vortex-duckdb", default-features = false }
353355
vortex-tui = { path = "./vortex-tui" }

‎vortex-cuda/driver/Cargo.toml‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
[package]
5+
name = "vortex-cuda-driver"
6+
authors.workspace = true
7+
description = "Minimal CUDA driver API bindings for Vortex"
8+
edition = { workspace = true }
9+
categories = { workspace = true }
10+
include = { workspace = true }
11+
keywords = { workspace = true }
12+
license = { workspace = true }
13+
readme = { workspace = true }
14+
repository = { workspace = true }
15+
rust-version = { workspace = true }
16+
version = { workspace = true }
17+
publish = false
18+
19+
[lints]
20+
workspace = true
21+
22+
[dependencies]
23+
libloading = { workspace = true }

‎vortex-cuda/driver/src/lib.rs‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
//! Minimal CUDA driver API bindings for Vortex.
5+
//!
6+
//! The driver library is loaded lazily, so dependents build and run without CUDA until
7+
//! they first use it. The raw bindings and result wrappers operate on CUDA driver handles.
8+
9+
pub mod result;
10+
pub mod sys;
11+
12+
pub use result::DriverError;

‎vortex-cuda/driver/src/result.rs‎

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
//! Driver calls returning [`Result`]s, for callers that need the raw handles.
5+
6+
use std::ffi::CStr;
7+
use std::ffi::c_uint;
8+
use std::ffi::c_void;
9+
use std::fmt;
10+
use std::mem::MaybeUninit;
11+
12+
use crate::sys;
13+
use crate::sys::CUresult;
14+
15+
/// A failed CUDA driver call.
16+
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
17+
pub struct DriverError(pub CUresult);
18+
19+
impl DriverError {
20+
/// The driver's name for the error, such as `CUDA_ERROR_INVALID_VALUE`.
21+
pub fn error_name(&self) -> Result<&'static CStr, DriverError> {
22+
let mut name = std::ptr::null();
23+
// SAFETY: The driver writes a pointer to a static string.
24+
unsafe { sys::cuGetErrorName(self.0, &raw mut name) }.result()?;
25+
// SAFETY: Successful calls return a NUL-terminated static string.
26+
Ok(unsafe { CStr::from_ptr(name) })
27+
}
28+
29+
/// The driver's description of the error.
30+
pub fn error_string(&self) -> Result<&'static CStr, DriverError> {
31+
let mut description = std::ptr::null();
32+
// SAFETY: The driver writes a pointer to a static string.
33+
unsafe { sys::cuGetErrorString(self.0, &raw mut description) }.result()?;
34+
// SAFETY: Successful calls return a NUL-terminated static string.
35+
Ok(unsafe { CStr::from_ptr(description) })
36+
}
37+
}
38+
39+
impl fmt::Debug for DriverError {
40+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41+
match self.error_name() {
42+
Ok(name) => write!(f, "DriverError({}, {})", self.0.0, name.to_string_lossy()),
43+
Err(_) => write!(f, "DriverError({})", self.0.0),
44+
}
45+
}
46+
}
47+
48+
impl fmt::Display for DriverError {
49+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50+
match (self.error_name(), self.error_string()) {
51+
(Ok(name), Ok(description)) => write!(
52+
f,
53+
"{}: {}",
54+
name.to_string_lossy(),
55+
description.to_string_lossy()
56+
),
57+
_ => write!(f, "CUDA driver error {}", self.0.0),
58+
}
59+
}
60+
}
61+
62+
impl std::error::Error for DriverError {}
63+
64+
impl CUresult {
65+
/// `Ok` for [`CUresult::CUDA_SUCCESS`], otherwise the error.
66+
#[inline]
67+
pub fn result(self) -> Result<(), DriverError> {
68+
if self == CUresult::CUDA_SUCCESS {
69+
Ok(())
70+
} else {
71+
Err(DriverError(self))
72+
}
73+
}
74+
}
75+
76+
/// Initializes the driver; repeated calls are cheap.
77+
pub fn init() -> Result<(), DriverError> {
78+
// SAFETY: cuInit has no preconditions.
79+
unsafe { sys::cuInit(0) }.result()
80+
}
81+
82+
/// Allocates `bytes` of page-locked host memory with the given `cuMemHostAlloc` flags.
83+
///
84+
/// # Safety
85+
///
86+
/// A CUDA context must be current; the memory is uninitialized.
87+
pub unsafe fn malloc_host(bytes: usize, flags: c_uint) -> Result<*mut c_void, DriverError> {
88+
let mut pointer = MaybeUninit::uninit();
89+
// SAFETY: The driver writes the allocation into `pointer` on success.
90+
unsafe { sys::cuMemHostAlloc(pointer.as_mut_ptr(), bytes, flags) }.result()?;
91+
// SAFETY: Initialized by the successful call.
92+
Ok(unsafe { pointer.assume_init() })
93+
}
94+
95+
/// Frees memory allocated by [`malloc_host`].
96+
///
97+
/// # Safety
98+
///
99+
/// `pointer` must come from [`malloc_host`] and no pending copy may still use it.
100+
pub unsafe fn free_host(pointer: *mut c_void) -> Result<(), DriverError> {
101+
// SAFETY: Under the caller's guarantees.
102+
unsafe { sys::cuMemFreeHost(pointer) }.result()
103+
}
104+
105+
/// Synchronously copies `target.len()` elements from device memory at `source`.
106+
///
107+
/// # Safety
108+
///
109+
/// `source` must address at least `size_of_val(target)` readable device bytes.
110+
pub unsafe fn memcpy_dtoh_sync<T>(
111+
target: &mut [T],
112+
source: sys::CUdeviceptr,
113+
) -> Result<(), DriverError> {
114+
// SAFETY: Under the caller's guarantees; `target` is writable for its whole size.
115+
unsafe { sys::cuMemcpyDtoH_v2(target.as_mut_ptr().cast(), source, size_of_val(target)) }
116+
.result()
117+
}
118+
119+
/// Device queries.
120+
pub mod device {
121+
use super::*;
122+
123+
/// The device with the given ordinal.
124+
pub fn get(ordinal: i32) -> Result<sys::CUdevice, DriverError> {
125+
let mut device = MaybeUninit::uninit();
126+
// SAFETY: The driver writes the device into `device` on success.
127+
unsafe { sys::cuDeviceGet(device.as_mut_ptr(), ordinal) }.result()?;
128+
// SAFETY: Initialized by the successful call.
129+
Ok(unsafe { device.assume_init() })
130+
}
131+
132+
/// The number of devices.
133+
pub fn get_count() -> Result<i32, DriverError> {
134+
let mut count = 0;
135+
// SAFETY: The driver writes the count.
136+
unsafe { sys::cuDeviceGetCount(&raw mut count) }.result()?;
137+
Ok(count)
138+
}
139+
140+
/// A device attribute.
141+
///
142+
/// # Safety
143+
///
144+
/// `device` must be a valid device handle.
145+
pub unsafe fn get_attribute(
146+
device: sys::CUdevice,
147+
attribute: sys::CUdevice_attribute,
148+
) -> Result<i32, DriverError> {
149+
let mut value = 0;
150+
// SAFETY: Under the caller's guarantees.
151+
unsafe { sys::cuDeviceGetAttribute(&raw mut value, attribute, device) }.result()?;
152+
Ok(value)
153+
}
154+
155+
/// The device's current memory pool.
156+
///
157+
/// # Safety
158+
///
159+
/// `device` must be a valid device handle.
160+
pub unsafe fn get_mem_pool(device: sys::CUdevice) -> Result<sys::CUmemoryPool, DriverError> {
161+
let mut pool = MaybeUninit::uninit();
162+
// SAFETY: Under the caller's guarantees.
163+
unsafe { sys::cuDeviceGetMemPool(pool.as_mut_ptr(), device) }.result()?;
164+
// SAFETY: Initialized by the successful call.
165+
Ok(unsafe { pool.assume_init() })
166+
}
167+
}
168+
169+
/// Memory pool attributes.
170+
pub mod mem_pool {
171+
use super::*;
172+
173+
/// Reads a pool attribute into `value`.
174+
///
175+
/// # Safety
176+
///
177+
/// `value` must point to storage of the attribute's type.
178+
pub unsafe fn get_attribute(
179+
pool: sys::CUmemoryPool,
180+
attribute: sys::CUmemPool_attribute,
181+
value: *mut c_void,
182+
) -> Result<(), DriverError> {
183+
// SAFETY: Under the caller's guarantees.
184+
unsafe { sys::cuMemPoolGetAttribute(pool, attribute, value) }.result()
185+
}
186+
187+
/// Sets a pool attribute from `value`.
188+
///
189+
/// # Safety
190+
///
191+
/// `value` must point to a value of the attribute's type.
192+
pub unsafe fn set_attribute(
193+
pool: sys::CUmemoryPool,
194+
attribute: sys::CUmemPool_attribute,
195+
value: *mut c_void,
196+
) -> Result<(), DriverError> {
197+
// SAFETY: Under the caller's guarantees.
198+
unsafe { sys::cuMemPoolSetAttribute(pool, attribute, value) }.result()
199+
}
200+
}
201+
202+
/// Stream operations on raw handles.
203+
pub mod stream {
204+
use super::*;
205+
206+
/// The (least, greatest) stream priorities of the current context.
207+
pub fn get_priority_range() -> Result<(i32, i32), DriverError> {
208+
let (mut least, mut greatest) = (0, 0);
209+
// SAFETY: The driver writes both values.
210+
unsafe { sys::cuCtxGetStreamPriorityRange(&raw mut least, &raw mut greatest) }.result()?;
211+
Ok((least, greatest))
212+
}
213+
214+
/// Makes `stream` wait for the work recorded in `event`.
215+
///
216+
/// # Safety
217+
///
218+
/// Both handles must be valid and belong to the current context.
219+
pub unsafe fn wait_event(
220+
stream: sys::CUstream,
221+
event: sys::CUevent,
222+
flags: sys::CUevent_wait_flags,
223+
) -> Result<(), DriverError> {
224+
// SAFETY: Under the caller's guarantees.
225+
unsafe { sys::cuStreamWaitEvent(stream, event, flags.0) }.result()
226+
}
227+
228+
/// Enqueues a host function on `stream`.
229+
///
230+
/// # Safety
231+
///
232+
/// `stream` must be valid, and `function` must be safe to call once with `argument` from a
233+
/// driver thread, without making CUDA calls.
234+
pub unsafe fn launch_host_function(
235+
stream: sys::CUstream,
236+
function: unsafe extern "C" fn(*mut c_void),
237+
argument: *mut c_void,
238+
) -> Result<(), DriverError> {
239+
// SAFETY: Under the caller's guarantees.
240+
unsafe { sys::cuLaunchHostFunc(stream, Some(function), argument) }.result()
241+
}
242+
}
243+
244+
/// Event operations on raw handles.
245+
pub mod event {
246+
use super::*;
247+
248+
/// Blocks until the work recorded in `event` completes.
249+
///
250+
/// # Safety
251+
///
252+
/// `event` must be a valid event handle.
253+
pub unsafe fn synchronize(event: sys::CUevent) -> Result<(), DriverError> {
254+
// SAFETY: Under the caller's guarantees.
255+
unsafe { sys::cuEventSynchronize(event) }.result()
256+
}
257+
}

0 commit comments

Comments
 (0)