Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion cuda-oxide/crates/cargo-oxide/src/commands/doctor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -568,7 +568,8 @@ pub fn doctor(ctx: &Context) {
Err(e) => {
println!("✗ {}", e);
eprintln!(" Required only when kernels call CUDA libdevice math.");
eprintln!(" Ships with the CUDA Toolkit at <CUDA>/lib64/libnvJitLink.so.");
eprintln!(" Ships with the CUDA Toolkit at <CUDA>/lib64/libnvJitLink.so,");
eprintln!(" or <CUDA>/lib/ in conda-forge and nixpkgs layouts.");
ok = false;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ pub fn print_materializer_handshake() {
});
let handshake = finalizer.materializer_handshake().unwrap_or_else(|| {
eprintln!(
"the loaded libNVVM or nvJitLink library cannot be tied to an exact file; refusing materialization because Cargo could not fingerprint the compiler inputs"
"the loaded libNVVM or nvJitLink library cannot be tied to an exact file; refusing materialization because Cargo could not fingerprint the compiler inputs. The usual cause is a library found only by SONAME: set LIBNVVM_PATH or LIBNVJITLINK_PATH to the library file, or CUDA_TOOLKIT_PATH or CUDA_HOME to a toolkit root that contains both"
);
std::process::exit(1);
});
Expand Down
4 changes: 2 additions & 2 deletions cuda-oxide/crates/cuda-host/src/ltoir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@
//! - **libNVVM**: `LIBNVVM_PATH`, then `<root>/nvvm/lib64/libnvvm.so` for
//! `<root>` in `CUDA_TOOLKIT_PATH`, `CUDA_HOME`, `CUDA_PATH`,
//! `/usr/local/cuda`, `/opt/cuda`, then the system loader.
//! - **nvJitLink**: the same order, using `LIBNVJITLINK_PATH` and
//! `<root>/lib64/libnvJitLink.so`.
//! - **nvJitLink**: the same order, using `LIBNVJITLINK_PATH`, then
//! `<root>/lib64/libnvJitLink.so` and `<root>/lib/libnvJitLink.so`.
//! - **libdevice**: `CUDA_OXIDE_LIBDEVICE` env var, then
//! `<root>/nvvm/libdevice/libdevice.10.bc` for the same roots.
//! - **Artifact target and options**: the emitted `<name>.target` records the
Expand Down
4 changes: 2 additions & 2 deletions cuda-oxide/crates/nvjitlink-sys/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ None. The library is loaded at runtime, so the CUDA Toolkit only needs to be pre
`LibNvJitLink::load()` tries (in order):

1. `LIBNVJITLINK_PATH` env var, if set.
2. `<root>/lib64/libnvJitLink.so` for `<root>` in `CUDA_TOOLKIT_PATH`, `CUDA_HOME`, `CUDA_PATH`, `/usr/local/cuda`, `/opt/cuda`.
2. `<root>/lib64/libnvJitLink.so`, then `<root>/lib/libnvJitLink.so`, for `<root>` in `CUDA_TOOLKIT_PATH`, `CUDA_HOME`, `CUDA_PATH`, `/usr/local/cuda`, `/opt/cuda`.
3. The system loader (`libnvJitLink.so.13`, `libnvJitLink.so.12`, `libnvJitLink.so`).

nvJitLink ships with the standard CUDA Toolkit at `<cuda>/lib64/`. No separate download.
nvJitLink ships with the standard CUDA Toolkit at `<cuda>/lib64/`; conda-forge and nixpkgs layouts place it at `<cuda>/lib/`. No separate download. Only the unversioned `libnvJitLink.so` name is probed under a root. A layout that ships only `libnvJitLink.so.13` is found through the system loader instead, and such a load cannot be tied to one file on disk, which cubin materialization (`cargo oxide build --materialize-cubin`) requires. Set `LIBNVJITLINK_PATH` to the versioned file in that case.

## Symbol naming

Expand Down
171 changes: 164 additions & 7 deletions cuda-oxide/crates/nvjitlink-sys/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,24 @@
//!
//! nvJitLink links one or more LTOIR modules (and other input forms) into
//! a final cubin or PTX. It is part of the CUDA Toolkit and ships at
//! `<cuda>/lib64/libnvJitLink.so`.
//! `<cuda>/lib64/libnvJitLink.so`. conda-forge and nixpkgs lay the toolkit
//! out with `lib/` instead of `lib64/`, so both directories are probed.
//!
//! # Library discovery
//!
//! [`LibNvJitLink::load`] tries (in order):
//! 1. `LIBNVJITLINK_PATH` env var, if set.
//! 2. `<root>/lib64/libnvJitLink.so`, then `<root>/lib/libnvJitLink.so`, for
//! `<root>` in `CUDA_TOOLKIT_PATH`, `CUDA_HOME`, `CUDA_PATH`,
//! `/usr/local/cuda`, `/opt/cuda`.
//! 3. The system loader (`libnvJitLink.so.13`, `libnvJitLink.so.12`,
//! `libnvJitLink.so`).
//!
//! Only the unversioned `libnvJitLink.so` name is probed under a root. A
//! layout that ships only `libnvJitLink.so.13` is reached through the system
//! loader, which does not say which file it mapped, so that load cannot be
//! tied to one file on disk. Set `LIBNVJITLINK_PATH` to the file when the
//! exact file matters, as it does for cubin materialization.
//!
//! # Symbol naming
//!
Expand Down Expand Up @@ -729,12 +746,8 @@ fn open_library(tried: &mut Vec<String>, retain_exact_file: bool) -> Option<Open
}
}

