Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
138c094
install: let UNSLOTH_TORCH_INDEX_FAMILY / _URL override CUDA wheel de…
danielhanchen Jun 26, 2026
b02a609
install: make the torch-index override authoritative across ROCm paths
danielhanchen Jun 26, 2026
997155d
install: honor pinned torch index in CUDA/ROCm repair paths
danielhanchen Jun 26, 2026
0c0e2cb
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 26, 2026
5d69193
Merge remote-tracking branch 'origin/main' into cuda-torch-index-over…
danielhanchen Jun 26, 2026
5736d5d
install: honor torch-index override on the Windows installers too
danielhanchen Jun 26, 2026
5a017ec
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 26, 2026
d2d5f90
install: complete pinned-index handling for ROCm/Windows edge cases
danielhanchen Jun 26, 2026
4a5baba
install: finish pinned ROCm/CUDA edge cases on Windows + repair path
danielhanchen Jun 26, 2026
f6c5e46
install: keep the ROCm to CPU fallback install inside the retry-helpe…
danielhanchen Jun 26, 2026
c290cac
install: address #6692 review round 5 (ROCm/CPU pin edge cases)
danielhanchen Jun 27, 2026
4018a69
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 27, 2026
37ce507
torch-index override: classify CUDA pin by leaf; trim blank shell ove…
danielhanchen Jun 29, 2026
c1e956c
Merge remote-tracking branch 'origin/main' into cuda-torch-index-over…
danielhanchen Jul 1, 2026
7845276
Merge remote-tracking branch 'origin/main' into HEAD
danielhanchen Jul 5, 2026
00781b7
install: honor pinned torch index over CVD/GPU gates and fix leaf-bas…
danielhanchen Jul 5, 2026
ffd11cc
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 5, 2026
7814e2c
install: tighten pinned torch-index override edge cases
danielhanchen Jul 5, 2026
881ec95
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 5, 2026
e1f05c2
install: fix second-order edge cases in pinned torch-index ROCm/CUDA …
danielhanchen Jul 6, 2026
0275432
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 6, 2026
3b17c9b
Fix ROCm/gfx pin case normalization, ROCm-tag requirement, and CUDA-l…
danielhanchen Jul 6, 2026
1213135
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 6, 2026
753ef2f
install: converge torch-index pin detection via a per-venv marker
danielhanchen Jul 6, 2026
35c62da
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 6, 2026
6bbc692
install: keep the torch-index marker additive to flavor validation
danielhanchen Jul 6, 2026
257ca41
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 6, 2026
5c93ffd
install: honor custom pins and repair pinned venvs in place
danielhanchen Jul 6, 2026
0d48584
setup.ps1: keep the ROCm CPU-fallback force line the pr5940 test guards
danielhanchen Jul 6, 2026
5ed0737
install: honor exact CUDA/custom index URL pins in the torch-index ma…
danielhanchen Jul 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 42 additions & 3 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1985,6 +1985,28 @@ _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
# Strip ALL trailing slashes (match the Python side's .rstrip("/") and the
# Strix mirror handling below) -- a double/triple-slash URL 404s on strict
# pip proxies (artifactory, sonatype).
_url="${UNSLOTH_TORCH_INDEX_URL}"
while [ "${_url%/}" != "$_url" ]; do _url="${_url%/}"; done
echo "$_url"; return

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Allow gfx URL overrides to select torch 2.11 wheels

When UNSLOTH_TORCH_INDEX_URL points at an AMD per-gfx index such as https://repo.amd.com/rocm/whl/gfx1151, this return makes TORCH_INDEX_URL end in gfx1151 without taking the Strix reroute path that normally also raises TORCH_CONSTRAINT to torch>=2.11.0,<2.12.0. The only generic constraint adjustment below handles */rocm7.2, so the fresh install still asks that per-gfx index for torch>=2.4,<2.11.0 even though the existing Strix path documents that this index serves the torch 2.11+ ROCm build, causing the pinned full-URL install to fail to resolve.

