Skip to main content

launchbound_bench/
cuda.rs

1//! Minimal CUDA driver API surface, loaded at runtime with dlopen so the
2//! crate builds (and its tests run) on machines with no CUDA at all —
3//! including CI and the Mac. Only the box can actually produce a timing.
4
5#![allow(clippy::missing_safety_doc)]
6
7use libloading::Library;
8use std::ffi::c_void;
9
10/// A CUDA driver API status code. `0` is `CUDA_SUCCESS`; anything else is
11/// reported with the name of the call that returned it.
12pub type CUresult = i32;
13type CUdeviceptr = u64;
14
15macro_rules! driver_api {
16    ($( $name:ident : fn( $($arg:ty),* ) ; )*) => {
17        // Fields carry the C symbol names verbatim.
18        /// The driver entry points this crate uses, resolved at runtime.
19        #[allow(non_snake_case)]
20        pub struct Cuda {
21            _lib: Library,
22            $( $name: unsafe extern "C" fn($($arg),*) -> CUresult, )*
23        }
24
25        impl Cuda {
26            /// dlopen libcuda and resolve the surface. Errors on machines
27            /// without a driver — that is the honest answer there.
28            pub fn load() -> Result<Self, String> {
29                let lib = ["libcuda.so.1", "libcuda.so"]
30                    .iter()
31                    .find_map(|n| unsafe { Library::new(n).ok() })
32                    .ok_or_else(|| {
33                        "libcuda not found: benchmarks need an NVIDIA driver".to_string()
34                    })?;
35                unsafe {
36                    Ok(Cuda {
37                        $( $name: *lib
38                            .get(concat!(stringify!($name), "\0").as_bytes())
39                            .map_err(|e| format!("missing {}: {e}", stringify!($name)))?, )*
40                        _lib: lib,
41                    })
42                }
43            }
44        }
45    };
46}
47
48driver_api! {
49    cuInit: fn(u32);
50    cuDriverGetVersion: fn(*mut i32);
51    cuDeviceGet: fn(*mut i32, i32);
52    cuDeviceGetName: fn(*mut u8, i32, i32);
53    cuDeviceGetAttribute: fn(*mut i32, i32, i32);
54    cuCtxCreate_v2: fn(*mut *mut c_void, u32, i32);
55    cuCtxDestroy_v2: fn(*mut c_void);
56    cuCtxSynchronize: fn();
57    cuModuleLoadData: fn(*mut *mut c_void, *const c_void);
58    cuModuleUnload: fn(*mut c_void);
59    cuModuleGetFunction: fn(*mut *mut c_void, *mut c_void, *const u8);
60    cuMemAlloc_v2: fn(*mut CUdeviceptr, usize);
61    cuMemFree_v2: fn(CUdeviceptr);
62    cuMemcpyHtoD_v2: fn(CUdeviceptr, *const c_void, usize);
63    cuMemcpyDtoH_v2: fn(*mut c_void, CUdeviceptr, usize);
64    cuLaunchKernel: fn(*mut c_void, u32, u32, u32, u32, u32, u32, u32, *mut c_void, *mut *mut c_void, *mut *mut c_void);
65    cuEventCreate: fn(*mut *mut c_void, u32);
66    cuEventDestroy_v2: fn(*mut c_void);
67    cuEventRecord: fn(*mut c_void, *mut c_void);
68    cuEventSynchronize: fn(*mut c_void);
69    cuEventElapsedTime: fn(*mut f32, *mut c_void, *mut c_void);
70}
71
72const CU_DEVICE_ATTRIBUTE_COMPUTE_CAPABILITY_MAJOR: i32 = 75;
73const CU_DEVICE_ATTRIBUTE_COMPUTE_CAPABILITY_MINOR: i32 = 76;
74
75fn check(what: &str, code: CUresult) -> Result<(), String> {
76    if code == 0 {
77        Ok(())
78    } else {
79        Err(format!("{what} failed: CUresult {code}"))
80    }
81}
82
83/// An initialized CUDA device with a primary context retained.
84///
85/// Opening one is what a machine without a GPU cannot do; every other type
86/// here borrows from it, so nothing can outlive the context it was
87/// allocated in.
88pub struct Device {
89    cuda: Cuda,
90    ctx: *mut c_void,
91    /// Product name as the driver reports it, e.g. `NVIDIA A10G`.
92    pub name: String,
93    /// Compute capability as `"<major>.<minor>"`.
94    pub cc: String,
95    /// Driver version, recorded in `results.v1` for provenance.
96    pub driver_version: String,
97}
98
99/// A loaded PTX module, borrowed from the device that holds it.
100pub struct Module<'d> {
101    device: &'d Device,
102    module: *mut c_void,
103    /// The resolved entry function, ready to pass to a launch.
104    pub function: *mut c_void,
105}
106
107/// A device allocation, freed when dropped.
108pub struct Buffer<'d> {
109    device: &'d Device,
110    /// The device pointer, as a launch's parameter table needs it.
111    pub ptr: CUdeviceptr,
112    /// Allocated size in bytes.
113    pub bytes: usize,
114}
115
116impl Device {
117    /// Load the driver, initialize it, and retain a primary context on
118    /// device 0.
119    ///
120    /// Fails with a readable message rather than a panic when there is no
121    /// driver to load — which is the normal state on CI and on the Mac.
122    pub fn open() -> Result<Self, String> {
123        let cuda = Cuda::load()?;
124        unsafe {
125            check("cuInit", (cuda.cuInit)(0))?;
126            let mut version = 0i32;
127            check(
128                "cuDriverGetVersion",
129                (cuda.cuDriverGetVersion)(&mut version),
130            )?;
131            let mut dev = 0i32;
132            check("cuDeviceGet", (cuda.cuDeviceGet)(&mut dev, 0))?;
133            let mut name = [0u8; 128];
134            check(
135                "cuDeviceGetName",
136                (cuda.cuDeviceGetName)(name.as_mut_ptr(), name.len() as i32, dev),
137            )?;
138            let (mut major, mut minor) = (0i32, 0i32);
139            check(
140                "cc major",
141                (cuda.cuDeviceGetAttribute)(
142                    &mut major,
143                    CU_DEVICE_ATTRIBUTE_COMPUTE_CAPABILITY_MAJOR,
144                    dev,
145                ),
146            )?;
147            check(
148                "cc minor",
149                (cuda.cuDeviceGetAttribute)(
150                    &mut minor,
151                    CU_DEVICE_ATTRIBUTE_COMPUTE_CAPABILITY_MINOR,
152                    dev,
153                ),
154            )?;
155            let mut ctx = std::ptr::null_mut();
156            check("cuCtxCreate", (cuda.cuCtxCreate_v2)(&mut ctx, 0, dev))?;
157            let name = String::from_utf8_lossy(
158                &name[..name.iter().position(|&b| b == 0).unwrap_or(name.len())],
159            )
160            .to_string();
161            Ok(Device {
162                cuda,
163                ctx,
164                name,
165                cc: format!("{major}.{minor}"),
166                driver_version: format!("{}.{}", version / 1000, (version % 1000) / 10),
167            })
168        }
169    }
170
171    /// Load PTX and resolve one entry function by name.
172    pub fn load_module(&self, ptx: &str, entry: &str) -> Result<Module<'_>, String> {
173        let mut ptx_z = ptx.as_bytes().to_vec();
174        ptx_z.push(0);
175        let mut entry_z = entry.as_bytes().to_vec();
176        entry_z.push(0);
177        unsafe {
178            let mut module = std::ptr::null_mut();
179            check(
180                "cuModuleLoadData",
181                (self.cuda.cuModuleLoadData)(&mut module, ptx_z.as_ptr().cast()),
182            )?;
183            let mut function = std::ptr::null_mut();
184            let got = (self.cuda.cuModuleGetFunction)(&mut function, module, entry_z.as_ptr());
185            if got != 0 {
186                (self.cuda.cuModuleUnload)(module);
187                return Err(format!(
188                    "cuModuleGetFunction({entry}) failed: CUresult {got}"
189                ));
190            }
191            Ok(Module {
192                device: self,
193                module,
194                function,
195            })
196        }
197    }
198
199    /// Allocate `bytes` of device memory.
200    pub fn alloc(&self, bytes: usize) -> Result<Buffer<'_>, String> {
201        let mut ptr = 0u64;
202        unsafe { check("cuMemAlloc", (self.cuda.cuMemAlloc_v2)(&mut ptr, bytes))? };
203        Ok(Buffer {
204            device: self,
205            ptr,
206            bytes,
207        })
208    }
209
210    /// Copy host bytes into a device buffer. `data` must fit.
211    pub fn copy_in(&self, buffer: &Buffer<'_>, data: &[u8]) -> Result<(), String> {
212        assert!(data.len() <= buffer.bytes);
213        unsafe {
214            check(
215                "cuMemcpyHtoD",
216                (self.cuda.cuMemcpyHtoD_v2)(buffer.ptr, data.as_ptr().cast(), data.len()),
217            )
218        }
219    }
220
221    /// Copy device bytes back into a host slice. `out` must fit.
222    pub fn copy_out(&self, buffer: &Buffer<'_>, out: &mut [u8]) -> Result<(), String> {
223        assert!(out.len() <= buffer.bytes);
224        unsafe {
225            check(
226                "cuMemcpyDtoH",
227                (self.cuda.cuMemcpyDtoH_v2)(out.as_mut_ptr().cast(), buffer.ptr, out.len()),
228            )
229        }
230    }
231
232    /// Block until the context's queued work has finished.
233    ///
234    /// Timings come from CUDA events rather than from wrapping this, so a
235    /// measurement excludes launch and transfer overhead that a real
236    /// application pays (`docs/LIMITATIONS.md`).
237    pub fn synchronize(&self) -> Result<(), String> {
238        unsafe { check("cuCtxSynchronize", (self.cuda.cuCtxSynchronize)()) }
239    }
240
241    /// Launch once and return the kernel-only elapsed milliseconds,
242    /// measured with a cuEvent pair on the default stream.
243    pub fn timed_launch(
244        &self,
245        module: &Module<'_>,
246        grid: [u32; 3],
247        block: [u32; 3],
248        params: &mut [*mut c_void],
249    ) -> Result<f64, String> {
250        unsafe {
251            let mut ev0 = std::ptr::null_mut();
252            let mut ev1 = std::ptr::null_mut();
253            check("cuEventCreate", (self.cuda.cuEventCreate)(&mut ev0, 0))?;
254            check("cuEventCreate", (self.cuda.cuEventCreate)(&mut ev1, 0))?;
255            let stream = std::ptr::null_mut();
256            check("cuEventRecord", (self.cuda.cuEventRecord)(ev0, stream))?;
257            let launched = (self.cuda.cuLaunchKernel)(
258                module.function,
259                grid[0],
260                grid[1],
261                grid[2],
262                block[0],
263                block[1],
264                block[2],
265                0, // static SharedArray only: no dynamic smem
266                stream,
267                params.as_mut_ptr(),
268                std::ptr::null_mut(),
269            );
270            if launched != 0 {
271                (self.cuda.cuEventDestroy_v2)(ev0);
272                (self.cuda.cuEventDestroy_v2)(ev1);
273                return Err(format!("cuLaunchKernel failed: CUresult {launched}"));
274            }
275            check("cuEventRecord", (self.cuda.cuEventRecord)(ev1, stream))?;
276            check("cuEventSynchronize", (self.cuda.cuEventSynchronize)(ev1))?;
277            let mut ms = 0f32;
278            check(
279                "cuEventElapsedTime",
280                (self.cuda.cuEventElapsedTime)(&mut ms, ev0, ev1),
281            )?;
282            (self.cuda.cuEventDestroy_v2)(ev0);
283            (self.cuda.cuEventDestroy_v2)(ev1);
284            Ok(ms as f64)
285        }
286    }
287}
288
289impl Drop for Module<'_> {
290    fn drop(&mut self) {
291        unsafe {
292            (self.device.cuda.cuModuleUnload)(self.module);
293        }
294    }
295}
296
297impl Drop for Buffer<'_> {
298    fn drop(&mut self) {
299        unsafe {
300            (self.device.cuda.cuMemFree_v2)(self.ptr);
301        }
302    }
303}
304
305impl Drop for Device {
306    fn drop(&mut self) {
307        unsafe {
308            (self.cuda.cuCtxDestroy_v2)(self.ctx);
309        }
310    }
311}