|
| 1 | +#!/usr/bin/env bash |
| 2 | +# Check that the CUDA compilation target reaches nvcc and invalidates stale objects. |
| 3 | +set -euo pipefail |
| 4 | + |
| 5 | +usage() { |
| 6 | + cat <<'EOF' |
| 7 | +Usage: scripts/checks/cuda_arch_target.sh [options] |
| 8 | +
|
| 9 | +Verify the `cuda_arch` Lake option end to end without a CUDA toolkit or a GPU. |
| 10 | +
|
| 11 | +A recording stand-in for nvcc is placed first on PATH; it logs its argument vector and emits an |
| 12 | +empty object file. One extern library is then built repeatedly under different targets, and the |
| 13 | +log is asserted against what each build should have compiled: |
| 14 | +
|
| 15 | + no target no -arch flag; nvcc keeps its built-in default |
| 16 | + -K cuda_arch=sm_86 -arch=sm_86 |
| 17 | + -K cuda_arch=sm_86 (repeated) no recompilation |
| 18 | + -K cuda_arch=sm_89 recompiled, because the target is a traced argument |
| 19 | + TORCHLEAN_CUDA_ARCH=sm_90 -arch=sm_90, the environment fallback |
| 20 | + both, option and environment the option wins |
| 21 | + -K cuda_arch=-gencode ... passed through verbatim, for multi-architecture binaries |
| 22 | +
|
| 23 | +The stand-in's objects are removed afterwards, so a later real CUDA build cannot mistake them |
| 24 | +for its own. Any genuine object for that library goes with them, which costs one recompilation |
| 25 | +on the next real CUDA build. The stored build configuration is returned to its default, since |
| 26 | +every build here enables CUDA. |
| 27 | +
|
| 28 | +Options: |
| 29 | + --target LIB Extern library to build. Default: torchlean_dgemm_cuda. |
| 30 | + -h, --help Show this help message. |
| 31 | +
|
| 32 | +Environment: |
| 33 | + LAKE Lake executable to use (default: lake). |
| 34 | +
|
| 35 | +Examples: |
| 36 | + scripts/checks/cuda_arch_target.sh |
| 37 | + LAKE=~/.elan/bin/lake scripts/checks/cuda_arch_target.sh |
| 38 | +EOF |
| 39 | +} |
| 40 | + |
| 41 | +LAKE="${LAKE:-lake}" |
| 42 | +lib="torchlean_dgemm_cuda" |
| 43 | + |
| 44 | +while [[ $# -gt 0 ]]; do |
| 45 | + case "$1" in |
| 46 | + --target) [[ $# -ge 2 ]] || { echo "--target needs a value" >&2; exit 2; }; lib="$2"; shift 2 ;; |
| 47 | + --target=*) lib="${1#--target=}"; shift ;; |
| 48 | + -h|--help) usage; exit 0 ;; |
| 49 | + *) echo "unknown argument: $1" >&2; usage >&2; exit 2 ;; |
| 50 | + esac |
| 51 | +done |
| 52 | + |
| 53 | +repo="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" |
| 54 | +cd "$repo" |
| 55 | + |
| 56 | +work="$(mktemp -d)" |
| 57 | +build="$repo/.lake/build" |
| 58 | + |
| 59 | +# The stand-in writes into the normal build directory, so its objects have to go whether the |
| 60 | +# check passes or fails: Lake would otherwise record them as up to date and a real CUDA build |
| 61 | +# would archive an empty object instead of recompiling. |
| 62 | +cleanup() { |
| 63 | + rm -f "$build/$lib.o" "$build/$lib.o.trace" "$build/lib$lib.a" "$build/lib$lib.a.trace" |
| 64 | + rm -rf "$work" |
| 65 | + # Lake remembers the last build configuration, and every build below sets `cuda=true`. Put the |
| 66 | + # default back, or the next plain `lake build` in this checkout reaches for a CUDA toolkit that |
| 67 | + # the developer running this check need not have. |
| 68 | + "$LAKE" -R check-build >/dev/null 2>&1 || true |
| 69 | +} |
| 70 | +trap cleanup EXIT |
| 71 | + |
| 72 | +mkdir -p "$work/bin" |
| 73 | +cat > "$work/bin/nvcc" <<'STUB' |
| 74 | +#!/usr/bin/env bash |
| 75 | +# Recording stand-in for nvcc: log the argument vector, emit an empty object at -o. |
| 76 | +printf '%s\n' "$*" >> "${NVCC_LOG:?}" |
| 77 | +out=""; prev="" |
| 78 | +for a in "$@"; do [ "$prev" = "-o" ] && out="$a"; prev="$a"; done |
| 79 | +[ -n "$out" ] || { echo "recording nvcc: no -o in argument vector" >&2; exit 1; } |
| 80 | +printf 'int torchlean_recording_nvcc_probe;\n' | cc -x c -c -o "$out" - |
| 81 | +STUB |
| 82 | +chmod +x "$work/bin/nvcc" |
| 83 | + |
| 84 | +export PATH="$work/bin:$PATH" |
| 85 | +export NVCC_LOG="$work/nvcc.log" |
| 86 | +: > "$NVCC_LOG" |
| 87 | + |
| 88 | +# Start from no object at all, so the first build below is guaranteed to compile. |
| 89 | +rm -f "$build/$lib.o" "$build/$lib.o.trace" "$build/lib$lib.a" "$build/lib$lib.a.trace" |
| 90 | + |
| 91 | +failures=0 |
| 92 | + |
| 93 | +# Build once and report how many compilations that build triggered. |
| 94 | +compilations_for() { |
| 95 | + local before after |
| 96 | + before="$(wc -l < "$NVCC_LOG")" |
| 97 | + "$@" >/dev/null 2>&1 || { echo " build failed: $*" >&2; return 1; } |
| 98 | + after="$(wc -l < "$NVCC_LOG")" |
| 99 | + echo "$((after - before))" |
| 100 | +} |
| 101 | + |
| 102 | +check() { |
| 103 | + local name="$1" expect_compilations="$2" expect_flags="$3"; shift 3 |
| 104 | + local n last |
| 105 | + n="$(compilations_for "$@")" || { failures=$((failures + 1)); return; } |
| 106 | + last="$(tail -n 1 "$NVCC_LOG")" |
| 107 | + if [[ "$n" != "$expect_compilations" ]]; then |
| 108 | + echo "FAIL $name: expected $expect_compilations compilation(s), saw $n" >&2 |
| 109 | + failures=$((failures + 1)) |
| 110 | + return |
| 111 | + fi |
| 112 | + if [[ -n "$expect_flags" && "$last" != *"$expect_flags"* ]]; then |
| 113 | + echo "FAIL $name: expected argument vector to contain '$expect_flags'" >&2 |
| 114 | + echo " saw: $last" >&2 |
| 115 | + failures=$((failures + 1)) |
| 116 | + return |
| 117 | + fi |
| 118 | + if [[ -z "$expect_flags" && "$n" != "0" && "$last" == *"-arch"* ]]; then |
| 119 | + echo "FAIL $name: expected no -arch flag" >&2 |
| 120 | + echo " saw: $last" >&2 |
| 121 | + failures=$((failures + 1)) |
| 122 | + return |
| 123 | + fi |
| 124 | + echo "ok $name" |
| 125 | +} |
| 126 | + |
| 127 | +echo "Checking the CUDA compilation target through a recording nvcc ($lib)" |
| 128 | + |
| 129 | +check "no target leaves nvcc on its default" 1 "" \ |
| 130 | + "$LAKE" -R -K cuda=true build "$lib" |
| 131 | +check "-K cuda_arch=sm_86 compiles for sm_86" 1 "-arch=sm_86" \ |
| 132 | + "$LAKE" -R -K cuda=true -K cuda_arch=sm_86 build "$lib" |
| 133 | +check "an unchanged target does not recompile" 0 "" \ |
| 134 | + "$LAKE" -R -K cuda=true -K cuda_arch=sm_86 build "$lib" |
| 135 | +check "a changed target recompiles" 1 "-arch=sm_89" \ |
| 136 | + "$LAKE" -R -K cuda=true -K cuda_arch=sm_89 build "$lib" |
| 137 | +check "TORCHLEAN_CUDA_ARCH is the fallback" 1 "-arch=sm_90" \ |
| 138 | + env TORCHLEAN_CUDA_ARCH=sm_90 "$LAKE" -R -K cuda=true build "$lib" |
| 139 | +check "the option outranks the environment" 1 "-arch=sm_75" \ |
| 140 | + env TORCHLEAN_CUDA_ARCH=sm_90 "$LAKE" -R -K cuda=true -K cuda_arch=sm_75 build "$lib" |
| 141 | +check "an explicit flag list passes through" 1 "-gencode arch=compute_120,code=[sm_120,compute_120]" \ |
| 142 | + "$LAKE" -R -K cuda=true \ |
| 143 | + -K cuda_arch="-gencode arch=compute_75,code=sm_75 -gencode arch=compute_120,code=[sm_120,compute_120]" \ |
| 144 | + build "$lib" |
| 145 | + |
| 146 | +if [[ "$failures" -ne 0 ]]; then |
| 147 | + echo "$failures check(s) failed" >&2 |
| 148 | + exit 1 |
| 149 | +fi |
| 150 | +echo "All CUDA compilation target checks passed." |
0 commit comments