Useful? React with 👍 / 👎.

fi
if [ -n "${UNSLOTH_TORCH_INDEX_FAMILY:-}" ]; then
_family="${UNSLOTH_TORCH_INDEX_FAMILY}"
while [ "${_family#/}" != "$_family" ]; do _family="${_family#/}"; done
while [ "${_family%/}" != "$_family" ]; do _family="${_family%/}"; done
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.
Expand Down Expand Up @@ -2381,7 +2403,16 @@ _maybe_bootstrap_rocm_wsl() {
[ -n "$_rw_tmp" ] && rm -f "$_rw_tmp"
return 0
}
_maybe_bootstrap_rocm_wsl || true
# When the caller pins the wheel index (UNSLOTH_TORCH_INDEX_URL / _FAMILY),
# honour it everywhere downstream: skip the WSL ROCm bootstrap (which can run
# sudo + large downloads after probing /dev/dxg) and the Radeon/Strix rerouting
# below (which would re-probe the GPU and overwrite the pinned URL). A headless /
# container / CI build must get exactly the index it asked for.
_torch_index_pinned=false
if [ -n "${UNSLOTH_TORCH_INDEX_URL:-}" ] || [ -n "${UNSLOTH_TORCH_INDEX_FAMILY:-}" ]; then
_torch_index_pinned=true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 Badge Trim override vars before marking the index pinned

get_torch_index_url trims whitespace-only overrides and treats them as unset, but this guard checks the raw environment values. If a CI variable/template sets UNSLOTH_TORCH_INDEX_FAMILY=" " (or the URL var) on an AMD/WSL host, _torch_index_pinned becomes true while detection still chooses the normal CPU/ROCm URL, so the installer skips WSL bootstrap and Radeon/Strix reroutes that should have run.

Useful? React with 👍 / 👎.

fi
[ "$_torch_index_pinned" = true ] || _maybe_bootstrap_rocm_wsl || true

TORCH_INDEX_URL=$(get_torch_index_url)

Expand All @@ -2400,16 +2431,23 @@ case "$_torch_index_leaf" in
*) export UNSLOTH_TORCH_BACKEND="cuda" ;;
esac

# rocm7.2 ships torch 2.11.0 -- adjust the constraint to allow it.
# rocm7.2 and the AMD per-gfx indexes (repo.amd.com/.../gfxNNNN) ship torch
# 2.11.0 -- adjust the constraint to allow it. This also covers a pinned full-URL
# or family override (e.g. UNSLOTH_TORCH_INDEX_URL=.../gfx1151) that returns early
# above and so never hits the Strix reroute that otherwise raises this constraint.
# All other ROCm tags and CUDA stay within <2.11.0.
case "$TORCH_INDEX_URL" in
*/rocm7.2) TORCH_CONSTRAINT="torch>=2.11.0,<2.12.0" ;;
*/rocm7.2|*/gfx*) TORCH_CONSTRAINT="torch>=2.11.0,<2.12.0" ;;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Match gfx constraints on the index leaf

This pattern checks the whole URL, so a custom mirror whose base path contains a gfx* segment, e.g. UNSLOTH_PYTORCH_MIRROR=https://mirror.local/gfx-cache with a CUDA family, is treated like an AMD per-arch index and raises TORCH_CONSTRAINT to torch>=2.11.0,<2.12.0. The backend classification just above intentionally uses only the final path segment to avoid this mirror-path false positive; the constraint should do the same or CUDA/CPU installs from such mirrors can resolve the wrong torch family.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. install.sh's torch>=2.11 constraint case now switches on the index leaf (case $_torch_index_leaf in rocm7.2|gfx*)) instead of the whole URL, so a custom UNSLOTH_PYTORCH_MIRROR whose base path contains a gfx/rocm7.2 segment with a cu*/cpu family is no longer false-matched onto the 2.11 line. This mirrors the leaf-only UNSLOTH_TORCH_BACKEND classification just above, and install.ps1 / setup.ps1 already classify on the leaf.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Pin ROCm companion wheels for pinned gfx installs

