Skip to content

Commit 4c108a1

Browse files
authored
fix(math-cuda): driver-independent cuda builds (cudarc pin + cubin AOT) (#800)
* fix(math-cuda): robust CUDA version handling for cuda builds Two driver-independence fixes so `--features cuda` builds AND runs on the team's GPU hardware without CUDARC_CUDA_VERSION env or a hand-picked toolkit. 1. cudarc symbol floor. Pin cudarc's CUDA version to `cuda-12080` in crypto/math-cuda/Cargo.toml (was `cuda-version-from-build-system` + `fallback-latest`). Auto-detect bound the newest symbol set the build toolkit knew (e.g. CUDA 13.1 -> cuDevSmResourceSplit, gated behind cuda-13010/13020), which cudarc eagerly resolves at init and panics on a driver that predates it (580.x = CUDA 13.0 max). Our cudarc surface is all CUDA-11-era, so the 12.8 symbol set (a strict subset every >=12.8 driver exports) resolves cleanly everywhere. Replaces the per-script sed pin; drops it from scripts/gpu_test.sh and adjusts the compat shim in scripts/bench_abba.sh (still rewrites pre-pin baseline shas). 2. PTX ISA version. AOT-compile kernels to native cubin (SASS) via `nvcc --cubin -arch=sm_XX` instead of `--ptx`, loaded through `Ptx::from_binary` (cuModuleLoadData). A cubin carries pre-compiled SASS for a real arch, so the driver loads it regardless of the toolkit's PTX ISA version -- no CUDA_ERROR_UNSUPPORTED_PTX_VERSION and no silent CPU fallback when the toolkit is newer than the driver. Build+run are co-located and the arch is detected from nvidia-smi, so the arch always matches; a too-old toolkit now fails loudly at nvcc build time. CPU-only and nvcc-less stub builds unaffected (empty cubin stub). README "GPU Tests" updated. * fix(math-cuda): review fixes — warn on cubin load failure + bench pin no-op - backend(): warn once when GPU init fails, so a cubin arch mismatch (AOT cubins are arch-specific) doesn't silently disable the GPU with no signal. Stale ptx->cubin comment fixed. - bench_abba.sh: warn when CUDARC_PIN can't apply (post-pin sha, anchor gone) instead of silently no-opping. * fix(math-cuda,scripts): review fixes — warn on sm_89 arch fallback + bench_abba_gpu pin guard/restore + cubin comment * refactor(scripts): shared ABBA bench lib + README GPU driver floor bench_abba.sh and bench_abba_gpu.sh carried near-verbatim copies of the prove-time parsing, the CUDARC_PIN compat sed, and the paired-stats python — a stats fix applied to one would silently leave the other reporting different numbers for the same pairs. scripts/lib/bench_abba_common.sh now holds all three; the GPU bench gains the full analysis (exact Wilcoxon, stability diagnostics, verdicts) it previously lacked. README: state the cuda-12080 pin's driver floor (CUDA >= 12.8 / 570+) and that older drivers abort at CUDA init rather than CPU-fallback. * chore(scripts): drop unused bench_abba_gpu.sh No automation calls it (the GPU bench CI runs bench_abba.sh), and its whole premise — the rigorous ABBA paired-t + Wilcoxon method — is overkill for the question it wrapped: a GPU-on-vs-off delta is 10-40%, which two runs and your eyes resolve. The ABBA machinery earns its keep only for the ~1% effects (bench_abba.sh's PR-vs-baseline deltas), so one bench script is enough. * fix(math-cuda): hard-fail arch detection instead of guessing sm_89 A cubin is arch-locked, so when nvcc is present but no GPU arch can be detected there is no safe default — the old sm_89 fallback produced a binary that loads on exactly one GPU model (Ada) and silently CPU-falls-back on every other, including lower-arch cards like the 3090 (sm_86, which an sm_89 cubin can't even run — cubins aren't backward-compatible). Panic with an actionable message (set CUDARC_NVCC_ARCH or build on the target host) instead. Only reachable with nvcc present + no visible GPU + no override; nvcc-absent hosts take the empty-stub path and never hit this, so CPU-only CI is unaffected. * chore(scripts): move ABBA bench cleanup to its own PR (#812) The bench-tooling dedup + bench_abba_gpu.sh removal are split into #812 so this PR is purely the cuda build-robustness changes (which want a GPU test run), leaving the trivial script cleanup free to merge independently.
1 parent f4a5887 commit 4c108a1

8 files changed

Lines changed: 178 additions & 114 deletions

File tree

.github/workflows/benchmark-gpu.yml

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -358,14 +358,13 @@ jobs:
358358
# the build through the CUDA prover path. NOTE: requires this PR's bench_abba.sh change
359359
# (the BENCH_FEATURES env) to be on main — i.e. it only takes effect after merge.
360360
# REBUILD=1: each Vast box is fresh, GPU-specific hardware — always rebuild both
361-
# binaries (PTX is compiled for the detected arch); never trust a cached binary.
362-
# CUDARC_PIN: pin cudarc to a fixed CUDA version (cuda-12080 = CUDA 12.8, matching the
363-
# cuda_max_good>=12.8 offer floor) and drop fallback-latest, so cudarc binds a known
364-
# symbol set instead of its newest. With fallback-latest cudarc requested a symbol the
365-
# box's driver doesn't export (e.g. cuDevSmResourceSplit) -> runtime panic. This is the
366-
# too-new end of the same compatibility window that MIN_DRIVER>=580 guards at the
367-
# too-old end (older drivers lack cuCtxGetDevice_v2 and the GPU path falls back to CPU).
368-
# nvidia-smi is logged for diagnosing driver issues.
361+
# binaries (cubin is compiled for the detected arch); never trust a cached binary.
362+
# CUDARC_PIN: compat shim for pre-pin baseline shas. cudarc's CUDA version is now pinned
363+
# permanently in crypto/math-cuda/Cargo.toml (cuda-12080), so this no-ops on shas that
364+
# carry the pin and only rewrites older baselines (where fallback-latest could request a
365+
# symbol the box's driver doesn't export, e.g. cuDevSmResourceSplit -> runtime panic).
366+
# MIN_DRIVER>=580 still guards the too-old end (older drivers lack cuCtxGetDevice_v2 and
367+
# the GPU path falls back to CPU). nvidia-smi is logged for diagnosing driver issues.
369368
REMOTE="set -e; cd /workspace/lambda_vm; \
370369
command -v python3 >/dev/null || { apt-get update -qq && apt-get install -y -qq python3; }; \
371370
nvidia-smi || true; \

.github/workflows/gpu-tests.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,8 @@ jobs:
287287
''|*[!A-Za-z0-9._/-]*) echo "::error::invalid ref: '$REF'"; exit 1 ;;
288288
esac
289289
# Check out the ref under test on the box, then run the CUDA test groups.
290-
# gpu_test.sh owns the CUDARC_PIN / SYSROOT_DIR defaults — don't duplicate them here.
290+
# gpu_test.sh owns the SYSROOT_DIR default — don't duplicate it here. (cudarc's CUDA
291+
# version is pinned in crypto/math-cuda/Cargo.toml, so no CUDARC_PIN is needed.)
291292
REMOTE="set -e; cd /workspace/lambda_vm; \
292293
git fetch --force origin '$REF'; \
293294
git checkout -f FETCH_HEAD; \

README.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -233,10 +233,18 @@ The CUDA test groups run only on a machine with an NVIDIA GPU and `nvcc`:
233233
- `make test-prover-cuda` — the prover/stark/crypto/ecsm suite with the GPU path enabled
234234
- `make test-prover-comprehensive-cuda` — the comprehensive all-instructions prove on the GPU path
235235

236-
The kernels are compiled by `nvcc` into PTX that the driver JIT-compiles at load, so the GPU's
237-
driver must be new enough for the toolkit — an older driver rejects the PTX with
238-
`CUDA_ERROR_UNSUPPORTED_PTX_VERSION`. These groups run automatically on a rented GPU in the merge
239-
queue via `.github/workflows/gpu-tests.yml` (which filters offers on `cuda_max_good`).
236+
The kernels are AOT-compiled by `nvcc` into native cubin (SASS) for the host GPU's real arch
237+
(detected via `nvidia-smi`, or overridden with `CUDARC_NVCC_ARCH`), not PTX. This sidesteps the
238+
PTX-ISA JIT version check, so a CUDA toolkit *newer* than the driver still loads and runs — no
239+
`CUDA_ERROR_UNSUPPORTED_PTX_VERSION` and no need to hand-match the toolkit to the driver. The only
240+
requirement is that the toolkit knows the GPU's compute capability (a too-old toolkit fails loudly
241+
at `nvcc` build time). cudarc's host-side driver-API symbol set is likewise pinned to a safe floor
242+
(`cuda-12080`) in `crypto/math-cuda/Cargo.toml`, so no `CUDARC_CUDA_VERSION` env is needed either.
243+
That pin makes the GPU path require a driver of CUDA >= 12.8 (driver branch 570+ — any
244+
Blackwell-capable driver qualifies); on an older driver cudarc's eager symbol resolution aborts at
245+
CUDA init rather than falling back to CPU.
246+
These groups run automatically on a rented GPU in the merge queue via
247+
`.github/workflows/gpu-tests.yml` (which filters offers on `cuda_max_good`).
240248

