Files
miner/crates/engine-gpu-cuda/build.rs
T

490 lines
16 KiB
Rust

use std::env;
use std::ffi::OsStr;
use std::fs;
use std::io::{self};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
/// Return true when the host GCC major version is greater than 14.
/// We try $CC --version first, then gcc --version, and parse the first X.Y-like token.
fn host_gcc_too_new() -> bool {
fn probe(bin: &str) -> Option<String> {
Command::new(bin)
.arg("--version")
.stdout(Stdio::piped())
.output()
.ok()
.and_then(|o| {
if o.status.success() {
Some(String::from_utf8_lossy(&o.stdout).into_owned())
} else {
None
}
})
}
let cc = env::var("CC").unwrap_or_else(|_| "gcc".to_string());
let ver_out = probe(&cc).or_else(|| probe("gcc"));
if let Some(s) = ver_out {
// Scan tokens separated by non [0-9|.] and take the first X.Y...
for tok in s.split(|c: char| !c.is_ascii_digit() && c != '.') {
if tok.is_empty() {
continue;
}
if let Some((maj, _)) = tok.split_once('.') {
if let Ok(m) = maj.parse::<u32>() {
return m > 14;
}
}
}
}
false
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Only do CUDA work if this crate is built with the `cuda` feature.
let cuda_feature_enabled = env::var_os("CARGO_FEATURE_CUDA").is_some();
println!("cargo:rerun-if-env-changed=CARGO_FEATURE_CUDA");
if !cuda_feature_enabled {
// Generate empty bindings so include! compiles; skip CUDA work.
let out_dir = PathBuf::from(env::var("OUT_DIR")?);
generate_empty_bindings(&out_dir)?;
println!("cargo:warning=engine-gpu-cuda built without 'cuda' feature; skipping kernel compilation.");
return Ok(());
}
// Gather kernel sources (.cu) from common locations
let kernel_roots = [
Path::new("src").join("kernels"),
Path::new("src").join("cuda"),
];
let mut cu_files: Vec<PathBuf> = Vec::new();
for root in kernel_roots.iter() {
if root.is_dir() {
scan_cu_files(root, &mut cu_files)?;
}
}
// If there are no kernels yet, still generate an empty bindings file so include! compiles.
if cu_files.is_empty() {
let out_dir = PathBuf::from(env::var("OUT_DIR")?);
generate_empty_bindings(&out_dir)?;
println!("cargo:warning=No CUDA kernels (.cu) found under src/kernels or src/cuda; generated empty PTX/CUBIN bindings.");
return Ok(());
}
// Emit rebuild hints for kernels and env vars that affect compilation
for cu in &cu_files {
println!("cargo:rerun-if-changed={}", cu.display());
}
println!("cargo:rerun-if-env-changed=NVCC");
println!("cargo:rerun-if-env-changed=CUDA_HOME");
println!("cargo:rerun-if-env-changed=CUDA_PATH");
println!("cargo:rerun-if-env-changed=CUDA_ARCH");
println!("cargo:rerun-if-env-changed=MINER_CUDA_ALLOW_UNSUPPORTED_COMPILER");
println!("cargo:rerun-if-env-changed=MINER_NVCC_CCBIN");
// Determine nvcc path
let nvcc = find_nvcc().ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
"Unable to locate `nvcc`. Set NVCC or CUDA_HOME/CUDA_PATH, or ensure nvcc is in PATH.",
)
})?;
// Architecture (SM version) can be overridden; default to sm_70 as a reasonable baseline for modern GPUs
let raw_arch = env::var("CUDA_ARCH").unwrap_or_else(|_| "sm_70".to_string());
// Normalize to a compute/sm pair:
// - Use compute_* for -arch
// - Use sm_* for -code
let (arch_compute, arch_sm) = if let Some(s) = raw_arch.strip_prefix("sm_") {
(format!("compute_{s}"), raw_arch.clone())
} else if let Some(s) = raw_arch.strip_prefix("compute_") {
(raw_arch.clone(), format!("sm_{s}"))
} else if let Some(s) = raw_arch.strip_prefix("sm") {
(format!("compute{s}"), raw_arch.clone())
} else {
("compute_70".to_string(), "sm_70".to_string())
};
println!("cargo:warning=NVCC={}", nvcc.display());
println!("cargo:warning=CUDA_ARCH (normalized): compute={arch_compute}, sm={arch_sm}");
// Output directory for artifacts
let out_dir = PathBuf::from(env::var("OUT_DIR")?);
println!(
"cargo:rustc-env=ENGINE_GPU_CUDA_PTX_DIR={}",
out_dir.display()
);
// Preflight: locate CUDA include directories and ensure cuda_runtime.h exists.
// Prefer $CUDA_HOME/targets/x86_64-linux/include, then $CUDA_HOME/include. Derive CUDA_HOME from NVCC if needed.
let cuda_home_env = env::var_os("CUDA_HOME").map(PathBuf::from).or_else(|| {
// Derive CUDA_HOME from NVCC path (<home>/bin/nvcc)
nvcc.parent()
.and_then(Path::parent)
.map(|p| p.to_path_buf())
});
let mut include_dirs: Vec<PathBuf> = Vec::new();
// If CUDA_HOME was found (either env or derived from NVCC), add its common include locations.
if let Some(home) = cuda_home_env.as_ref() {
let t_inc = home.join("targets").join("x86_64-linux").join("include");
let i_inc = home.join("include");
if t_inc.is_dir() {
include_dirs.push(t_inc);
}
if i_inc.is_dir() {
include_dirs.push(i_inc);
}
}
// Fallback: common system install prefix in CUDA devel images.
// This allows builds to succeed even when CUDA_HOME isn't set in the container.
for fallback in [
Path::new("/usr/local/cuda/targets/x86_64-linux/include"),
Path::new("/usr/local/cuda/include"),
] {
if fallback.is_dir() {
include_dirs.push(fallback.to_path_buf());
}
}
// Deduplicate include dirs to avoid noisy logs and duplicate -I flags
include_dirs.sort();
include_dirs.dedup();
for inc in &include_dirs {
println!("cargo:warning=CUDA_INCLUDE_DIR={}", inc.display());
}
let has_runtime_h = include_dirs
.iter()
.any(|d| d.join("cuda_runtime.h").is_file());
if !has_runtime_h {
return Err(Box::new(io::Error::other(format!(
"engine-gpu-cuda: missing CUDA headers (cuda_runtime.h). \
Ensure the matching CUDA toolkit is installed (see README), \
and CUDA_HOME is set (current CUDA_HOME={:?}). Searched: {}",
cuda_home_env,
include_dirs
.iter()
.map(|p| p.display().to_string())
.collect::<Vec<_>>()
.join(", ")
))));
}
// Extra nvcc options and optional host compiler override
let mut extra_nvcc_flags: Vec<String> = Vec::new();
let allow_unsupported = env::var("MINER_CUDA_ALLOW_UNSUPPORTED_COMPILER")
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
.unwrap_or(false);
if allow_unsupported || host_gcc_too_new() {
println!("cargo:warning=nvcc: enabling -allow-unsupported-compiler");
extra_nvcc_flags.push("-allow-unsupported-compiler".to_string());
}
let ccbin = env::var("MINER_NVCC_CCBIN").ok();
if let Some(ref cc) = ccbin {
println!("cargo:warning=nvcc: using -ccbin {cc}");
}
// Compile each .cu into artifacts
// - PTX: for driver JIT (fallback)
// - CUBIN: native SASS per-SM (preferred; avoids PTX JIT/ISA mismatches)
let mut generated_ptx: Vec<(String, PathBuf)> = Vec::new();
let mut generated_cubin: Vec<(String, PathBuf)> = Vec::new();
for cu in &cu_files {
let stem = cu
.file_stem()
.and_then(OsStr::to_str)
.ok_or_else(|| io::Error::other("Invalid kernel filename"))?
.to_string();
let ptx_path = out_dir.join(format!("{stem}.ptx"));
let cubin_path = out_dir.join(format!("{stem}.cubin"));
// PTX
match compile_to_ptx(
&nvcc,
&arch_compute,
cu,
&ptx_path,
&include_dirs,
ccbin.as_deref(),
&extra_nvcc_flags,
) {
Ok(()) => {
println!(
"cargo:warning=nvcc PTX OK: {} -> {}",
cu.display(),
ptx_path.display()
);
generated_ptx.push((stem.clone(), ptx_path.clone()));
}
Err(err) => {
eprintln!("cargo:warning=nvcc PTX failed for {}: {err}", cu.display());
eprintln!(
"cargo:warning=Skipping PTX for {stem} (will rely on CUBIN if available)"
);
}
}
// CUBIN (preferred at runtime)
match compile_to_cubin(
&nvcc,
&arch_compute,
&arch_sm,
cu,
&cubin_path,
&include_dirs,
ccbin.as_deref(),
&extra_nvcc_flags,
) {
Ok(()) => {
println!(
"cargo:warning=nvcc CUBIN OK: {} -> {}",
cu.display(),
cubin_path.display()
);
generated_cubin.push((stem, cubin_path.clone()));
}
Err(err) => {
eprintln!(
"cargo:warning=nvcc CUBIN failed for {}: {err}",
cu.display()
);
eprintln!(
"cargo:warning=Skipping CUBIN for {stem} (will fall back to PTX if present)"
);
}
}
}
// Fail fast if no artifacts were generated (prevents producing a GPU binary without embeds)
if generated_ptx.is_empty() && generated_cubin.is_empty() {
return Err(Box::new(io::Error::other(
"engine-gpu-cuda: no CUDA artifacts embedded (PTX/CUBIN). Ensure NVCC is set and CUDA_ARCH=sm_XX; see README Fedora section.",
)));
}
// Generate an embedded PTX/CUBIN bindings file so the crate can include!() the artifacts at compile time.
// Output: $OUT_DIR/ptx_bindings.rs with:
// - pub mod ptx_embedded { pub const <STEM>_PTX: &str = "..."; pub fn get(name: &str) -> Option<&'static str>; }
// - pub mod cubin_embedded { pub static <STEM>_CUBIN: &'static [u8] = &[…]; pub fn get_cubin(name: &str) -> Option<&'static [u8]>; }
let bindings_path = out_dir.join("ptx_bindings.rs");
let mut rs = String::new();
rs.push_str("// @generated by engine-gpu-cuda/build.rs — DO NOT EDIT MANUALLY\n");
rs.push_str("#[allow(dead_code)]\n");
// PTX module (string)
rs.push_str("pub mod ptx_embedded {\n");
for (stem, path) in &generated_ptx {
let const_name = to_const_name(stem, "_PTX");
let ptx = std::fs::read_to_string(path).unwrap_or_else(|_| String::new());
rs.push_str(&format!(" pub const {const_name}: &str = r###\""));
rs.push_str(&ptx);
rs.push_str("\"###;\n");
}
rs.push_str(" pub fn get(name: &str) -> Option<&'static str> {\n");
rs.push_str(" match name {\n");
for (stem, _) in &generated_ptx {
let const_name = to_const_name(stem, "_PTX");
rs.push_str(&format!(" \"{stem}\" => Some({const_name}),\n"));
}
rs.push_str(" _ => None,\n");
rs.push_str(" }\n");
rs.push_str(" }\n");
rs.push_str("}\n");
// CUBIN module (bytes)
rs.push_str("pub mod cubin_embedded {\n");
for (stem, path) in &generated_cubin {
let const_name = to_const_name(stem, "_CUBIN");
let bytes = std::fs::read(path).unwrap_or_default();
rs.push_str(&format!(
" pub static {const_name}: &'static [u8] = &[\n "
));
for (i, b) in bytes.iter().enumerate() {
if i > 0 && i % 16 == 0 {
rs.push_str("\n ");
}
rs.push_str(&format!("{b},"));
}
rs.push_str("\n ];\n");
}
rs.push_str(" pub fn get_cubin(name: &str) -> Option<&'static [u8]> {\n");
rs.push_str(" match name {\n");
for (stem, _) in &generated_cubin {
let const_name = to_const_name(stem, "_CUBIN");
rs.push_str(&format!(" \"{stem}\" => Some({const_name}),\n"));
}
rs.push_str(" _ => None,\n");
rs.push_str(" }\n");
rs.push_str(" }\n");
rs.push_str("}\n");
std::fs::write(&bindings_path, rs)?;
Ok(())
}
fn to_const_name(stem: &str, suffix: &str) -> String {
let mut s = String::with_capacity(stem.len() + suffix.len());
for c in stem.chars() {
if c.is_ascii_alphanumeric() {
s.push(c.to_ascii_uppercase());
} else {
s.push('_');
}
}
s.push_str(suffix);
s
}
fn generate_empty_bindings(out_dir: &Path) -> io::Result<()> {
let bindings_path = out_dir.join("ptx_bindings.rs");
let mut rs = String::new();
rs.push_str("// @generated by engine-gpu-cuda/build.rs — DO NOT EDIT MANUALLY\n");
rs.push_str("#[allow(dead_code)]\n");
rs.push_str("pub mod ptx_embedded { pub fn get(_: &str) -> Option<&'static str> { None } }\n");
rs.push_str(
"pub mod cubin_embedded { pub fn get_cubin(_: &str) -> Option<&'static [u8]> { None } }\n",
);
std::fs::write(&bindings_path, rs)
}
fn scan_cu_files(root: &Path, out: &mut Vec<PathBuf>) -> io::Result<()> {
for entry in fs::read_dir(root)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
scan_cu_files(&path, out)?;
} else if path.extension().and_then(OsStr::to_str) == Some("cu") {
out.push(path);
}
}
Ok(())
}
fn find_nvcc() -> Option<PathBuf> {
// 1) Explicit override
if let Some(p) = env::var_os("NVCC") {
let pb = PathBuf::from(p);
if pb.is_file() {
return Some(pb);
}
}
// 2) CUDA_HOME or CUDA_PATH
if let Some(home) = env::var_os("CUDA_HOME").or_else(|| env::var_os("CUDA_PATH")) {
let mut candidate = PathBuf::from(home);
candidate.push("bin");
candidate.push(exe("nvcc"));
if candidate.is_file() {
return Some(candidate);
}
}
// 3) PATH lookup — try invoking `nvcc --version`
if let Ok(output) = Command::new("nvcc").arg("--version").output() {
if output.status.success() {
return Some(PathBuf::from("nvcc"));
}
}
None
}
fn compile_to_ptx(
nvcc: &Path,
arch: &str,
src: &Path,
out: &Path,
includes: &[PathBuf],
ccbin: Option<&str>,
extra_flags: &[String],
) -> io::Result<()> {
// Ensure parent directory exists
if let Some(parent) = out.parent() {
fs::create_dir_all(parent)?;
}
// nvcc -ptx -arch=<compute_XX> -I<inc>... [-ccbin cc] [extra] -o <out.ptx> <src.cu>
let mut cmd = Command::new(nvcc);
cmd.args(["-ptx", "-arch", arch]).arg("-o").arg(out);
if let Some(cc) = ccbin {
cmd.arg("-ccbin").arg(cc);
}
for f in extra_flags {
cmd.arg(f);
}
for inc in includes {
cmd.arg("-I").arg(inc);
}
cmd.arg(src);
let status = cmd.status()?;
if !status.success() {
return Err(io::Error::other(format!(
"nvcc returned non-zero exit status (ptx): {status}"
)));
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn compile_to_cubin(
nvcc: &Path,
arch_compute: &str,
arch_sm: &str,
src: &Path,
out: &Path,
includes: &[PathBuf],
ccbin: Option<&str>,
extra_flags: &[String],
) -> io::Result<()> {
// Ensure parent directory exists
if let Some(parent) = out.parent() {
fs::create_dir_all(parent)?;
}
// nvcc -cubin -arch=<compute_XX> -code=<sm_XX> -I<inc>... [-ccbin cc] [extra] -o <out.cubin> <src.cu>
let mut cmd = Command::new(nvcc);
cmd.args(["-cubin", "-arch", arch_compute, "-code", arch_sm])
.arg("-o")
.arg(out);
if let Some(cc) = ccbin {
cmd.arg("-ccbin").arg(cc);
}
for f in extra_flags {
cmd.arg(f);
}
for inc in includes {
cmd.arg("-I").arg(inc);
}
cmd.arg(src);
let status = cmd.status()?;
if !status.success() {
return Err(io::Error::other(format!(
"nvcc returned non-zero exit status (cubin): {status}"
)));
}
Ok(())
}
#[cfg(target_os = "windows")]
fn exe(name: &str) -> String {
format!("{name}.exe")
}
#[cfg(not(target_os = "windows"))]
fn exe(name: &str) -> String {
name.to_string()
}