With a full UNSLOTH_TORCH_INDEX_URL=.../gfx1151/gfx120X override, this new */gfx* case only raises the torch range; the later shell install still passes bare torchvision torchaudio from the same AMD per-arch index. The PowerShell/Python paths in this change pin those companions because AMD's per-gfx indexes publish them independently, so the shell path can resolve a torchvision/torchaudio build for a different torch minor even while torch is constrained to <2.12, leaving the venv ABI-mismatched.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Pin companion wheels for gfx overrides

For a newly supported pinned gfx override such as UNSLOTH_TORCH_INDEX_URL=.../gfx1151, this only raises the torch constraint; the install/repair commands still pass bare torchvision torchaudio to uv with --index-url "$TORCH_INDEX_URL". I checked uv pip install --help: --index-url selects the package index and extra indexes require --extra-index-url, so the AMD gfx index can resolve newer independently published companions than the torch 2.11 wheel (the PS/Python paths pin these for the same ABI-mismatch reason).

Useful? React with 👍 / 👎.

esac

# Auto-detect GPU for AMD ROCm based
# get_torch_index_url must have chosen */rocm*
# (gfx in rocminfo or amd-smi list). Then require rocminfo "Marketing Name:.*Radeon".
# Skipped entirely when the index is pinned: an explicit override (even a ROCm
# one like UNSLOTH_TORCH_INDEX_FAMILY=rocm6.4) must not be rerouted to the
# Radeon/Strix repos by GPU probing.
_amd_gpu_radeon=false
if [ "$_torch_index_pinned" = false ]; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Set 2.11 constraint for pinned gfx indexes

When a caller pins an AMD per-arch index, for example UNSLOTH_TORCH_INDEX_URL=https://repo.amd.com/rocm/whl/gfx1151/ for a Strix/air-gapped install, this guard skips the Strix block that would have set TORCH_CONSTRAINT to torch>=2.11.0,<2.12.0. The fresh install then reaches the generic PyTorch install with the default <2.11.0 constraint, but these gfx* indexes are the 2.11 ROCm wheel path, so uv cannot select the intended torch build. Please derive the 2.11 constraint from a pinned gfx* leaf before bypassing GPU probing.

Useful? React with 👍 / 👎.

case "$TORCH_INDEX_URL" in
*/rocm*)
if _has_amd_rocm_gpu && command -v rocminfo >/dev/null 2>&1 && \
Expand Down Expand Up @@ -2490,6 +2528,7 @@ case "$TORCH_INDEX_URL" in
fi
;;
esac
fi # _torch_index_pinned guard (Radeon + Strix reroute)
_TAURI_TORCH_INDEX_FAMILY=$(_tauri_torch_index_family "$TORCH_INDEX_URL")
if [ "$_amd_gpu_radeon" = true ] && [ "$SKIP_TORCH" = false ]; then
_TAURI_TORCH_INDEX_FAMILY="radeon"
Expand Down
107 changes: 87 additions & 20 deletions studio/install_python_stack.py
Original file line number Diff line number Diff line change
Expand Up @@ -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('/')}"
Comment on lines +1176 to +1178

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor family overrides before ROCm repair

In standalone unsloth studio update, this override is only read inside the CUDA repair helper, while _TORCH_BACKEND remains empty and _ensure_rocm_torch() still runs afterward. On an AMD Linux host with CPU torch, setting UNSLOTH_TORCH_INDEX_FAMILY=cpu (or a CUDA family for a headless build) therefore still lets ROCm GPU detection reinstall ROCm torch from the detected ROCm index, ignoring the requested family; derive the backend from the override before both CUDA and ROCm repair paths.

Useful? React with 👍 / 👎.