241249
## Benchmarking & Profiling
242250

crypto/math-cuda/Cargo.toml

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,26 @@ edition = "2024"
66
license.workspace = true
77

88
[dependencies]
9+
# cudarc CUDA version is PINNED to `cuda-12080` (CUDA 12.8) — do NOT restore
10+
# `cuda-version-from-build-system` + `fallback-latest`. Rationale:
11+
# * That auto-detect binds the newest symbol set the *build toolkit* knows
12+
# (e.g. a CUDA 13.1 toolkit pulls in `cuDevSmResourceSplit`, gated behind
13+
# `cuda-13010`/`cuda-13020`). cudarc eagerly resolves those symbols at CUDA
14+
# init; a driver that predates them (e.g. 580.x = CUDA 13.0 max) has no such
15+
# export, so the `dynamic-loading` resolver `.expect()`s and PANICS.
16+
# * This crate's cudarc surface is entirely CUDA-11-era
17+
# (CudaContext/CudaFunction/CudaSlice/CudaStream/LaunchConfig/PushKernelArg/
18+
# DriverError/Ptx — no green contexts). The 12.8 symbol set is a strict
19+
# subset every >=12.8 driver exports, so pinning it resolves cleanly on any
20+
# supported driver and a *newer* driver loses nothing we use.
21+
# * This replaces the fragile per-script `sed` pin in scripts/gpu_test.sh.
22+
# To move the floor (e.g. to use a newer driver-API symbol), bump this one
23+
# feature deliberately — see crypto/math-cuda/build.rs and README "GPU Tests".
924
cudarc = { version = "0.19", default-features = false, features = [
1025
"driver",
1126
"nvrtc",
1227
"std",
13-
"cuda-version-from-build-system",
14-
"fallback-latest",
28+
"cuda-12080",
1529
"dynamic-loading",
1630
] }
1731
math = { path = "../math" }

