use libloading::{Library, Symbol};
use once_cell::sync::OnceCell;
use rattler_conda_types::Version;
use serde::{Deserialize, Serialize};
use std::process::Command;
use std::sync::atomic::{AtomicBool, Ordering};
use std::{
os::raw::{c_int, c_uint, c_void},
path::Path,
ptr,
str::FromStr,
};
mod cache;
const NVML_SUCCESS: c_int = 0;
const NVML_ERROR_UNINITIALIZED: c_int = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum NvmlCudaVersionError {
MissingSymbol,
Nvml(c_int),
InvalidVersion,
}
impl NvmlCudaVersionError {
fn should_retry_after_init(self) -> bool {
matches!(self, Self::Nvml(NVML_ERROR_UNINITIALIZED))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub(super) enum CudaDetectionMethod {
NvmlNoInit,
NvmlInitialized,
Libcuda,
NvidiaSmi,
}
impl CudaDetectionMethod {
fn as_str(self) -> &'static str {
match self {
Self::NvmlNoInit => "nvml_no_init",
Self::NvmlInitialized => "nvml_initialized",
Self::Libcuda => "libcuda",
Self::NvidiaSmi => "nvidia_smi",
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(super) struct CudaInfoSources {
pub version: Option<CudaDetectionMethod>,
pub arch: Option<CudaDetectionMethod>,
}
impl CudaInfoSources {
fn version_str(self) -> &'static str {
self.version
.map_or("<unknown>", CudaDetectionMethod::as_str)
}
fn arch_str(self) -> &'static str {
self.arch.map_or("<unknown>", CudaDetectionMethod::as_str)
}
}
struct DetectedCudaInfo {
info: CudaInfo,
sources: CudaInfoSources,
}
fn parse_cuda_driver_version(version: c_int) -> Option<Version> {
if !(1000..=99_990).contains(&version) {
tracing::trace!(version, "rejecting implausible CUDA driver version integer");
return None;
}
Version::from_str(&format!("{}.{}", version / 1000, (version % 1000) / 10)).ok()
}
pub(crate) fn is_valid_cuda_version_format(s: &str) -> bool {
let mut parts = s.split('.');
match (parts.next(), parts.next(), parts.next()) {
(Some(major), Some(minor), None) => {
!major.is_empty()
&& major.chars().all(|c| c.is_ascii_digit())
&& !minor.is_empty()
&& minor.chars().all(|c| c.is_ascii_digit())
}
_ => false,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CudaArchInfo {
pub major: u32,
pub minor: u32,
}
#[derive(Debug, Clone)]
pub struct CudaInfo {
pub version: Option<Version>,
pub arch_info: Option<CudaArchInfo>,
}
fn display_cuda_version(version: Option<&Version>) -> String {
version.map_or_else(|| "<none>".to_string(), ToString::to_string)
}
fn display_cuda_arch(arch_info: Option<&CudaArchInfo>) -> String {
arch_info.map_or_else(
|| "<none>".to_string(),
|arch| format!("{}.{}", arch.major, arch.minor),
)
}
pub fn cuda_info(cache_dir: Option<&Path>) -> &'static CudaInfo {
static DETECTED_CUDA_INFO: OnceCell<DetectedCudaInfo> = OnceCell::new();
static PERSISTED: AtomicBool = AtomicBool::new(false);
cuda_info_impl(
&cache::CacheEnv::current(),
&DETECTED_CUDA_INFO,
&PERSISTED,
cache_dir,
)
}
fn cuda_info_impl<'a>(
env: &cache::CacheEnv,
state: &'a OnceCell<DetectedCudaInfo>,
persisted: &AtomicBool,
cache_dir: Option<&Path>,
) -> &'a CudaInfo {
if let Some(detected) = state.get() {
tracing::trace!(info = ?detected.info, "using process-cached CUDA info");
maybe_persist(env, detected, persisted, cache_dir);
return &detected.info;
}
let detected = state.get_or_init(|| {
if let Some(cache_dir) = cache_dir {
tracing::trace!(cache_dir = %cache_dir.display(), "checking CUDA info cache");
if let Some(cached) = cache::read_with_env(env, cache_dir) {
tracing::debug!(
version = %display_cuda_version(cached.info.version.as_ref()),
arch = %display_cuda_arch(cached.info.arch_info.as_ref()),
version_source = cached.sources.version_str(),
arch_source = cached.sources.arch_str(),
"using disk-cached CUDA info"
);
persisted.store(true, Ordering::Relaxed);
return cached;
}
} else {
tracing::trace!("CUDA info disk cache disabled");
}
tracing::trace!("detecting CUDA info from host");
let detected = detect_cuda_info();
tracing::debug!(
version = %display_cuda_version(detected.info.version.as_ref()),
arch = %display_cuda_arch(detected.info.arch_info.as_ref()),
version_source = detected.sources.version_str(),
arch_source = detected.sources.arch_str(),
"detected CUDA info from host"
);
detected
});
maybe_persist(env, detected, persisted, cache_dir);
&detected.info
}
fn maybe_persist(
env: &cache::CacheEnv,
detected: &DetectedCudaInfo,
persisted: &AtomicBool,
cache_dir: Option<&Path>,
) {
let Some(cache_dir) = cache_dir else {
return;
};
if persisted.load(Ordering::Relaxed) {
return;
}
cache::write_with_env(env, cache_dir, &detected.info, detected.sources);
persisted.store(true, Ordering::Relaxed);
}
pub fn cuda_version(cache_dir: Option<&Path>) -> Option<Version> {
cuda_info(cache_dir).version.clone()
}
pub fn cuda_arch(cache_dir: Option<&Path>) -> Option<CudaArchInfo> {
cuda_info(cache_dir).arch_info.clone()
}
fn detect_cuda_info() -> DetectedCudaInfo {
let mut detected = if cfg!(target_env = "musl") {
tracing::trace!("detecting CUDA info via nvidia-smi because musl cannot load NVML");
let version = detect_cuda_version_via_nvidia_smi();
let arch_info = version
.as_ref()
.and_then(|_| detect_cuda_arch_via_nvidia_smi());
DetectedCudaInfo {
sources: CudaInfoSources {
version: version.is_some().then_some(CudaDetectionMethod::NvidiaSmi),
arch: arch_info
.is_some()
.then_some(CudaDetectionMethod::NvidiaSmi),
},
info: CudaInfo { version, arch_info },
}
} else {
tracing::trace!("detecting CUDA info via NVML");
let mut detected = detect_cuda_info_via_nvml();
if detected.info.version.is_none() {
tracing::debug!(
"NVML did not detect a CUDA driver version; trying libcuda/nvidia-smi fallbacks"
);
if let Some((version, source)) = detect_cuda_version_fallbacks() {
detected.info.version = Some(version);
detected.sources.version = Some(source);
}
}
if detected.info.version.is_some() && detected.info.arch_info.is_none() {
tracing::debug!(
"NVML did not detect CUDA compute capability; trying nvidia-smi fallback"
);
detected.info.arch_info = detect_cuda_arch_via_nvidia_smi();
if detected.info.arch_info.is_some() {
detected.sources.arch = Some(CudaDetectionMethod::NvidiaSmi);
}
}
if detected.info.version.is_some() && detected.info.arch_info.is_none() {
tracing::debug!(
"nvidia-smi did not detect CUDA compute capability; trying libcuda fallback"
);
detected.info.arch_info = detect_cuda_arch_via_libcuda();
if detected.info.arch_info.is_some() {
detected.sources.arch = Some(CudaDetectionMethod::Libcuda);
}
}
detected
};
if detected.info.version.is_none()
&& (detected.info.arch_info.is_some() || detected.sources.arch.is_some())
{
tracing::debug!(
"dropping CUDA compute capability because no CUDA driver version was detected"
);
detected.info.arch_info = None;
detected.sources.arch = None;
}
detected
}
fn detect_cuda_info_via_nvml() -> DetectedCudaInfo {
let mut library = None;
for path in nvml_library_paths() {
match unsafe { Library::new(*path) } {
Ok(loaded) => {
tracing::trace!(library_path = *path, "loaded NVML library");
library = Some(loaded);
break;
}
Err(err) => {
tracing::trace!(library_path = *path, error = %err, "failed to load NVML library");
}
}
}
let Some(library) = library else {
tracing::debug!("could not load NVML library from any known path");
return DetectedCudaInfo {
info: CudaInfo {
version: None,
arch_info: None,
},
sources: CudaInfoSources::default(),
};
};
let no_init_version = match cuda_version_from_nvml_library(&library) {
Ok(version) => {
tracing::trace!(%version, "detected CUDA driver version via NVML without init");
Some(version)
}
Err(err) => {
tracing::trace!(
?err,
"CUDA driver version query via NVML without init failed"
);
None
}
};
let (initialized_version, arch_info) =
detect_cuda_initialized_info_via_nvml(&library, true, true);
let (version, version_source) = if let Some(version) = initialized_version {
(Some(version), Some(CudaDetectionMethod::NvmlInitialized))
} else if let Some(version) = no_init_version {
(Some(version), Some(CudaDetectionMethod::NvmlNoInit))
} else {
(None, None)
};
let arch_source = arch_info
.as_ref()
.map(|_| CudaDetectionMethod::NvmlInitialized);
DetectedCudaInfo {
info: CudaInfo { version, arch_info },
sources: CudaInfoSources {
version: version_source,
arch: arch_source,
},
}
}
fn cuda_version_from_nvml_library(library: &Library) -> Result<Version, NvmlCudaVersionError> {
let nvml_system_get_cuda_driver_version: Symbol<'_, unsafe extern "C" fn(*mut c_int) -> c_int> =
unsafe {
library
.get(b"nvmlSystemGetCudaDriverVersion_v2\0")
.or_else(|_| library.get(b"nvmlSystemGetCudaDriverVersion\0"))
}
.map_err(|_err| NvmlCudaVersionError::MissingSymbol)?;
let mut cuda_driver_version: c_int = 0;
let result = unsafe { nvml_system_get_cuda_driver_version(&mut cuda_driver_version) };
if result != NVML_SUCCESS {
return Err(NvmlCudaVersionError::Nvml(result));
}
parse_cuda_driver_version(cuda_driver_version).ok_or(NvmlCudaVersionError::InvalidVersion)
}
fn detect_cuda_initialized_info_via_nvml(
library: &Library,
query_version: bool,
query_arch: bool,
) -> (Option<Version>, Option<CudaArchInfo>) {
type NvmlDevice = *mut c_void;
let Some(nvml_init): Option<Symbol<'_, unsafe extern "C" fn() -> c_int>> = (unsafe {
library
.get(b"nvmlInit_v2\0")
.or_else(|_| library.get(b"nvmlInit\0"))
.ok()
}) else {
tracing::debug!("missing nvmlInit symbol");
return (None, None);
};
let Some(nvml_shutdown): Option<Symbol<'_, unsafe extern "C" fn() -> c_int>> =
(unsafe { library.get(b"nvmlShutdown\0").ok() })
else {
tracing::debug!("missing nvmlShutdown symbol");
return (None, None);
};
tracing::trace!(query_version, query_arch, "initializing NVML");
let init_result = unsafe { nvml_init() };
if init_result != NVML_SUCCESS {
tracing::debug!(return_code = init_result, "nvmlInit failed");
return (None, None);
}
let version = if query_version {
match cuda_version_from_nvml_library(library) {
Ok(version) => {
tracing::debug!(%version, "detected CUDA driver version via initialized NVML");
Some(version)
}
Err(err) => {
tracing::debug!(
?err,
"CUDA driver version query via initialized NVML failed"
);
None
}
}
} else {
None
};
let arch_info = query_arch
.then(|| {
tracing::trace!("querying CUDA compute capability via NVML");
let nvml_device_get_count: Symbol<'_, unsafe extern "C" fn(*mut c_uint) -> c_int> =
match unsafe {
library
.get(b"nvmlDeviceGetCount_v2\0")
.or_else(|_| library.get(b"nvmlDeviceGetCount\0"))
} {
Ok(symbol) => symbol,
Err(err) => {
tracing::trace!(error = %err, "missing NVML device count symbol");
return None;
}
};
let nvml_device_get_handle_by_index: Symbol<
'_,
unsafe extern "C" fn(c_uint, *mut NvmlDevice) -> c_int,
> = match unsafe {
library
.get(b"nvmlDeviceGetHandleByIndex_v2\0")
.or_else(|_| library.get(b"nvmlDeviceGetHandleByIndex\0"))
} {
Ok(symbol) => symbol,
Err(err) => {
tracing::trace!(error = %err, "missing NVML device handle symbol");
return None;
}
};
let nvml_device_get_cuda_compute_capability: Symbol<
'_,
unsafe extern "C" fn(NvmlDevice, *mut c_int, *mut c_int) -> c_int,
> = match unsafe { library.get(b"nvmlDeviceGetCudaComputeCapability\0") } {
Ok(symbol) => symbol,
Err(err) => {
tracing::trace!(
error = %err,
"missing NVML CUDA compute capability symbol"
);
return None;
}
};
let mut device_count: c_uint = 0;
let device_count_result = unsafe { nvml_device_get_count(&mut device_count) };
if device_count_result != NVML_SUCCESS {
tracing::trace!(
return_code = device_count_result,
"nvmlDeviceGetCount failed"
);
return None;
}
tracing::trace!(device_count, "enumerating CUDA devices via NVML");
let mut min_arch: Option<CudaArchInfo> = None;
for device_idx in 0..device_count {
let mut device: NvmlDevice = ptr::null_mut();
let handle_result =
unsafe { nvml_device_get_handle_by_index(device_idx, &mut device) };
if handle_result != NVML_SUCCESS {
tracing::trace!(
device_idx,
return_code = handle_result,
"failed to get NVML device handle"
);
continue;
}
let mut cc_major: c_int = 0;
let mut cc_minor: c_int = 0;
let compute_capability_result = unsafe {
nvml_device_get_cuda_compute_capability(device, &mut cc_major, &mut cc_minor)
};
if compute_capability_result != NVML_SUCCESS {
tracing::trace!(
device_idx,
return_code = compute_capability_result,
"failed to get CUDA compute capability via NVML"
);
continue;
}
let cc_major = cc_major as u32;
let cc_minor = cc_minor as u32;
tracing::trace!(
device_idx,
major = cc_major,
minor = cc_minor,
"detected CUDA compute capability via NVML"
);
let is_new_minimum = min_arch.as_ref().is_none_or(|min| {
cc_major < min.major || (cc_major == min.major && cc_minor < min.minor)
});
if is_new_minimum {
min_arch = Some(CudaArchInfo {
major: cc_major,
minor: cc_minor,
});
}
}
if let Some(arch) = min_arch.as_ref() {
tracing::debug!(
major = arch.major,
minor = arch.minor,
"selected minimum CUDA compute capability"
);
} else {
tracing::debug!("no CUDA compute capability detected via NVML");
}
min_arch
})
.flatten();
let shutdown_result = unsafe { nvml_shutdown() };
if shutdown_result != NVML_SUCCESS {
tracing::debug!(return_code = shutdown_result, "nvmlShutdown failed");
}
(version, arch_info)
}
pub fn detect_cuda_version() -> Option<Version> {
if cfg!(target_env = "musl") {
detect_cuda_version_via_nvidia_smi()
} else {
detect_cuda_version_via_nvml().or_else(|| {
tracing::debug!(
"NVML did not detect a CUDA driver version; trying libcuda/nvidia-smi fallbacks"
);
detect_cuda_version_fallbacks().map(|(version, _source)| version)
})
}
}
fn detect_cuda_version_fallbacks() -> Option<(Version, CudaDetectionMethod)> {
if cfg!(target_env = "musl") {
return detect_cuda_version_via_nvidia_smi()
.map(|version| (version, CudaDetectionMethod::NvidiaSmi));
}
let version = detect_cuda_version_via_libcuda();
if let Some(version) = version {
return Some((version, CudaDetectionMethod::Libcuda));
}
tracing::debug!("libcuda did not detect a CUDA driver version; trying nvidia-smi fallback");
detect_cuda_version_via_nvidia_smi().map(|version| (version, CudaDetectionMethod::NvidiaSmi))
}
pub fn detect_cuda_version_via_nvml() -> Option<Version> {
let mut library = None;
for path in nvml_library_paths() {
match unsafe { libloading::Library::new(*path) } {
Ok(loaded) => {
tracing::trace!(library_path = *path, "loaded NVML library");
library = Some(loaded);
break;
}
Err(err) => {
tracing::trace!(library_path = *path, error = %err, "failed to load NVML library");
}
}
}
let library = library?;
match cuda_version_from_nvml_library(&library) {
Ok(version) => {
tracing::trace!(%version, "detected CUDA driver version via NVML without init");
Some(version)
}
Err(err) if err.should_retry_after_init() => {
tracing::debug!(?err, "retrying CUDA driver version query after nvmlInit");
detect_cuda_initialized_info_via_nvml(&library, true, false).0
}
Err(err) => {
tracing::debug!(?err, "CUDA driver version query via NVML failed");
None
}
}
}
fn nvml_library_paths() -> &'static [&'static str] {
#[cfg(target_os = "macos")]
static FILENAMES: &[&str] = &[
"libnvidia-ml.1.dylib", "libnvidia-ml.dylib",
"/usr/local/cuda/lib/libnvidia-ml.1.dylib",
"/usr/local/cuda/lib/libnvidia-ml.dylib",
];
#[cfg(target_os = "linux")]
static FILENAMES: &[&str] = &[
"libnvidia-ml.so.1", "libnvidia-ml.so",
"/usr/lib64/nvidia/libnvidia-ml.so.1", "/usr/lib64/nvidia/libnvidia-ml.so",
"/usr/lib/x86_64-linux-gnu/libnvidia-ml.so.1", "/usr/lib/x86_64-linux-gnu/libnvidia-ml.so",
"/usr/lib/wsl/lib/libnvidia-ml.so.1", "/usr/lib/wsl/lib/libnvidia-ml.so",
];
#[cfg(windows)]
static FILENAMES: &[&str] = &["nvml.dll"];
#[cfg(not(any(target_os = "linux", target_os = "macos", windows)))]
static FILENAMES: &[&str] = &[];
FILENAMES
}
pub fn detect_cuda_version_via_libcuda() -> Option<Version> {
let mut cuda_library = None;
for path in cuda_library_paths() {
match unsafe { libloading::Library::new(*path) } {
Ok(loaded) => {
tracing::trace!(library_path = *path, "loaded CUDA driver library");
cuda_library = Some(loaded);
break;
}
Err(err) => {
tracing::trace!(
library_path = *path,
error = %err,
"failed to load CUDA driver library"
);
}
}
}
let cuda_library = cuda_library?;
let cu_init: Symbol<'_, unsafe extern "C" fn(c_uint) -> c_int> =
match unsafe { cuda_library.get(b"cuInit\0") } {
Ok(symbol) => symbol,
Err(err) => {
tracing::debug!(error = %err, "missing cuInit symbol");
return None;
}
};
let cu_driver_get_version: Symbol<'_, unsafe extern "C" fn(*mut c_int) -> c_int> =
match unsafe { cuda_library.get(b"cuDriverGetVersion\0") } {
Ok(symbol) => symbol,
Err(err) => {
tracing::debug!(error = %err, "missing cuDriverGetVersion symbol");
return None;
}
};
let init_result = unsafe { cu_init(0) };
if init_result != 0 {
tracing::debug!(return_code = init_result, "cuInit failed");
return None;
}
let mut version_int: c_int = 0;
let version_result = unsafe { cu_driver_get_version(&mut version_int) };
if version_result != 0 {
tracing::debug!(return_code = version_result, "cuDriverGetVersion failed");
return None;
}
let version = parse_cuda_driver_version(version_int);
if let Some(version) = &version {
tracing::trace!(%version, "detected CUDA driver version via libcuda");
} else {
tracing::trace!("failed to parse CUDA driver version reported by libcuda");
}
version
}
fn detect_cuda_arch_via_libcuda() -> Option<CudaArchInfo> {
const CU_DEVICE_ATTRIBUTE_COMPUTE_CAPABILITY_MAJOR: c_int = 75;
const CU_DEVICE_ATTRIBUTE_COMPUTE_CAPABILITY_MINOR: c_int = 76;
let mut cuda_library = None;
for path in cuda_library_paths() {
match unsafe { libloading::Library::new(*path) } {
Ok(loaded) => {
tracing::trace!(library_path = *path, "loaded CUDA driver library");
cuda_library = Some(loaded);
break;
}
Err(err) => {
tracing::trace!(
library_path = *path,
error = %err,
"failed to load CUDA driver library"
);
}
}
}
let cuda_library = cuda_library?;
let cu_init: Symbol<'_, unsafe extern "C" fn(c_uint) -> c_int> =
match unsafe { cuda_library.get(b"cuInit\0") } {
Ok(symbol) => symbol,
Err(err) => {
tracing::debug!(error = %err, "missing cuInit symbol");
return None;
}
};
let cu_device_get_count: Symbol<'_, unsafe extern "C" fn(*mut c_int) -> c_int> =
match unsafe { cuda_library.get(b"cuDeviceGetCount\0") } {
Ok(symbol) => symbol,
Err(err) => {
tracing::debug!(error = %err, "missing cuDeviceGetCount symbol");
return None;
}
};
let cu_device_get: Symbol<'_, unsafe extern "C" fn(*mut c_int, c_int) -> c_int> =
match unsafe { cuda_library.get(b"cuDeviceGet\0") } {
Ok(symbol) => symbol,
Err(err) => {
tracing::debug!(error = %err, "missing cuDeviceGet symbol");
return None;
}
};
let cu_device_get_attribute: Symbol<
'_,
unsafe extern "C" fn(*mut c_int, c_int, c_int) -> c_int,
> = match unsafe { cuda_library.get(b"cuDeviceGetAttribute\0") } {
Ok(symbol) => symbol,
Err(err) => {
tracing::debug!(error = %err, "missing cuDeviceGetAttribute symbol");
return None;
}
};
let init_result = unsafe { cu_init(0) };
if init_result != 0 {
tracing::debug!(return_code = init_result, "cuInit failed");
return None;
}
let mut device_count: c_int = 0;
let device_count_result = unsafe { cu_device_get_count(&mut device_count) };
if device_count_result != 0 {
tracing::trace!(return_code = device_count_result, "cuDeviceGetCount failed");
return None;
}
tracing::trace!(device_count, "enumerating CUDA devices via libcuda");
let mut min_arch: Option<CudaArchInfo> = None;
for device_idx in 0..device_count {
let mut device: c_int = 0;
let device_result = unsafe { cu_device_get(&mut device, device_idx) };
if device_result != 0 {
tracing::trace!(
device_idx,
return_code = device_result,
"failed to get CUDA device handle"
);
continue;
}
let mut cc_major: c_int = 0;
let mut cc_minor: c_int = 0;
let major_result = unsafe {
cu_device_get_attribute(
&mut cc_major,
CU_DEVICE_ATTRIBUTE_COMPUTE_CAPABILITY_MAJOR,
device,
)
};
let minor_result = unsafe {
cu_device_get_attribute(
&mut cc_minor,
CU_DEVICE_ATTRIBUTE_COMPUTE_CAPABILITY_MINOR,
device,
)
};
if major_result != 0 || minor_result != 0 {
tracing::trace!(
device_idx,
major_return_code = major_result,
minor_return_code = minor_result,
"failed to get CUDA compute capability via libcuda"
);
continue;
}
let cc_major = cc_major as u32;
let cc_minor = cc_minor as u32;
tracing::trace!(
device_idx,
major = cc_major,
minor = cc_minor,
"detected CUDA compute capability via libcuda"
);
let is_new_minimum = min_arch.as_ref().is_none_or(|min| {
cc_major < min.major || (cc_major == min.major && cc_minor < min.minor)
});
if is_new_minimum {
min_arch = Some(CudaArchInfo {
major: cc_major,
minor: cc_minor,
});
}
}
if let Some(arch) = min_arch.as_ref() {
tracing::debug!(
major = arch.major,
minor = arch.minor,
"selected minimum CUDA compute capability from libcuda"
);
} else {
tracing::debug!("no CUDA compute capability detected via libcuda");
}
min_arch
}
fn cuda_library_paths() -> &'static [&'static str] {
#[cfg(target_os = "macos")]
static FILENAMES: &[&str] = &[
"libcuda.1.dylib", "libcuda.dylib",
"/usr/local/cuda/lib/libcuda.1.dylib",
"/usr/local/cuda/lib/libcuda.dylib",
];
#[cfg(target_os = "linux")]
static FILENAMES: &[&str] = &[
"libcuda.so.1", "libcuda.so",
"/usr/lib64/nvidia/libcuda.so.1", "/usr/lib64/nvidia/libcuda.so",
"/usr/lib/x86_64-linux-gnu/libcuda.so.1", "/usr/lib/x86_64-linux-gnu/libcuda.so",
"/usr/lib/wsl/lib/libcuda.so.1", "/usr/lib/wsl/lib/libcuda.so",
];
#[cfg(windows)]
static FILENAMES: &[&str] = &["nvcuda.dll"];
#[cfg(not(any(target_os = "linux", target_os = "macos", windows)))]
static FILENAMES: &[&str] = &[];
FILENAMES
}
fn detect_cuda_version_via_nvidia_smi() -> Option<Version> {
tracing::trace!("detecting CUDA driver version via nvidia-smi");
let nvidia_smi_output = match Command::new("nvidia-smi")
.arg("--query")
.arg("-u")
.arg("-x")
.env_remove("CUDA_VISIBLE_DEVICES")
.output()
{
Ok(output) => output,
Err(err) => {
tracing::debug!(error = %err, "failed to run nvidia-smi for CUDA driver version");
return None;
}
};
if !nvidia_smi_output.status.success() {
tracing::debug!(
status = %nvidia_smi_output.status,
stderr = %String::from_utf8_lossy(&nvidia_smi_output.stderr),
"nvidia-smi CUDA driver version query exited non-zero; attempting to parse output anyway"
);
}
let output = String::from_utf8_lossy(&nvidia_smi_output.stdout);
parse_nvidia_smi_cuda_version(&output)
}
fn parse_nvidia_smi_cuda_version(output: &str) -> Option<Version> {
static CUDA_VERSION_RE: once_cell::sync::Lazy<regex::Regex> =
once_cell::sync::Lazy::new(|| {
regex::Regex::new("<cuda_version>(.*)<\\/cuda_version>").unwrap()
});
let Some(version_match) = CUDA_VERSION_RE.captures(output) else {
tracing::trace!("nvidia-smi output did not contain a CUDA driver version");
return None;
};
let Some(version_match) = version_match.get(1) else {
tracing::trace!("nvidia-smi CUDA driver version match was empty");
return None;
};
let version_str = version_match.as_str();
match Version::from_str(version_str) {
Ok(version) => {
tracing::trace!(%version, "detected CUDA driver version via nvidia-smi");
Some(version)
}
Err(err) => {
tracing::trace!(version = version_str, error = %err, "failed to parse nvidia-smi CUDA driver version");
None
}
}
}
fn detect_cuda_arch_via_nvidia_smi() -> Option<CudaArchInfo> {
tracing::trace!("detecting CUDA compute capability via nvidia-smi");
let nvidia_smi_output = match Command::new("nvidia-smi")
.arg("--query-gpu=compute_cap")
.arg("--format=csv,noheader")
.env_remove("CUDA_VISIBLE_DEVICES")
.output()
{
Ok(output) => output,
Err(err) => {
tracing::debug!(error = %err, "failed to run nvidia-smi for CUDA compute capability");
return None;
}
};
if !nvidia_smi_output.status.success() {
tracing::debug!(
status = %nvidia_smi_output.status,
stderr = %String::from_utf8_lossy(&nvidia_smi_output.stderr),
"nvidia-smi CUDA compute capability query exited non-zero; attempting to parse output anyway"
);
}
let output = String::from_utf8_lossy(&nvidia_smi_output.stdout);
parse_nvidia_smi_compute_capabilities(&output)
}
fn parse_nvidia_smi_compute_capabilities(output: &str) -> Option<CudaArchInfo> {
let mut min_arch: Option<CudaArchInfo> = None;
for (device_idx, line) in output.lines().enumerate() {
let line = line.trim();
let Some((major, minor)) = line.split_once('.') else {
tracing::trace!(
device_idx,
line,
"ignoring invalid nvidia-smi compute capability line"
);
continue;
};
let (Ok(major), Ok(minor)) = (major.parse::<u32>(), minor.parse::<u32>()) else {
tracing::trace!(
device_idx,
line,
"ignoring unparsable nvidia-smi compute capability line"
);
continue;
};
tracing::trace!(
device_idx,
major,
minor,
"detected CUDA compute capability via nvidia-smi"
);
let is_new_minimum = min_arch
.as_ref()
.is_none_or(|min| major < min.major || (major == min.major && minor < min.minor));
if is_new_minimum {
min_arch = Some(CudaArchInfo { major, minor });
}
}
if let Some(arch) = min_arch.as_ref() {
tracing::debug!(
major = arch.major,
minor = arch.minor,
"selected minimum CUDA compute capability from nvidia-smi"
);
} else {
tracing::debug!("no CUDA compute capability detected via nvidia-smi");
}
min_arch
}
#[cfg(test)]
mod test {
use super::*;
#[test]
#[ignore = "benchmark, run manually and in isolation"]
fn bench_cold_library_load() {
let start = std::time::Instant::now();
let loaded = nvml_library_paths()
.iter()
.find_map(|path| unsafe { Library::new(*path).ok() })
.is_some();
println!(
"cold NVML library load: {:?} -> loaded={loaded}",
start.elapsed()
);
}
#[test]
#[ignore = "benchmark, run manually and in isolation"]
fn bench_cold_version() {
let start = std::time::Instant::now();
let version = detect_cuda_version_via_nvml();
println!(
"cold __cuda (no init): {:?} -> {version:?}",
start.elapsed()
);
}
#[test]
#[ignore = "benchmark, run manually and in isolation"]
fn bench_cold_arch() {
let start = std::time::Instant::now();
let info = detect_cuda_info();
println!(
"cold __cuda + __cuda_arch: {:?} -> {:?}",
start.elapsed(),
info.info.arch_info
);
}
#[test]
#[ignore = "benchmark, run manually and in isolation"]
fn bench_cold_cached() {
let cache_dir = std::env::temp_dir().join("rattler-cuda-bench-cache");
std::fs::create_dir_all(&cache_dir).unwrap();
let start = std::time::Instant::now();
let info = cuda_info(Some(&cache_dir));
println!(
"cold detection via cache: {:?} -> {:?} / {:?}",
start.elapsed(),
info.version,
info.arch_info
);
println!(
"(run again to measure a cache hit; cache dir: {})",
cache_dir.display()
);
}
#[test]
#[ignore = "benchmark, run manually on a machine with an NVIDIA GPU"]
fn bench_cuda_detection() {
fn legacy_detect_cuda_version_via_nvml() -> Option<Version> {
let library = nvml_library_paths()
.iter()
.find_map(|path| unsafe { Library::new(*path).ok() })?;
let nvml_init: Symbol<'_, unsafe extern "C" fn() -> c_int> = unsafe {
library
.get(b"nvmlInit_v2\0")
.or_else(|_| library.get(b"nvmlInit\0"))
}
.ok()?;
let nvml_shutdown: Symbol<'_, unsafe extern "C" fn() -> c_int> =
unsafe { library.get(b"nvmlShutdown\0") }.ok()?;
let get_version: Symbol<'_, unsafe extern "C" fn(*mut c_int) -> c_int> = unsafe {
library
.get(b"nvmlSystemGetCudaDriverVersion_v2\0")
.or_else(|_| library.get(b"nvmlSystemGetCudaDriverVersion\0"))
}
.ok()?;
if unsafe { nvml_init() } != 0 {
return None;
}
let mut raw = std::mem::MaybeUninit::uninit();
let result = unsafe { get_version(raw.as_mut_ptr()) };
let _ = unsafe { nvml_shutdown() };
if result != 0 {
return None;
}
parse_cuda_driver_version(unsafe { raw.assume_init() })
}
fn time<T>(label: &str, mut f: impl FnMut() -> T) -> T {
let start = std::time::Instant::now();
let mut result = f();
let first = start.elapsed();
let mut best = std::time::Duration::MAX;
for _ in 0..5 {
let start = std::time::Instant::now();
result = f();
best = best.min(start.elapsed());
}
println!("{label:<48} {first:>12.3?} {best:>12.3?}");
result
}
println!("\n{:<48} {:>12} {:>12}", "", "first (cold)", "best of 5");
println!("--- detection paths ---");
let legacy = time("version via NVML (legacy, with init)", || {
legacy_detect_cuda_version_via_nvml()
});
let version = time("version via NVML (no init)", detect_cuda_version_via_nvml);
let info = time("version + arch via NVML (one init)", detect_cuda_info);
let smi_version = time("version via nvidia-smi", detect_cuda_version_via_nvidia_smi);
let smi_arch = time("arch via nvidia-smi", detect_cuda_arch_via_nvidia_smi);
println!("legacy version: {legacy:?}");
println!("\n--- breakdown ---");
if let Some(path) = nvml_library_paths()
.iter()
.copied()
.find(|path| unsafe { Library::new(*path) }.is_ok())
{
time("load NVML library only", || {
drop(unsafe { Library::new(path) });
});
let library = unsafe { Library::new(path) }.expect("just loaded successfully");
time("version query (library already loaded)", || {
cuda_version_from_nvml_library(&library).ok()
});
time("init + arch (library already loaded)", || {
detect_cuda_initialized_info_via_nvml(&library, false, true)
});
}
if let Some(path) = cuda_library_paths()
.iter()
.copied()
.find(|path| unsafe { Library::new(*path) }.is_ok())
{
time("load CUDA driver library only", || {
drop(unsafe { Library::new(path) });
});
}
println!("\n--- cache paths ---");
let dir = tempfile::tempdir().unwrap();
let env = time("cache env (boot + driver + device keys)", || {
cache::CacheEnv::current()
});
time("cache write", || {
cache::write_with_env(&env, dir.path(), &info.info, info.sources);
});
let cache_read = time("cache read (hit)", || {
cache::read_with_env(&env, dir.path())
});
println!("\n--- results ---");
println!("version: {version:?}");
println!("arch: {:?}", info.info.arch_info);
println!("nvidia-smi: {smi_version:?} / {smi_arch:?}");
println!("cache read: {:?}", cache_read.map(|c| c.info));
if version.is_none() {
println!(
"\nNOTE: no CUDA driver found, so the driver paths short-circuit and their\n\
timings are meaningless. Run this on a machine with an NVIDIA GPU."
);
}
}
#[test]
pub fn doesnt_crash() {
let version = detect_cuda_version_via_nvml();
println!("Cuda {version:?}");
}
#[test]
pub fn doesnt_crash_nvidia_smi() {
let version = detect_cuda_version_via_nvidia_smi();
println!("Cuda {version:?}");
}
#[test]
pub fn doesnt_crash_nvidia_smi_arch() {
let arch = detect_cuda_arch_via_nvidia_smi();
println!("Cuda arch {arch:?}");
}
#[test]
pub fn test_cuda_info() {
let info = cuda_info(None);
println!("CUDA Info: {info:?}");
if let Some(ref arch) = info.arch_info {
println!(" Compute capability: {}.{}", arch.major, arch.minor);
}
}
#[test]
pub fn test_cuda_arch() {
let arch = cuda_arch(None);
println!("CUDA Arch: {arch:?}");
}
fn fake_env(
boot: &str,
driver: Option<&str>,
device: Option<&str>,
now: u64,
) -> cache::CacheEnv {
cache::CacheEnv {
boot_id: Some(cache::BootId::Uuid(boot.to_owned())),
driver_fingerprint: driver.map(|version| cache::DriverFingerprint::Module {
version: version.to_owned(),
}),
device_fingerprint: device.map(|gpu| cache::DeviceFingerprint {
gpus: vec![gpu.to_owned()],
device_nodes: Vec::new(),
}),
now,
}
}
fn full_info() -> (CudaInfo, CudaInfoSources) {
(
CudaInfo {
version: Some(Version::from_str("12.4").unwrap()),
arch_info: Some(CudaArchInfo { major: 8, minor: 6 }),
},
CudaInfoSources {
version: Some(CudaDetectionMethod::NvmlInitialized),
arch: Some(CudaDetectionMethod::NvidiaSmi),
},
)
}
#[test]
fn test_cache_roundtrip() {
let dir = tempfile::tempdir().unwrap();
let env = fake_env("boot-1", Some("driver-1"), Some("dev-1"), 1_000);
assert!(cache::read_with_env(&env, dir.path()).is_none());
cache::write_with_env(
&env,
dir.path(),
&CudaInfo {
version: None,
arch_info: None,
},
CudaInfoSources::default(),
);
assert!(cache::read_with_env(&env, dir.path()).is_none());
let (info, sources) = full_info();
cache::write_with_env(&env, dir.path(), &info, sources);
let cached = cache::read_with_env(&env, dir.path()).unwrap();
assert_eq!(cached.info.version, info.version);
assert_eq!(cached.info.arch_info, info.arch_info);
assert_eq!(cached.sources, sources);
let updated_info = CudaInfo {
version: Some(Version::from_str("12.5").unwrap()),
arch_info: None,
};
let updated_sources = CudaInfoSources {
version: Some(CudaDetectionMethod::Libcuda),
arch: None,
};
cache::write_with_env(&env, dir.path(), &updated_info, updated_sources);
let cached = cache::read_with_env(&env, dir.path()).unwrap();
assert_eq!(cached.info.version, updated_info.version);
assert_eq!(cached.info.arch_info, updated_info.arch_info);
assert_eq!(cached.sources, updated_sources);
}
#[test]
fn test_cache_ttl_version_only_vs_full() {
let dir = tempfile::tempdir().unwrap();
let write_env = fake_env("boot-1", Some("driver-1"), Some("dev-1"), 1_000);
let version_only = CudaInfo {
version: Some(Version::from_str("12.4").unwrap()),
arch_info: None,
};
cache::write_with_env(
&write_env,
dir.path(),
&version_only,
CudaInfoSources::default(),
);
assert!(cache::read_with_env(&write_env, dir.path()).is_some());
let just_before = fake_env("boot-1", Some("driver-1"), Some("dev-1"), 1_000 + 600);
assert!(cache::read_with_env(&just_before, dir.path()).is_some());
let after_10m = fake_env("boot-1", Some("driver-1"), Some("dev-1"), 1_000 + 601);
assert!(cache::read_with_env(&after_10m, dir.path()).is_none());
let (info, sources) = full_info();
cache::write_with_env(&write_env, dir.path(), &info, sources);
assert!(cache::read_with_env(&after_10m, dir.path()).is_some());
let after_24h = fake_env(
"boot-1",
Some("driver-1"),
Some("dev-1"),
1_000 + 24 * 3600 + 1,
);
assert!(cache::read_with_env(&after_24h, dir.path()).is_none());
}
#[test]
fn test_cache_rejects_future_write_time() {
let dir = tempfile::tempdir().unwrap();
let write_env = fake_env("boot-1", Some("driver-1"), Some("dev-1"), 10_000);
let (info, sources) = full_info();
cache::write_with_env(&write_env, dir.path(), &info, sources);
let past = fake_env("boot-1", Some("driver-1"), Some("dev-1"), 10_000 - 301);
assert!(cache::read_with_env(&past, dir.path()).is_none());
let slight_past = fake_env("boot-1", Some("driver-1"), Some("dev-1"), 10_000 - 299);
assert!(cache::read_with_env(&slight_past, dir.path()).is_some());
}
#[test]
fn test_cache_invalidated_on_device_change() {
let dir = tempfile::tempdir().unwrap();
let env = fake_env("boot-1", Some("driver-1"), Some("dev-a"), 1_000);
let (info, sources) = full_info();
cache::write_with_env(&env, dir.path(), &info, sources);
let other_device = fake_env("boot-1", Some("driver-1"), Some("dev-b"), 1_000);
assert!(cache::read_with_env(&other_device, dir.path()).is_none());
let no_device = fake_env("boot-1", Some("driver-1"), None, 1_000);
assert!(cache::read_with_env(&no_device, dir.path()).is_none());
assert!(cache::read_with_env(&env, dir.path()).is_some());
}
#[test]
fn test_cache_requires_driver_fingerprint() {
let dir = tempfile::tempdir().unwrap();
let (info, sources) = full_info();
let no_driver = fake_env("boot-1", None, Some("dev-1"), 1_000);
cache::write_with_env(&no_driver, dir.path(), &info, sources);
assert!(!dir.path().join("cuda-info-v1.json").exists());
std::fs::write(
dir.path().join("cuda-info-v1.json"),
r#"{"boot_id":{"uuid":"boot-1"},"driver_fingerprint":{"module":{"version":"driver-1"}},"device_fingerprint":{"gpus":["dev-1"],"device_nodes":[]},"written_at":1000,"version":"12.4","arch":[8,6]}"#,
)
.unwrap();
assert!(cache::read_with_env(&no_driver, dir.path()).is_none());
let with_driver = fake_env("boot-1", Some("driver-1"), Some("dev-1"), 1_000);
assert!(cache::read_with_env(&with_driver, dir.path()).is_some());
}
#[test]
fn test_cache_invalidated_after_driver_change() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("cuda-info-v1.json"),
r#"{"boot_id":{"uuid":"boot-1"},"driver_fingerprint":{"module":{"version":"535.0"}},"device_fingerprint":null,"written_at":1000,"version":"12.4","arch":[8,6]}"#,
)
.unwrap();
let stale = fake_env("boot-1", Some("550.0"), None, 1_000);
assert!(cache::read_with_env(&stale, dir.path()).is_none());
let current = fake_env("boot-1", Some("535.0"), None, 1_000);
assert!(cache::read_with_env(¤t, dir.path()).is_some());
}
#[test]
fn test_cache_invalidated_after_reboot() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("cuda-info-v1.json"),
r#"{"boot_id":{"uuid":"boot-A"},"driver_fingerprint":{"module":{"version":"driver-1"}},"device_fingerprint":null,"written_at":1000,"version":"12.4","arch":[8,6]}"#,
)
.unwrap();
let other_boot = fake_env("boot-B", Some("driver-1"), None, 1_000);
assert!(cache::read_with_env(&other_boot, dir.path()).is_none());
let same_boot = fake_env("boot-A", Some("driver-1"), None, 1_000);
assert!(cache::read_with_env(&same_boot, dir.path()).is_some());
}
#[test]
fn test_boot_time_tolerance() {
assert!(cache::boot_times_within_tolerance(1_000, 1_000));
assert!(cache::boot_times_within_tolerance(1_000, 1_120));
assert!(cache::boot_times_within_tolerance(1_120, 1_000));
assert!(!cache::boot_times_within_tolerance(1_000, 1_121));
let a = cache::BootId::BootTime(1000);
let b = cache::BootId::BootTime(1050);
assert!(a.matches(&b));
let c = cache::BootId::BootTime(2000);
assert!(!a.matches(&c));
let count = cache::BootId::BootCount(5);
assert!(count.matches(&cache::BootId::BootCount(5)));
assert!(!count.matches(&cache::BootId::BootCount(6)));
assert!(!count.matches(&a));
}
#[test]
fn test_late_persistence() {
let dir = tempfile::tempdir().unwrap();
let env = fake_env("boot-1", Some("driver-1"), Some("dev-1"), 1_000);
let state: OnceCell<DetectedCudaInfo> = OnceCell::new();
let (info, sources) = full_info();
let _ = state.set(DetectedCudaInfo { info, sources });
let persisted = AtomicBool::new(false);
cuda_info_impl(&env, &state, &persisted, None);
assert!(!persisted.load(Ordering::Relaxed));
assert!(cache::read_with_env(&env, dir.path()).is_none());
cuda_info_impl(&env, &state, &persisted, Some(dir.path()));
assert!(persisted.load(Ordering::Relaxed));
let cached = cache::read_with_env(&env, dir.path()).unwrap();
assert_eq!(
cached.info.version,
Some(Version::from_str("12.4").unwrap())
);
assert_eq!(
cached.info.arch_info,
Some(CudaArchInfo { major: 8, minor: 6 })
);
}
#[test]
fn test_is_nvidia_pci_device_id() {
assert!(cache::is_nvidia_pci_device_id(
"PCI\\VEN_10DE&DEV_2484&SUBSYS_147D10DE&REV_A1\\4&2D2E5D1F&0&0008"
));
assert!(cache::is_nvidia_pci_device_id("PCI\\VEN_10DE&DEV_1EB1"));
assert!(cache::is_nvidia_pci_device_id("pci\\ven_10de&dev_2484"));
assert!(!cache::is_nvidia_pci_device_id(
"PCI\\VEN_8086&DEV_9A49&SUBSYS_00011025&REV_01\\3&11583659&0&10"
));
assert!(!cache::is_nvidia_pci_device_id("PCI\\VEN_1002&DEV_73FF"));
assert!(!cache::is_nvidia_pci_device_id(""));
}
#[test]
fn test_is_valid_cuda_version_format() {
assert!(is_valid_cuda_version_format("8.6"));
assert!(is_valid_cuda_version_format("7.5"));
assert!(is_valid_cuda_version_format("10.2"));
assert!(is_valid_cuda_version_format("0.0"));
assert!(is_valid_cuda_version_format("12.0"));
assert!(!is_valid_cuda_version_format("8"));
assert!(!is_valid_cuda_version_format("8.6.1"));
assert!(!is_valid_cuda_version_format("8.6.1.0"));
assert!(!is_valid_cuda_version_format(""));
assert!(!is_valid_cuda_version_format(".6"));
assert!(!is_valid_cuda_version_format("8."));
assert!(!is_valid_cuda_version_format("."));
assert!(!is_valid_cuda_version_format("8.6a"));
assert!(!is_valid_cuda_version_format("a.6"));
assert!(!is_valid_cuda_version_format("8.b"));
assert!(!is_valid_cuda_version_format("eight.six"));
assert!(!is_valid_cuda_version_format("8-6"));
assert!(!is_valid_cuda_version_format("8_6"));
}
#[test]
fn test_parse_cuda_driver_version() {
assert_eq!(
parse_cuda_driver_version(12_040),
Some(Version::from_str("12.4").unwrap())
);
assert_eq!(
parse_cuda_driver_version(11_080),
Some(Version::from_str("11.8").unwrap())
);
assert_eq!(
parse_cuda_driver_version(1_000),
Some(Version::from_str("1.0").unwrap())
);
assert_eq!(
parse_cuda_driver_version(99_990),
Some(Version::from_str("99.99").unwrap())
);
assert_eq!(parse_cuda_driver_version(0), None);
assert_eq!(parse_cuda_driver_version(-1), None);
assert_eq!(parse_cuda_driver_version(999), None);
assert_eq!(parse_cuda_driver_version(100_000), None);
assert_eq!(parse_cuda_driver_version(c_int::MAX), None);
assert_eq!(parse_cuda_driver_version(c_int::MIN), None);
}
#[test]
fn test_parse_nvidia_smi_cuda_version() {
let xml = "<nvidia_smi_log>\n <cuda_version>12.4</cuda_version>\n</nvidia_smi_log>";
assert_eq!(
parse_nvidia_smi_cuda_version(xml),
Some(Version::from_str("12.4").unwrap())
);
assert_eq!(
parse_nvidia_smi_cuda_version("<nvidia_smi_log></nvidia_smi_log>"),
None
);
assert_eq!(parse_nvidia_smi_cuda_version(""), None);
}
#[test]
fn test_parse_nvidia_smi_compute_capabilities() {
assert_eq!(
parse_nvidia_smi_compute_capabilities("8.6\n7.5\n9.0"),
Some(CudaArchInfo { major: 7, minor: 5 })
);
assert_eq!(
parse_nvidia_smi_compute_capabilities("8.6\n[N/A]\n7.5\ngarbage"),
Some(CudaArchInfo { major: 7, minor: 5 })
);
assert_eq!(
parse_nvidia_smi_compute_capabilities("x.y\n8.0"),
Some(CudaArchInfo { major: 8, minor: 0 })
);
assert_eq!(
parse_nvidia_smi_compute_capabilities("[N/A]\ngarbage\n"),
None
);
assert_eq!(parse_nvidia_smi_compute_capabilities(""), None);
}
}