exe = shutil.which("nvidia-smi")
if not exe and os.path.isfile("/usr/bin/nvidia-smi"):
exe = "/usr/bin/nvidia-smi"
Expand Down Expand Up @@ -972,6 +981,30 @@ def _detect_cuda_torch_index_url() -> str:
return f"{_PYTORCH_WHL_BASE}/{tag}"


def _explicit_torch_index_url() -> "str | None":
"""The wheel index URL pinned via UNSLOTH_TORCH_INDEX_URL / _FAMILY, else None.

Lets the CUDA/ROCm repair helpers honour the exact pinned family/URL instead
of re-probing the GPU. Mirrors install.sh::get_torch_index_url's override.
"""
url = os.environ.get("UNSLOTH_TORCH_INDEX_URL", "").strip()
if url:
return url.rstrip("/")
family = os.environ.get("UNSLOTH_TORCH_INDEX_FAMILY", "").strip()
if family:
return f"{_PYTORCH_WHL_BASE}/{family.strip('/')}"
return None


def _explicit_rocm_torch_index_url() -> "str | None":
"""The pinned wheel index URL when it names a ROCm family (rocm*/gfx*), else None."""
url = _explicit_torch_index_url()
if url is None:
return None
leaf = url.rstrip("/").rsplit("/", 1)[-1].lower()
return url if leaf.startswith(("rocm", "gfx")) else None
Comment on lines +1231 to +1234

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor custom ROCm URL pins during repair

For an explicit UNSLOTH_TORCH_INDEX_URL whose final segment is not rocm* or gfx* (for example a private mirror ending in /simple or /current), this returns None, so _ensure_rocm_torch() falls back to GPU probing and reinstalls from the auto-detected PyTorch ROCm index instead of the pinned URL. On AMD hosts where the current torch is CPU/CUDA or gets overwritten during the same install, that violates the documented “URL wins, verbatim” override and can pull wheels from the public/default index despite the pin.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Added _ensure_verbatim_torch_index: for an explicit UNSLOTH_TORCH_INDEX_URL whose leaf is not rocm/gfx/cu/cpu (a private mirror ending in /simple, /current, ...), when the marker differs from the pin it reinstalls torch/vision/audio verbatim from that URL (exclusive --index-url, bare specs). With no marker it is a no-op so an old venv is not blindly force-reinstalled. This honors "URL wins verbatim" instead of returning None and re-probing the GPU.