crypto/math-cuda/build.rs

Lines changed: 83 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -15,38 +15,64 @@ fn nvcc_path() -> PathBuf {
1515
}
1616

1717
/// Query `nvidia-smi` for the local GPU's compute capability (e.g. "12.0"
18-
/// for Blackwell). Returns a `compute_XX` target on success, falling back
19-
/// to `compute_89` (Ada) when no GPU is visible or the query fails.
18+
/// for Blackwell) and return a *real* arch (`sm_XX`) suitable for cubin
19+
/// (SASS) generation. Hard-fails the build when no GPU is visible and the
20+
/// query fails: a cubin is arch-locked, so there is no safe default — any
21+
/// guess produces a binary that loads on exactly one GPU model and silently
22+
/// CPU-falls-back everywhere else. Failing here is loud and fixable (set
23+
/// `CUDARC_NVCC_ARCH` or build on the target host); a guess is neither.
24+
///
25+
/// This is only reached when `nvcc` is present but the arch can't be detected
26+
/// (a toolkit-installed host with no visible GPU). A host without `nvcc` takes
27+
/// the empty-stub path in `compile_kernel` and never calls this.
2028
fn detect_arch() -> String {
21-
const FALLBACK: &str = "compute_89";
29+
detect_arch_from_smi().unwrap_or_else(|| {
30+
panic!(
31+
"math-cuda: nvcc is present but no GPU arch could be detected via nvidia-smi, \
32+
and a cubin must target a concrete arch. Set CUDARC_NVCC_ARCH=sm_XX (e.g. sm_120 \
33+
for RTX 5090, sm_86 for RTX 3090) or build on the target GPU host."
34+
)
35+
})
36+
}
37+
38+
/// Parse the compute capability out of `nvidia-smi` and format it as a real
39+
/// `sm_XX` arch. Returns `None` on every path where no capability can be read
40+
/// (nvidia-smi missing, command failed, or unparsable output) so the caller
41+
/// warns before falling back.
42+
fn detect_arch_from_smi() -> Option<String> {
2243
let output = match Command::new("nvidia-smi")
2344
.args(["--query-gpu=compute_cap", "--format=csv,noheader"])
2445
.output()
2546
{
2647
Ok(o) if o.status.success() => o,
27-
_ => return FALLBACK.to_string(),
28-
};
29-
let line = match std::str::from_utf8(&output.stdout) {
30-
Ok(s) => s,
31-
Err(_) => return FALLBACK.to_string(),
48+
_ => return None,
3249
};
50+
let line = std::str::from_utf8(&output.stdout).ok()?;
3351
// First line, first comma-separated value (covers multi-GPU hosts).
34-
let cap = match line.lines().next() {
35-
Some(l) => l.split(',').next().unwrap_or("").trim(),
36-
None => return FALLBACK.to_string(),
37-
};
38-
let (major, minor) = match cap.split_once('.') {
39-
Some((m, n)) => (m.trim(), n.trim()),
40-
None => return FALLBACK.to_string(),
41-
};
52+
let cap = line.lines().next()?.split(',').next().unwrap_or("").trim();
53+
let (major, minor) = cap.split_once('.')?;
54+
let (major, minor) = (major.trim(), minor.trim());
4255
if major.chars().all(|c| c.is_ascii_digit()) && minor.chars().all(|c| c.is_ascii_digit()) {
43-
format!("compute_{major}{minor}")
56+
Some(format!("sm_{major}{minor}"))
57+
} else {
58+
None
59+
}
60+
}
61+
62+
/// Normalize a user-supplied `CUDARC_NVCC_ARCH` override to a *real* arch
63+
/// (`sm_XX`). cubin (SASS) generation rejects the *virtual* `compute_XX`
64+
/// form, but we accept it (and a bare `XX`) for backwards compatibility.
65+
fn to_real_arch(arch: &str) -> String {
66+
if let Some(n) = arch.strip_prefix("compute_") {
67+
format!("sm_{n}")
68+
} else if arch.starts_with("sm_") {
69+
arch.to_string()
4470
} else {
45-
FALLBACK.to_string()
71+
format!("sm_{arch}")
4672
}
4773
}
4874

