Compare commits

...
Sign in to create a new pull request.

17 commits

Author SHA1 Message Date
pre-commit-ci[bot]
534ba3bfa2 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-07-27 23:19:38 +00:00
Daniel Han
10bc8aa538 Keep probing for nvidia-smi after an unusable one on PATH
_has_usable_nvidia_gpu gated its Windows fixed-location fallback on the
PATH lookup missing, not on the GPU check failing. A stale or driverless
nvidia-smi exits non-zero listing nothing, so the search stopped there
and the function reported no NVIDIA GPU even with a working driver
binary under NVSMI or System32.

That answer routes a mixed AMD iGPU plus NVIDIA dGPU Windows host into
_ensure_rocm_torch() and replaces its CUDA stack with ROCm wheels.

install.ps1 and studio/setup.ps1 already do the right thing: both call
Test-NvidiaSmiHasGpu on the PATH result and fall through to the two fixed
paths when it fails, with the same reasoning recorded at install.ps1:1708
("a stale/driverless nvidia-smi can exit 0 while listing no GPU"). This
brings the Python helper to the same rule: collect the candidates, then
take the first that lists a GPU.

Reproduced with real stub executables through the real subprocess call,
before and after:

    PATH exe   fixed-location exe   before   after
    absent     working              True     True
    stale      working              False    True
    working    -                    True     True
    none       none                 False    False

Only the stale row changes. An AMD-only host with a leftover nvidia-smi
still gets False, so it is not denied the ROCm wheels.