def _ensure_cuda_torch() -> None:
"""Repair a venv whose torch is a ROCm build on an NVIDIA host.

Expand Down Expand Up @@ -1004,7 +1037,10 @@ def _ensure_cuda_torch() -> None:
return
# Only NVIDIA hosts should carry CUDA torch. _has_usable_nvidia_gpu()
# covers the /proc/driver/nvidia/gpus fallback when nvidia-smi is absent.
if not _has_usable_nvidia_gpu():
# An explicit CUDA wheel-index pin (headless / container / CI cross-install)
# commits to CUDA wheels regardless of whether a GPU is visible here, so it
# overrides the GPU-presence gate.
if not _has_usable_nvidia_gpu() and _explicit_torch_index_url() is None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reinstall CPU torch when CUDA is explicitly pinned

In standalone unsloth studio update with UNSLOTH_TORCH_INDEX_FAMILY=cu128 on a headless/no-visible-GPU builder, this bypasses the GPU gate, but the helper still returns unless the existing torch probe prints hip; an existing CPU torch is treated as deliberate and never reinstalled from the pinned CUDA index. Since the update path preserves torch rather than preinstalling it from install.sh, the explicit CUDA override remains ineffective for the common CPU-venv-to-CUDA cross-install case.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Limit CUDA gate bypass to cu pins*

When UNSLOTH_TORCH_INDEX_URL is a custom mirror whose final segment is not a CUDA family, for example a PEP 503 URL ending in /simple, _TORCH_BACKEND remains empty but this condition still bypasses the no-NVIDIA gate merely because some explicit URL exists. On a GPU-less or AMD builder with an existing ROCm torch, the later hip branch runs the CUDA repair against that arbitrary URL even though no cu* pin was provided, replacing the current build instead of leaving unknown custom indexes alone. Restrict this bypass to classified CUDA pins.

Useful? React with 👍 / 👎.

return

# Classify the installed torch: "hip" (ROCm build -- the poisoning
Expand Down Expand Up @@ -1247,7 +1283,9 @@ def _ensure_rocm_torch() -> None:
# an incompatible wheel. Use HIP_VISIBLE_DEVICES for the runtime target.
_strix_override_url: "str | None" = None
_strix_override_pkgs: "tuple[str, str, str] | None" = None
if ver < (7, 2):
# An explicit ROCm wheel-index pin is authoritative: never auto-reroute it to
# the AMD per-gfx index (the caller already chose the family/URL).
if ver < (7, 2) and _explicit_rocm_torch_index_url() is None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor pinned ROCm indexes when HIP torch exists

When a ROCm/GFX index is pinned, this guard suppresses the Strix auto-reroute, but the pinned index is only installed later inside the elif not has_hip_torch path. On a Strix ROCm 7.1 venv that already has HIP torch, running UNSLOTH_TORCH_INDEX_URL=https://repo.amd.com/rocm/whl/gfx1151 unsloth studio update now skips the Strix fix and never reinstalls from the pinned index, leaving the known-bad 7.1 wheel in place.

Useful? React with 👍 / 👎.

gfx_codes = _detect_amd_gfx_codes()
_strix_gfx = {"gfx1151", "gfx1150"}
_detected_strix = _strix_gfx.intersection(gfx_codes)
Expand Down Expand Up @@ -1310,23 +1348,35 @@ def _ensure_rocm_torch() -> None:
)
rocm_torch_ready = True
elif not has_hip_torch:
# Select best matching wheel tag (newest ROCm version <= installed)
tag = next(
(
t
for (maj, mn), t in sorted(_ROCM_TORCH_INDEX.items(), reverse = True)
if ver >= (maj, mn)
),
None,
)
# Honour an explicit ROCm wheel-index pin verbatim instead of re-detecting
# the host ROCm version; otherwise select the best wheel tag (newest ROCm
# version <= installed). gfx*/rocm7.2 indexes serve torch 2.11+, so match
# the constraints to the index leaf when overridden.
_override_idx = _explicit_rocm_torch_index_url()
if _override_idx is not None:
index_url = _override_idx
Comment on lines +1960 to +1962

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply ROCm overrides before host GPU gates

The explicit ROCm index is only consulted here, after _ensure_rocm_torch() has already required a visible AMD GPU and a readable host ROCm version. In a headless/container cross-install, or on a builder with only NVIDIA visible, UNSLOTH_TORCH_INDEX_FAMILY=rocm6.4 sets _TORCH_BACKEND to rocm but the function returns before reaching this block, so the pinned ROCm wheel is never installed and the venv keeps its CPU/CUDA torch.

Useful? React with 👍 / 👎.

tag = index_url.rstrip("/").rsplit("/", 1)[-1].lower()
Comment on lines +1960 to +1963

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bypass GPU gates for explicit ROCm pins

In standalone unsloth studio update with UNSLOTH_TORCH_INDEX_FAMILY=rocm6.4 or a full ROCm URL, this override is only consulted after _ensure_rocm_torch() has already returned for a visible NVIDIA GPU, no visible AMD GPU, or an unreadable ROCm version. That leaves an existing CPU/CUDA torch in place in headless CI/container or mixed-GPU cross-install scenarios, even though the new override is meant to skip host GPU probing like the CUDA pin path does.

Useful? React with 👍 / 👎.

Comment on lines +1960 to +1963

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor ROCm pins when ROCm torch is already installed

The pinned ROCm index is only consulted inside the elif not has_hip_torch path, so a standalone studio update with UNSLOTH_TORCH_INDEX_FAMILY=rocm7.2 or a gfx* URL will skip the reinstall whenever the venv already contains any ROCm torch, such as an older +rocm6.4/+rocm7.1 build. That leaves the explicit wheel-family override unapplied exactly in the case where users are trying to move to a different ROCm/gfx index to avoid incompatible or known-bad wheels.

Useful? React with 👍 / 👎.

else:
tag = next(
(
t
for (maj, mn), t in sorted(_ROCM_TORCH_INDEX.items(), reverse = True)
if ver >= (maj, mn)
),
None,
)
if tag is None:
print(f" No PyTorch wheel for ROCm {ver[0]}.{ver[1]} -- " f"skipping torch reinstall")
else:
index_url = f"{_PYTORCH_WHL_BASE}/{tag}"
print(f" ROCm {ver[0]}.{ver[1]} -- installing torch from {index_url}")
_torch_pkg, _vision_pkg, _audio_pkg = _ROCM_TORCH_PKG_SPECS.get(
tag, _ROCM_TORCH_PKG_SPECS["_default"]
)
if _override_idx is None:
index_url = f"{_PYTORCH_WHL_BASE}/{tag}"
print(f" ROCm torch -- installing from {index_url}")
if tag.startswith("gfx"):
_torch_pkg, _vision_pkg, _audio_pkg = _ROCM_TORCH_PKG_SPECS["rocm7.2"]
else:
_torch_pkg, _vision_pkg, _audio_pkg = _ROCM_TORCH_PKG_SPECS.get(
tag, _ROCM_TORCH_PKG_SPECS["_default"]
)
pip_install(
f"ROCm torch ({tag})",
"--force-reinstall",
Expand Down Expand Up @@ -1420,6 +1470,23 @@ def _infer_no_torch() -> bool:
# GPU detection. Values: "cuda", "rocm", or "cpu". Empty means unknown
# (standalone `unsloth studio update` runs, where we re-detect normally).
_TORCH_BACKEND: str = os.environ.get("UNSLOTH_TORCH_BACKEND", "").lower()
# When install.sh did not run (standalone `unsloth studio update`) but the caller
# pinned the wheel index explicitly, derive the backend from that override so the
# CUDA/ROCm repair helpers honour it instead of re-probing the GPU and possibly
# reinstalling a different family. Classify on the final URL/family segment,
# mirroring install.sh's UNSLOTH_TORCH_BACKEND case.
if not _TORCH_BACKEND:
_idx_override = (
os.environ.get("UNSLOTH_TORCH_INDEX_URL", "").strip()
or os.environ.get("UNSLOTH_TORCH_INDEX_FAMILY", "").strip()
)
_idx_leaf = _idx_override.rstrip("/").rsplit("/", 1)[-1].lower()
if _idx_leaf.startswith(("rocm", "gfx")):
_TORCH_BACKEND = "rocm"
Comment on lines +2097 to +2098

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor pinned ROCm family in Studio repair

The new override handling classifies UNSLOTH_TORCH_INDEX_FAMILY=rocm6.4 or a full .../rocm6.4 URL only as _TORCH_BACKEND = "rocm"; it does not pass the requested family/index into _ensure_rocm_torch(), which still computes its install URL from the detected host ROCm version. In standalone unsloth studio update on an AMD Linux host with CPU torch, pinning rocm6.4 on a ROCm 7.2 machine will therefore reinstall from rocm7.2 instead of the requested family/mirror. Please make the ROCm repair path consume the explicit override, not just the backend classification.

Useful? React with 👍 / 👎.

Comment on lines +2097 to +2098

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the pinned ROCm index in the Python repair path

When unsloth studio update (or the post-install Python stack step) is run with UNSLOTH_TORCH_INDEX_FAMILY=rocm6.4 or a ROCm UNSLOTH_TORCH_INDEX_URL, this block only collapses the override to _TORCH_BACKEND = "rocm". _ensure_rocm_torch() still re-detects the host ROCm version and installs from f"{_PYTORCH_WHL_BASE}/{tag}" (and can still take the Strix per-gfx override), so a CPU/CUDA torch repair ignores the exact pinned family/URL that the shell path just promised to honor. This breaks the explicit override contract for standalone Studio updates and for installs where the Python repair step runs after a pinned ROCm selection.

Useful? React with 👍 / 👎.

elif _idx_leaf == "cpu":
_TORCH_BACKEND = "cpu"
Comment on lines +2099 to +2100

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add a CPU repair path for explicit CPU pins

When standalone update runs with UNSLOTH_TORCH_INDEX_FAMILY=cpu or a URL ending in /cpu, this sets _TORCH_BACKEND to cpu; both repair helpers then treat that as a skip signal (_ensure_cuda_torch only runs for ""/cuda, and _ensure_rocm_torch returns for cuda/cpu). There is no CPU reinstall path, so an existing CUDA/ROCm torch remains in place even though the explicit CPU pin is supposed to be authoritative.

Useful? React with 👍 / 👎.

elif _idx_leaf.startswith("cu"):
_TORCH_BACKEND = "cuda"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor CUDA overrides without requiring visible hardware

For a standalone unsloth studio update in the headless/container case this change classifies UNSLOTH_TORCH_INDEX_FAMILY=cu128 as CUDA, but _ensure_cuda_torch() still returns before using _detect_cuda_torch_index_url() unless _has_usable_nvidia_gpu() succeeds. That means an existing ROCm-poisoned venv on a GPU-less builder is left as ROCm even though the caller explicitly pinned a CUDA wheel family, so the new Python-side override does not actually cover the no-visible-GPU scenario it was added for.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use the CUDA leaf helper when deriving the backend

When studio update is run with a full override URL whose last segment is current, this startswith("cu") check classifies the unknown mirror as CUDA even though the new _is_cuda_family_leaf() helper intentionally rejects current/custom. That sets _TORCH_BACKEND="cuda"; on AMD hosts _ensure_rocm_torch() then returns before probing, so an existing CPU or wrong torch build is not repaired despite the override. Use the same ^cu[0-9] helper here.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. The _TORCH_BACKEND derivation now uses _is_cuda_family_leaf (^cu[0-9]), so current/custom fall through to backend "" and the GPU is probed. Also aligned install.sh to brand cuda only on cu[0-9]* (unset otherwise). Added tests.



def _torch_step_label(suffix: str) -> str:
Expand Down
44 changes: 44 additions & 0 deletions tests/sh/test_get_torch_index_url.sh
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,50 @@ _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"

# 46) ALL trailing slashes are stripped from a URL override (not just one).
_result=$(UNSLOTH_TORCH_INDEX_URL="https://mirror.example.com/whl/cu128///" run_func "none")
assert_eq "url override double slash stripped" "https://mirror.example.com/whl/cu128" "$_result"

# 47) Leading and trailing slashes stripped from a family override.
_result=$(UNSLOTH_TORCH_INDEX_FAMILY="//cu128//" run_func "none")
assert_eq "family override slashes stripped" "https://download.pytorch.org/whl/cu128" "$_result"

rm -f "$_FUNC_FILE"
rm -rf "$_FAKE_SMI_DIR"
rm -rf "$_TOOLS_DIR"
Expand Down
22 changes: 21 additions & 1 deletion tests/studio/install/test_cuda_repair.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,19 @@ def _run_cuda_repair(
rocm_marker = False,
smi_path = "/usr/bin/nvidia-smi",
cvd = None,
index_family = None,
):
"""Invoke _ensure_cuda_torch under a fully mocked host; return the pip mock.