49-
fn compile_ptx(src: &str, out_name: &str, have_nvcc: bool) {
75+
fn compile_kernel(src: &str, out_name: &str, have_nvcc: bool) {
5076
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
5177
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
5278
let src_path = manifest_dir.join("kernels").join(src);
@@ -57,30 +83,40 @@ fn compile_ptx(src: &str, out_name: &str, have_nvcc: bool) {
5783
println!("cargo:rerun-if-env-changed=CUDA_PATH");
5884
println!("cargo:rerun-if-env-changed=CUDARC_NVCC_ARCH");
5985

60-
// When nvcc is missing from PATH, emit an empty PTX stub so the crate
61-
// still compiles. include_str! in src/device.rs needs the file to exist
62-
// at build time. Any runtime kernel call panics in cudarc when loading
63-
// the empty module. We can't run GPU code without nvcc on the build
64-
// host anyway.
86+
// When nvcc is missing from PATH, emit an empty cubin stub so the crate
87+
// still compiles. include_bytes! in src/device.rs needs the file to exist
88+
// at build time. Any runtime kernel call fails to load the empty module and
89+
// the caller falls back to CPU. We can't run GPU code without nvcc on the
90+
// build host anyway.
6591
if !have_nvcc {
66-
fs::write(&out_path, "").expect("failed to write empty PTX stub");
92+
fs::write(&out_path, "").expect("failed to write empty cubin stub");
6793
return;
6894
}
6995

70-
// Emit PTX for a virtual architecture; the CUDA driver JIT-compiles it for the
71-
// actual GPU at load time. Override with CUDARC_NVCC_ARCH to pin a specific
72-
// compute capability. If unset, try `nvidia-smi` to match the host GPU
73-
// (avoids JIT failures like nvcc-13.0 PTX rejected on Blackwell drivers);
74-
// fall back to compute_89 (Ada) when detection fails.
96+
// AOT-compile each kernel to a native cubin (SASS) for the host GPU's real
97+
// arch, NOT to PTX. This sidesteps the driver's PTX-ISA JIT version check:
98+
// a toolkit's PTX ISA is fixed by its CUDA version (e.g. CUDA 13.1 emits PTX
99+
// .version 9.1), and a driver older than that toolkit rejects the module at
100+
// load with CUDA_ERROR_UNSUPPORTED_PTX_VERSION -> every kernel silently
101+
// falls back to CPU. A cubin carries pre-compiled SASS for a real arch, so
102+
// the driver loads it directly as long as it supports that GPU (which the
103+
// driver installed for that GPU always does) — regardless of the toolkit's
104+
// CUDA version. See README "GPU Tests".
75105
//
76-
// NOTE: this `-arch` only sets the *virtual arch*, not the PTX ISA version, which is
77-
// fixed by this nvcc's CUDA toolkit. The runtime driver must support that toolkit's CUDA
78-
// version or it rejects the PTX with CUDA_ERROR_UNSUPPORTED_PTX_VERSION — i.e. the box's
79-
// driver CUDA must be >= the build toolkit's CUDA. See README "GPU Tests".
80-
let arch = env::var("CUDARC_NVCC_ARCH").unwrap_or_else(|_| detect_arch());
106+
// Trade-off: a cubin is arch-specific (an `sm_120` cubin runs only on
107+
// `sm_120`). We build+run on the same GPU box in every flow and detect the
108+
// arch from that box's `nvidia-smi`, so this is exactly right. Override with
109+
// CUDARC_NVCC_ARCH (compute_XX / sm_XX / bare XX all accepted) to
110+
// cross-compile for a different arch. If nvcc is present but no GPU is
111+
// detectable and no override is given, `detect_arch` hard-fails rather than
112+
// guessing an arch that would load on one GPU model and CPU-fall-back on
113+
// every other.
114+
let arch = env::var("CUDARC_NVCC_ARCH")
115+
.map(|a| to_real_arch(&a))
116+
.unwrap_or_else(|_| detect_arch());
81117

82118
let status = Command::new(nvcc_path())
83-
.args(["--ptx", "-O3", "-std=c++17", "-arch", &arch, "-o"])
119+
.args(["--cubin", "-O3", "-std=c++17", "-arch", &arch, "-o"])
84120
.arg(&out_path)
85121
.arg(&src_path)
86122
.status()
@@ -107,19 +143,19 @@ fn main() {
107143
.unwrap_or(false);
108144
if !have_nvcc {
109145
println!(
110-
"cargo:warning=math-cuda: nvcc not found at {} — emitting empty PTX stubs. \
111-
Runtime GPU calls will panic. Install CUDA and rebuild for a working backend.",
146+
"cargo:warning=math-cuda: nvcc not found at {} — emitting empty cubin stubs. \
147+
Runtime GPU calls fall back to CPU. Install CUDA and rebuild for a working backend.",
112148
nvcc_path().display()
113149
);
114150
}
115151

116-
compile_ptx("arith.cu", "arith.ptx", have_nvcc);
117-
compile_ptx("ntt.cu", "ntt.ptx", have_nvcc);
118-
compile_ptx("keccak.cu", "keccak.ptx", have_nvcc);
119-
compile_ptx("barycentric.cu", "barycentric.ptx", have_nvcc);
120-
compile_ptx("deep.cu", "deep.ptx", have_nvcc);
121-
compile_ptx("fri.cu", "fri.ptx", have_nvcc);
122-
compile_ptx("inverse.cu", "inverse.ptx", have_nvcc);
123-
compile_ptx("logup.cu", "logup.ptx", have_nvcc);
124-
compile_ptx("constraint_interp.cu", "constraint_interp.ptx", have_nvcc);
152+
compile_kernel("arith.cu", "arith.cubin", have_nvcc);
153+
compile_kernel("ntt.cu", "ntt.cubin", have_nvcc);
154+
compile_kernel("keccak.cu", "keccak.cubin", have_nvcc);
155+
compile_kernel("barycentric.cu", "barycentric.cubin", have_nvcc);
156+
compile_kernel("deep.cu", "deep.cubin", have_nvcc);
157+
compile_kernel("fri.cu", "fri.cubin", have_nvcc);
158+
compile_kernel("inverse.cu", "inverse.cubin", have_nvcc);
159+
compile_kernel("logup.cu", "logup.cubin", have_nvcc);
160+
compile_kernel("constraint_interp.cu", "constraint_interp.cubin", have_nvcc);
125161
}

0 commit comments

Comments
 (0)