Implements the CUDA kernels (`interpolate_input`, `perturb_dimension`)
and wires `compute_gpu` in `integrated_gradients.rs` to run the full
IG algorithm on-device. Replaces the stub that previously returned
`MLError::ModelError("IG GPU kernels not available: cubins not yet
wired")` and fell back to CPU.
Algorithm: for each of `num_steps` interpolation points along the
baseline->input path, compute central-finite-difference gradients for
all `num_features` dimensions via two GPU forward passes per feature.
Single cubin (`ig_kernels.cubin`) compiled via build.rs (following the
crates/ml/build.rs pattern — nvcc, `-arch=sm_\${CUDA_COMPUTE_CAP}`, O3,
f32) and embedded with `include_bytes!`. The `forward_fn` consumers
operate on `GpuTensor`, so no dtoh round-trips inside the inner loop —
only the scalar `[1]` tensor output of each forward pass is pulled to
host (via `to_scalar`) per gradient sample. The three scratch buffers
(`interpolated`, `x_plus`, `x_minus`) are allocated once outside the
step loop and reused across all steps and features. No atomicAdd —
the kernels are trivial 1-D element-wise writes.
Tests: existing CPU tests pass unchanged. Added GPU smoke test
`test_ig_compute_gpu_linear_model` (gated `#[cfg(feature = \"cuda\")]`
+ `#[ignore]`) that builds a linear model as a `GpuTensor`-native
forward (elementwise mul + mean), verifies the completeness axiom
within 1%, and cross-checks GPU attributions against the CPU path
within 5%. Passes locally on RTX 3050 Ti (sm_86).
Removes 3 TODO markers and the embedded `_IG_CUDA_SRC` const that
were awaiting this work, along with the `#[allow(unused_variables)]`
stub attribute on `compute_gpu`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
103 lines
3.0 KiB
Rust
103 lines
3.0 KiB
Rust
use std::path::{Path, PathBuf};
|
|
use std::process::Command;
|
|
|
|
fn main() {
|
|
println!("cargo:rerun-if-changed=build.rs");
|
|
|
|
// Only compile CUDA kernels when the cuda feature is enabled.
|
|
if std::env::var("CARGO_FEATURE_CUDA").is_err() {
|
|
return;
|
|
}
|
|
|
|
let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap());
|
|
let kernel_dir = Path::new("src");
|
|
|
|
// Detect GPU architecture from env or default to sm_80 (matches crates/ml/build.rs).
|
|
let cuda_compute_cap = std::env::var("CUDA_COMPUTE_CAP").unwrap_or_else(|_| "80".to_string());
|
|
let arch = format!("sm_{cuda_compute_cap}");
|
|
|
|
// Check if nvcc is available -- gracefully skip if not (non-CUDA builds).
|
|
let nvcc_path = find_nvcc();
|
|
let nvcc = match nvcc_path {
|
|
Some(p) => p,
|
|
None => {
|
|
eprintln!(" warning: nvcc not found, skipping CUDA kernel precompilation");
|
|
eprintln!(" Install CUDA toolkit or set CUDA_HOME for GPU builds");
|
|
return;
|
|
}
|
|
};
|
|
|
|
let kernel_name = "ig_kernels.cu";
|
|
if !compile_kernel(&nvcc, kernel_dir, kernel_name, &arch, &out_dir) {
|
|
panic!("nvcc failed to compile {kernel_name}");
|
|
}
|
|
eprintln!(" Precompiled 1/1 CUDA kernels ({arch}) — f32");
|
|
}
|
|
|
|
/// Compile a single .cu kernel file to a .cubin via nvcc.
|
|
fn compile_kernel(
|
|
nvcc: &Path,
|
|
kernel_dir: &Path,
|
|
kernel_name: &str,
|
|
arch: &str,
|
|
out_dir: &Path,
|
|
) -> bool {
|
|
let kernel_path = kernel_dir.join(kernel_name);
|
|
let cubin_name = kernel_name.replace(".cu", ".cubin");
|
|
let cubin_path = out_dir.join(&cubin_name);
|
|
|
|
println!("cargo:rerun-if-changed={}", kernel_path.display());
|
|
|
|
// Compile with nvcc.
|
|
let status = Command::new(nvcc)
|
|
.args([
|
|
"-cubin",
|
|
&format!("-arch={arch}"),
|
|
"-O3",
|
|
"--ftz=true",
|
|
"--fmad=true",
|
|
"--prec-div=true",
|
|
"--prec-sqrt=true",
|
|
"-o", cubin_path.to_str().unwrap(),
|
|
kernel_path.to_str().unwrap(),
|
|
])
|
|
.status();
|
|
|
|
match status {
|
|
Ok(s) if s.success() => {
|
|
eprintln!(" Compiled {kernel_name} -> {cubin_name} ({arch})");
|
|
true
|
|
}
|
|
Ok(s) => {
|
|
eprintln!(" FAILED: {kernel_name} (exit={})", s.code().unwrap_or(-1));
|
|
false
|
|
}
|
|
Err(e) => {
|
|
eprintln!(" FAILED: {kernel_name} (nvcc error: {e})");
|
|
false
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Find nvcc: prefer $CUDA_HOME/bin/nvcc, then check common paths / $PATH.
|
|
fn find_nvcc() -> Option<PathBuf> {
|
|
if let Ok(home) = std::env::var("CUDA_HOME") {
|
|
let nvcc = PathBuf::from(home).join("bin/nvcc");
|
|
if nvcc.exists() {
|
|
return Some(nvcc);
|
|
}
|
|
}
|
|
|
|
for path in &["/usr/local/cuda/bin/nvcc", "/usr/bin/nvcc"] {
|
|
let p = PathBuf::from(path);
|
|
if p.exists() {
|
|
return Some(p);
|
|
}
|
|
}
|
|
|
|
match Command::new("nvcc").arg("--version").output() {
|
|
Ok(output) if output.status.success() => Some(PathBuf::from("nvcc")),
|
|
_ => None,
|
|
}
|
|
}
|