tests/studio/install/test_nvidia_smi_candidate_probing.py pins all four
rows plus the CUDA_VISIBLE_DEVICES cases: 1 failed / 8 passed before,
9 passed after.
2026-07-27 23:18:39 +00:00
pre-commit-ci[bot]
267601e52a [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-07-27 13:44:27 +00:00
Daniel Han
ed415f5b83 Merge origin/main into fix/windows-amd-rocm-install
main grew its own Windows AMD ROCm install path while this branch sat open,
and it is a strict superset of what this branch built. main routes Windows
AMD torch through the per-arch pip indexes at repo.amd.com/rocm/whl/<family>/
(install.ps1 $archFamilyMap, setup.ps1 $ROCmIndexUrl, install_python_stack.py
_windows_rocm_index_url), which serve cp310 through cp313 win_amd64 wheels up
to torch 2.11.0+rocm7.13.0 and resolve the ROCm runtime (rocm, rocm-sdk-core,
rocm-sdk-libraries-<gfx>) from the same index.

This branch's route pinned torch 2.9.x from repo.radeon.com/rocm/windows/,
cp312 only, and hand-installed four SDK artefacts because
rocm-sdk-libraries-custom is absent from PyPI. Against main that is a
regression on three counts: it locks AMD hosts to Python 3.12, it installs
exactly the torch <2.11 wheels main blocks on gfx115x/gfx120x for the
torch._C._grouped_mm null-pointer bug (TheRock #5284, #3284), and it skips
bitsandbytes on Windows, which main now installs via
_install_bnb_windows_rocm(). So every conflicting hunk in install.ps1,
studio/setup.ps1 and studio/install_python_stack.py resolves to main.

One hunk from this branch is not superseded and is kept: the Windows
nvidia-smi.exe fallback paths in _has_usable_nvidia_gpu(). main only probes
PATH there, while install.ps1 and setup.ps1 both probe
%ProgramFiles%\NVIDIA Corporation\NVSMI and %SystemRoot%\System32 as well.
That gap matters on an AMD iGPU plus NVIDIA dGPU laptop whose PATH misses
nvidia-smi: the host reads as NVIDIA-free and gets routed to the ROCm wheels.
Grafted onto main's function, below its CUDA_VISIBLE_DEVICES guard.
2026-07-27 13:39:28 +00:00
Daniel Han
ed41a25439 Trim verbose review-fix comments
Shorten multi-line comments added during review iterations to 1-2 lines
each. Remove redundant explanations where the code is self-evident.
2026-04-12 22:52:35 +00:00
Daniel Han
e00eeffe2c Move $_NoTorch initialization before stale-venv detection in setup.ps1
$_NoTorch was defined at line 1672 but read at lines 1521/1533 in the
stale-venv detection block. In PowerShell, reading an undefined variable
returns $null, and -not $null evaluates to $true, so the $_NoTorch
guards added in the previous commit were silently no-ops.

Fix: move the $env:UNSLOTH_NO_TORCH check to before the stale-venv
block so --no-torch mode is correctly respected in the torch tag
comparison and ROCm repair decision.
2026-04-12 22:27:53 +00:00
Daniel Han
f542c231dc Fix setup.ps1 version fast-path skipping ROCm repair on up-to-date installs
When an existing Windows AMD user runs `unsloth studio update` and the
unsloth package is already at the latest version, the version fast-path
set $SkipPythonDeps = $true, which bypassed the entire ROCm install
block. This left the user permanently stuck on CPU-only torch even
though the stale-venv check correctly identified the cpu->rocm mismatch.

Fix: track $_NeedRocmRepair flag from the stale-venv check and use it
to override $SkipPythonDeps so the ROCm wheel download still runs.

Also gate the stale-venv expectedTorchTag on $_NoTorch so --no-torch
users on AMD don't see a misleading "will repair to ROCm" message when
the actual $CuTag is "cpu".
2026-04-12 22:09:27 +00:00
Daniel Han
d41d593d15 Fix setup.ps1 no-torch crash, idempotency, stale-venv, and Python version guard
- setup.ps1: respect UNSLOTH_NO_TORCH when choosing $CuTag so AMD +
  --no-torch users on Python 3.13 do not crash on the 3.12 version check.

- setup.ps1: add torch.version.hip idempotency probe before the ROCm
  wheel download block. Without this, fresh installs (install.ps1 ->
  setup.ps1) and every studio update re-downloaded 2.1-3.9 GB of wheels
  even when ROCm torch was already installed.

- setup.ps1: skip venv rebuild for cpu->rocm transitions on AMD hosts.
  The old behavior deleted the venv then exited with "Run install.ps1
  first", breaking the upgrade path for existing CPU-only AMD users.
  Now keeps the venv and lets the ROCm install block repair torch
  in-place.

- setup.ps1: capture stderr (2>&1) in the non-verbose ROCm pip install
  so failure diagnostics appear in the error banner.

- install_python_stack.py: change the Python 3.12 version check from
  warn-and-return to sys.exit(1). The old behavior continued to
  completion and returned exit code 0 without installing ROCm torch.

- install_python_stack.py: reorder _ensure_rocm_torch_windows() to put
  the cheap torch.version.hip probe before the expensive GPU detection
  subprocess calls, saving ~1-4s per call when ROCm is already installed.

- install_python_stack.py: cache _has_rocm_gpu_windows() result so the
  PowerShell/WMI subprocess is spawned at most once per process.
2026-04-12 21:45:57 +00:00
pre-commit-ci[bot]
b91f47c3b7 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-04-12 21:23:03 +00:00
Daniel Han
7d722faedc Fix Windows ROCm install: idempotency guard, nvidia-smi fallback, Python version gates
- Add torch.version.hip probe to _ensure_rocm_torch_windows() so steps 2b
  and 13 skip the 2.1-3.9 GB reinstall when ROCm torch is already healthy.
  Mirrors the existing Linux idempotency guard.

- Add Windows fallback paths for nvidia-smi.exe in _has_usable_nvidia_gpu()
  (NVSMI dir + System32) matching the PowerShell scripts, so NVIDIA wins on
  mixed GPU systems even when nvidia-smi is not on PATH.

- install.ps1: only narrow Python to 3.12 when AMD + torch (not --no-torch);
  set $PythonVersion = "3.12" so the existing winget auto-install flow works
  on clean AMD machines instead of hard-stopping.

- install.ps1: handle empty $venvPyVer as an error in the AMD version check
  instead of silently continuing into a guaranteed pip failure.

- setup.ps1: upgrade Python 3.12 version check from warning to hard error
  (exit 1), preventing multi-GB downloads that pip will reject on non-3.12.

- install_python_stack.py: use os.environ["ProgramFiles"] for ROCm root
  fallback instead of hardcoded C:\Program Files.

- Remove unused _HIP_SDK_DOWNLOAD_URL constant.
2026-04-12 21:22:45 +00:00
Daniel Han
f338c49365 Merge remote-tracking branch 'origin/main' into pr-4958-head 2026-04-12 20:53:43 +00:00
Daniel Han
7c6086e408 Install ROCm SDK + torch in a single pip call on Windows AMD
torch's metadata on repo.radeon.com declares
`Requires-Dist: rocm[libraries]==<ver>`, which cascades to
`rocm-sdk-libraries-custom==<ver>`. That package does not exist on
PyPI at all, and rocm/rocm-sdk-core/rocm-sdk-devel on PyPI are pinned
to 0.1.0 (wrong version). AMD's docs show a two-step install but when
pip is invoked twice with --force-reinstall on the torch step, the
resolver re-evaluates transitive deps, looks for rocm-sdk-libraries-custom
on PyPI, fails, and aborts.

Fix: pass all 7 Radeon artefacts (4 SDK + 3 torch) in a single
pip invocation across install.ps1, studio/setup.ps1, and
studio/install_python_stack.py so pip sees the full dep graph upfront
and never falls back to PyPI for the rocm-sdk-* chain.

Also:
- install.ps1 now bootstraps pip/setuptools/wheel into the uv venv
  before the ROCm install, because rocm-<ver>.tar.gz is a Python
  source distribution pip needs setuptools as its build backend for.
- Size estimates are now release-dependent: 7.2.1 is ~2.1 GB, 7.1.1
  is ~3.9 GB (its sdk_devel wheel alone is 2.4 GB).
- setup.ps1 Fast-Installs pip/setuptools/wheel for the same sdist
  build-isolation reason.
2026-04-11 01:49:05 +00:00
pre-commit-ci[bot]
a128c82b7d [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-04-11 01:14:36 +00:00
Daniel Han
2dce261f4a Install ROCm SDK wheels and relax HIP SDK prerequisite on Windows
The previous Windows AMD install was incomplete in two material ways,
both surfaced while verifying whether PyTorch has upstream Windows ROCm
wheels (it does not -- pytorch.org's get-started page states "ROCm is
not available on Windows" and every wheel under download.pytorch.org
/whl/rocm7.x is manylinux_2_28_x86_64 only; upstream work is tracked in
pytorch/pytorch#159520 and targeted for a future release).

repo.radeon.com therefore remains the only source for Windows ROCm
torch until that RFC lands, and AMD's install docs at
rocm.docs.amd.com/projects/radeon-ryzen/.../install-pytorch.html
document a two-step pip procedure that we were only implementing half
of. Fixing both bugs here so the PR actually produces a working
torch.version.hip import on a fresh Windows host.

Bug 1: missing ROCm SDK wheels install step

AMD's procedure first installs rocm_sdk_core, rocm_sdk_devel,
rocm_sdk_libraries_custom, and rocm-<ver>.tar.gz (about 1.4 GB
total). These wheels ship the ROCm runtime libraries that torch links
against at import time. Without them `import torch` fails with missing
DLL errors even when the torch wheels themselves are installed. Both
7.1.1 and 7.2.1 need this step; 7.1.1 stamps its SDK wheels with the
`0.1.dev0` version string while 7.2.1 uses `7.2.1`.

This adds the 4 SDK URLs per release to the wheel map and installs
them as Step 1 ahead of the existing torch install (Step 2). Both
steps are passed in a single pip call each so pip's dep resolver does
not reset torch between wheels (matches AMD's troubleshooting guidance
for the same failure mode).

Bug 2: HIP SDK was a hard prerequisite, should be optional

install.ps1 / setup.ps1 / install_python_stack.py all errored out when
$env:HIP_PATH was absent, pointing users at the HIP SDK download page.
But the HIP SDK developer toolkit at C:\Program Files\AMD\ROCm\ is for
people compiling HIP kernels, not for running PyTorch. AMD's docs list
only (a) the AMD graphics driver 26.2.2+ and (b) Python 3.12 as
prerequisites. Gating on HIP_PATH was blocking the exact audience that
#4280 is about -- regular Radeon users running Unsloth.

HIP_PATH is now an optional version hint. When present and valid we
use it to select the matching ROCm release; when absent or unsupported
(e.g. HIP 6.4) we fall back to the newest stable release
(_DEFAULT_WINDOWS_ROCM_VERSION = (7, 2)) and print a visible note
pointing at the graphics driver download page. The HIP SDK install
prompts have been removed from all three files.

Use pip (not uv) for the Radeon wheels

Both the SDK and torch install steps call `python -m pip install`
directly via the new force_pip=True path added in the previous commit.
AMD's documented procedure uses pip, uv has known wheel-corruption
issues on similar large ROCm wheels (unslothai/unsloth#4966 for
bitsandbytes), and pip is the combination AMD validates. This matches
the fix applied to bitsandbytes on Linux ROCm.
2026-04-11 01:14:23 +00:00
Daniel Han
f03d6dbe84 Correct repo.radeon.com layout comment on _ROCM_WINDOWS_TORCH_WHEELS
Earlier comment claimed rocm-rel-6.4.4 uses a "nested layout and alpha
version strings". Verified against repo.radeon.com and the reality is
more specific: 6.4.4 exposes wheels through a PEP 503 simple index
(torch/, torchvision/, torchaudio/ sub-indexes that link back to wheels
at the top of the release dir), and the wheels carry alpha plus opaque
git-hash build tags like torch-2.8.0a0+gitfc14c65-cp312-cp312-win_amd64.whl
which change whenever AMD rebuilds, so they cannot be hardcoded.

Also documents that rocm-rel-7.2/ (January) is a distinct release from
rocm-rel-7.2.1/ (March) and that the map intentionally routes HIP SDK
7.2.x requests to the newer 7.2.1 wheels because torch bundles its own
ROCm runtime.

Comment-only change; no behavioural impact.
2026-04-11 00:54:10 +00:00
Daniel Han
c32ff8c650 Force pip (skip uv) for bitsandbytes ROCm install
Aligns install_python_stack.py with unslothai/unsloth#4966. uv's installer
corrupts the bitsandbytes continuous-release_main wheel on ROCm even when
the command reports success, leaving the venv with a broken bnb import at
runtime. The install.sh fix in #4966 switched to python -m pip install for
the Linux shell installer; this commit does the same for the Python
installer that runs from unsloth studio update.

Changes
- pip_install_try and pip_install gain a force_pip: bool = False parameter.
  When True, the uv attempt is skipped entirely and the call goes straight
  to python -m pip install via the existing pip_cmd builder.
- _ensure_rocm_torch passes force_pip=True for both the bnb pre-release URL
  install AND the PyPI fallback. Both code paths install bitsandbytes and
  both are affected by the uv corruption bug, so keeping them consistent
  matches the gemini-code-assist review comment on #4966 (the fallback in
  #4966 itself is still uv-backed).
- Non-bnb calls (torch install, base packages, extras, etc.) keep the
  default force_pip=False and continue to prefer uv for speed.
2026-04-11 00:02:02 +00:00
Daniel Han
c6a9585659 Install ROCm PyTorch on Windows AMD via repo.radeon.com
AMD support on Windows fell back to CPU-only torch because install.ps1,
studio/setup.ps1, and the Windows branch of studio/install_python_stack.py
only detected nvidia-smi. This fixes unslothai/unsloth#4280 by teaching the
installers to pick ROCm torch from repo.radeon.com when an AMD GPU plus
HIP SDK 7.1.x or 7.2.x is present.

Changes
- install.ps1: Get-HipSdkVersion + Get-RocmWheelUrls helpers, AMD GPU
  detection (WMI Win32_VideoController), Python 3.12 enforcement on the
  AMD path (Radeon wheels are cp312 only), and a dedicated AMD torch
  install branch that skips bitsandbytes.
- studio/setup.ps1: mirrors the install.ps1 helpers (self-contained copy),
  adds an AMD branch to the torch install flow, and teaches the stale-venv
  check to match both +rocm and +rocmsdk suffixes so ROCm minor updates
  do not trigger spurious venv rebuilds.
- studio/install_python_stack.py: new _ROCM_WINDOWS_TORCH_WHEELS mapping,
  _detect_rocm_version_windows (HIP_PATH primary + ProgramFiles scan
  fallback, uses ntpath so path parsing works on Linux test runners),
  _has_rocm_gpu_windows via PowerShell WMI, and a new
  _ensure_rocm_torch_windows helper that respects the NVIDIA-wins rule.
  The bnb install section returns early on Windows because there is no
  Windows ROCm wheel (bitsandbytes-foundation/bitsandbytes#1844).

NVIDIA, CPU-only, Linux AMD, and macOS paths are untouched. On Windows
NVIDIA+AMD mixed hosts NVIDIA takes precedence, matching install.sh
behaviour.
2026-04-10 15:14:11 +00:00
2 changed files with 161 additions and 5 deletions

View file

@ -1162,6 +1162,11 @@ def _has_usable_nvidia_gpu() -> bool:
timeout, driver initialisation race). If either probe confirms an
NVIDIA GPU the function returns True so _has_rocm_gpu() is blocked.
On Windows nvidia-smi.exe is not always on PATH, so fall back to the two
fixed driver locations install.ps1 / setup.ps1 already probe. Without this
an AMD iGPU + NVIDIA dGPU host whose PATH misses nvidia-smi looks
NVIDIA-free here and gets routed to the ROCm wheels.
CUDA_VISIBLE_DEVICES set to "" or "-1" hides every NVIDIA device (mixed
AMD+NVIDIA hosts steering work to the AMD card); neither probe honours
that env var, so check it first and report the GPU as not usable. Unset
@ -1170,8 +1175,8 @@ def _has_usable_nvidia_gpu() -> bool:
cvd = os.environ.get("CUDA_VISIBLE_DEVICES")
if cvd is not None and cvd.strip() in ("", "-1"):
return False
exe = shutil.which("nvidia-smi")
if exe:
def _lists_a_gpu(exe: str) -> bool:
try:
result = subprocess.run(
[exe, "-L"],
@ -1180,10 +1185,42 @@ def _has_usable_nvidia_gpu() -> bool:
text = True,
timeout = 10,
)
if result.returncode == 0 and "GPU " in result.stdout:
return True
except Exception:
pass
return False
return result.returncode == 0 and "GPU " in result.stdout
# Try every candidate until one lists a GPU, rather than committing to the
# first executable found. A stale or driverless nvidia-smi on PATH exits
# non-zero listing nothing; stopping there would report the host as
# NVIDIA-free and route it into _ensure_rocm_torch() even though a working
# driver binary sits at a fixed location. install.ps1 and setup.ps1 both
# gate their fallback on the GPU check failing, not on the PATH lookup
# missing, so mirror that.
candidates = []
_path_exe = shutil.which("nvidia-smi")
if _path_exe:
candidates.append(_path_exe)
if IS_WINDOWS:
candidates.extend(
(
os.path.join(
os.environ.get("ProgramFiles", r"C:\Program Files"),
"NVIDIA Corporation",
"NVSMI",
"nvidia-smi.exe",
),
os.path.join(
os.environ.get("SystemRoot", r"C:\Windows"),
"System32",
"nvidia-smi.exe",
),
)
)
for _candidate in candidates:
if _candidate != _path_exe and not os.path.isfile(_candidate):
continue
if _lists_a_gpu(_candidate):
return True
# Fallback: the NVIDIA driver exposes one subdirectory per GPU under
# /proc/driver/nvidia/gpus/ on Linux regardless of nvidia-smi state.
if sys.platform != "win32":

View file

@ -0,0 +1,119 @@
# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved.
"""_has_usable_nvidia_gpu must keep probing after an unusable nvidia-smi.
A stale or driverless nvidia-smi on PATH exits non-zero and lists no GPU.
Treating the first executable found as the answer reports a mixed AMD+NVIDIA
Windows host as NVIDIA-free, which routes it into _ensure_rocm_torch() and
replaces a working CUDA stack with ROCm wheels. install.ps1 and setup.ps1 both
gate their fixed-location fallback on the GPU check failing rather than on the
PATH lookup missing; this pins the Python helper to the same rule.
The stubs are real executables run through the real subprocess call, so the
test exercises the actual control flow rather than a mocked return value.
"""
import importlib.util
import os
import pathlib
import sys
import types
import pytest
_REPO_ROOT = pathlib.Path(__file__).resolve().parents[3]
_STUDIO = _REPO_ROOT / "studio"
_SRC = _STUDIO / "install_python_stack.py"
_STALE = 'echo "No devices were found"; exit 9'
_WORKING = 'echo "GPU 0: NVIDIA H100 (UUID: GPU-abc)"; exit 0'
def _load_module():
# install_python_stack imports backend.utils.wheel_utils, which resolves
# only with studio/ on sys.path. That is how the installer invokes it.
if str(_STUDIO) not in sys.path:
sys.path.insert(0, str(_STUDIO))
spec = importlib.util.spec_from_file_location("_ips_probe_under_test", _SRC)
module = importlib.util.module_from_spec(spec)
sys.modules["_ips_probe_under_test"] = module
spec.loader.exec_module(module)
return module
def _write_stub(path: pathlib.Path, body: str) -> None:
path.parent.mkdir(parents = True, exist_ok = True)
# Not /usr/bin/env: PATH is narrowed to the stub directory below, so env
# would not find an interpreter.
path.write_text("#!/bin/bash\n" + body + "\n")
path.chmod(0o755)
@pytest.fixture
def probe(tmp_path, monkeypatch):
"""Run _has_usable_nvidia_gpu as if on Windows, with stubbed nvidia-smi."""
def _run(
path_smi: str | None,
fixed_smi: str | None,
cuda_visible_devices: str | None = None,
) -> bool:
path_dir = tmp_path / "pathbin"
path_dir.mkdir(exist_ok = True)
if path_smi is not None:
_write_stub(path_dir / "nvidia-smi", path_smi)
program_files = tmp_path / "ProgramFiles"
if fixed_smi is not None:
_write_stub(
program_files / "NVIDIA Corporation" / "NVSMI" / "nvidia-smi.exe",
fixed_smi,
)
monkeypatch.setenv("PATH", str(path_dir))
monkeypatch.setenv("ProgramFiles", str(program_files))
monkeypatch.setenv("SystemRoot", str(tmp_path / "Windows"))
if cuda_visible_devices is None:
monkeypatch.delenv("CUDA_VISIBLE_DEVICES", raising = False)
else:
monkeypatch.setenv("CUDA_VISIBLE_DEVICES", cuda_visible_devices)
module = _load_module()
monkeypatch.setattr(module, "IS_WINDOWS", True)
# A real NVIDIA host has /proc/driver/nvidia/gpus, and the helper's
# Linux-only fallback would then answer True for every case and mask
# what the Windows path did. Present as win32 to isolate it.
monkeypatch.setattr(module, "sys", types.SimpleNamespace(platform = "win32"))
return module._has_usable_nvidia_gpu()
return _run
def test_stale_path_nvidia_smi_still_reaches_the_fixed_locations(probe):
# The regression: a driverless nvidia-smi on PATH used to end the search.
assert probe(_STALE, _WORKING) is True
def test_absent_path_nvidia_smi_reaches_the_fixed_locations(probe):
assert probe(None, _WORKING) is True
def test_working_path_nvidia_smi_is_enough(probe):
assert probe(_WORKING, None) is True
def test_no_nvidia_smi_anywhere_reports_no_gpu(probe):
assert probe(None, None) is False
def test_stale_everywhere_reports_no_gpu(probe):
# Every candidate answering "no GPU" must stay False, or an AMD-only host
# with a leftover nvidia-smi would be denied the ROCm wheels.
assert probe(_STALE, _STALE) is False
@pytest.mark.parametrize("hidden", ["", "-1", " "])
def test_cuda_visible_devices_hidden_wins_over_a_working_probe(probe, hidden):
assert probe(_WORKING, _WORKING, cuda_visible_devices = hidden) is False
def test_cuda_visible_devices_listing_a_device_does_not_block_detection(probe):
assert probe(_WORKING, None, cuda_visible_devices = "0") is True