1use std::collections::BTreeMap;
7use std::ffi::{c_void, CString};
8use std::path::Path;
9use std::sync::{Arc, RwLock};
10
11use cudarc::driver::result::{self, DriverError};
12use cudarc::driver::{
13 sys, CudaContext as CudarcContext, CudaSlice, CudaStream, DevicePtr, DevicePtrMut, DeviceRepr,
14 HostSlice, LaunchConfig, ValidAsZeroBits,
15};
16use cudarc::nvrtc::Ptx;
17use xlog_core::{Result, XlogError};
18
19#[derive(Debug)]
20struct LoadedModule {
21 cu_module: sys::CUmodule,
22 functions: BTreeMap<String, sys::CUfunction>,
23}
24
25unsafe impl Send for LoadedModule {}
26unsafe impl Sync for LoadedModule {}
27
28#[derive(Debug, Clone)]
30pub struct CudaFunction {
31 cu_function: sys::CUfunction,
32 context: Arc<CudarcContext>,
33 stream: Arc<CudaStream>,
34}
35
36impl CudaFunction {
37 pub(crate) unsafe fn launch_raw(
38 &self,
39 cfg: LaunchConfig,
40 params: &mut [*mut c_void],
41 ) -> std::result::Result<(), DriverError> {
42 self.context.bind_to_thread()?;
43 result::launch_kernel(
44 self.cu_function,
45 cfg.grid_dim,
46 cfg.block_dim,
47 cfg.shared_mem_bytes,
48 self.stream.cu_stream(),
49 params,
50 )
51 }
52
53 pub(crate) unsafe fn launch_raw_on_stream(
54 &self,
55 stream: &CudaStream,
56 cfg: LaunchConfig,
57 params: &mut [*mut c_void],
58 ) -> std::result::Result<(), DriverError> {
59 self.context.bind_to_thread()?;
60 result::launch_kernel(
61 self.cu_function,
62 cfg.grid_dim,
63 cfg.block_dim,
64 cfg.shared_mem_bytes,
65 stream.cu_stream(),
66 params,
67 )
68 }
69
70 pub(crate) unsafe fn launch_raw_cooperative(
71 &self,
72 cfg: LaunchConfig,
73 params: &mut [*mut c_void],
74 ) -> std::result::Result<(), DriverError> {
75 self.launch_raw_cooperative_on_stream(&self.stream, cfg, params)
76 }
77
78 pub(crate) unsafe fn launch_raw_cooperative_on_stream(
79 &self,
80 stream: &CudaStream,
81 cfg: LaunchConfig,
82 params: &mut [*mut c_void],
83 ) -> std::result::Result<(), DriverError> {
84 self.context.bind_to_thread()?;
85 result::launch_cooperative_kernel(
86 self.cu_function,
87 cfg.grid_dim,
88 cfg.block_dim,
89 cfg.shared_mem_bytes,
90 stream.cu_stream(),
91 params,
92 )
93 }
94
95 pub fn occupancy_available_dynamic_smem_per_block(
96 &self,
97 num_blocks: u32,
98 block_size: u32,
99 ) -> std::result::Result<usize, DriverError> {
100 let mut dynamic_smem_size: usize = 0;
101 unsafe {
102 sys::cuOccupancyAvailableDynamicSMemPerBlock(
103 &mut dynamic_smem_size,
104 self.cu_function,
105 num_blocks as std::ffi::c_int,
106 block_size as std::ffi::c_int,
107 )
108 .result()?
109 };
110 Ok(dynamic_smem_size)
111 }
112
113 pub fn occupancy_max_active_blocks_per_multiprocessor(
114 &self,
115 block_size: u32,
116 dynamic_smem_size: usize,
117 flags: Option<sys::CUoccupancy_flags_enum>,
118 ) -> std::result::Result<u32, DriverError> {
119 let mut num_blocks: std::ffi::c_int = 0;
120 let flags = flags.unwrap_or(sys::CUoccupancy_flags_enum::CU_OCCUPANCY_DEFAULT);
121 unsafe {
122 sys::cuOccupancyMaxActiveBlocksPerMultiprocessorWithFlags(
123 &mut num_blocks,
124 self.cu_function,
125 block_size as std::ffi::c_int,
126 dynamic_smem_size,
127 flags as std::ffi::c_uint,
128 )
129 .result()?
130 };
131 Ok(num_blocks as u32)
132 }
133
134 pub fn occupancy_max_active_clusters(
135 &self,
136 config: LaunchConfig,
137 ) -> std::result::Result<u32, DriverError> {
138 let mut num_clusters: std::ffi::c_int = 0;
139 let cfg = sys::CUlaunchConfig {
140 gridDimX: config.grid_dim.0,
141 gridDimY: config.grid_dim.1,
142 gridDimZ: config.grid_dim.2,
143 blockDimX: config.block_dim.0,
144 blockDimY: config.block_dim.1,
145 blockDimZ: config.block_dim.2,
146 sharedMemBytes: config.shared_mem_bytes,
147 hStream: self.stream.cu_stream(),
148 attrs: std::ptr::null_mut(),
149 numAttrs: 0,
150 };
151 unsafe {
152 sys::cuOccupancyMaxActiveClusters(&mut num_clusters, self.cu_function, &cfg).result()?
153 };
154 Ok(num_clusters as u32)
155 }
156
157 pub fn occupancy_max_potential_block_size(
158 &self,
159 block_size_to_dynamic_smem_size: extern "C" fn(block_size: std::ffi::c_int) -> usize,
160 dynamic_smem_size: usize,
161 block_size_limit: u32,
162 flags: Option<sys::CUoccupancy_flags_enum>,
163 ) -> std::result::Result<(u32, u32), DriverError> {
164 let mut min_grid_size: std::ffi::c_int = 0;
165 let mut block_size: std::ffi::c_int = 0;
166 let flags = flags.unwrap_or(sys::CUoccupancy_flags_enum::CU_OCCUPANCY_DEFAULT);
167 unsafe {
168 sys::cuOccupancyMaxPotentialBlockSizeWithFlags(
169 &mut min_grid_size,
170 &mut block_size,
171 self.cu_function,
172 Some(block_size_to_dynamic_smem_size),
173 dynamic_smem_size,
174 block_size_limit as std::ffi::c_int,
175 flags as std::ffi::c_uint,
176 )
177 .result()?
178 };
179 Ok((min_grid_size as u32, block_size as u32))
180 }
181
182 pub fn occupancy_max_potential_cluster_size(
183 &self,
184 config: LaunchConfig,
185 ) -> std::result::Result<u32, DriverError> {
186 let mut cluster_size: std::ffi::c_int = 0;
187 let cfg = sys::CUlaunchConfig {
188 gridDimX: config.grid_dim.0,
189 gridDimY: config.grid_dim.1,
190 gridDimZ: config.grid_dim.2,
191 blockDimX: config.block_dim.0,
192 blockDimY: config.block_dim.1,
193 blockDimZ: config.block_dim.2,
194 sharedMemBytes: config.shared_mem_bytes,
195 hStream: self.stream.cu_stream(),
196 attrs: std::ptr::null_mut(),
197 numAttrs: 0,
198 };
199 unsafe {
200 sys::cuOccupancyMaxPotentialClusterSize(&mut cluster_size, self.cu_function, &cfg)
201 .result()?
202 };
203 Ok(cluster_size as u32)
204 }
205
206 pub fn get_attribute(
207 &self,
208 attribute: sys::CUfunction_attribute_enum,
209 ) -> std::result::Result<i32, DriverError> {
210 self.context.bind_to_thread()?;
211 unsafe { result::function::get_function_attribute(self.cu_function, attribute) }
212 }
213
214 pub fn num_regs(&self) -> std::result::Result<i32, DriverError> {
215 self.get_attribute(sys::CUfunction_attribute_enum::CU_FUNC_ATTRIBUTE_NUM_REGS)
216 }
217
218 pub fn shared_size_bytes(&self) -> std::result::Result<i32, DriverError> {
219 self.get_attribute(sys::CUfunction_attribute_enum::CU_FUNC_ATTRIBUTE_SHARED_SIZE_BYTES)
220 }
221
222 pub fn const_size_bytes(&self) -> std::result::Result<i32, DriverError> {
223 self.get_attribute(sys::CUfunction_attribute_enum::CU_FUNC_ATTRIBUTE_CONST_SIZE_BYTES)
224 }
225
226 pub fn local_size_bytes(&self) -> std::result::Result<i32, DriverError> {
227 self.get_attribute(sys::CUfunction_attribute_enum::CU_FUNC_ATTRIBUTE_LOCAL_SIZE_BYTES)
228 }
229
230 pub fn max_threads_per_block(&self) -> std::result::Result<i32, DriverError> {
231 self.get_attribute(sys::CUfunction_attribute_enum::CU_FUNC_ATTRIBUTE_MAX_THREADS_PER_BLOCK)
232 }
233
234 pub fn ptx_version(&self) -> std::result::Result<i32, DriverError> {
235 self.get_attribute(sys::CUfunction_attribute_enum::CU_FUNC_ATTRIBUTE_PTX_VERSION)
236 }
237
238 pub fn binary_version(&self) -> std::result::Result<i32, DriverError> {
239 self.get_attribute(sys::CUfunction_attribute_enum::CU_FUNC_ATTRIBUTE_BINARY_VERSION)
240 }
241
242 pub fn set_attribute(
243 &self,
244 attribute: sys::CUfunction_attribute_enum,
245 value: i32,
246 ) -> std::result::Result<(), DriverError> {
247 unsafe { result::function::set_function_attribute(self.cu_function, attribute, value) }
248 }
249
250 pub fn set_function_cache_config(
251 &self,
252 config: sys::CUfunc_cache,
253 ) -> std::result::Result<(), DriverError> {
254 unsafe { result::function::set_function_cache_config(self.cu_function, config) }
255 }
256}
257
258#[derive(Debug)]
259pub struct CudaDeviceInner {
260 context: Arc<CudarcContext>,
261 stream: Arc<CudaStream>,
262 modules: RwLock<BTreeMap<String, LoadedModule>>,
263}
264
265impl Drop for CudaDeviceInner {
266 fn drop(&mut self) {
267 let _ = self.context.bind_to_thread();
268 if let Ok(modules) = self.modules.get_mut() {
269 for module in modules.values() {
270 let _ = unsafe { result::module::unload(module.cu_module) };
271 }
272 modules.clear();
273 }
274 }
275}
276
277impl CudaDeviceInner {
278 fn insert_module(
279 &self,
280 module_name: &str,
281 cu_module: sys::CUmodule,
282 kernels: &[&str],
283 ) -> std::result::Result<(), DriverError> {
284 let mut functions = BTreeMap::new();
285 for &kernel in kernels {
286 let name_c = CString::new(kernel).unwrap();
287 let cu_function = unsafe { result::module::get_function(cu_module, name_c) }?;
288 functions.insert(kernel.to_string(), cu_function);
289 }
290 let module = LoadedModule {
291 cu_module,
292 functions,
293 };
294
295 let mut modules = self.modules.write().unwrap();
296 if let Some(prev) = modules.insert(module_name.to_string(), module) {
297 unsafe { result::module::unload(prev.cu_module) }?;
298 }
299 Ok(())
300 }
301
302 pub fn stream(&self) -> &Arc<CudaStream> {
303 &self.stream
304 }
305
306 pub fn has_func(&self, module_name: &str, func_name: &str) -> bool {
307 let modules = self.modules.read().unwrap();
308 modules
309 .get(module_name)
310 .is_some_and(|module| module.functions.contains_key(func_name))
311 }
312
313 pub fn get_func(&self, module_name: &str, func_name: &str) -> Option<CudaFunction> {
314 let modules = self.modules.read().unwrap();
315 let cu_function = modules
316 .get(module_name)
317 .and_then(|module| module.functions.get(func_name))
318 .copied()?;
319 Some(CudaFunction {
320 cu_function,
321 context: self.context.clone(),
322 stream: self.stream.clone(),
323 })
324 }
325
326 pub fn load_file(
327 &self,
328 path: &Path,
329 module_name: &str,
330 kernels: &[&str],
331 ) -> std::result::Result<(), DriverError> {
332 self.context.bind_to_thread()?;
333 let name_c = CString::new(path.to_string_lossy().as_bytes()).unwrap();
334 let cu_module = result::module::load(name_c)?;
335 self.insert_module(module_name, cu_module, kernels)
336 }
337
338 pub fn load_ptx(
339 &self,
340 ptx: Ptx,
341 module_name: &str,
342 kernels: &[&str],
343 ) -> std::result::Result<(), DriverError> {
344 self.context.bind_to_thread()?;
345 let cu_module = if let Some(bytes) = ptx.as_bytes() {
346 unsafe { result::module::load_data(bytes.as_ptr() as *const _) }?
347 } else {
348 let src = CString::new(ptx.to_src()).unwrap();
349 unsafe { result::module::load_data(src.as_ptr() as *const _) }?
350 };
351 self.insert_module(module_name, cu_module, kernels)
352 }
353
354 pub unsafe fn alloc<T: DeviceRepr>(
361 &self,
362 len: usize,
363 ) -> std::result::Result<CudaSlice<T>, DriverError> {
364 self.stream.alloc(len)
365 }
366
367 pub fn alloc_zeros<T: DeviceRepr + ValidAsZeroBits>(
368 &self,
369 len: usize,
370 ) -> std::result::Result<CudaSlice<T>, DriverError> {
371 self.stream.alloc_zeros(len)
372 }
373
374 pub fn memset_zeros<T: DeviceRepr + ValidAsZeroBits, Dst: DevicePtrMut<T>>(
375 &self,
376 dst: &mut Dst,
377 ) -> std::result::Result<(), DriverError> {
378 self.stream.memset_zeros(dst)?;
379 self.stream.synchronize()
380 }
381
382 pub fn htod_sync_copy_into<T: DeviceRepr, Dst: DevicePtrMut<T>, Src: HostSlice<T> + ?Sized>(
383 &self,
384 src: &Src,
385 dst: &mut Dst,
386 ) -> std::result::Result<(), DriverError> {
387 self.stream.memcpy_htod(src, dst)?;
388 self.stream.synchronize()
389 }
390
391 pub fn dtoh_sync_copy_into<T: DeviceRepr, Src: DevicePtr<T>, Dst: HostSlice<T> + ?Sized>(
392 &self,
393 src: &Src,
394 dst: &mut Dst,
395 ) -> std::result::Result<(), DriverError> {
396 self.stream.memcpy_dtoh(src, dst)?;
397 self.stream.synchronize()
398 }
399
400 pub fn htod_sync_copy<T: DeviceRepr, Src: HostSlice<T> + ?Sized>(
401 &self,
402 src: &Src,
403 ) -> std::result::Result<CudaSlice<T>, DriverError> {
404 let dst = self.stream.clone_htod(src)?;
405 self.stream.synchronize()?;
406 Ok(dst)
407 }
408
409 pub fn dtoh_sync_copy<T: DeviceRepr, Src: DevicePtr<T>>(
410 &self,
411 src: &Src,
412 ) -> std::result::Result<Vec<T>, DriverError> {
413 let dst = self.stream.clone_dtoh(src)?;
414 self.stream.synchronize()?;
415 Ok(dst)
416 }
417
418 pub fn dtod_copy<T, Src: DevicePtr<T>, Dst: DevicePtrMut<T>>(
419 &self,
420 src: &Src,
421 dst: &mut Dst,
422 ) -> std::result::Result<(), DriverError> {
423 self.stream.memcpy_dtod(src, dst)?;
424 self.stream.synchronize()
425 }
426
427 pub fn dtod_copy_async<T, Src: DevicePtr<T>, Dst: DevicePtrMut<T>>(
432 &self,
433 src: &Src,
434 dst: &mut Dst,
435 ) -> std::result::Result<(), DriverError> {
436 self.stream.memcpy_dtod(src, dst)
437 }
438
439 pub unsafe fn upgrade_device_ptr<T>(
447 &self,
448 cu_device_ptr: sys::CUdeviceptr,
449 len: usize,
450 ) -> CudaSlice<T> {
451 self.stream.upgrade_device_ptr(cu_device_ptr, len)
452 }
453
454 pub fn attribute(
455 &self,
456 attrib: sys::CUdevice_attribute,
457 ) -> std::result::Result<i32, DriverError> {
458 self.context.attribute(attrib)
459 }
460
461 pub fn synchronize(&self) -> std::result::Result<(), DriverError> {
462 self.stream.synchronize()
463 }
464
465 pub fn ordinal(&self) -> usize {
466 self.context.ordinal()
467 }
468}
469
470pub struct CudaDevice {
475 device: Arc<CudaDeviceInner>,
476}
477
478impl CudaDevice {
479 pub fn new(ordinal: usize) -> Result<Self> {
481 let context = std::panic::catch_unwind(|| CudarcContext::new(ordinal))
482 .map_err(|_| {
483 XlogError::Kernel(format!(
484 "Failed to create CUDA device {}: cudarc panicked during driver initialization",
485 ordinal
486 ))
487 })?
488 .map_err(|e| {
489 XlogError::Kernel(format!("Failed to create CUDA device {}: {}", ordinal, e))
490 })?;
491
492 let stream = context.default_stream();
493 Ok(Self {
494 device: Arc::new(CudaDeviceInner {
495 context,
496 stream,
497 modules: RwLock::new(BTreeMap::new()),
498 }),
499 })
500 }
501
502 pub fn count() -> Result<i32> {
503 std::panic::catch_unwind(|| {
504 result::init()?;
505 result::device::get_count()
506 })
507 .map_err(|_| {
508 XlogError::Kernel(
509 "Failed to count CUDA devices: cudarc panicked during driver initialization"
510 .to_string(),
511 )
512 })?
513 .map_err(|e| XlogError::Kernel(format!("Failed to count CUDA devices: {}", e)))
514 }
515
516 pub fn synchronize(&self) -> Result<()> {
517 self.device
518 .synchronize()
519 .map_err(|e| XlogError::Kernel(format!("Failed to synchronize device: {}", e)))
520 }
521
522 pub fn inner(&self) -> &Arc<CudaDeviceInner> {
523 &self.device
524 }
525
526 pub fn ordinal(&self) -> usize {
527 self.device.ordinal()
528 }
529}
530
531const _: () = {
533 fn _assert_send<T: Send>() {}
534 fn _check() {
535 _assert_send::<CudaDevice>();
536 }
537};
538
539#[cfg(test)]
540mod tests {
541 use super::*;
542
543 #[test]
544 fn test_device_creation() {
545 let device = match CudaDevice::new(0) {
546 Ok(d) => d,
547 Err(e) => {
548 eprintln!("Skipping test: CUDA runtime unavailable: {}", e);
549 return;
550 }
551 };
552 drop(device);
553 }
554
555 #[test]
556 fn test_device_synchronize() {
557 let device = match CudaDevice::new(0) {
558 Ok(d) => d,
559 Err(e) => {
560 eprintln!("Skipping test: CUDA runtime unavailable: {}", e);
561 return;
562 }
563 };
564 let result = device.synchronize();
565 assert!(result.is_ok(), "Failed to synchronize: {:?}", result.err());
566 }
567
568 #[test]
569 fn test_device_ordinal() {
570 let device = match CudaDevice::new(0) {
571 Ok(d) => d,
572 Err(e) => {
573 eprintln!("Skipping test: CUDA runtime unavailable: {}", e);
574 return;
575 }
576 };
577 assert_eq!(device.ordinal(), 0);
578 }
579
580 #[test]
581 fn test_device_inner_access() {
582 let device = match CudaDevice::new(0) {
583 Ok(d) => d,
584 Err(e) => {
585 eprintln!("Skipping test: CUDA runtime unavailable: {}", e);
586 return;
587 }
588 };
589 let inner = device.inner();
590 assert_eq!(inner.ordinal(), 0);
591 }
592
593 #[test]
594 fn test_invalid_device_ordinal() {
595 let result = CudaDevice::new(9999);
596 assert!(result.is_err(), "Should fail with invalid ordinal");
597
598 if let Err(XlogError::Kernel(msg)) = result {
599 assert!(msg.contains("9999"), "Error should mention device ordinal");
600 } else {
601 panic!("Expected XlogError::Kernel");
602 }
603 }
604}