diff --git a/install.sh b/install.sh index 548e6f702a..8e30264df0 100755 --- a/install.sh +++ b/install.sh @@ -1985,6 +1985,21 @@ _has_usable_nvidia_gpu() { get_torch_index_url() { _base="${UNSLOTH_PYTORCH_MIRROR:-https://download.pytorch.org/whl}" _base="${_base%/}" + # Explicit override -- skip ALL GPU probing when the caller pins the wheel + # index. Headless / container / CI builds (and anyone cross-installing for a + # different target) must not let the build host's GPU -- or the lack of one -- + # decide the wheel family. This is the same "tell the build, don't ask the + # hardware" approach the Docker base image and vLLM/SGLang's Dockerfiles take. + # UNSLOTH_TORCH_INDEX_URL wins (full URL, verbatim); UNSLOTH_TORCH_INDEX_FAMILY + # is the convenience form (cpu, cu124, cu126, cu128, cu130, rocm6.4, ...) + # appended to the mirror base so UNSLOTH_PYTORCH_MIRROR is still honoured. + if [ -n "${UNSLOTH_TORCH_INDEX_URL:-}" ]; then + echo "${UNSLOTH_TORCH_INDEX_URL%/}"; return + fi + if [ -n "${UNSLOTH_TORCH_INDEX_FAMILY:-}" ]; then + _family="${UNSLOTH_TORCH_INDEX_FAMILY#/}" + echo "$_base/${_family%/}"; return + fi # macOS: always CPU (no CUDA support) case "$(uname -s)" in Darwin) echo "$_base/cpu"; return ;; esac # Try nvidia-smi -- require the binary to actually list a usable GPU. diff --git a/studio/install_python_stack.py b/studio/install_python_stack.py index 9060b57542..222cfacb72 100644 --- a/studio/install_python_stack.py +++ b/studio/install_python_stack.py @@ -933,11 +933,20 @@ def _detect_cuda_torch_index_url() -> str: Mirrors install.sh::get_torch_index_url's CUDA ladder so `studio update` repairs to the same wheel family a fresh `curl | sh` install would pick. - Probes nvidia-smi (PATH, then /usr/bin/nvidia-smi) and parses both the - legacy "CUDA Version:" and the newer "CUDA UMD Version:" spellings. - Defaults to cu126 when nvidia-smi is missing or the version is unreadable - (e.g. NVIDIA detected only via the /proc/driver/nvidia/gpus fallback). + Honours the same explicit overrides first (UNSLOTH_TORCH_INDEX_URL / + UNSLOTH_TORCH_INDEX_FAMILY) so a headless / container / CI install never lets + the host GPU decide the wheel family. Otherwise probes nvidia-smi (PATH, then + /usr/bin/nvidia-smi) and parses both the legacy "CUDA Version:" and the newer + "CUDA UMD Version:" spellings. Defaults to cu126 when nvidia-smi is missing or + the version is unreadable (e.g. NVIDIA detected only via the + /proc/driver/nvidia/gpus fallback). """ + _override_url = os.environ.get("UNSLOTH_TORCH_INDEX_URL", "").strip() + if _override_url: + return _override_url.rstrip("/") + _override_family = os.environ.get("UNSLOTH_TORCH_INDEX_FAMILY", "").strip() + if _override_family: + return f"{_PYTORCH_WHL_BASE}/{_override_family.strip('/')}" exe = shutil.which("nvidia-smi") if not exe and os.path.isfile("/usr/bin/nvidia-smi"): exe = "/usr/bin/nvidia-smi" diff --git a/tests/sh/test_get_torch_index_url.sh b/tests/sh/test_get_torch_index_url.sh index 6656142625..b9d0c5e5a1 100755 --- a/tests/sh/test_get_torch_index_url.sh +++ b/tests/sh/test_get_torch_index_url.sh @@ -379,6 +379,42 @@ _result=$(run_func "$_dir" " -1 ") assert_eq "CVD=' -1 ' hides NVIDIA -> cpu" "https://download.pytorch.org/whl/cpu" "$_result" rm -rf "$_dir" +# --- explicit overrides (headless / container / CI; no GPU probing) ---------- +# 39) UNSLOTH_TORCH_INDEX_FAMILY pins the family with no GPU present -> that +# family (not the cpu fallback detection would pick). +_result=$(UNSLOTH_TORCH_INDEX_FAMILY="cu128" run_func "none") +assert_eq "family override (no GPU) -> cu128" "https://download.pytorch.org/whl/cu128" "$_result" + +# 40) Family override wins over a real detection: a host whose nvidia-smi reports +# 12.6 still gets cu128. This is the exact Docker-build case (the builder sees +# the host driver but must publish a cu128 image). +_dir=$(make_mock_smi "12.6") +_result=$(UNSLOTH_TORCH_INDEX_FAMILY="cu128" run_func "$_dir") +assert_eq "family override beats detected 12.6 -> cu128" "https://download.pytorch.org/whl/cu128" "$_result" +rm -rf "$_dir" + +# 41) UNSLOTH_TORCH_INDEX_URL is used verbatim and wins over detection. +_dir=$(make_mock_smi "12.6") +_result=$(UNSLOTH_TORCH_INDEX_URL="https://mirror.example.com/whl/cu999" run_func "$_dir") +assert_eq "url override beats detection -> verbatim" "https://mirror.example.com/whl/cu999" "$_result" +rm -rf "$_dir" + +# 42) Family override is appended to UNSLOTH_PYTORCH_MIRROR (mirror still honoured). +_result=$(UNSLOTH_PYTORCH_MIRROR="https://mirror.example.com/whl" UNSLOTH_TORCH_INDEX_FAMILY="cu128" run_func "none") +assert_eq "mirror + family override -> mirror/cu128" "https://mirror.example.com/whl/cu128" "$_result" + +# 43) Trailing slash in UNSLOTH_TORCH_INDEX_URL is stripped. +_result=$(UNSLOTH_TORCH_INDEX_URL="https://mirror.example.com/whl/cu128/" run_func "none") +assert_eq "url override trailing slash stripped" "https://mirror.example.com/whl/cu128" "$_result" + +# 44) URL override takes precedence over family override. +_result=$(UNSLOTH_TORCH_INDEX_URL="https://mirror.example.com/whl/cu130" UNSLOTH_TORCH_INDEX_FAMILY="cu128" run_func "none") +assert_eq "url override beats family override -> url" "https://mirror.example.com/whl/cu130" "$_result" + +# 45) An empty override is ignored (falls through to normal detection). +_result=$(UNSLOTH_TORCH_INDEX_FAMILY="" UNSLOTH_TORCH_INDEX_URL="" run_func "none") +assert_eq "empty overrides ignored -> detected cpu" "https://download.pytorch.org/whl/cpu" "$_result" + rm -f "$_FUNC_FILE" rm -rf "$_FAKE_SMI_DIR" rm -rf "$_TOOLS_DIR"