diff --git a/Cargo.lock b/Cargo.lock index ac8cca04..24ad5092 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3531,6 +3531,10 @@ dependencies = [ "serde_json", ] +[[package]] +name = "openinfer-build" +version = "0.0.0" + [[package]] name = "openinfer-comm" version = "0.0.0" @@ -3554,13 +3558,9 @@ dependencies = [ "cc", "cxx", "cxx-build", - "openinfer-comm-build-utils", + "openinfer-build", ] -[[package]] -name = "openinfer-comm-build-utils" -version = "0.0.0" - [[package]] name = "openinfer-comm-cuda-lib" version = "0.0.0" @@ -3579,7 +3579,7 @@ name = "openinfer-comm-cuda-sys" version = "0.0.0" dependencies = [ "bindgen", - "openinfer-comm-build-utils", + "openinfer-build", ] [[package]] @@ -3587,7 +3587,7 @@ name = "openinfer-comm-cudart-sys" version = "0.0.0" dependencies = [ "bindgen", - "openinfer-comm-build-utils", + "openinfer-build", ] [[package]] @@ -3633,7 +3633,7 @@ name = "openinfer-comm-gdrapi-sys" version = "0.0.0" dependencies = [ "bindgen", - "openinfer-comm-build-utils", + "openinfer-build", ] [[package]] @@ -3642,7 +3642,7 @@ version = "0.0.0" dependencies = [ "bindgen", "cc", - "openinfer-comm-build-utils", + "openinfer-build", ] [[package]] @@ -3715,6 +3715,7 @@ version = "0.0.0" dependencies = [ "cxx", "cxx-build", + "openinfer-build", "openinfer-comm-cuda-lib", "pkg-config", "pyo3", @@ -3745,6 +3746,7 @@ name = "openinfer-cupti" version = "0.1.0" dependencies = [ "cc", + "openinfer-build", ] [[package]] @@ -3805,6 +3807,7 @@ dependencies = [ "cc", "cudarc", "half", + "openinfer-build", "serde", "tvm-ffi", ] diff --git a/Cargo.toml b/Cargo.toml index 2861654e..ec75637d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ members = [ "openinfer-cupti", "openinfer-kernels", "openinfer-bench", + "openinfer-build", "openinfer-deepseek-v4", "openinfer-deepseek-v2-lite", "openinfer-kimi-k2", @@ -20,7 +21,6 @@ members = [ "openinfer-kv-offload", # ---- openinfer-comm (EP all-to-all) ---- "openinfer-comm", - "openinfer-comm/crates/openinfer-comm-build-utils", "openinfer-comm/crates/openinfer-comm-cuda-sys", "openinfer-comm/crates/openinfer-comm-cudart-sys", "openinfer-comm/crates/openinfer-comm-gdrapi-sys", @@ -118,6 +118,7 @@ ordered-float = "4" oneshot = "0.1.11" parking_lot = "0.12.5" openinfer-bench = { path = "openinfer-bench" } +openinfer-build = { path = "openinfer-build" } openinfer-core = { path = "openinfer-core" } openinfer-kv-cache = { path = "openinfer-kv-cache" } openinfer-kv-offload = { path = "openinfer-kv-offload" } diff --git a/openinfer-comm/crates/openinfer-comm-build-utils/Cargo.toml b/openinfer-build/Cargo.toml similarity index 64% rename from openinfer-comm/crates/openinfer-comm-build-utils/Cargo.toml rename to openinfer-build/Cargo.toml index 23ed7786..cdd408a6 100644 --- a/openinfer-comm/crates/openinfer-comm-build-utils/Cargo.toml +++ b/openinfer-build/Cargo.toml @@ -1,5 +1,5 @@ [package] edition = "2024" -name = "openinfer-comm-build-utils" +name = "openinfer-build" license = "Apache-2.0" publish = false diff --git a/openinfer-build/src/lib.rs b/openinfer-build/src/lib.rs new file mode 100644 index 00000000..e2708b03 --- /dev/null +++ b/openinfer-build/src/lib.rs @@ -0,0 +1,355 @@ +use std::{ + env, + path::{Path, PathBuf}, +}; + +/// Finds a package's install root: probes `$env_var` first, then each of +/// `default_paths`, for any of the `check_files` — several cover layout +/// variants like `include/` vs `targets//include/`. Returns the +/// matched root and check file. +/// +/// # Panics +/// When nothing matches. +pub fn find_package( + provider: &str, + env_var: &str, + default_paths: &[&str], + check_files: &[&str], +) -> (PathBuf, PathBuf) { + println!("cargo:rerun-if-env-changed={}", env_var); + let env_root = env::var_os(env_var).map(PathBuf::from); + let roots: Vec = env_root + .clone() + .into_iter() + .chain(default_paths.iter().map(PathBuf::from)) + .collect(); + for root in &roots { + for check in check_files { + let found = root.join(check); + if found.is_file() { + if let Some(env_root) = &env_root + && env_root != root + { + println!( + "cargo:warning={provider}: ${env_var} ({}) contains none of \ + {check_files:?}; using {} instead", + env_root.display(), + root.display() + ); + } + return (root.clone(), found); + } + } + } + panic!( + "{provider} build error: none of {check_files:?} found. \ + Looked at `${env_var}` ({env_status}) and default paths {default_paths:?}. \ + Hint: install the provider headers or set `{env_var}` to their install root.", + env_status = env_root + .map(|root| format!("set to {root:?}")) + .unwrap_or_else(|| "unset".to_string()), + ) +} + +/// `targets/` names for the build target; aarch64 toolkits ship as +/// either `aarch64-linux` or `sbsa-linux`. Host arch outside build scripts. +fn target_dirs() -> Vec { + let arch = + env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_else(|_| std::env::consts::ARCH.to_string()); + match arch.as_str() { + "aarch64" => vec!["aarch64-linux".to_string(), "sbsa-linux".to_string()], + arch => vec![format!("{arch}-linux")], + } +} + +/// One build-time CUDA toolkit resolution covering the classic, conda, and +/// NVIDIA HPC SDK layouts; runtime loading (`LD_LIBRARY_PATH`, rpath) is out of scope. +pub struct CudaToolkit { + pub root: PathBuf, + /// `{root}/bin/nvcc` when present, otherwise bare `nvcc` from `$PATH`. + pub nvcc: PathBuf, + pub include_dirs: Vec, + pub lib_dirs: Vec, +} + +impl CudaToolkit { + /// Warns when an explicitly configured root does not exist; the silent + /// `/usr/local/cuda` default surfaces at the consuming probe or link step. + pub fn discover() -> Self { + println!("cargo:rerun-if-env-changed=CUDA_HOME"); + println!("cargo:rerun-if-env-changed=CUDA_PATH"); + let env_root = env::var("CUDA_HOME") + .or_else(|_| env::var("CUDA_PATH")) + .ok(); + if let Some(root) = env_root.as_deref().filter(|root| !Path::new(root).is_dir()) { + println!( + "cargo:warning=CUDA root {root} (from CUDA_HOME/CUDA_PATH) is not a directory" + ); + } + let root = env_root.map_or_else(|| PathBuf::from("/usr/local/cuda"), PathBuf::from); + Self::from_root(root) + } + + pub fn from_root(root: PathBuf) -> Self { + let nvcc = root.join("bin/nvcc"); + let nvcc = if nvcc.is_file() { + nvcc + } else { + PathBuf::from("nvcc") + }; + + let mut include_dirs = vec![root.join("include")]; + let mut lib_dirs = vec![root.join("lib64"), root.join("lib")]; + for target in target_dirs() { + include_dirs.push(root.join(format!("targets/{target}/include"))); + lib_dirs.push(root.join(format!("targets/{target}/lib"))); + } + // HPC SDK roots look like .../hpc_sdk///cuda/; the + // math libraries live in the /math_libs/ sibling tree. + if let (Some(version), Some(release)) = + (root.file_name(), root.parent().and_then(Path::parent)) + { + let math = release.join("math_libs").join(version); + lib_dirs.push(math.join("lib64")); + lib_dirs.push(math.join("lib")); + } + + Self { + nvcc, + include_dirs: existing_deduped(include_dirs), + lib_dirs: existing_deduped(lib_dirs), + root, + } + } + + /// The include dir that actually contains `header` — host-compiler `-I` + /// flags need this; on conda `include/` exists but lacks the CUDA headers. + pub fn header_dir(&self, header: &str) -> Option { + self.include_dirs + .iter() + .find(|dir| dir.join(header).is_file()) + .cloned() + } + + pub fn link_search(&self) { + if self.lib_dirs.is_empty() { + println!( + "cargo:warning=no CUDA library dir found under {}", + self.root.display() + ); + } + for dir in &self.lib_dirs { + println!("cargo:rustc-link-search=native={}", dir.display()); + } + } + + /// Driver-stub variant, for linking `libcuda` without a GPU driver. + pub fn link_search_stubs(&self) { + let dirs: Vec = self + .lib_dirs + .iter() + .map(|dir| dir.join("stubs")) + .filter(|dir| dir.is_dir()) + .collect(); + if dirs.is_empty() { + println!( + "cargo:warning=no CUDA stub dir found under {}", + self.root.display() + ); + } + for dir in dirs { + println!("cargo:rustc-link-search=native={}", dir.display()); + } + } +} + +fn existing_deduped(dirs: Vec) -> Vec { + let mut seen = Vec::new(); + let mut out = Vec::new(); + for dir in dirs { + if !dir.is_dir() { + continue; + } + let canon = dir.canonicalize().unwrap_or_else(|_| dir.clone()); + if !seen.contains(&canon) { + seen.push(canon); + out.push(dir); + } + } + out +} + +/// Recursively emits `cargo:rerun-if-changed` for all files under `src_dir` +/// with one of the given `extensions`. +pub fn emit_rerun_if_changed_files(src_dir: &str, extensions: &[&str]) { + fn visit_dir(dir: &Path, extensions: &[&str]) -> std::io::Result<()> { + for entry in std::fs::read_dir(dir)? { + let entry = entry?; + let path = entry.path(); + if path.is_dir() { + visit_dir(&path, extensions)?; + } else if let Some(ext) = path.extension().and_then(|s| s.to_str()) + && extensions.contains(&ext) + { + println!("cargo:rerun-if-changed={}", path.display()); + } + } + Ok(()) + } + + let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + let root = manifest_dir.join(src_dir); + + if let Err(err) = visit_dir(&root, extensions) { + eprintln!("cargo:warning=Failed to scan {}: {}", root.display(), err); + } + + // Also watch the directory itself so new files trigger rebuilds + println!("cargo:rerun-if-changed={}", root.display()); +} + +#[cfg(test)] +mod tests { + use super::*; + + struct TempTree(PathBuf); + + impl TempTree { + fn new(name: &str) -> Self { + let root = + std::env::temp_dir().join(format!("openinfer-build-{name}-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&root); + std::fs::create_dir_all(&root).unwrap(); + Self(root) + } + + fn mkdirs(&self, rel: &str) -> PathBuf { + let dir = self.0.join(rel); + std::fs::create_dir_all(&dir).unwrap(); + dir + } + + fn touch(&self, rel: &str) { + let file = self.0.join(rel); + std::fs::create_dir_all(file.parent().unwrap()).unwrap(); + std::fs::write(&file, b"").unwrap(); + } + } + + impl Drop for TempTree { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(&self.0); + } + } + + fn target_dir() -> String { + target_dirs().remove(0) + } + + #[test] + fn classic_layout() { + let tree = TempTree::new("classic"); + tree.touch("include/cuda.h"); + tree.touch("bin/nvcc"); + let lib64 = tree.mkdirs("lib64"); + tree.mkdirs("lib64/stubs"); + + let tk = CudaToolkit::from_root(tree.0.clone()); + assert_eq!(tk.nvcc, tree.0.join("bin/nvcc")); + assert_eq!(tk.header_dir("cuda.h"), Some(tree.0.join("include"))); + assert_eq!(tk.lib_dirs, vec![lib64.clone()]); + assert!(lib64.join("stubs").is_dir()); + } + + #[test] + fn conda_layout() { + let tree = TempTree::new("conda"); + let target = target_dir(); + tree.mkdirs("include"); + tree.touch(&format!("targets/{target}/include/cuda.h")); + let lib = tree.mkdirs("lib"); + let targets_lib = tree.mkdirs(&format!("targets/{target}/lib")); + + let tk = CudaToolkit::from_root(tree.0.clone()); + assert_eq!(tk.nvcc, PathBuf::from("nvcc")); + assert_eq!( + tk.header_dir("cuda.h"), + Some(tree.0.join(format!("targets/{target}/include"))) + ); + assert_eq!(tk.lib_dirs, vec![lib, targets_lib]); + } + + #[test] + fn hpc_sdk_layout_adds_math_libs_sibling() { + let tree = TempTree::new("hpcsdk"); + let root = tree.mkdirs("release/cuda/12.6"); + let lib64 = tree.mkdirs("release/cuda/12.6/lib64"); + let math = tree.mkdirs("release/math_libs/12.6/lib64"); + + assert_eq!(CudaToolkit::from_root(root).lib_dirs, vec![lib64, math]); + } + + #[cfg(unix)] + #[test] + fn symlinked_dirs_dedupe() { + let tree = TempTree::new("symlink"); + let target = target_dir(); + tree.touch(&format!("targets/{target}/include/cuda.h")); + tree.mkdirs(&format!("targets/{target}/lib")); + std::os::unix::fs::symlink( + tree.0.join(format!("targets/{target}/include")), + tree.0.join("include"), + ) + .unwrap(); + std::os::unix::fs::symlink( + tree.0.join(format!("targets/{target}/lib")), + tree.0.join("lib"), + ) + .unwrap(); + + let tk = CudaToolkit::from_root(tree.0.clone()); + assert_eq!(tk.include_dirs.len(), 1); + assert_eq!(tk.lib_dirs.len(), 1); + assert_eq!(tk.header_dir("cuda.h"), Some(tree.0.join("include"))); + } + + #[test] + fn unknown_layout_yields_nothing() { + let tree = TempTree::new("unknown"); + tree.mkdirs("weird/place"); + + let tk = CudaToolkit::from_root(tree.0.clone()); + assert!(tk.lib_dirs.is_empty()); + assert!(tk.include_dirs.is_empty()); + assert_eq!(tk.header_dir("cuda.h"), None); + } + + #[test] + fn find_package_returns_matching_root_and_check_file() { + let tree = TempTree::new("findpkg"); + tree.touch("include/gdrapi.h"); + let root_str = tree.0.to_str().unwrap().to_string(); + + let (root, header) = find_package( + "test", + "OPENINFER_TEST_UNSET_ENV", + &[&root_str], + &["targets/missing/include/gdrapi.h", "include/gdrapi.h"], + ); + assert_eq!(root, tree.0); + assert_eq!(header, tree.0.join("include/gdrapi.h")); + } + + #[test] + #[should_panic(expected = "none of")] + fn missing_header_panics_with_all_candidates() { + let tree = TempTree::new("empty"); + let root_str = tree.0.to_str().unwrap().to_string(); + find_package( + "test", + "OPENINFER_TEST_UNSET_ENV", + &[&root_str], + &["include/cuda.h"], + ); + } +} diff --git a/openinfer-comm/crates/openinfer-comm-a2a-kernels/Cargo.toml b/openinfer-comm/crates/openinfer-comm-a2a-kernels/Cargo.toml index a87eb735..8368deba 100644 --- a/openinfer-comm/crates/openinfer-comm-a2a-kernels/Cargo.toml +++ b/openinfer-comm/crates/openinfer-comm-a2a-kernels/Cargo.toml @@ -18,4 +18,4 @@ cxx = { workspace = true } [build-dependencies] cc = { workspace = true, features = ["parallel"] } cxx-build = { workspace = true } -build-utils = { path = "../openinfer-comm-build-utils", package = "openinfer-comm-build-utils" } +openinfer-build = { workspace = true } diff --git a/openinfer-comm/crates/openinfer-comm-a2a-kernels/build.rs b/openinfer-comm/crates/openinfer-comm-a2a-kernels/build.rs index 188f53b7..7688aa48 100644 --- a/openinfer-comm/crates/openinfer-comm-a2a-kernels/build.rs +++ b/openinfer-comm/crates/openinfer-comm-a2a-kernels/build.rs @@ -11,7 +11,6 @@ fn main() { let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); - // Generate bindings cxx_build::bridge("src/hw_cuda_impl.rs") .debug(false) .cuda(true) @@ -27,8 +26,8 @@ fn main() { .file("src/a2a/a2a_dispatch_send.cu") .compile("liba2a_kernels.a"); - build_utils::emit_rerun_if_changed_files("src", &["cu", "cuh", "h"]); + openinfer_build::emit_rerun_if_changed_files("src", &["cu", "cuh", "h"]); - println!("cargo:rustc-link-search=native=/usr/local/cuda/lib64"); + openinfer_build::CudaToolkit::discover().link_search(); println!("cargo:rustc-link-lib=cudart"); } diff --git a/openinfer-comm/crates/openinfer-comm-build-utils/src/lib.rs b/openinfer-comm/crates/openinfer-comm-build-utils/src/lib.rs deleted file mode 100644 index 51791e9c..00000000 --- a/openinfer-comm/crates/openinfer-comm-build-utils/src/lib.rs +++ /dev/null @@ -1,79 +0,0 @@ -use std::{ - env, - path::{Path, PathBuf}, -}; - -/// Finds the path to a package directory by checking an environment variable and a list of default paths. -/// -/// The function checks if the environment variable `env_var` is set and points to a directory containing `check_file`. -/// If not, it searches each path in `default_paths` for the presence of `check_file`. -/// Returns the first directory containing `check_file`, or panics with a provider-specific -/// error pointing at `provider` (e.g. "cuda-sys", "gdrapi-sys", "libibverbs-sys"). -/// -/// # Arguments -/// * `provider` - The name of the *-sys / provider crate calling this helper. Surfaced -/// in the panic message so consumers can tell which provider failed. -/// * `env_var` - The name of the environment variable to check. -/// * `default_paths` - A slice of default directory paths to search. -/// * `check_file` - The relative path to the file that must exist in the directory. -/// -/// # Panics -/// Panics if neither the environment variable nor any of the default paths contain `check_file`. -pub fn find_package( - provider: &str, - env_var: &str, - default_paths: &[&str], - check_file: &str, -) -> PathBuf { - println!("cargo:rerun-if-env-changed={}", env_var); - env::var_os(env_var) - .map(PathBuf::from) - .into_iter() - .chain(default_paths.iter().map(PathBuf::from)) - .find(|dir| dir.join(check_file).is_file()) - .unwrap_or_else(|| { - panic!( - "{provider} build error: required header `{check_file}` not found. \ - Looked at `${env_var}` ({env_status}) and default paths {default_paths:?}. \ - Hint: install the provider headers or set `{env_var}` to their install root.", - env_status = env::var_os(env_var) - .map(|v| format!("set to {:?}", v)) - .unwrap_or_else(|| "unset".to_string()), - ) - }) -} - -/// Recursively emits `cargo:rerun-if-changed` for all files under `src_dir` -/// with one of the given `extensions`. -/// -/// Example: -/// ```no_run -/// use build_utils::emit_rerun_if_changed_files; -/// emit_rerun_if_changed_files("src", &["cu", "cuh", "h"]); -/// ``` -pub fn emit_rerun_if_changed_files(src_dir: &str, extensions: &[&str]) { - fn visit_dir(dir: &Path, extensions: &[&str]) -> std::io::Result<()> { - for entry in std::fs::read_dir(dir)? { - let entry = entry?; - let path = entry.path(); - if path.is_dir() { - visit_dir(&path, extensions)?; - } else if let Some(ext) = path.extension().and_then(|s| s.to_str()) - && extensions.contains(&ext) - { - println!("cargo:rerun-if-changed={}", path.display()); - } - } - Ok(()) - } - - let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); - let root = manifest_dir.join(src_dir); - - if let Err(err) = visit_dir(&root, extensions) { - eprintln!("cargo:warning=Failed to scan {}: {}", root.display(), err); - } - - // Also watch the directory itself so new files trigger rebuilds - println!("cargo:rerun-if-changed={}", root.display()); -} diff --git a/openinfer-comm/crates/openinfer-comm-cuda-sys/Cargo.toml b/openinfer-comm/crates/openinfer-comm-cuda-sys/Cargo.toml index 18b0a645..a04f080f 100644 --- a/openinfer-comm/crates/openinfer-comm-cuda-sys/Cargo.toml +++ b/openinfer-comm/crates/openinfer-comm-cuda-sys/Cargo.toml @@ -15,6 +15,6 @@ default = [] system-bindings = [] [build-dependencies] -build-utils = { path = "../openinfer-comm-build-utils", package = "openinfer-comm-build-utils" } +openinfer-build = { workspace = true } bindgen = { workspace = true } diff --git a/openinfer-comm/crates/openinfer-comm-cuda-sys/build.rs b/openinfer-comm/crates/openinfer-comm-cuda-sys/build.rs index d62782fa..ab4fe124 100644 --- a/openinfer-comm/crates/openinfer-comm-cuda-sys/build.rs +++ b/openinfer-comm/crates/openinfer-comm-cuda-sys/build.rs @@ -10,14 +10,19 @@ fn main() -> Result<(), Box> { return Ok(()); } - let cuda_home = build_utils::find_package( - "cuda-sys", - "CUDA_HOME", - &["/usr/local/cuda"], - "include/cuda.h", - ); + let toolkit = openinfer_build::CudaToolkit::discover(); + let cuda_h = toolkit + .header_dir("cuda.h") + .map(|dir| dir.join("cuda.h")) + .unwrap_or_else(|| { + panic!( + "cuda-sys build error: cuda.h not found under {}. \ + Hint: install the CUDA SDK and/or set CUDA_HOME to its install root.", + toolkit.root.display() + ) + }); let bindings = bindgen::Builder::default() - .header(cuda_home.join("include/cuda.h").to_string_lossy()) + .header(cuda_h.to_string_lossy()) .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) .prepend_enum_name(false) .allowlist_item(r"(cu|CU).*") @@ -26,9 +31,9 @@ fn main() -> Result<(), Box> { .map_err(|e| { format!( "cuda-sys build error: failed to generate CUDA driver bindings via bindgen \ - (looked under CUDA_HOME={}). Underlying error: {}. \ + (looked under {}). Underlying error: {}. \ Hint: install the CUDA SDK and/or set CUDA_HOME to its install root.", - cuda_home.display(), + toolkit.root.display(), e ) })?; @@ -37,8 +42,7 @@ fn main() -> Result<(), Box> { format!("cuda-sys build error: cannot write cuda-bindings.rs: {}", e) })?; - // Dynamic link dependencies - println!("cargo:rustc-link-search=native={}/lib64/stubs", cuda_home.display()); + toolkit.link_search_stubs(); println!("cargo:rustc-link-lib=cuda"); Ok(()) diff --git a/openinfer-comm/crates/openinfer-comm-cudart-sys/Cargo.toml b/openinfer-comm/crates/openinfer-comm-cudart-sys/Cargo.toml index 4860cf02..b02ef474 100644 --- a/openinfer-comm/crates/openinfer-comm-cudart-sys/Cargo.toml +++ b/openinfer-comm/crates/openinfer-comm-cudart-sys/Cargo.toml @@ -11,6 +11,6 @@ default = [] system-bindings = [] [build-dependencies] -build-utils = { path = "../openinfer-comm-build-utils", package = "openinfer-comm-build-utils" } +openinfer-build = { workspace = true } bindgen = { workspace = true } diff --git a/openinfer-comm/crates/openinfer-comm-cudart-sys/build.rs b/openinfer-comm/crates/openinfer-comm-cudart-sys/build.rs index 47859b82..ace24f61 100644 --- a/openinfer-comm/crates/openinfer-comm-cudart-sys/build.rs +++ b/openinfer-comm/crates/openinfer-comm-cudart-sys/build.rs @@ -23,15 +23,17 @@ fn main() -> Result<(), Box> { return Ok(()); } - let cuda_home = build_utils::find_package( - "cudart-sys", - "CUDA_HOME", - &["/usr/local/cuda"], - "include/cuda.h", - ); + let toolkit = openinfer_build::CudaToolkit::discover(); + let cuda_include = toolkit.header_dir("cuda.h").unwrap_or_else(|| { + panic!( + "cudart-sys build error: cuda.h not found under {}. \ + Hint: install the CUDA SDK and/or set CUDA_HOME to its install root.", + toolkit.root.display() + ) + }); let bindings = bindgen::Builder::default() .header("wrapper.h") - .clang_arg(format!("-I{}/include", cuda_home.display())) + .clang_arg(format!("-I{}", cuda_include.display())) .parse_callbacks(Box::new(RenameCallback)) .prepend_enum_name(false) .allowlist_item(r"cuda.*") @@ -40,9 +42,9 @@ fn main() -> Result<(), Box> { .map_err(|e| { format!( "cudart-sys build error: failed to generate CUDA runtime bindings via bindgen \ - (looked under CUDA_HOME={}). Underlying error: {}. \ + (looked under {}). Underlying error: {}. \ Hint: install the CUDA SDK and/or set CUDA_HOME to its install root.", - cuda_home.display(), + toolkit.root.display(), e ) })?; @@ -51,8 +53,7 @@ fn main() -> Result<(), Box> { format!("cudart-sys build error: cannot write cudart-bindings.rs: {}", e) })?; - // Dynamic link dependencies - println!("cargo:rustc-link-search=native={}/lib64", cuda_home.display()); + toolkit.link_search(); println!("cargo:rustc-link-lib=cudart"); Ok(()) diff --git a/openinfer-comm/crates/openinfer-comm-gdrapi-sys/Cargo.toml b/openinfer-comm/crates/openinfer-comm-gdrapi-sys/Cargo.toml index ec6eea9c..5ef45b72 100644 --- a/openinfer-comm/crates/openinfer-comm-gdrapi-sys/Cargo.toml +++ b/openinfer-comm/crates/openinfer-comm-gdrapi-sys/Cargo.toml @@ -11,6 +11,6 @@ default = [] system-bindings = [] [build-dependencies] -build-utils = { path = "../openinfer-comm-build-utils", package = "openinfer-comm-build-utils" } +openinfer-build = { workspace = true } bindgen = { workspace = true } diff --git a/openinfer-comm/crates/openinfer-comm-gdrapi-sys/build.rs b/openinfer-comm/crates/openinfer-comm-gdrapi-sys/build.rs index e386f5a9..d0cb4178 100644 --- a/openinfer-comm/crates/openinfer-comm-gdrapi-sys/build.rs +++ b/openinfer-comm/crates/openinfer-comm-gdrapi-sys/build.rs @@ -6,11 +6,11 @@ fn main() -> Result<(), Box> { return Ok(()); } - let gdrapi_home = build_utils::find_package( + let (gdrapi_home, _) = openinfer_build::find_package( "gdrapi-sys", "GDRAPI_HOME", &["/usr"], - "include/gdrapi.h", + &["include/gdrapi.h"], ); let bindings = bindgen::Builder::default() .header_contents("wrapper.h", "#include ") @@ -35,7 +35,6 @@ fn main() -> Result<(), Box> { format!("gdrapi-sys build error: cannot write gdrapi-bindings.rs: {}", e) })?; - // Dynamic link dependencies println!("cargo:rustc-link-lib=gdrapi"); println!("cargo:rustc-link-search=native={}/lib", gdrapi_home.display()); diff --git a/openinfer-comm/crates/openinfer-comm-libibverbs-sys/Cargo.toml b/openinfer-comm/crates/openinfer-comm-libibverbs-sys/Cargo.toml index 076e0e7d..2693f684 100644 --- a/openinfer-comm/crates/openinfer-comm-libibverbs-sys/Cargo.toml +++ b/openinfer-comm/crates/openinfer-comm-libibverbs-sys/Cargo.toml @@ -11,7 +11,7 @@ default = [] system-bindings = [] [build-dependencies] -build-utils = { path = "../openinfer-comm-build-utils", package = "openinfer-comm-build-utils" } +openinfer-build = { workspace = true } bindgen = { workspace = true } cc = { workspace = true } diff --git a/openinfer-comm/crates/openinfer-comm-libibverbs-sys/build.rs b/openinfer-comm/crates/openinfer-comm-libibverbs-sys/build.rs index a9bd0f18..59e7b4df 100644 --- a/openinfer-comm/crates/openinfer-comm-libibverbs-sys/build.rs +++ b/openinfer-comm/crates/openinfer-comm-libibverbs-sys/build.rs @@ -6,15 +6,14 @@ fn main() -> Result<(), Box> { return Ok(()); } - let libibverbs_home = build_utils::find_package( + let (libibverbs_home, _) = openinfer_build::find_package( "libibverbs-sys", "LIBIBVERBS_HOME", &["/usr"], - "include/infiniband/verbs.h", + &["include/infiniband/verbs.h"], ); let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); - // Generate bindings // https://rust-lang.github.io/rust-bindgen/tutorial-3.html let bindings = bindgen::Builder::default() .header("wrapper.h") @@ -48,14 +47,12 @@ fn main() -> Result<(), Box> { ) })?; - // Compile wrap_static_fns.c cc::Build::new() .file(out_dir.join("wrap_static_fns.c")) .include(libibverbs_home.join("include")) .include(env!("CARGO_MANIFEST_DIR")) .compile("wrap_static_fns"); - // Dynamic link dependencies println!("cargo:rustc-link-search=native={}/lib", libibverbs_home.display()); println!("cargo:rustc-link-lib=ibverbs"); diff --git a/openinfer-comm/crates/openinfer-comm-torch-lib/Cargo.toml b/openinfer-comm/crates/openinfer-comm-torch-lib/Cargo.toml index 82b59e21..177a7108 100644 --- a/openinfer-comm/crates/openinfer-comm-torch-lib/Cargo.toml +++ b/openinfer-comm/crates/openinfer-comm-torch-lib/Cargo.toml @@ -19,4 +19,5 @@ cuda-lib = { path = "../openinfer-comm-cuda-lib", package = "openinfer-comm-cuda [build-dependencies] cxx-build = { workspace = true } +openinfer-build = { workspace = true } pkg-config = { workspace = true } diff --git a/openinfer-comm/crates/openinfer-comm-torch-lib/build.rs b/openinfer-comm/crates/openinfer-comm-torch-lib/build.rs index 1179788e..b9d61764 100644 --- a/openinfer-comm/crates/openinfer-comm-torch-lib/build.rs +++ b/openinfer-comm/crates/openinfer-comm-torch-lib/build.rs @@ -11,6 +11,7 @@ fn main() { return; } + let toolkit = openinfer_build::CudaToolkit::discover(); let cmake_prefix_path = resolve_torch_cmake_prefix_path(); let torch_path = resolve_torch_install_root(&cmake_prefix_path); let torch_include = torch_path.join("include"); @@ -26,7 +27,7 @@ fn main() { .flag("-Wno-unused-parameter") .includes(config.include_paths) .include(torch_include) - .include("/usr/local/cuda/include") + .includes(&toolkit.include_dirs) .std("c++20") .compile("torch-lib"); diff --git a/openinfer-cupti/Cargo.toml b/openinfer-cupti/Cargo.toml index 25a558ae..39b82935 100644 --- a/openinfer-cupti/Cargo.toml +++ b/openinfer-cupti/Cargo.toml @@ -6,6 +6,7 @@ edition = "2024" [build-dependencies] cc = { workspace = true } +openinfer-build = { workspace = true } [lints] workspace = true diff --git a/openinfer-cupti/build.rs b/openinfer-cupti/build.rs index e93375fd..b594dec6 100644 --- a/openinfer-cupti/build.rs +++ b/openinfer-cupti/build.rs @@ -1,42 +1,27 @@ -use std::path::{Path, PathBuf}; - -fn cuda_root() -> PathBuf { - std::env::var("CUDA_HOME") - .or_else(|_| std::env::var("CUDA_PATH")) - .map_or_else(|_| PathBuf::from("/usr/local/cuda"), PathBuf::from) -} - -fn add_if_exists(build: &mut cc::Build, path: &Path) { - if path.exists() { - build.include(path); - } -} +use std::path::PathBuf; fn main() { let manifest_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()); - let cuda_root = cuda_root(); - let cuda_include = cuda_root.join("include"); - let cuda_target_include = cuda_root.join("targets/x86_64-linux/include"); - let cupti_include = cuda_root.join("extras/CUPTI/include"); + let toolkit = openinfer_build::CudaToolkit::discover(); + // CUPTI ships outside the main toolkit dirs in classic installs. + let cupti_include = toolkit.root.join("extras/CUPTI/include"); + let cupti_lib64 = toolkit.root.join("extras/CUPTI/lib64"); let mut build = cc::Build::new(); build .cpp(true) .std("c++17") .warnings(false) - .file(manifest_dir.join("csrc/range_profiler.cpp")); - add_if_exists(&mut build, &cuda_include); - add_if_exists(&mut build, &cuda_target_include); - add_if_exists(&mut build, &cupti_include); + .file(manifest_dir.join("csrc/range_profiler.cpp")) + .includes(&toolkit.include_dirs); + if cupti_include.is_dir() { + build.include(&cupti_include); + } build.compile("openinfer_cupti_range_profiler"); - let cuda_lib64 = cuda_root.join("lib64"); - let cuda_target_lib = cuda_root.join("targets/x86_64-linux/lib"); - let cupti_lib64 = cuda_root.join("extras/CUPTI/lib64"); - for path in [&cuda_lib64, &cuda_target_lib, &cupti_lib64] { - if path.exists() { - println!("cargo:rustc-link-search=native={}", path.display()); - } + toolkit.link_search(); + if cupti_lib64.is_dir() { + println!("cargo:rustc-link-search=native={}", cupti_lib64.display()); } println!("cargo:rustc-link-lib=cuda"); @@ -46,6 +31,4 @@ fn main() { } println!("cargo:rerun-if-changed=csrc/range_profiler.cpp"); - println!("cargo:rerun-if-env-changed=CUDA_HOME"); - println!("cargo:rerun-if-env-changed=CUDA_PATH"); } diff --git a/openinfer-kernels/Cargo.toml b/openinfer-kernels/Cargo.toml index 548de2e6..cc070762 100644 --- a/openinfer-kernels/Cargo.toml +++ b/openinfer-kernels/Cargo.toml @@ -13,6 +13,7 @@ tvm-ffi = { version = "0.1.0-alpha.0", optional = true } [build-dependencies] cc = { workspace = true } +openinfer-build = { workspace = true } [features] default = [] diff --git a/openinfer-kernels/build.rs b/openinfer-kernels/build.rs index 8a83ad32..0055c908 100644 --- a/openinfer-kernels/build.rs +++ b/openinfer-kernels/build.rs @@ -894,7 +894,14 @@ fn write_wrapper(generated_c: &Path, file_name: &str, wrapper_src: String) -> Pa wrapper_path } -fn compile_triton_aot_kernels(cuda_path: &str, out_dir: &Path, sm_targets: &[String]) { +fn compile_triton_aot_kernels(cuda_include: &Path, out_dir: &Path, sm_targets: &[String]) { + // The generated wrappers are compiled with the host C compiler, which + // unlike nvcc does not know the toolkit headers on its own. + assert!( + cuda_include.join("cuda.h").is_file(), + "cuda.h not found in {}; set CUDA_HOME to a CUDA toolkit root", + cuda_include.display() + ); let python = find_triton_python().unwrap_or_else(|message| panic!("{message}")); let triton_target = triton_target(sm_targets); let mut generated_sources = Vec::new(); @@ -1072,7 +1079,7 @@ fn compile_triton_aot_kernels(cuda_path: &str, out_dir: &Path, sm_targets: &[Str let mut build = cc::Build::new(); build .cuda(false) - .include(format!("{}/include", cuda_path)) + .include(cuda_include) .flag("-std=c11") .warnings(false); for source in &generated_sources { @@ -1099,12 +1106,11 @@ fn compile_triton_aot_kernels(cuda_path: &str, out_dir: &Path, sm_targets: &[Str } fn main() { - let cuda_path = std::env::var("CUDA_HOME") - .or_else(|_| std::env::var("CUDA_PATH")) - .unwrap_or_else(|_| "/usr/local/cuda".to_string()); - - let nvcc = format!("{}/bin/nvcc", cuda_path); - let cuda_include = Path::new(&cuda_path).join("include"); + let toolkit = openinfer_build::CudaToolkit::discover(); + let nvcc = toolkit.nvcc.to_string_lossy().into_owned(); + let cuda_include = toolkit + .header_dir("cuda.h") + .unwrap_or_else(|| toolkit.root.join("include")); let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap()); let sm_targets = detect_sm_targets(); let nvcc_sm_targets = normalize_nvcc_sms(&sm_targets, &nvcc); @@ -1467,7 +1473,7 @@ fn main() { assert!(status.success(), "ar failed"); if qwen35_enabled { - compile_triton_aot_kernels(&cuda_path, &out_dir, &sm_targets); + compile_triton_aot_kernels(&cuda_include, &out_dir, &sm_targets); } else { println!( "cargo:warning=Qwen3.5 Triton AOT kernels disabled; enable the openinfer-kernels `qwen35-4b` feature to build them (needs Python + Triton at build time)" @@ -1476,9 +1482,12 @@ fn main() { println!("cargo:rustc-link-search=native={}", out_dir.display()); if cfg!(target_os = "windows") { - println!("cargo:rustc-link-search=native={}/lib/x64", cuda_path); + println!( + "cargo:rustc-link-search=native={}/lib/x64", + toolkit.root.display() + ); } else { - println!("cargo:rustc-link-search=native={}/lib64", cuda_path); + toolkit.link_search(); } for dir in &cutedsl_runtime_lib_dirs { println!("cargo:rustc-link-search=native={}", dir.display()); @@ -1499,8 +1508,6 @@ fn main() { println!("cargo:rerun-if-changed={}", root.join("csrc").display()); println!("cargo:rerun-if-changed=build.rs"); - println!("cargo:rerun-if-env-changed=CUDA_HOME"); - println!("cargo:rerun-if-env-changed=CUDA_PATH"); println!("cargo:rerun-if-env-changed=OPENINFER_CUDA_SM"); println!("cargo:rerun-if-env-changed=CUDA_SM"); println!("cargo:rerun-if-env-changed=OPENINFER_FLASHINFER_INCLUDE");