diff --git a/.github/workflows/studio-backend-ci.yml b/.github/workflows/studio-backend-ci.yml index 6e53a290cf..ea60252cf6 100644 --- a/.github/workflows/studio-backend-ci.yml +++ b/.github/workflows/studio-backend-ci.yml @@ -226,6 +226,7 @@ jobs: tests/sh/test_studio_home_node_dir.sh \ tests/sh/test_system_node_readonly.sh \ tests/sh/test_nvcc_meets_llama_minimum.sh \ + tests/sh/test_resolve_cuda_archs.sh \ tests/sh/test_tauri_install_exit_order.sh \ tests/sh/test_torch_constraint.sh \ tests/sh/test_torch_flavor.sh; do diff --git a/studio/setup.ps1 b/studio/setup.ps1 index 20f5bd1ada..6b16702d3a 100644 --- a/studio/setup.ps1 +++ b/studio/setup.ps1 @@ -3704,35 +3704,49 @@ if (-not $NeedLlamaSourceBuild) { $CmakeArgs += '-DCMAKE_EXE_LINKER_FLAGS=/NODEFAULTLIB:LIBCMT' # CUDA flags -- only if GPU available, otherwise explicitly disable if ($HasNvidiaSmi -and $NvccPath) { - $CmakeArgs += '-DGGML_CUDA=ON' - # Accept a host MSVC newer than nvcc's whitelist; a fresh toolkit - # (e.g. CUDA 13.3) otherwise aborts with "#error -- unsupported - # Microsoft Visual Studio version!". Mirrors the Linux fix. Via env - # (covers the configure probe + build), after Refresh-Environment, idempotent. - $nvccAllowFlag = '-allow-unsupported-compiler' - if ([string]::IsNullOrEmpty($env:NVCC_PREPEND_FLAGS)) { - $env:NVCC_PREPEND_FLAGS = $nvccAllowFlag - } elseif ($env:NVCC_PREPEND_FLAGS -notlike "*$nvccAllowFlag*") { - $env:NVCC_PREPEND_FLAGS = "$($env:NVCC_PREPEND_FLAGS) $nvccAllowFlag" - } - substep "NVCC_PREPEND_FLAGS = $env:NVCC_PREPEND_FLAGS" - $CmakeArgs += "-DCUDAToolkit_ROOT=$CudaToolkitRoot" - $CmakeArgs += "-DCUDA_TOOLKIT_ROOT_DIR=$CudaToolkitRoot" - $CmakeArgs += "-DCMAKE_CUDA_COMPILER=$NvccPath" - if ($CudaArch) { - # Validate nvcc actually supports this architecture - if (Test-NvccArchSupport -NvccExe $NvccPath -Arch $CudaArch) { - $CmakeArgs += "-DCMAKE_CUDA_ARCHITECTURES=$CudaArch" - } else { - # GPU arch too new for this toolkit -- fall back to highest supported. - # PTX forward-compatibility will JIT-compile for the actual GPU at runtime. - $maxArch = Get-NvccMaxArch -NvccExe $NvccPath - if ($maxArch) { - $CmakeArgs += "-DCMAKE_CUDA_ARCHITECTURES=$maxArch" - substep "GPU is sm_$CudaArch but nvcc only supports up to sm_$maxArch" "Yellow" - substep "Building with sm_$maxArch (PTX will JIT for your GPU at runtime)" "Yellow" + # UNSLOTH_LLAMA_CUDA_ARCHS (e.g. "120" or "89;86") forces the build + # arch and wins over detection, matching setup.sh. + $CudaArchOverride = if ($env:UNSLOTH_LLAMA_CUDA_ARCHS) { ($env:UNSLOTH_LLAMA_CUDA_ARCHS -replace '\s', '') } else { '' } + if ((-not $CudaArch) -and (-not $CudaArchOverride)) { + # No detectable compute capability (#5854): -DGGML_CUDA=ON with no + # arch builds a PTX-only binary, so build CPU instead. Mirrors the + # Linux fix; set UNSLOTH_LLAMA_CUDA_ARCHS=120 to force a CUDA build. + substep "could not detect a CUDA compute capability; building CPU llama.cpp instead of a PTX-only binary (set UNSLOTH_LLAMA_CUDA_ARCHS=120 to force a CUDA build)." "Yellow" + $CmakeArgs += '-DGGML_CUDA=OFF' + } else { + $CmakeArgs += '-DGGML_CUDA=ON' + # Accept a host MSVC newer than nvcc's whitelist; a fresh toolkit + # (e.g. CUDA 13.3) otherwise aborts with "#error -- unsupported + # Microsoft Visual Studio version!". Mirrors the Linux fix. Via env + # (covers the configure probe + build), after Refresh-Environment, idempotent. + $nvccAllowFlag = '-allow-unsupported-compiler' + if ([string]::IsNullOrEmpty($env:NVCC_PREPEND_FLAGS)) { + $env:NVCC_PREPEND_FLAGS = $nvccAllowFlag + } elseif ($env:NVCC_PREPEND_FLAGS -notlike "*$nvccAllowFlag*") { + $env:NVCC_PREPEND_FLAGS = "$($env:NVCC_PREPEND_FLAGS) $nvccAllowFlag" + } + substep "NVCC_PREPEND_FLAGS = $env:NVCC_PREPEND_FLAGS" + $CmakeArgs += "-DCUDAToolkit_ROOT=$CudaToolkitRoot" + $CmakeArgs += "-DCUDA_TOOLKIT_ROOT_DIR=$CudaToolkitRoot" + $CmakeArgs += "-DCMAKE_CUDA_COMPILER=$NvccPath" + if ($CudaArchOverride) { + # Forced arch wins verbatim (no nvcc validation), matching setup.sh. + $CmakeArgs += "-DCMAKE_CUDA_ARCHITECTURES=$CudaArchOverride" + } elseif ($CudaArch) { + # Validate nvcc actually supports this architecture + if (Test-NvccArchSupport -NvccExe $NvccPath -Arch $CudaArch) { + $CmakeArgs += "-DCMAKE_CUDA_ARCHITECTURES=$CudaArch" + } else { + # GPU arch too new for this toolkit -- fall back to highest supported. + # PTX forward-compatibility will JIT-compile for the actual GPU at runtime. + $maxArch = Get-NvccMaxArch -NvccExe $NvccPath + if ($maxArch) { + $CmakeArgs += "-DCMAKE_CUDA_ARCHITECTURES=$maxArch" + substep "GPU is sm_$CudaArch but nvcc only supports up to sm_$maxArch" "Yellow" + substep "Building with sm_$maxArch (PTX will JIT for your GPU at runtime)" "Yellow" + } + # else: omit flag entirely, let cmake pick defaults } - # else: omit flag entirely, let cmake pick defaults } } } else { diff --git a/studio/setup.sh b/studio/setup.sh index ac82416f9b..b5b64a8d4d 100755 --- a/studio/setup.sh +++ b/studio/setup.sh @@ -155,6 +155,31 @@ _nvcc_meets_llama_minimum() { echo "$_raw" } +# Echo a ';'-separated CUDA arch list (e.g. "86;120"). Override ($2, +# UNSLOTH_LLAMA_CUDA_ARCHS) wins verbatim; else parse+dedupe compute_cap text +# ($1). Empty means "no arch detected", so the caller builds CPU instead of a +# PTX-only binary that fails on an old driver (#5854). +_resolve_cuda_archs() { + local _raw_caps=$1 + local _arch_override=$2 + if [ -n "$_arch_override" ]; then + printf '%s' "$_arch_override" + return 0 + fi + local _archs="" _cap _arch + while IFS= read -r _cap; do + _cap=$(printf '%s' "$_cap" | tr -d '[:space:]') + if [[ "$_cap" =~ ^([0-9]+)\.([0-9]+)$ ]]; then + _arch="${BASH_REMATCH[1]}${BASH_REMATCH[2]}" + case ";$_archs;" in + *";$_arch;"*) ;; + *) _archs="${_archs:+$_archs;}$_arch" ;; + esac + fi + done <<< "$_raw_caps" + printf '%s' "$_archs" +} + # Run a GPU probe under a 10s timeout when `timeout` is available so a wedged # NVIDIA driver cannot hang setup; fall back to a bare call where it is not. _setup_run_smi() { @@ -1517,35 +1542,38 @@ else fi if [ "$_CUDA_TOOLKIT_ALLOWED" = true ]; then - CMAKE_ARGS="$CMAKE_ARGS -DGGML_CUDA=ON" - - CUDA_ARCHS="" - if command -v nvidia-smi &>/dev/null; then - _raw_caps=$(_setup_run_smi nvidia-smi --query-gpu=compute_cap --format=csv,noheader 2>/dev/null || true) - while IFS= read -r _cap; do - _cap=$(echo "$_cap" | tr -d '[:space:]') - if [[ "$_cap" =~ ^([0-9]+)\.([0-9]+)$ ]]; then - _arch="${BASH_REMATCH[1]}${BASH_REMATCH[2]}" - case ";$CUDA_ARCHS;" in - *";$_arch;"*) ;; - *) CUDA_ARCHS="${CUDA_ARCHS:+$CUDA_ARCHS;}$_arch" ;; - esac - fi - done <<< "$_raw_caps" + # Resolve the arch list before committing to a CUDA build; + # an empty list means CPU instead of a PTX-only binary (#5854). + _raw_caps="" + # Resolve nvidia-smi as _setup_has_usable_nvidia_gpu does + # (PATH, then /usr/bin); `command -v` alone would miss an + # off-PATH binary and wrongly drop a CUDA host to CPU. + _smi_bin="" + if command -v nvidia-smi >/dev/null 2>&1; then + _smi_bin="nvidia-smi" + elif [ -x "/usr/bin/nvidia-smi" ]; then + _smi_bin="/usr/bin/nvidia-smi" fi + if [ -n "$_smi_bin" ]; then + _raw_caps=$(_setup_run_smi "$_smi_bin" --query-gpu=compute_cap --format=csv,noheader 2>/dev/null || true) + fi + CUDA_ARCHS="$(_resolve_cuda_archs "$_raw_caps" "${UNSLOTH_LLAMA_CUDA_ARCHS:-}")" if [ -n "$CUDA_ARCHS" ]; then - CMAKE_ARGS="$CMAKE_ARGS -DCMAKE_CUDA_ARCHITECTURES=${CUDA_ARCHS}" + CMAKE_ARGS="$CMAKE_ARGS -DGGML_CUDA=ON -DCMAKE_CUDA_ARCHITECTURES=${CUDA_ARCHS}" + CMAKE_ARGS="$CMAKE_ARGS -DCMAKE_CUDA_FLAGS=--threads=0" _BUILD_DESC="building (CUDA, sm_${CUDA_ARCHS//;/+sm_})" + + # Allow a host gcc/clang newer than nvcc's whitelist (else a fresh + # toolkit aborts with "unsupported GNU version"); via env to avoid word-splitting. + export NVCC_PREPEND_FLAGS="${NVCC_PREPEND_FLAGS:+$NVCC_PREPEND_FLAGS }-allow-unsupported-compiler" else - _BUILD_DESC="building (CUDA)" + # No detectable arch: build CPU (CMAKE_ARGS has no + # -DGGML_CUDA=ON yet, so clearing GPU_BACKEND yields CPU). + substep "could not detect a CUDA compute capability; building CPU llama.cpp instead of a PTX-only binary (set UNSLOTH_LLAMA_CUDA_ARCHS, e.g. \"120\", to force a CUDA build)." "$C_WARN" + GPU_BACKEND="" + _BUILD_DESC="building (CPU, CUDA arch undetectable)" fi - - CMAKE_ARGS="$CMAKE_ARGS -DCMAKE_CUDA_FLAGS=--threads=0" - - # Allow a host gcc/clang newer than nvcc's whitelist (else a fresh - # toolkit aborts with "unsupported GNU version"); via env to avoid word-splitting. - export NVCC_PREPEND_FLAGS="${NVCC_PREPEND_FLAGS:+$NVCC_PREPEND_FLAGS }-allow-unsupported-compiler" fi fi elif [ "$GPU_BACKEND" = "rocm" ]; then diff --git a/tests/run_all.sh b/tests/run_all.sh index 2c495f5561..18182d9db7 100755 --- a/tests/run_all.sh +++ b/tests/run_all.sh @@ -11,6 +11,7 @@ sh "$TESTS_DIR/sh/test_get_torch_index_url.sh" sh "$TESTS_DIR/sh/test_mac_intel_compat.sh" sh "$TESTS_DIR/sh/test_torch_constraint.sh" sh "$TESTS_DIR/sh/test_nvcc_meets_llama_minimum.sh" +sh "$TESTS_DIR/sh/test_resolve_cuda_archs.sh" sh "$TESTS_DIR/sh/test_strixhalo_wsl_reroute.sh" sh "$TESTS_DIR/sh/test_uninstall_shared_icon.sh" sh "$TESTS_DIR/sh/test_torch_flavor.sh" diff --git a/tests/sh/test_resolve_cuda_archs.sh b/tests/sh/test_resolve_cuda_archs.sh new file mode 100755 index 0000000000..be32bee6ad --- /dev/null +++ b/tests/sh/test_resolve_cuda_archs.sh @@ -0,0 +1,68 @@ +#!/bin/bash +# Unit tests for _resolve_cuda_archs() from studio/setup.sh (#5854). +# Turns nvidia-smi compute_cap text into a deduped ';'-separated arch list; +# empty result signals a CPU build, and an explicit override wins. +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +SETUP_SH="$SCRIPT_DIR/../../studio/setup.sh" +PASS=0 +FAIL=0 + +# Extract just the helper function (same sed range as the other function tests). +_FUNC_FILE=$(mktemp) +sed -n '/^_resolve_cuda_archs()/,/^}/p' "$SETUP_SH" > "$_FUNC_FILE" + +assert_eq() { + _label="$1"; _expected="$2"; _actual="$3" + if [ "$_actual" = "$_expected" ]; then + echo " PASS: $_label" + PASS=$((PASS + 1)) + else + echo " FAIL: $_label (expected '$_expected', got '$_actual')" + FAIL=$((FAIL + 1)) + fi +} + +# $1 = raw compute_cap text, $2 = override +run_resolve() { + bash -c ". '$_FUNC_FILE'; _resolve_cuda_archs \"\$1\" \"\$2\"" _ "$1" "$2" +} + +echo "=== test_resolve_cuda_archs ===" + +# 1) Single GPU -> single arch. +assert_eq "single 8.6" "86" "$(run_resolve "8.6" "")" + +# 2) Two distinct GPUs -> both archs, order preserved. +assert_eq "distinct 8.6 + 9.0" "86;90" "$(run_resolve "$(printf '8.6\n9.0\n')" "")" + +# 3) Duplicate caps (multi-GPU same model) -> deduped. +assert_eq "dedup 12.0 x2" "120" "$(run_resolve "$(printf '12.0\n12.0\n')" "")" + +# 4) Empty input -> empty (the CPU-fallback signal; #5854). +assert_eq "empty input" "" "$(run_resolve "" "")" + +# 5) Garbage / N/A lines are ignored -> empty. +assert_eq "garbage N/A" "" "$(run_resolve "$(printf 'N/A\n[Not Supported]\n')" "")" + +# 6) Mixed valid + junk -> only the valid caps survive. +assert_eq "mixed valid+junk" "86;90" "$(run_resolve "$(printf '8.6\nfoo\n9.0\n')" "")" + +# 7) Whitespace / CR around a cap is stripped. +assert_eq "whitespace stripped" "86" "$(run_resolve "$(printf ' 8.6 \r\n')" "")" + +# 8) Override wins verbatim, ignoring detection. +assert_eq "override wins" "120" "$(run_resolve "8.6" "120")" + +# 9) Override works even with no detected caps. +assert_eq "override no detection" "86;90" "$(run_resolve "" "86;90")" + +# 10) Future arch (compute 10.0 -> 100) parses. +assert_eq "future 10.0" "100" "$(run_resolve "10.0" "")" + +rm -f "$_FUNC_FILE" + +echo "" +echo "Results: $PASS passed, $FAIL failed" +[ "$FAIL" -eq 0 ] || exit 1 diff --git a/tests/studio/install/test_gpu_detection_followups.py b/tests/studio/install/test_gpu_detection_followups.py index 0926cf9bd5..f5ad9566d7 100644 --- a/tests/studio/install/test_gpu_detection_followups.py +++ b/tests/studio/install/test_gpu_detection_followups.py @@ -249,7 +249,20 @@ class TestSetupShHardening: ), "ROCm toolkit search must require a detected AMD GPU, not just hipcc" def test_compute_cap_probe_timeout_wrapped(self, setup_src): - assert "_setup_run_smi nvidia-smi --query-gpu=compute_cap" in setup_src + # nvidia-smi is now a variable ($_smi_bin), so check the wrapper precedes + # the probe rather than matching a literal. The string also appears in a + # comment, so scan all occurrences and accept if any is wrapped. + wrapped = False + start = 0 + while True: + idx = setup_src.find("--query-gpu=compute_cap", start) + if idx < 0: + break + if "_setup_run_smi" in setup_src[max(0, idx - 80) : idx]: + wrapped = True + break + start = idx + 1 + assert wrapped, "compute_cap probe must be wrapped in _setup_run_smi (timeout-bounded)" def test_driver_version_probe_timeout_wrapped(self, setup_src): start = setup_src.find("_cuda_driver_max_version()") diff --git a/tests/studio/install/test_pr4562_bugfixes.py b/tests/studio/install/test_pr4562_bugfixes.py index b19c300fec..a8cec0cb85 100644 --- a/tests/studio/install/test_pr4562_bugfixes.py +++ b/tests/studio/install/test_pr4562_bugfixes.py @@ -728,13 +728,16 @@ class TestSourceCodePatterns: assert all( "-allow-unsupported-compiler" not in line for line in cmake_args_lines ), "flag must not be pushed into the $CmakeArgs array" - # Must be scoped to the CUDA branch, not set for CPU-only builds. + # Must be scoped to the CUDA-on branch, not set for CPU-only builds. The + # branch also has an early GGML_CUDA=OFF (undetectable-arch CPU fallback, + # #5854), so anchor on GGML_CUDA=ON and the final (no-GPU) GGML_CUDA=OFF. flag_idx = content.index("-allow-unsupported-compiler") cuda_guard_idx = content.index("if ($HasNvidiaSmi -and $NvccPath)") - cuda_disable_idx = content.index("'-DGGML_CUDA=OFF'") - assert cuda_guard_idx < flag_idx < cuda_disable_idx, ( - "NVCC_PREPEND_FLAGS must be set inside the CUDA-on branch, " - "before the GGML_CUDA=OFF (CPU) branch" + cuda_on_idx = content.index("'-DGGML_CUDA=ON'") + cpu_else_idx = content.rindex("'-DGGML_CUDA=OFF'") + assert cuda_guard_idx < cuda_on_idx < flag_idx < cpu_else_idx, ( + "NVCC_PREPEND_FLAGS must be set inside the CUDA-on branch, after " + "-DGGML_CUDA=ON and before the final CPU GGML_CUDA=OFF branch" ) def test_macos_arm64_cpu_fallback_args_exclude_rpath(self):