for root in cuda_roots() {
let path = root.join("lib64/libnvJitLink.so");
tried.push(path.display().to_string());
if let Some(opened) = open_library_path(&path, retain_exact_file) {
return Some(opened);
}
if let Some(opened) = open_library_under_roots(&cuda_roots(), tried, retain_exact_file) {
return Some(opened);
}

for soname in [
Expand All @@ -755,6 +768,25 @@ fn open_library(tried: &mut Vec<String>, retain_exact_file: bool) -> Option<Open
None
}

/// Probe `library_candidates(roots)` in order and open the first path that
/// loads. Every probed path is appended to `tried`, hits and misses alike, so
/// `LibraryNotFound` can list them. The `LIBNVJITLINK_PATH` override and the
/// SONAME fallback stay in `open_library`: this function sees only the roots
/// it is given, so a test can hand it a directory of its own.
fn open_library_under_roots(
roots: &[PathBuf],
tried: &mut Vec<String>,
retain_exact_file: bool,
) -> Option<OpenedLibrary> {
for path in library_candidates(roots) {
tried.push(path.display().to_string());
if let Some(opened) = open_library_path(&path, retain_exact_file) {
return Some(opened);
}
}
None
}

fn open_library_path(path: &Path, retain_exact_file: bool) -> Option<OpenedLibrary> {
#[cfg(not(target_os = "linux"))]
let _ = retain_exact_file;
Expand Down Expand Up @@ -792,6 +824,21 @@ fn open_library_path(path: &Path, retain_exact_file: bool) -> Option<OpenedLibra
})
}

/// The library paths probed under each CUDA root, in order: `lib64/` as in
/// the CUDA Toolkit installer's layout, then `lib/` as in the conda-forge and
/// nixpkgs layouts, which have no `lib64/`. Only the unversioned
/// `libnvJitLink.so` name is probed: a root that ships only versioned names
/// such as `libnvJitLink.so.13` is left to the SONAME step, and
/// `LIBNVJITLINK_PATH` names such a file directly. Finding the exact file
/// matters: a library found only by SONAME cannot be fingerprinted for cubin
/// materialization.
fn library_candidates(roots: &[PathBuf]) -> Vec<PathBuf> {
roots
.iter()
.flat_map(|root| ["lib64", "lib"].map(|dir| root.join(dir).join("libnvJitLink.so")))
.collect()
}

fn cuda_roots() -> Vec<PathBuf> {
cuda_roots_from_env(|var| std::env::var(var).ok())
}
Expand Down Expand Up @@ -922,6 +969,102 @@ mod tests {
std::fs::remove_dir_all(directory).unwrap();
}

/// A fresh directory under the system temp dir, used as a CUDA root.
#[cfg(target_os = "linux")]
fn scratch_root(tag: &str) -> PathBuf {
let nonce = SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let root = std::env::temp_dir().join(format!(
"nvjitlink-sys-{tag}-{}-{nonce}",
std::process::id()
));
std::fs::create_dir(&root).unwrap();
root
}

