* Reduce and tighten comments and docstrings in tests Shorten verbose comments and docstrings across the test suite without changing any test logic. Remove narration that restates the next line, collapse long module and test docstrings to a single line, and drop banner separators. Keep regression context (issue and PR references, run ids), skip reasons, mocking and timing rationale, license headers, lint and type directives, and commented-out code. Comments and docstrings only: an AST signature check confirms no code, assertions, or string literals changed, and the suite byte-compiles cleanly. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
448 lines
18 KiB
Python
448 lines
18 KiB
Python
"""GPU-detection follow-ups to PR 6174: NVIDIA precedence + /proc/driver/nvidia/gpus fallback ported to install_llama_prebuilt.py and setup.sh. Mocks/source-level only, no GPU."""
|
|
|
|
import importlib.util
|
|
import sys
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
|
|
PACKAGE_ROOT = Path(__file__).resolve().parents[3]
|
|
|
|
# Load studio/install_llama_prebuilt.py the same way the sibling suite does.
|
|
_MODULE_PATH = PACKAGE_ROOT / "studio" / "install_llama_prebuilt.py"
|
|
_SPEC = importlib.util.spec_from_file_location(
|
|
"studio_install_llama_prebuilt_followups", _MODULE_PATH
|
|
)
|
|
assert _SPEC is not None and _SPEC.loader is not None
|
|
prebuilt_mod = importlib.util.module_from_spec(_SPEC)
|
|
sys.modules[_SPEC.name] = prebuilt_mod
|
|
_SPEC.loader.exec_module(prebuilt_mod)
|
|
|
|
detect_host = prebuilt_mod.detect_host
|
|
_apply_host_overrides = prebuilt_mod._apply_host_overrides
|
|
|
|
SETUP_SH = PACKAGE_ROOT / "studio" / "setup.sh"
|
|
|
|
|
|
def _make_run_capture(rocminfo_stdout: str = ""):
|
|
"""Fake run_capture: rocminfo returns rocminfo_stdout, everything else empty."""
|
|
|
|
def _run_capture(cmd, *args, **kwargs):
|
|
exe = str(cmd[0]) if cmd else ""
|
|
result = MagicMock()
|
|
if exe.endswith("rocminfo"):
|
|
result.returncode = 0
|
|
result.stdout = rocminfo_stdout
|
|
else:
|
|
result.returncode = 1
|
|
result.stdout = ""
|
|
result.stderr = ""
|
|
return result
|
|
|
|
return _run_capture
|
|
|
|
|
|
def _run_detect_host(
|
|
*,
|
|
machine: str = "x86_64",
|
|
system: str = "Linux",
|
|
which_map: dict | None = None,
|
|
proc_dir_entries: list | None = None,
|
|
rocminfo_stdout: str = "",
|
|
env: dict | None = None,
|
|
):
|
|
"""Drive detect_host() against a fully synthetic host."""
|
|
which_map = which_map or {}
|
|
proc_dir_entries = proc_dir_entries if proc_dir_entries is not None else []
|
|
|
|
real_isdir = prebuilt_mod.os.path.isdir
|
|
real_listdir = prebuilt_mod.os.listdir
|
|
proc_path = "/proc/driver/nvidia/gpus"
|
|
|
|
def fake_isdir(p):
|
|
if str(p) == proc_path:
|
|
return bool(proc_dir_entries)
|
|
return real_isdir(p)
|
|
|
|
def fake_listdir(p):
|
|
if str(p) == proc_path:
|
|
if not proc_dir_entries:
|
|
raise OSError("no such dir")
|
|
return list(proc_dir_entries)
|
|
return real_listdir(p)
|
|
|
|
patches = [
|
|
patch.object(prebuilt_mod.platform, "system", return_value = system),
|
|
patch.object(prebuilt_mod.platform, "machine", return_value = machine),
|
|
patch.object(prebuilt_mod.platform, "mac_ver", return_value = ("", ("", "", ""), "")),
|
|
patch.object(prebuilt_mod.shutil, "which", side_effect = lambda n: which_map.get(n)),
|
|
patch.object(prebuilt_mod, "run_capture", side_effect = _make_run_capture(rocminfo_stdout)),
|
|
patch.object(prebuilt_mod.os.path, "isdir", side_effect = fake_isdir),
|
|
patch.object(prebuilt_mod.os, "listdir", side_effect = fake_listdir),
|
|
patch.object(prebuilt_mod.os, "access", return_value = False),
|
|
patch.dict(prebuilt_mod.os.environ, env or {}, clear = False),
|
|
]
|
|
for p in patches:
|
|
p.start()
|
|
try:
|
|
# Don't let the host's CUDA_VISIBLE_DEVICES leak in unless the scenario sets it.
|
|
if env is None or "CUDA_VISIBLE_DEVICES" not in env:
|
|
prebuilt_mod.os.environ.pop("CUDA_VISIBLE_DEVICES", None)
|
|
return detect_host()
|
|
finally:
|
|
for p in patches:
|
|
p.stop()
|
|
|
|
|
|
# ── install_llama_prebuilt.detect_host(): /proc NVIDIA fallback ──────────────
|
|
|
|
|
|
class TestDetectHostProcFallback:
|
|
def test_proc_fallback_marks_physical_nvidia_when_smi_absent(self):
|
|
"""No nvidia-smi, but /proc/driver/nvidia/gpus is populated -> NVIDIA."""
|
|
host = _run_detect_host(
|
|
which_map = {}, # nvidia-smi resolves to None
|
|
proc_dir_entries = ["0000:01:00.0"],
|
|
)
|
|
assert host.has_physical_nvidia is True
|
|
|
|
def test_proc_fallback_has_usable_nvidia_when_devices_visible(self):
|
|
"""Default CUDA_VISIBLE_DEVICES (unset) -> visible tokens non-empty -> usable."""
|
|
host = _run_detect_host(
|
|
which_map = {},
|
|
proc_dir_entries = ["0000:01:00.0"],
|
|
)
|
|
assert host.has_usable_nvidia is True
|
|
|
|
def test_proc_fallback_not_usable_when_devices_hidden(self):
|
|
"""CUDA_VISIBLE_DEVICES='' hides all GPUs -> physical yes, usable no."""
|
|
host = _run_detect_host(
|
|
which_map = {},
|
|
proc_dir_entries = ["0000:01:00.0"],
|
|
env = {"CUDA_VISIBLE_DEVICES": ""},
|
|
)
|
|
assert host.has_physical_nvidia is True
|
|
assert host.has_usable_nvidia is False
|
|
|
|
def test_empty_proc_dir_does_not_mark_nvidia(self):
|
|
"""A driver dir that exists but is empty must not assert a GPU."""
|
|
host = _run_detect_host(which_map = {}, proc_dir_entries = [])
|
|
assert host.has_physical_nvidia is False
|
|
|
|
def test_proc_fallback_is_linux_only(self):
|
|
"""The /proc fallback must not run on Windows (path is Linux-only)."""
|
|
host = _run_detect_host(
|
|
system = "Windows",
|
|
machine = "amd64",
|
|
which_map = {},
|
|
proc_dir_entries = ["0000:01:00.0"],
|
|
)
|
|
assert host.has_physical_nvidia is False
|
|
|
|
|
|
# ── install_llama_prebuilt.detect_host(): NVIDIA precedence over ROCm ────────
|
|
|
|
|
|
class TestDetectHostNvidiaPrecedence:
|
|
def test_rocm_probe_skipped_when_proc_nvidia_present(self):
|
|
"""rocminfo reports gfx1100, but a proc-detected NVIDIA GPU wins."""
|
|
host = _run_detect_host(
|
|
which_map = {"rocminfo": "/usr/bin/rocminfo"},
|
|
proc_dir_entries = ["0000:01:00.0"],
|
|
rocminfo_stdout = " Name: gfx1100\n",
|
|
)
|
|
assert host.has_usable_nvidia is True
|
|
assert host.has_rocm is False
|
|
|
|
def test_rocm_detected_when_no_nvidia(self):
|
|
"""With no NVIDIA signal at all, rocminfo gfx1100 -> has_rocm True."""
|
|
host = _run_detect_host(
|
|
which_map = {"rocminfo": "/usr/bin/rocminfo"},
|
|
proc_dir_entries = [],
|
|
rocminfo_stdout = " Name: gfx1100\n",
|
|
)
|
|
assert host.has_usable_nvidia is False
|
|
assert host.has_rocm is True
|
|
|
|
|
|
# ── _apply_host_overrides: forwarded --rocm-gfx / --has-rocm still win ───────
|
|
|
|
|
|
class TestOverridesStillWin:
|
|
def test_forwarded_gfx_forces_rocm_on_non_nvidia_host(self):
|
|
host = _run_detect_host(which_map = {}, proc_dir_entries = [])
|
|
assert host.has_rocm is False
|
|
overridden = _apply_host_overrides(host, override_rocm_gfx = "gfx1100")
|
|
assert overridden.has_rocm is True
|
|
assert overridden.rocm_gfx_target == "gfx1100"
|
|
|
|
def test_override_has_rocm_forces_rocm(self):
|
|
host = _run_detect_host(which_map = {}, proc_dir_entries = [])
|
|
overridden = _apply_host_overrides(host, override_has_rocm = True)
|
|
assert overridden.has_rocm is True
|
|
|
|
def test_force_cpu_drops_nvidia_attributes(self):
|
|
host = _run_detect_host(which_map = {}, proc_dir_entries = ["0000:01:00.0"])
|
|
assert host.has_usable_nvidia is True
|
|
overridden = _apply_host_overrides(host, force_cpu = True)
|
|
assert overridden.has_usable_nvidia is False
|
|
assert overridden.has_physical_nvidia is False
|
|
assert overridden.has_rocm is False
|
|
|
|
|
|
# ── setup.sh source-level guarantees ────────────────────────────────────────
|
|
|
|
|
|
class TestSetupShHardening:
|
|
@pytest.fixture(scope = "class")
|
|
def setup_src(self) -> str:
|
|
return SETUP_SH.read_text(encoding = "utf-8")
|
|
|
|
def test_has_usable_nvidia_helper_exists(self, setup_src):
|
|
assert "_setup_has_usable_nvidia_gpu()" in setup_src
|
|
|
|
def test_helper_uses_proc_fallback(self, setup_src):
|
|
start = setup_src.find("_setup_has_usable_nvidia_gpu()")
|
|
end = setup_src.find("\n}", start)
|
|
body = setup_src[start:end]
|
|
assert (
|
|
"/proc/driver/nvidia/gpus" in body
|
|
), "_setup_has_usable_nvidia_gpu must fall back to /proc/driver/nvidia/gpus"
|
|
|
|
def test_gpu_summary_uses_helper(self, setup_src):
|
|
assert "if _setup_has_usable_nvidia_gpu; then" in setup_src
|
|
|
|
def test_timeout_wrapper_exists(self, setup_src):
|
|
start = setup_src.find("_setup_run_smi()")
|
|
assert start >= 0, "_setup_run_smi timeout wrapper must exist"
|
|
end = setup_src.find("\n}", start)
|
|
body = setup_src[start:end]
|
|
assert "timeout 10" in body
|
|
assert "command -v timeout" in body
|
|
|
|
def test_cuda_source_build_gated_on_usable_nvidia(self, setup_src):
|
|
"""The nvcc source-build search must be gated on _setup_nvidia_usable."""
|
|
anchor = setup_src.find('NVCC_PATH=""\n')
|
|
assert anchor >= 0
|
|
window = setup_src[anchor : anchor + 700]
|
|
assert (
|
|
'if [ "$_setup_nvidia_usable" = true ]' in window
|
|
), "CUDA toolkit search must require a usable NVIDIA GPU, not just nvcc"
|
|
|
|
def test_nvidia_helper_honours_hidden_cvd(self, setup_src):
|
|
"""_setup_has_usable_nvidia_gpu must consult the hidden-CVD helper so CVD ""/-1 suppresses NVIDIA before AMD gating."""
|
|
assert "_setup_cvd_hides_nvidia()" in setup_src
|
|
start = setup_src.find("_setup_has_usable_nvidia_gpu() {")
|
|
end = setup_src.find("\n}", start)
|
|
body = setup_src[start:end]
|
|
assert "_setup_cvd_hides_nvidia" in body
|
|
|
|
def test_rocm_source_build_gated_on_amd_detected(self, setup_src):
|
|
"""The hipcc source-build search must be gated on _setup_amd_detected."""
|
|
anchor = setup_src.find('ROCM_HIPCC=""')
|
|
assert anchor >= 0
|
|
window = setup_src[anchor : anchor + 400]
|
|
assert (
|
|
'[ "$_setup_amd_detected" = true ]' in window
|
|
), "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
|
|
|
|
def test_driver_version_probe_timeout_wrapped(self, setup_src):
|
|
start = setup_src.find("_cuda_driver_max_version()")
|
|
end = setup_src.find("\n}", start)
|
|
body = setup_src[start:end]
|
|
assert "_setup_run_smi nvidia-smi" in body
|
|
|
|
|
|
# TEST: install.sh -- UNSLOTH_TORCH_BACKEND classified on the final path segment
|
|
|
|
|
|
class TestBackendExportLeafClassification:
|
|
"""A mirror base path containing "rocm"/"gfx" must not mislabel a cu*/cpu index; classification uses TORCH_INDEX_URL's leaf only."""
|
|
|
|
@pytest.fixture(scope = "class")
|
|
def install_src(self) -> str:
|
|
return (PACKAGE_ROOT / "install.sh").read_text(encoding = "utf-8")
|
|
|
|
def test_export_block_uses_leaf(self, install_src):
|
|
anchor = install_src.find("_torch_index_leaf=")
|
|
assert anchor >= 0, "backend export must classify on the final path segment"
|
|
window = install_src[anchor : anchor + 500]
|
|
assert 'export UNSLOTH_TORCH_BACKEND="rocm"' in window
|
|
assert 'export UNSLOTH_TORCH_BACKEND="cpu"' in window
|
|
assert 'export UNSLOTH_TORCH_BACKEND="cuda"' in window
|
|
|
|
def test_leaf_classification_behaviour(self, tmp_path):
|
|
import subprocess as sp
|
|
|
|
script = tmp_path / "leaf.sh"
|
|
src = (PACKAGE_ROOT / "install.sh").read_text(encoding = "utf-8")
|
|
anchor = src.find("_torch_index_leaf=")
|
|
block = src[anchor : src.find("esac", anchor) + 4]
|
|
# Drive the extracted block with adversarial mirror URLs.
|
|
script.write_text(
|
|
"#!/bin/sh\n"
|
|
'TORCH_INDEX_URL="$1"\n' + block + "\n"
|
|
'printf "%s" "$UNSLOTH_TORCH_BACKEND"\n'
|
|
)
|
|
cases = {
|
|
"https://download.pytorch.org/whl/cu128": "cuda",
|
|
"https://download.pytorch.org/whl/cpu": "cpu",
|
|
"https://download.pytorch.org/whl/rocm6.4": "rocm",
|
|
"https://repo.radeon.com/rocm/manylinux/rocm-rel-7.2.1/": "rocm",
|
|
"https://repo.amd.com/rocm/whl/gfx1151/": "rocm",
|
|
"https://mirror.local/rocm-cache/cu128": "cuda",
|
|
"https://mirror.local/gfx-cache/cpu": "cpu",
|
|
}
|
|
for url, expected in cases.items():
|
|
out = sp.run(
|
|
["sh", str(script), url], capture_output = True, text = True, timeout = 30
|
|
).stdout.strip()
|
|
assert out == expected, f"{url} classified as {out!r}, expected {expected!r}"
|
|
|
|
|
|
# TEST: CUDA_VISIBLE_DEVICES=""/-1 hides NVIDIA in every usable-GPU helper
|
|
|
|
|
|
_STACK_PATH = PACKAGE_ROOT / "studio" / "install_python_stack.py"
|
|
_STACK_SPEC = importlib.util.spec_from_file_location(
|
|
"studio_install_python_stack_followups", _STACK_PATH
|
|
)
|
|
assert _STACK_SPEC is not None and _STACK_SPEC.loader is not None
|
|
stack_mod = importlib.util.module_from_spec(_STACK_SPEC)
|
|
sys.modules[_STACK_SPEC.name] = stack_mod
|
|
_STACK_SPEC.loader.exec_module(stack_mod)
|
|
|
|
|
|
def _stack_nvidia_usable(cvd):
|
|
"""Drive _has_usable_nvidia_gpu with a mocked nvidia-smi that always reports a GPU; cvd=None unsets the env var."""
|
|
|
|
def fake_run(cmd, *args, **kwargs):
|
|
result = MagicMock()
|
|
result.returncode = 0
|
|
result.stdout = "GPU 0: NVIDIA Fake (UUID: GPU-x)\n"
|
|
return result
|
|
|
|
env = {} if cvd is None else {"CUDA_VISIBLE_DEVICES": cvd}
|
|
with (
|
|
patch.object(
|
|
stack_mod.shutil,
|
|
"which",
|
|
side_effect = lambda n: "/usr/bin/nvidia-smi" if n == "nvidia-smi" else None,
|
|
),
|
|
patch.object(stack_mod.subprocess, "run", side_effect = fake_run),
|
|
patch.dict(stack_mod.os.environ, env, clear = False),
|
|
):
|
|
if cvd is None:
|
|
stack_mod.os.environ.pop("CUDA_VISIBLE_DEVICES", None)
|
|
return stack_mod._has_usable_nvidia_gpu()
|
|
|
|
|
|
class TestHiddenCvdNotUsable:
|
|
"""CVD ""/-1 hides every NVIDIA device; all three _has_usable_nvidia_gpu impls must report not-usable so AMD/CPU routes run."""
|
|
|
|
def test_python_unset_cvd_is_usable(self):
|
|
assert _stack_nvidia_usable(None) is True
|
|
|
|
def test_python_empty_cvd_not_usable(self):
|
|
assert _stack_nvidia_usable("") is False
|
|
|
|
def test_python_minus_one_not_usable(self):
|
|
assert _stack_nvidia_usable("-1") is False
|
|
|
|
def test_python_padded_minus_one_not_usable(self):
|
|
assert _stack_nvidia_usable(" -1 ") is False
|
|
|
|
def test_python_explicit_device_is_usable(self):
|
|
assert _stack_nvidia_usable("0") is True
|
|
|
|
def test_python_device_list_is_usable(self):
|
|
assert _stack_nvidia_usable("0,1") is True
|
|
|
|
def test_hidden_nvidia_restores_rocm_detection(self):
|
|
"""Mixed host, NVIDIA hidden via CVD=-1: _has_rocm_gpu must pass the NVIDIA guard and return True (pre-fix it ignored CVD)."""
|
|
|
|
def fake_run(cmd, *args, **kwargs):
|
|
result = MagicMock()
|
|
result.returncode = 0
|
|
exe = str(cmd[0])
|
|
if exe.endswith("rocminfo"):
|
|
result.stdout = " Name: gfx1100\n"
|
|
else:
|
|
result.stdout = "GPU 0: NVIDIA Fake (UUID: GPU-x)\n"
|
|
return result
|
|
|
|
which_map = {
|
|
"rocminfo": "/usr/bin/rocminfo",
|
|
"nvidia-smi": "/usr/bin/nvidia-smi",
|
|
}
|
|
with (
|
|
patch.object(stack_mod.shutil, "which", side_effect = which_map.get),
|
|
patch.object(stack_mod.subprocess, "run", side_effect = fake_run),
|
|
patch.dict(stack_mod.os.environ, {"CUDA_VISIBLE_DEVICES": "-1"}, clear = False),
|
|
):
|
|
assert stack_mod._has_rocm_gpu() is True
|
|
|
|
@staticmethod
|
|
def _run_sh_helper(tmp_path, src: str, fn_names: list, cvd):
|
|
"""Extract shell functions, run the usable-GPU one against a fake nvidia-smi; return "usable"/"not_usable"."""
|
|
import os as _os
|
|
import subprocess as sp
|
|
|
|
blocks = []
|
|
for name in fn_names:
|
|
start = src.find(f"{name}() {{")
|
|
assert start >= 0, f"{name} missing"
|
|
end = src.find("\n}", start) + 2
|
|
blocks.append(src[start:end])
|
|
fake_bin = tmp_path / "bin"
|
|
fake_bin.mkdir(exist_ok = True)
|
|
smi = fake_bin / "nvidia-smi"
|
|
smi.write_text("#!/bin/sh\necho 'GPU 0: NVIDIA Fake (UUID: GPU-x)'\n")
|
|
smi.chmod(0o755)
|
|
script = tmp_path / "probe.sh"
|
|
script.write_text(
|
|
"#!/bin/sh\n" + "\n".join(blocks) + "\n"
|
|
f"if {fn_names[-1]}; then echo usable; else echo not_usable; fi\n"
|
|
)
|
|
env = dict(_os.environ)
|
|
env["PATH"] = f"{fake_bin}:{env['PATH']}"
|
|
if cvd is None:
|
|
env.pop("CUDA_VISIBLE_DEVICES", None)
|
|
else:
|
|
env["CUDA_VISIBLE_DEVICES"] = cvd
|
|
return sp.run(
|
|
["sh", str(script)], capture_output = True, text = True, timeout = 30, env = env
|
|
).stdout.strip()
|
|
|
|
@pytest.mark.parametrize(
|
|
"cvd, expected",
|
|
[(None, "usable"), ("", "not_usable"), ("-1", "not_usable"), ("0", "usable")],
|
|
)
|
|
def test_install_sh_helper_cvd(self, tmp_path, cvd, expected):
|
|
src = (PACKAGE_ROOT / "install.sh").read_text(encoding = "utf-8")
|
|
out = self._run_sh_helper(
|
|
tmp_path,
|
|
src,
|
|
["_run_bounded", "_cvd_hides_nvidia", "_has_usable_nvidia_gpu"],
|
|
cvd,
|
|
)
|
|
assert out == expected
|
|
|
|
@pytest.mark.parametrize(
|
|
"cvd, expected",
|
|
[(None, "usable"), ("", "not_usable"), ("-1", "not_usable"), ("0", "usable")],
|
|
)
|
|
def test_setup_sh_helper_cvd(self, tmp_path, cvd, expected):
|
|
src = SETUP_SH.read_text(encoding = "utf-8")
|
|
out = self._run_sh_helper(
|
|
tmp_path,
|
|
src,
|
|
["_setup_run_smi", "_setup_cvd_hides_nvidia", "_setup_has_usable_nvidia_gpu"],
|
|
cvd,
|
|
)
|
|
assert out == expected
|