use std::path::{Path, PathBuf};
use std::sync::{Mutex, OnceLock};
use libloading::Library;
use onnx_genai_cuda_version_guard::{HostOs, cudart_candidates_for};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum CudaLibrary {
Driver,
Runtime,
#[allow(dead_code)]
Cublas,
CublasLt,
Cufft,
Cudnn,
Nvrtc,
#[allow(dead_code)]
Cupti,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum TargetOs {
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
Linux,
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
Macos,
Windows,
#[cfg_attr(
any(target_os = "linux", target_os = "macos", target_os = "windows"),
allow(dead_code)
)]
Other,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum TargetArch {
Aarch64,
Other,
}
#[cfg(target_os = "linux")]
fn target_os() -> TargetOs {
TargetOs::Linux
}
#[cfg(target_os = "macos")]
fn target_os() -> TargetOs {
TargetOs::Macos
}
#[cfg(target_os = "windows")]
fn target_os() -> TargetOs {
TargetOs::Windows
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
fn target_os() -> TargetOs {
TargetOs::Other
}
fn target_arch() -> TargetArch {
if cfg!(target_arch = "aarch64") {
TargetArch::Aarch64
} else {
TargetArch::Other
}
}
fn host_os(os: TargetOs) -> HostOs {
match os {
TargetOs::Linux => HostOs::Linux,
TargetOs::Macos => HostOs::Macos,
TargetOs::Windows => HostOs::Windows,
TargetOs::Other => HostOs::Other,
}
}
pub(crate) fn candidates(library: CudaLibrary) -> &'static [&'static str] {
candidates_for(target_os(), library)
}
fn candidates_for(os: TargetOs, library: CudaLibrary) -> &'static [&'static str] {
match (os, library) {
(os, CudaLibrary::Runtime) => cudart_candidates_for(host_os(os)),
(TargetOs::Linux, CudaLibrary::Driver) => &["libcuda.so.1", "libcuda.so"],
(TargetOs::Linux, CudaLibrary::Cublas) => {
&["libcublas.so.13", "libcublas.so.12", "libcublas.so"]
}
(TargetOs::Linux, CudaLibrary::CublasLt) => {
&["libcublasLt.so.13", "libcublasLt.so.12", "libcublasLt.so"]
}
(TargetOs::Linux, CudaLibrary::Cufft) => {
&["libcufft.so.12", "libcufft.so.11", "libcufft.so"]
}
(TargetOs::Linux, CudaLibrary::Cudnn) => &["libcudnn.so.9", "libcudnn.so"],
(TargetOs::Linux, CudaLibrary::Nvrtc) => {
&["libnvrtc.so.13", "libnvrtc.so.12", "libnvrtc.so"]
}
(TargetOs::Linux, CudaLibrary::Cupti) => {
&["libcupti.so.13", "libcupti.so.12", "libcupti.so"]
}
(TargetOs::Macos, CudaLibrary::Driver) => &["libcuda.dylib"],
(TargetOs::Macos, CudaLibrary::Cublas) => &["libcublas.dylib"],
(TargetOs::Macos, CudaLibrary::CublasLt) => &["libcublasLt.dylib"],
(TargetOs::Macos, CudaLibrary::Cufft) => &["libcufft.dylib"],
(TargetOs::Macos, CudaLibrary::Cudnn) => &["libcudnn.dylib"],
(TargetOs::Macos, CudaLibrary::Nvrtc) => &["libnvrtc.dylib"],
(TargetOs::Macos, CudaLibrary::Cupti) => &["libcupti.dylib"],
(TargetOs::Windows, CudaLibrary::Driver) => &["nvcuda.dll"],
(TargetOs::Windows, CudaLibrary::Cublas) => {
&["cublas64_13.dll", "cublas64_12.dll", "cublas.dll"]
}
(TargetOs::Windows, CudaLibrary::CublasLt) => {
&["cublasLt64_13.dll", "cublasLt64_12.dll", "cublasLt.dll"]
}
(TargetOs::Windows, CudaLibrary::Cufft) => {
&["cufft64_12.dll", "cufft64_11.dll", "cufft.dll"]
}
(TargetOs::Windows, CudaLibrary::Cudnn) => &["cudnn64_9.dll", "cudnn64_8.dll", "cudnn.dll"],
(TargetOs::Windows, CudaLibrary::Nvrtc) => &[
"nvrtc64_130_0.dll",
"nvrtc64_120_0.dll",
"nvrtc64_13.dll",
"nvrtc64_12.dll",
"nvrtc.dll",
],
(TargetOs::Windows, CudaLibrary::Cupti) => {
&["cupti64_13.dll", "cupti64_12.dll", "cupti.dll"]
}
(TargetOs::Other, _) => &[],
}
}
fn nvidia_component(library: CudaLibrary) -> Option<&'static str> {
match library {
CudaLibrary::Driver => None,
CudaLibrary::Runtime => Some("cuda_runtime"),
CudaLibrary::Cublas | CudaLibrary::CublasLt => Some("cublas"),
CudaLibrary::Cufft => Some("cufft"),
CudaLibrary::Cudnn => Some("cudnn"),
CudaLibrary::Nvrtc => Some("cuda_nvrtc"),
CudaLibrary::Cupti => Some("cuda_cupti"),
}
}
fn wheel_library_directories(root: &Path, os: TargetOs, library: CudaLibrary) -> Vec<PathBuf> {
let Some(component) = nvidia_component(library) else {
return Vec::new();
};
onnx_genai_cuda_version_guard::wheel_component_directories(component, host_os(os))
.into_iter()
.map(|relative| root.join(relative))
.collect()
}
fn wheel_candidates_for(root: &Path, os: TargetOs, library: CudaLibrary) -> Vec<PathBuf> {
if !root.is_absolute() {
return Vec::new();
}
let names = candidates_for(os, library);
wheel_library_directories(root, os, library)
.into_iter()
.flat_map(|directory| names.iter().map(move |name| directory.join(name)))
.collect()
}
fn wheel_roots_from_environment() -> Vec<PathBuf> {
let mut roots = Vec::new();
if let Some(value) = std::env::var_os("NXRT_CUDA_WHEEL_ROOTS") {
roots.extend(std::env::split_paths(&value));
}
for variable in ["PATH", "LD_LIBRARY_PATH", "DYLD_LIBRARY_PATH"] {
let Some(value) = std::env::var_os(variable) else {
continue;
};
for entry in std::env::split_paths(&value) {
if let Some(root) = wheel_root_of(&entry) {
roots.push(root);
}
}
}
roots.retain(|path| path.is_absolute());
let mut unique = Vec::with_capacity(roots.len());
for root in roots {
if !unique.contains(&root) {
unique.push(root);
}
}
unique
}
fn wheel_root_of(entry: &Path) -> Option<PathBuf> {
onnx_genai_cuda_version_guard::wheel_root_of(entry)
}
fn wheel_search_paths() -> &'static Mutex<Vec<PathBuf>> {
static PATHS: OnceLock<Mutex<Vec<PathBuf>>> = OnceLock::new();
PATHS.get_or_init(|| Mutex::new(wheel_roots_from_environment()))
}
fn loaded_libraries() -> &'static Mutex<Vec<(CudaLibrary, Library)>> {
static LIBRARIES: OnceLock<Mutex<Vec<(CudaLibrary, Library)>>> = OnceLock::new();
LIBRARIES.get_or_init(|| Mutex::new(Vec::new()))
}
fn explicit_root_count() -> &'static std::sync::atomic::AtomicUsize {
static COUNT: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
&COUNT
}
fn insert_explicit_roots(
roots: &mut Vec<PathBuf>,
at: usize,
paths: impl IntoIterator<Item = PathBuf>,
) -> usize {
let mut added = 0;
for path in paths {
if path.is_absolute() && !roots.contains(&path) {
roots.insert(at + added, path);
added += 1;
}
}
added
}
pub fn set_wheel_search_paths(paths: impl IntoIterator<Item = PathBuf>) {
use std::sync::atomic::Ordering;
let mut configured = wheel_search_paths()
.lock()
.expect("CUDA wheel search-path lock poisoned");
let at = explicit_root_count().load(Ordering::Relaxed);
let added = insert_explicit_roots(&mut configured, at, paths);
explicit_root_count().fetch_add(added, Ordering::Relaxed);
}
pub(crate) fn wheel_cuda_include_paths() -> Vec<PathBuf> {
let roots = wheel_search_paths()
.lock()
.expect("CUDA wheel search-path lock poisoned")
.clone();
roots
.into_iter()
.flat_map(|root| {
let nvidia = root.join("nvidia");
let consolidated = onnx_genai_cuda_version_guard::WHEEL_CUDA_MAJORS
.iter()
.map(|major| nvidia.join(major).join("include"))
.collect::<Vec<_>>();
let per_component = ["cuda_runtime", "cuda_nvcc"]
.into_iter()
.map(|component| nvidia.join(component).join("include"))
.collect::<Vec<_>>();
consolidated.into_iter().chain(per_component)
})
.collect()
}
fn load_library(library: CudaLibrary) -> Result<Library, Vec<String>> {
let os = target_os();
let roots = wheel_search_paths()
.lock()
.expect("CUDA wheel search-path lock poisoned")
.clone();
let wheel_candidates = roots
.iter()
.flat_map(|root| wheel_candidates_for(root, os, library))
.collect::<Vec<_>>();
let mut tried = Vec::new();
for path in wheel_candidates {
if !path.is_absolute() {
continue;
}
tried.push(path.display().to_string());
if let Ok(handle) = unsafe { Library::new(&path) } {
if let Some(directory) = path.parent() {
preload_component_siblings(directory, &path);
}
return Ok(handle);
}
}
for name in candidates(library) {
tried.push((*name).to_string());
if let Ok(handle) = unsafe { Library::new(name) } {
return Ok(handle);
}
}
Err(tried)
}
pub(crate) fn is_available(library: CudaLibrary) -> bool {
require(library).is_ok()
}
fn preload_component_siblings(directory: &Path, loaded: &Path) {
static SEEN: OnceLock<Mutex<Vec<PathBuf>>> = OnceLock::new();
let mut seen = SEEN
.get_or_init(|| Mutex::new(Vec::new()))
.lock()
.expect("CUDA sibling-preload lock poisoned");
if seen.iter().any(|existing| existing == directory) {
return;
}
seen.push(directory.to_path_buf());
let Ok(entries) = std::fs::read_dir(directory) else {
return;
};
let suffix = if cfg!(windows) { ".dll" } else { ".so" };
for entry in entries.flatten() {
let path = entry.path();
if path == loaded {
continue;
}
let name = entry.file_name();
let Some(name) = name.to_str() else { continue };
if !name.contains(suffix) {
continue;
}
if let Ok(handle) = unsafe { Library::new(&path) } {
std::mem::forget(handle);
}
}
}
fn cuda_supported(os: TargetOs, arch: TargetArch) -> bool {
!(os == TargetOs::Windows && arch == TargetArch::Aarch64)
}
pub(crate) fn require(library: CudaLibrary) -> Result<(), String> {
if !cuda_supported(target_os(), target_arch()) {
return Err(
"CUDA is unavailable on Windows ARM64 because NVIDIA ships x64-only CUDA libraries"
.into(),
);
}
let mut loaded = loaded_libraries()
.lock()
.expect("CUDA loaded-library lock poisoned");
if loaded
.iter()
.any(|(loaded_library, _)| *loaded_library == library)
{
return Ok(());
}
let handle = load_library(library)
.map_err(|tried| format!("CUDA {library:?} library not found; tried {tried:?}"))?;
loaded.push((library, handle));
Ok(())
}
pub(crate) fn symbol<T: Copy>(library: CudaLibrary, name: &[u8]) -> Result<T, String> {
require(library)?;
let loaded = loaded_libraries()
.lock()
.expect("CUDA loaded-library lock poisoned");
let handle = loaded
.iter()
.find_map(|(loaded_library, handle)| (*loaded_library == library).then_some(handle))
.ok_or_else(|| format!("CUDA {library:?} library was not retained after loading"))?;
unsafe { handle.get::<T>(name) }
.map(|function| *function)
.map_err(|error| {
format!(
"CUDA {library:?} symbol {:?} was not found: {error}",
String::from_utf8_lossy(name)
)
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn generates_linux_cuda_names() {
assert_eq!(
candidates_for(TargetOs::Linux, CudaLibrary::Runtime),
onnx_genai_cuda_version_guard::CUDART_CANDIDATES_LINUX
);
assert_eq!(
candidates_for(TargetOs::Linux, CudaLibrary::Cupti),
["libcupti.so.13", "libcupti.so.12", "libcupti.so"]
);
assert_eq!(
candidates_for(TargetOs::Linux, CudaLibrary::Cufft),
["libcufft.so.12", "libcufft.so.11", "libcufft.so"]
);
}
#[test]
fn generates_macos_cuda_names() {
assert_eq!(
candidates_for(TargetOs::Macos, CudaLibrary::Cublas),
["libcublas.dylib"]
);
assert_eq!(
candidates_for(TargetOs::Macos, CudaLibrary::Nvrtc),
["libnvrtc.dylib"]
);
}
#[test]
fn generates_windows_cuda_names() {
assert_eq!(
candidates_for(TargetOs::Windows, CudaLibrary::Runtime),
onnx_genai_cuda_version_guard::CUDART_CANDIDATES_WINDOWS
);
assert!(candidates_for(TargetOs::Windows, CudaLibrary::Cudnn).contains(&"cudnn64_9.dll"));
assert!(
candidates_for(TargetOs::Windows, CudaLibrary::Nvrtc).contains(&"nvrtc64_130_0.dll")
);
assert!(candidates_for(TargetOs::Windows, CudaLibrary::Cupti).contains(&"cupti64_13.dll"));
assert!(candidates_for(TargetOs::Windows, CudaLibrary::Cufft).contains(&"cufft64_12.dll"));
}
#[test]
fn locates_wheel_libraries_beneath_absolute_package_root() {
let root = std::env::current_dir()
.expect("current directory should be available")
.join("site-packages");
let linux = wheel_candidates_for(&root, TargetOs::Linux, CudaLibrary::CublasLt);
assert_eq!(
linux.first(),
Some(&root.join("nvidia/cu13/lib/x86_64/libcublasLt.so.13"))
);
assert!(linux.contains(&root.join("nvidia/cublas/lib/libcublasLt.so.13")));
assert!(linux.contains(&root.join("nvidia/cublas/lib/libcublasLt.so")));
let windows = wheel_candidates_for(&root, TargetOs::Windows, CudaLibrary::Nvrtc);
assert!(windows.contains(&root.join("nvidia/cu13/bin/x86_64/nvrtc64_130_0.dll")));
assert!(windows.contains(&root.join("nvidia/cuda_nvrtc/bin/nvrtc64_120_0.dll")));
let cufft = wheel_candidates_for(&root, TargetOs::Windows, CudaLibrary::Cufft);
assert!(cufft.contains(&root.join("nvidia/cu13/bin/x86_64/cufft64_12.dll")));
assert!(cufft.contains(&root.join("nvidia/cufft/bin/cufft64_12.dll")));
}
#[test]
fn a_consolidated_wheel_library_directory_identifies_its_root() {
let root = std::env::current_dir()
.expect("current directory should be available")
.join("site-packages");
assert_eq!(
wheel_root_of(&root.join("nvidia/cu13/bin/x86_64")),
Some(root.clone())
);
assert_eq!(
wheel_root_of(&root.join("nvidia/cu13/lib/x64")),
Some(root.clone())
);
assert_eq!(wheel_root_of(&root.join("nvidia/cu13/bin")), Some(root));
}
#[test]
fn empty_python_search_path_produces_no_relative_candidates() {
for root in ["", " ", "site-packages"] {
let candidates =
wheel_candidates_for(Path::new(root), TargetOs::Linux, CudaLibrary::CublasLt);
assert!(
candidates.is_empty(),
"relative sys.path entry {root:?} produced dlopen candidates: {candidates:?}"
);
}
}
#[test]
fn driver_is_not_expected_in_a_python_wheel() {
let root = std::env::current_dir()
.expect("current directory should be available")
.join("site-packages");
assert!(wheel_candidates_for(&root, TargetOs::Linux, CudaLibrary::Driver).is_empty());
}
#[test]
fn unsupported_platform_has_no_candidates() {
assert!(candidates_for(TargetOs::Other, CudaLibrary::Driver).is_empty());
}
#[test]
#[cfg(windows)]
fn a_wheel_library_directory_identifies_its_root() {
assert_eq!(
wheel_root_of(Path::new(r"C:\py\site-packages\nvidia\cuda_nvrtc\bin")),
Some(PathBuf::from(r"C:\py\site-packages"))
);
assert_eq!(
wheel_root_of(Path::new("C:/py/site-packages/nvidia/cublas/lib")),
Some(PathBuf::from("C:/py/site-packages"))
);
}
#[test]
#[cfg(not(windows))]
fn a_wheel_library_directory_identifies_its_root() {
assert_eq!(
wheel_root_of(Path::new("/opt/venv/site-packages/nvidia/cublas/lib")),
Some(PathBuf::from("/opt/venv/site-packages"))
);
assert_eq!(
wheel_root_of(Path::new("/opt/venv/site-packages/nvidia/cuda_nvrtc/bin")),
Some(PathBuf::from("/opt/venv/site-packages"))
);
}
#[test]
fn a_directory_that_is_not_a_wheel_component_is_not_a_root() {
for entry in [
"/usr/local/lib",
"/usr/bin",
"/opt/nvidia/lib",
"/opt/site-packages/nvidia/cublas",
"/opt/site-packages/nvidia/cublas/lib64",
"/opt/notnvidia/cublas/lib",
"/opt/nvidia/bin",
"/opt/site-packages/nvidia/cu13/bin/x86_64/extra",
] {
assert_eq!(
wheel_root_of(Path::new(entry)),
None,
"{entry} is not a wheel component directory"
);
}
}
#[test]
fn an_explicit_root_outranks_one_derived_from_the_environment() {
let base = std::env::current_dir().expect("a current directory");
let derived = base.join("derived-from-path");
let explicit = base.join("chosen-by-the-caller");
let second = base.join("also-chosen");
let mut roots = vec![derived.clone()];
assert_eq!(insert_explicit_roots(&mut roots, 0, [explicit.clone()]), 1);
assert_eq!(roots.first(), Some(&explicit));
assert_eq!(insert_explicit_roots(&mut roots, 1, [second.clone()]), 1);
assert_eq!(roots, vec![explicit.clone(), second, derived]);
assert_eq!(insert_explicit_roots(&mut roots, 2, [explicit]), 0);
assert_eq!(
insert_explicit_roots(&mut roots, 2, [PathBuf::from("site-packages")]),
0
);
}
#[test]
fn windows_arm64_is_an_explicit_cpu_only_target() {
assert!(!cuda_supported(TargetOs::Windows, TargetArch::Aarch64));
assert!(cuda_supported(TargetOs::Windows, TargetArch::Other));
}
}