/// Build the probe DSO as `<root>/<dir>/libnvJitLink.so`.
#[cfg(target_os = "linux")]
fn place_probe_library(root: &Path, dir: &str, value: i32) -> PathBuf {
let directory = root.join(dir);
std::fs::create_dir(&directory).unwrap();
let library_path = directory.join("libnvJitLink.so");
compile_probe_library(&directory.join("probe.c"), &library_path, value);
library_path
}

#[cfg(target_os = "linux")]
fn probe_value(opened: &OpenedLibrary) -> c_int {
let probe: Symbol<unsafe extern "C" fn() -> c_int> =
unsafe { opened.library.get(b"cuda_oxide_probe") }.unwrap();
unsafe { probe() }
}

#[cfg(target_os = "linux")]
fn file_identity(path: &Path) -> Option<LibraryFileIdentity> {
LibraryFileIdentity::capture_file(&File::open(path).unwrap())
}

#[cfg(target_os = "linux")]
fn candidate_strings(root: &Path, dirs: &[&str]) -> Vec<String> {
dirs.iter()
.map(|dir| root.join(dir).join("libnvJitLink.so").display().to_string())
.collect()
}

#[cfg(target_os = "linux")]
#[test]
fn root_probe_loads_lib_when_lib64_is_absent() {
let root = scratch_root("root-lib");
let library_path = place_probe_library(&root, "lib", 7);
let expected_identity = file_identity(&library_path);
assert!(expected_identity.is_some());

let mut tried = Vec::new();
let opened = open_library_under_roots(std::slice::from_ref(&root), &mut tried, true)
.expect("lib/libnvJitLink.so is found when lib64/ is absent");

assert_eq!(probe_value(&opened), 7);
assert_eq!(opened.loaded_identity, expected_identity);
assert_eq!(tried, candidate_strings(&root, &["lib64", "lib"]));

drop(opened);
std::fs::remove_dir_all(root).unwrap();
}

#[cfg(target_os = "linux")]
#[test]
fn root_probe_prefers_lib64_over_lib() {
let root = scratch_root("root-both");
let lib64_path = place_probe_library(&root, "lib64", 1);
place_probe_library(&root, "lib", 2);
let lib64_identity = file_identity(&lib64_path);

let mut tried = Vec::new();
let opened = open_library_under_roots(std::slice::from_ref(&root), &mut tried, true)
.expect("lib64/libnvJitLink.so is found");

assert_eq!(probe_value(&opened), 1);
assert_eq!(opened.loaded_identity, lib64_identity);
assert_eq!(tried, candidate_strings(&root, &["lib64"]));

drop(opened);
std::fs::remove_dir_all(root).unwrap();
}

#[cfg(target_os = "linux")]
#[test]
fn root_probe_reports_both_candidates_for_an_empty_root() {
let root = scratch_root("root-empty");

let mut tried = Vec::new();
assert!(open_library_under_roots(std::slice::from_ref(&root), &mut tried, true).is_none());
assert_eq!(tried, candidate_strings(&root, &["lib64", "lib"]));

std::fs::remove_dir_all(root).unwrap();
}

#[test]
fn open_descriptor_remains_bound_to_replaced_inode() {
let nonce = SystemTime::now()
Expand Down Expand Up @@ -1082,6 +1225,20 @@ mod tests {
assert_eq!(prepared.as_ref(), auto_detected);
}

#[test]
fn library_candidates_try_lib64_then_lib_under_each_root() {
let roots = [PathBuf::from("/cuda"), PathBuf::from("/conda")];
assert_eq!(
library_candidates(&roots),
vec![
PathBuf::from("/cuda/lib64/libnvJitLink.so"),
PathBuf::from("/cuda/lib/libnvJitLink.so"),
PathBuf::from("/conda/lib64/libnvJitLink.so"),
PathBuf::from("/conda/lib/libnvJitLink.so"),
]
);
}

#[test]
fn cuda_roots_prefers_project_toolkit_env_var() {
let roots = cuda_roots_from_env(|var| match var {
Expand Down
Loading