cvd controls CUDA_VISIBLE_DEVICES: None removes it from the env, any string sets it."""
cvd controls CUDA_VISIBLE_DEVICES: None removes it from the env, any string sets it.
index_family sets UNSLOTH_TORCH_INDEX_FAMILY (the explicit wheel-index pin)."""
env = {}
if rocm_marker:
env["UNSLOTH_ROCM_TORCH_INSTALLED"] = "1"
if cvd is not None:
env["CUDA_VISIBLE_DEVICES"] = cvd
if index_family is not None:
env["UNSLOTH_TORCH_INDEX_FAMILY"] = index_family

def _which(name, *a, **k):
if name == "nvidia-smi":
Expand All @@ -99,6 +103,9 @@ def _which(name, *a, **k):
stack_mod.os.environ.pop("UNSLOTH_ROCM_TORCH_INSTALLED", None)
if cvd is None:
stack_mod.os.environ.pop("CUDA_VISIBLE_DEVICES", None)
if index_family is None:
stack_mod.os.environ.pop("UNSLOTH_TORCH_INDEX_FAMILY", None)
stack_mod.os.environ.pop("UNSLOTH_TORCH_INDEX_URL", None)
_ensure_cuda_torch()
return mock_pip

Expand Down Expand Up @@ -128,6 +135,19 @@ def test_rocm_in_version_string_triggers_repair(self):
mock_pip = _run_cuda_repair(torch_state = "hip")
assert mock_pip.call_count == 1

