use std::collections::HashMap;
use std::ffi::{CStr, CString, c_void};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use cudarc::driver::sys::{CUdevice_attribute, CUdeviceptr, CUfunction_attribute_enum};
use cudarc::driver::{CudaContext, CudaFunction, CudaModule, CudaStream, LaunchConfig};
use onnx_runtime_ep_api::EpError;
use onnx_runtime_ep_api::Kernel;
use onnx_runtime_ep_api::Result;
use crate::blas::CublasLt;
use crate::cudnn::CudnnBackend;
use crate::dynamic_library::{CudaLibrary, require};
use crate::error::{driver_err, nvrtc_err};
use crate::graph::CudaGraphLifecycle;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct CudaAllocationCounts {
pub allocations: u64,
pub frees: u64,
}
fn nvrtc_include_paths() -> Vec<String> {
let mut candidates = Vec::<PathBuf>::new();
for variable in ["CUDA_HOME", "CUDA_PATH"] {
if let Some(root) = std::env::var_os(variable) {
candidates.push(PathBuf::from(root).join("include"));
}
}
candidates.push(PathBuf::from("/usr/local/cuda/include"));
if let Some(paths) = std::env::var_os("LD_LIBRARY_PATH") {
for path in std::env::split_paths(&paths) {
if path.ends_with(Path::new("nvidia/cuda_nvrtc/lib"))
&& let Some(nvidia) = path.parent().and_then(Path::parent)
{
candidates.push(nvidia.join("cuda_runtime/include"));
}
}
}
candidates.sort();
candidates.dedup();
candidates
.into_iter()
.filter(|path| path.join("cuda_fp16.h").is_file())
.map(|path| path.to_string_lossy().into_owned())
.collect()
}
fn ptx_arch_for(major: u32, minor: u32) -> String {
format!("compute_{major}{minor}")
}
fn cubin_arch_for(major: u32, minor: u32) -> String {
format!("sm_{major}{minor}")
}
const SAFE_MAX_THREADS_PER_BLOCK_FALLBACK: u32 = 256;
const SAFE_SHARED_MEMORY_PER_BLOCK_FALLBACK: u32 = 48 * 1024;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct CudaDeviceCapabilities {
compute_capability: (u32, u32),
max_threads_per_block: u32,
max_shared_memory_per_block: u32,
max_shared_memory_per_block_optin: u32,
multiprocessor_count: u32,
}
impl CudaDeviceCapabilities {
fn from_reported_limits(
compute_capability: (u32, u32),
max_threads_per_block: Option<u32>,
max_shared_memory_per_block: Option<u32>,
max_shared_memory_per_block_optin: Option<u32>,
multiprocessor_count: Option<u32>,
) -> Self {
let max_threads_per_block = max_threads_per_block
.filter(|&value| value > 0)
.unwrap_or(SAFE_MAX_THREADS_PER_BLOCK_FALLBACK);
let max_shared_memory_per_block = max_shared_memory_per_block
.filter(|&value| value > 0)
.unwrap_or(SAFE_SHARED_MEMORY_PER_BLOCK_FALLBACK);
let max_shared_memory_per_block_optin = max_shared_memory_per_block_optin
.filter(|&value| value > 0)
.unwrap_or(max_shared_memory_per_block)
.max(max_shared_memory_per_block);
let multiprocessor_count = multiprocessor_count.filter(|&value| value > 0).unwrap_or(1);
Self {
compute_capability,
max_threads_per_block,
max_shared_memory_per_block,
max_shared_memory_per_block_optin,
multiprocessor_count,
}
}
pub fn compute_capability(self) -> (u32, u32) {
self.compute_capability
}
pub fn max_shared_memory_per_block_optin(self) -> u32 {
self.max_shared_memory_per_block_optin
}
pub fn max_threads_per_block(self) -> u32 {
self.max_threads_per_block
}
pub fn multiprocessor_count(self) -> u32 {
self.multiprocessor_count
}
}
fn positive_attribute(context: &CudaContext, attribute: CUdevice_attribute) -> Option<u32> {
context
.attribute(attribute)
.ok()
.and_then(|value| u32::try_from(value).ok())
.filter(|&value| value > 0)
}
fn reduction_launch_params(
preferred_threads: u32,
max_threads: u32,
bytes_per_thread: u32,
max_dynamic_shared_memory: u32,
) -> Option<(u32, u32)> {
if preferred_threads == 0 || max_threads == 0 || bytes_per_thread == 0 {
return None;
}
let threads_by_shared_memory = max_dynamic_shared_memory / bytes_per_thread;
let thread_limit = preferred_threads
.min(max_threads)
.min(threads_by_shared_memory);
if thread_limit == 0 {
return None;
}
let threads = 1 << (31 - thread_limit.leading_zeros());
Some((threads, threads * bytes_per_thread))
}
pub struct CudaRuntime {
context: Arc<CudaContext>,
stream: Arc<CudaStream>,
graph: CudaGraphLifecycle,
blas: CublasLt,
cudnn: CudnnBackend,
ordinal: u32,
capabilities: CudaDeviceCapabilities,
ptx_arch: String,
cubin_arch: String,
modules: Mutex<HashMap<&'static str, Arc<CudaModule>>>,
nvrtc_cubin_fallback: AtomicBool,
allocations: AtomicU64,
frees: AtomicU64,
capture_error: CUdeviceptr,
}
impl std::fmt::Debug for CudaRuntime {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CudaRuntime")
.field("ordinal", &self.ordinal)
.field("capabilities", &self.capabilities)
.finish()
}
}
impl CudaRuntime {
pub fn new(ordinal: u32) -> Result<Self> {
require(CudaLibrary::Driver).map_err(|message| {
EpError::KernelFailed(format!(
"cuda_ep: {message}; CPU execution remains available"
))
})?;
require(CudaLibrary::CublasLt).map_err(|message| {
EpError::KernelFailed(format!(
"cuda_ep: {message}; CPU execution remains available"
))
})?;
let context =
CudaContext::new(ordinal as usize).map_err(|e| driver_err("CudaContext::new", e))?;
let major = context
.attribute(CUdevice_attribute::CU_DEVICE_ATTRIBUTE_COMPUTE_CAPABILITY_MAJOR)
.map_err(|e| driver_err("querying CUDA compute capability major", e))?;
let minor = context
.attribute(CUdevice_attribute::CU_DEVICE_ATTRIBUTE_COMPUTE_CAPABILITY_MINOR)
.map_err(|e| driver_err("querying CUDA compute capability minor", e))?;
let major = u32::try_from(major).map_err(|_| {
EpError::KernelFailed(format!(
"cuda_ep: CUDA device {ordinal} reported invalid compute capability major {major}"
))
})?;
let minor = u32::try_from(minor).map_err(|_| {
EpError::KernelFailed(format!(
"cuda_ep: CUDA device {ordinal} reported invalid compute capability minor {minor}"
))
})?;
if major == 0 {
return Err(EpError::KernelFailed(format!(
"cuda_ep: CUDA device {ordinal} reported invalid compute capability {major}.{minor}"
)));
}
let compute_capability = (major, minor);
let capabilities = CudaDeviceCapabilities::from_reported_limits(
compute_capability,
positive_attribute(
&context,
CUdevice_attribute::CU_DEVICE_ATTRIBUTE_MAX_THREADS_PER_BLOCK,
),
positive_attribute(
&context,
CUdevice_attribute::CU_DEVICE_ATTRIBUTE_MAX_SHARED_MEMORY_PER_BLOCK,
),
positive_attribute(
&context,
CUdevice_attribute::CU_DEVICE_ATTRIBUTE_MAX_SHARED_MEMORY_PER_BLOCK_OPTIN,
),
positive_attribute(
&context,
CUdevice_attribute::CU_DEVICE_ATTRIBUTE_MULTIPROCESSOR_COUNT,
),
);
let ptx_arch = ptx_arch_for(major, minor);
let cubin_arch = cubin_arch_for(major, minor);
let stream = context
.new_stream()
.map_err(|e| driver_err("create compute stream", e))?;
let blas = CublasLt::new()?;
let cudnn = CudnnBackend::new(stream.clone());
let graph = CudaGraphLifecycle::new(stream.clone());
Self {
context,
stream,
graph,
blas,
cudnn,
ordinal,
capabilities,
ptx_arch,
cubin_arch,
modules: Mutex::new(HashMap::new()),
nvrtc_cubin_fallback: AtomicBool::new(false),
allocations: AtomicU64::new(0),
frees: AtomicU64::new(0),
capture_error: 0,
}
.with_capture_error_word()
}
fn with_capture_error_word(mut self) -> Result<Self> {
let ptr = self.alloc_raw(std::mem::size_of::<u32>())?;
unsafe { self.htod(&0_u32.to_ne_bytes(), ptr) }?;
self.capture_error = ptr;
Ok(self)
}
pub fn ordinal(&self) -> u32 {
self.ordinal
}
pub fn capabilities(&self) -> CudaDeviceCapabilities {
self.capabilities
}
pub fn blas(&self) -> &CublasLt {
&self.blas
}
pub fn cudnn(&self) -> &CudnnBackend {
&self.cudnn
}
pub fn stream_ptr(&self) -> cudarc::driver::sys::CUstream {
self.stream.cu_stream()
}
pub fn stream(&self) -> &Arc<CudaStream> {
&self.stream
}
pub fn begin_graph_capture(&self, kernels: &[&dyn Kernel]) -> Result<()> {
crate::capture::require_subgraph_graph_capturable(kernels)?;
self.graph.begin()
}
pub fn end_graph_capture(&self) -> Result<()> {
self.graph.end()
}
pub fn abort_graph_capture(&self) -> Result<()> {
self.graph.abort()
}
pub fn replay_graph(&self) -> Result<()> {
self.graph.replay()
}
pub fn replay_graph_segment(&self, index: usize) -> Result<()> {
self.graph.replay_segment(index)
}
pub fn graph_segment_count(&self) -> Result<usize> {
self.graph.segment_count()
}
pub fn reset_graph(&self) -> Result<bool> {
self.graph.reset()
}
pub fn has_graph_executable(&self) -> Result<bool> {
self.graph.has_executable()
}
pub fn capture_error_ptr(&self) -> CUdeviceptr {
self.capture_error
}
pub fn check_capture_error(&self) -> Result<u32> {
let mut bytes = [0_u8; std::mem::size_of::<u32>()];
unsafe { self.dtoh(&mut bytes, self.capture_error) }?;
Ok(u32::from_ne_bytes(bytes))
}
pub fn reset_capture_error(&self) -> Result<()> {
unsafe { self.htod(&0_u32.to_ne_bytes(), self.capture_error) }
}
pub fn graph_capture_status(&self) -> Result<cudarc::driver::sys::CUstreamCaptureStatus> {
self.graph.capture_status()
}
pub fn allocation_counts(&self) -> CudaAllocationCounts {
CudaAllocationCounts {
allocations: self.allocations.load(Ordering::Relaxed),
frees: self.frees.load(Ordering::Relaxed),
}
}
pub fn reduction_launch_config(
&self,
function: &CudaFunction,
grid_x: u32,
preferred_threads: u32,
bytes_per_thread: u32,
) -> Result<LaunchConfig> {
let function_max_threads = function
.max_threads_per_block()
.map_err(|error| driver_err("querying CUDA function max threads", error))?;
let function_max_threads = u32::try_from(function_max_threads).map_err(|_| {
EpError::KernelFailed(format!(
"cuda_ep: CUDA function reported invalid max threads {function_max_threads}"
))
})?;
let static_shared_memory = function
.shared_size_bytes()
.map_err(|error| driver_err("querying CUDA function static shared memory", error))?;
let static_shared_memory = u32::try_from(static_shared_memory).map_err(|_| {
EpError::KernelFailed(format!(
"cuda_ep: CUDA function reported invalid static shared memory {static_shared_memory}"
))
})?;
let max_dynamic_shared_memory = self
.capabilities
.max_shared_memory_per_block_optin
.saturating_sub(static_shared_memory);
let max_threads = self
.capabilities
.max_threads_per_block
.min(function_max_threads);
let (threads, shared_mem_bytes) = reduction_launch_params(
preferred_threads,
max_threads,
bytes_per_thread,
max_dynamic_shared_memory,
)
.ok_or_else(|| {
EpError::KernelFailed(format!(
"cuda_ep: reduction launch needs {bytes_per_thread} shared-memory bytes per \
thread, but device SM {}.{} allows {max_dynamic_shared_memory} dynamic bytes",
self.capabilities.compute_capability.0, self.capabilities.compute_capability.1,
))
})?;
let default_dynamic_shared_memory = self
.capabilities
.max_shared_memory_per_block
.saturating_sub(static_shared_memory);
if shared_mem_bytes > default_dynamic_shared_memory {
let shared_mem_bytes_i32 = i32::try_from(shared_mem_bytes).map_err(|_| {
EpError::KernelFailed(format!(
"cuda_ep: dynamic shared-memory request {shared_mem_bytes} exceeds i32"
))
})?;
function
.set_attribute(
CUfunction_attribute_enum::CU_FUNC_ATTRIBUTE_MAX_DYNAMIC_SHARED_SIZE_BYTES,
shared_mem_bytes_i32,
)
.map_err(|error| {
driver_err("opting CUDA function into dynamic shared memory", error)
})?;
}
Ok(LaunchConfig {
grid_dim: (grid_x, 1, 1),
block_dim: (threads, 1, 1),
shared_mem_bytes,
})
}
pub fn nvrtc_function(
&self,
module_key: &'static str,
src: &str,
entry: &str,
) -> Result<CudaFunction> {
require(CudaLibrary::Nvrtc).map_err(|message| {
EpError::KernelFailed(format!(
"cuda_ep: {message}; CPU execution remains available"
))
})?;
self.bind()?;
let module = {
let mut cache = self.modules.lock().expect("cuda_ep module cache poisoned");
if let Some(m) = cache.get(module_key) {
m.clone()
} else {
let include_paths = nvrtc_include_paths();
let m = if self.nvrtc_cubin_fallback.load(Ordering::Relaxed) {
self.load_nvrtc_cubin(module_key, src, &include_paths)?
} else {
let opts = cudarc::nvrtc::CompileOptions {
include_paths: include_paths.clone(),
options: vec![format!("--gpu-architecture={}", self.ptx_arch)],
..Default::default()
};
let ptx = cudarc::nvrtc::compile_ptx_with_opts(src, opts).map_err(|e| {
nvrtc_err(&format!("compiling NVRTC module '{module_key}'"), e)
})?;
match self.context.load_module(ptx) {
Ok(module) => module,
Err(error)
if error.0
== cudarc::driver::sys::CUresult::CUDA_ERROR_UNSUPPORTED_PTX_VERSION =>
{
self.nvrtc_cubin_fallback.store(true, Ordering::Relaxed);
self.load_nvrtc_cubin(module_key, src, &include_paths)?
}
Err(error) => {
return Err(driver_err(
&format!("loading NVRTC module '{module_key}'"),
error,
));
}
}
};
cache.insert(module_key, m.clone());
m
}
};
module
.load_function(entry)
.map_err(|e| driver_err(&format!("loading NVRTC function '{entry}'"), e))
}
fn load_nvrtc_cubin(
&self,
module_key: &'static str,
src: &str,
include_paths: &[String],
) -> Result<Arc<CudaModule>> {
let source = CString::new(src).map_err(|_| {
EpError::KernelFailed(format!(
"cuda_ep: compiling NVRTC module '{module_key}': source contains a NUL byte"
))
})?;
let name = CString::new(module_key).expect("static module key cannot contain a NUL byte");
let program =
cudarc::nvrtc::result::create_program(source.as_c_str(), Some(name.as_c_str()))
.map_err(|error| {
EpError::KernelFailed(format!(
"cuda_ep: creating NVRTC CUBIN module '{module_key}': {error:?}"
))
})?;
let mut options = include_paths
.iter()
.map(|path| format!("--include-path={path}"))
.collect::<Vec<_>>();
options.push(format!("--gpu-architecture={}", self.cubin_arch));
let compile_result = unsafe { cudarc::nvrtc::result::compile_program(program, &options) };
if let Err(error) = compile_result {
let log = unsafe { cudarc::nvrtc::result::get_program_log(program) }
.ok()
.map(|bytes| {
unsafe { CStr::from_ptr(bytes.as_ptr()) }
.to_string_lossy()
.into_owned()
})
.unwrap_or_else(|| "<compiler log unavailable>".into());
let _ = unsafe { cudarc::nvrtc::result::destroy_program(program) };
return Err(EpError::KernelFailed(format!(
"cuda_ep: compiling NVRTC CUBIN module '{module_key}' failed ({error:?}); compiler log:\n{log}"
)));
}
let cubin: Result<Vec<u8>> = (|| {
let mut size = 0usize;
unsafe { cudarc::nvrtc::sys::nvrtcGetCUBINSize(program, &mut size) }
.result()
.map_err(|error| {
EpError::KernelFailed(format!(
"cuda_ep: getting NVRTC CUBIN size for '{module_key}': {error:?}"
))
})?;
let mut image = vec![0u8; size];
unsafe { cudarc::nvrtc::sys::nvrtcGetCUBIN(program, image.as_mut_ptr().cast()) }
.result()
.map_err(|error| {
EpError::KernelFailed(format!(
"cuda_ep: getting NVRTC CUBIN for '{module_key}': {error:?}"
))
})?;
Ok(image)
})();
let destroy_result = unsafe { cudarc::nvrtc::result::destroy_program(program) };
let image = cubin?;
destroy_result.map_err(|error| {
EpError::KernelFailed(format!(
"cuda_ep: destroying NVRTC CUBIN program '{module_key}': {error:?}"
))
})?;
self.context
.load_module(cudarc::nvrtc::Ptx::from_binary(image))
.map_err(|error| {
driver_err(
&format!("loading NVRTC CUBIN fallback module '{module_key}'"),
error,
)
})
}
pub fn require_nvrtc_half_headers(&self, op: &str) -> Result<()> {
if nvrtc_include_paths().is_empty() {
return Err(EpError::KernelFailed(format!(
"cuda_ep {op}: f16/bf16 NVRTC kernels require cuda_fp16.h and cuda_bf16.h. \
Install the CUDA runtime headers (for pip CUDA 13: `pip install \
nvidia-cuda-runtime`; alternatively set CUDA_HOME/CUDA_PATH)."
)));
}
Ok(())
}
pub fn bind(&self) -> Result<()> {
self.context
.bind_to_thread()
.map_err(|e| driver_err("bind_to_thread", e))
}
pub fn synchronize(&self) -> Result<()> {
self.stream
.synchronize()
.map_err(|e| driver_err("stream synchronize", e))
}
pub fn is_capturing(&self) -> Result<bool> {
Ok(self.graph_capture_status()?
!= cudarc::driver::sys::CUstreamCaptureStatus::CU_STREAM_CAPTURE_STATUS_NONE)
}
pub fn alloc_raw(&self, bytes: usize) -> Result<CUdeviceptr> {
self.bind()?;
let ptr = unsafe { cudarc::driver::result::malloc_sync(bytes.max(1)) }
.map_err(|e| driver_err("cuMemAlloc", e))?;
self.allocations.fetch_add(1, Ordering::Relaxed);
Ok(ptr)
}
pub unsafe fn free_raw(&self, ptr: CUdeviceptr) -> Result<()> {
self.bind()?;
unsafe { cudarc::driver::result::free_sync(ptr) }
.map_err(|e| driver_err("cuMemFree", e))?;
self.frees.fetch_add(1, Ordering::Relaxed);
Ok(())
}
pub unsafe fn htod(&self, src: &[u8], dst: CUdeviceptr) -> Result<()> {
self.bind()?;
unsafe { cudarc::driver::result::memcpy_htod_sync(dst, src) }
.map_err(|e| driver_err("cuMemcpyHtoD", e))
}
pub unsafe fn dtoh(&self, dst: &mut [u8], src: CUdeviceptr) -> Result<()> {
self.bind()?;
self.synchronize()?;
unsafe { cudarc::driver::result::memcpy_dtoh_sync(dst, src) }
.map_err(|e| driver_err("cuMemcpyDtoH", e))
}
pub unsafe fn dtod(&self, src: CUdeviceptr, dst: CUdeviceptr, bytes: usize) -> Result<()> {
self.bind()?;
unsafe { cudarc::driver::result::memcpy_dtod_sync(dst, src, bytes) }
.map_err(|e| driver_err("cuMemcpyDtoD", e))
}
}
impl Drop for CudaRuntime {
fn drop(&mut self) {
if self.capture_error != 0 {
let _ = unsafe { self.free_raw(self.capture_error) };
self.capture_error = 0;
}
}
}
#[inline]
pub fn cuptr(raw: *const c_void) -> CUdeviceptr {
raw as usize as CUdeviceptr
}
#[inline]
pub fn raw_ptr(dptr: CUdeviceptr) -> *mut c_void {
dptr as usize as *mut c_void
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn derives_ptx_arch_from_compute_capability() {
for (major, minor, expected) in [
(6, 0, "compute_60"),
(7, 5, "compute_75"),
(8, 0, "compute_80"),
(8, 6, "compute_86"),
(8, 9, "compute_89"),
(9, 0, "compute_90"),
(10, 0, "compute_100"),
(12, 0, "compute_120"),
] {
assert_eq!(ptx_arch_for(major, minor), expected);
}
}
#[test]
fn derives_cubin_arch_from_compute_capability() {
for (major, minor, expected) in [
(6, 0, "sm_60"),
(7, 5, "sm_75"),
(8, 0, "sm_80"),
(8, 6, "sm_86"),
(8, 9, "sm_89"),
(9, 0, "sm_90"),
(10, 0, "sm_100"),
(12, 0, "sm_120"),
] {
assert_eq!(cubin_arch_for(major, minor), expected);
}
}
#[test]
fn capability_limits_use_conservative_fallbacks() {
let capabilities =
CudaDeviceCapabilities::from_reported_limits((7, 0), None, None, None, None);
assert_eq!(capabilities.compute_capability(), (7, 0));
assert_eq!(capabilities.max_threads_per_block, 256);
assert_eq!(
capabilities.max_shared_memory_per_block,
SAFE_SHARED_MEMORY_PER_BLOCK_FALLBACK
);
assert_eq!(
capabilities.max_shared_memory_per_block_optin(),
SAFE_SHARED_MEMORY_PER_BLOCK_FALLBACK
);
assert_eq!(capabilities.multiprocessor_count(), 1);
}
#[test]
fn capability_limits_never_reduce_optin_below_default() {
let capabilities = CudaDeviceCapabilities::from_reported_limits(
(12, 0),
Some(1024),
Some(64 * 1024),
Some(48 * 1024),
Some(200),
);
assert_eq!(capabilities.max_shared_memory_per_block_optin(), 64 * 1024);
assert_eq!(capabilities.multiprocessor_count(), 200);
}
#[test]
fn reduction_launch_is_clamped_to_device_limits() {
assert_eq!(
reduction_launch_params(256, 1024, 4, 227 * 1024),
Some((256, 1024))
);
assert_eq!(
reduction_launch_params(256, 128, 4, 227 * 1024),
Some((128, 512))
);
assert_eq!(reduction_launch_params(256, 1024, 4, 768), Some((128, 512)));
assert_eq!(reduction_launch_params(256, 1024, 8, 0), None);
}
#[test]
fn nvrtc_include_paths_only_returns_cuda_header_dirs() {
assert!(
nvrtc_include_paths()
.iter()
.all(|path| Path::new(path).join("cuda_fp16.h").is_file())
);
}
}