def test_no_gpu_but_explicit_cuda_pin_repairs(self):
# Headless / container / CI cross-install: an explicit cu* index pin
# commits to CUDA wheels even though no NVIDIA GPU is visible here, so a
# ROCm-poisoned venv is still repaired (to the pinned family).
mock_pip = _run_cuda_repair(
nvidia = False,
backend = "cuda",
index_family = "cu128",
torch_state = "hip",
)
assert mock_pip.call_count == 1
assert "cu128" in _index_url(mock_pip)


# No-op cases.

Expand Down
30 changes: 30 additions & 0 deletions tests/studio/install/test_rocm_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -680,6 +680,36 @@ def test_rocm_72_selects_72_tag(self, mock_ver, mock_gpu, mock_nvidia, mock_pip)
torch_call = mock_pip.call_args_list[0]
assert "rocm7.2" in str(torch_call)

@patch.object(stack_mod, "IS_WINDOWS", False)
@patch.object(stack_mod, "pip_install_try", return_value = True)
@patch.object(stack_mod, "pip_install")
@patch.object(stack_mod, "_has_usable_nvidia_gpu", return_value = False)
@patch.object(stack_mod, "_has_rocm_gpu", return_value = True)
@patch.object(stack_mod, "_detect_rocm_version", return_value = (6, 4))
def test_explicit_gfx_index_honored_and_skips_strix_reroute(
self, mock_ver, mock_gpu, mock_nvidia, mock_pip, mock_pip_try
):
"""An explicit gfx wheel-index pin is authoritative: install from it verbatim
with torch 2.11, and never re-probe gfx codes to second-guess it (host ROCm 6.4
would otherwise pick the rocm6.4 wheel / trigger the Strix re-route)."""
mock_probe = MagicMock()
mock_probe.returncode = 0
mock_probe.stdout = b"\n" # cpu torch -> reinstall
env = {"UNSLOTH_TORCH_INDEX_URL": "https://repo.amd.com/rocm/whl/gfx1151"}
with patch.dict(stack_mod.os.environ, env, clear = False):
stack_mod.os.environ.pop("UNSLOTH_TORCH_INDEX_FAMILY", None)
with patch("os.path.isdir", return_value = True):
with patch("subprocess.run", return_value = mock_probe):
# Would raise if the Strix block ran (it is skipped on an explicit pin).
with patch.object(
stack_mod, "_detect_amd_gfx_codes", side_effect = AssertionError
):
_ensure_rocm_torch()
assert mock_pip.call_count == 1
torch_call = str(mock_pip.call_args_list[0])
assert "gfx1151" in torch_call
assert "torch>=2.11.0,<2.12.0" in torch_call

@patch.object(stack_mod, "IS_WINDOWS", False)
@patch.object(stack_mod, "pip_install_try", return_value = True)
@patch.object(stack_mod, "pip_install")
Expand Down
Loading