Add RDNA 2/3/4 ROCm routing tests via a CPU-only torch spoof - #6935
Conversation
Introduces tests/_zoo_rocm_spoof.py, the ROCm sibling of _zoo_aggressive_cuda_spoof.py: it reuses the CUDA spoof's torch.cuda no-op machinery and overlays an AMD Radeon identity (torch.version.hip, gcnArchName, capability) for any RDNA 2/3/4 gfx target, so hip code paths run on CPU-only CI with no AMD hardware. tests/studio/install/test_rocm_rdna_routing.py then asserts unsloth_zoo routes every RDNA arch (gfx1030/1031/1032/1034, gfx1100/1101/1102, gfx1150/1151, gfx1200/1201) correctly: device_type resolves to hip, llama.cpp target resolves to (rocm, gfx), and the per-family ROCm bundle suffix (gfx103X/gfx110X/gfx120X, or self for gfx1150/1151) is picked. The torch-facing checks run in a subprocess so the spoof never leaks into sibling tests and DEVICE_TYPE (cached at import) resolves from a clean process; the pure gfx-family mapping runs in-process. Guarded by importorskip so it runs where torch and unsloth_zoo are installed (the Repo tests CPU job) and skips elsewhere.
There was a problem hiding this comment.
Code Review
This pull request introduces ROCm/RDNA spoofing capabilities to enable testing AMD Radeon architectures in CPU-only CI environments. It adds tests/_zoo_rocm_spoof.py to present PyTorch as various AMD Radeon cards and tests/studio/install/test_rocm_rdna_routing.py to validate routing behavior. The review feedback suggests several improvements: defensively checking for None values when dynamically loading the CUDA spoof module, explicitly adding the repository root to sys.path in the subprocess to ensure robust imports, and resolving sys.executable to an absolute path while including the subprocess exit code in assertion failures.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| spec = importlib.util.spec_from_file_location("_zoo_aggressive_cuda_spoof", path) | ||
| mod = importlib.util.module_from_spec(spec) | ||
| spec.loader.exec_module(mod) |
There was a problem hiding this comment.
Defensively check if spec or spec.loader is None before attempting to load the module. If either is None, raising a clear ImportError is much more helpful for debugging than letting it fail with a generic AttributeError or ValueError.
| spec = importlib.util.spec_from_file_location("_zoo_aggressive_cuda_spoof", path) | |
| mod = importlib.util.module_from_spec(spec) | |
| spec.loader.exec_module(mod) | |
| spec = importlib.util.spec_from_file_location("_zoo_aggressive_cuda_spoof", path) | |
| if spec is None or spec.loader is None: | |
| raise ImportError(f"Could not load spec for _zoo_aggressive_cuda_spoof at {path}") | |
| mod = importlib.util.module_from_spec(spec) | |
| spec.loader.exec_module(mod) |
| _CHILD = """ | ||
| import json, sys | ||
| sys.path.insert(0, {tests!r}) | ||
| import _zoo_rocm_spoof as spoof | ||
| arches = {arches!r} | ||
| spoof.apply(arches[0]) | ||
| from unsloth_zoo.device_type import get_device_type, is_hip | ||
| device_type = [get_device_type(), is_hip()] | ||
| from unsloth_zoo import llama_cpp as lc | ||
| targets = {{}} | ||
| for gfx in arches: | ||
| spoof.apply(gfx) | ||
| targets[gfx] = list(lc._detect_gpu_target()) | ||
| print("RESULT " + json.dumps({{"device_type": device_type, "targets": targets}})) | ||
| """ |
There was a problem hiding this comment.
To make the test runner robust to where pytest is invoked from (especially if run from a subdirectory or if unsloth_zoo is not globally installed in the environment), explicitly insert the repository root directory (_TESTS_DIR.parent) into the child process's sys.path.
| _CHILD = """ | |
| import json, sys | |
| sys.path.insert(0, {tests!r}) | |
| import _zoo_rocm_spoof as spoof | |
| arches = {arches!r} | |
| spoof.apply(arches[0]) | |
| from unsloth_zoo.device_type import get_device_type, is_hip | |
| device_type = [get_device_type(), is_hip()] | |
| from unsloth_zoo import llama_cpp as lc | |
| targets = {{}} | |
| for gfx in arches: | |
| spoof.apply(gfx) | |
| targets[gfx] = list(lc._detect_gpu_target()) | |
| print("RESULT " + json.dumps({{"device_type": device_type, "targets": targets}})) | |
| """ | |
| _CHILD = """ | |
| import json, sys | |
| sys.path.insert(0, {tests!r}) | |
| sys.path.insert(0, {repo!r}) | |
| import _zoo_rocm_spoof as spoof | |
| arches = {arches!r} | |
| spoof.apply(arches[0]) | |
| from unsloth_zoo.device_type import get_device_type, is_hip | |
| device_type = [get_device_type(), is_hip()] | |
| from unsloth_zoo import llama_cpp as lc | |
| targets = {{}} | |
| for gfx in arches: | |
| spoof.apply(gfx) | |
| targets[gfx] = list(lc._detect_gpu_target()) | |
| print("RESULT " + json.dumps({{"device_type": device_type, "targets": targets}})) | |
| """ |
| @pytest.fixture(scope="module") | ||
| def routed(): | ||
| code = _CHILD.format(tests=str(_TESTS_DIR), arches=list(_ARCHES)) | ||
| proc = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True) | ||
| line = next((l for l in proc.stdout.splitlines() if l.startswith("RESULT ")), None) | ||
| assert line, f"child produced no result.\nstdout:\n{proc.stdout}\nstderr:\n{proc.stderr}" | ||
| return json.loads(line[len("RESULT "):]) |
There was a problem hiding this comment.
Include the subprocess exit code (proc.returncode) in the assertion failure message. Additionally, ensure that the executable path (sys.executable) passed to the subprocess is resolved to an absolute path using Path.resolve() to prevent relative paths from being incorrectly resolved via the system PATH.
| @pytest.fixture(scope="module") | |
| def routed(): | |
| code = _CHILD.format(tests=str(_TESTS_DIR), arches=list(_ARCHES)) | |
| proc = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True) | |
| line = next((l for l in proc.stdout.splitlines() if l.startswith("RESULT ")), None) | |
| assert line, f"child produced no result.\nstdout:\n{proc.stdout}\nstderr:\n{proc.stderr}" | |
| return json.loads(line[len("RESULT "):]) | |
| @pytest.fixture(scope="module") | |
| def routed(): | |
| from pathlib import Path | |
| code = _CHILD.format(tests=str(_TESTS_DIR), repo=str(_TESTS_DIR.parent), arches=list(_ARCHES)) | |
| resolved_executable = str(Path(sys.executable).resolve()) | |
| proc = subprocess.run([resolved_executable, "-c", code], capture_output=True, text=True) | |
| line = next((l for l in proc.stdout.splitlines() if l.startswith("RESULT ")), None) | |
| assert line, f"child produced no result (exit code {proc.returncode}).\nstdout:\n{proc.stdout}\nstderr:\n{proc.stderr}" | |
| return json.loads(line[len("RESULT "):]) |
References
- When resolving executable paths to be passed to subprocesses, always return an absolute path (e.g., using Path.resolve()) to prevent relative paths from being incorrectly resolved via the system PATH.
for more information, see https://pre-commit.ci
Summary
PR #6915 added the ROCm-on-WSL installer and RDNA arch detection for discrete Radeon (RDNA 2/3/4). This adds the test coverage for that path so it does not regress. We have no AMD hardware, so the tests fake an RDNA GPU end to end on CPU-only CI, mirroring how
tests/_zoo_aggressive_cuda_spoof.pyfakes an NVIDIA card.What this adds
tests/_zoo_rocm_spoof.py: the ROCm sibling of the CUDA spoof. It reuses the CUDA spoof's no-optorch.cudamachinery and overlays an AMD Radeon identity (torch.version.hip,gcnArchName, capability, marketing name) for a givengfxtarget. Apply it before importing unsloth/unsloth_zoo, sinceDEVICE_TYPEis cached at import.tests/studio/install/test_rocm_rdna_routing.py: parametrized routing tests over every RDNA 2/3/4 arch, asserting unsloth_zoo routes each one correctly.What is validated
For each arch:
device_typeresolves tohip, the llama.cpp GPU target resolves to("rocm", gfx), and the correct per-family ROCm bundle suffix is picked.Notes
device_typeresolve tohipnaturally, so no code change is needed to exercise the routing.DEVICE_TYPE(cached at import) is read from a clean process. The pure gfx-family mapping runs in-process.pytest.importorskipontorchandunsloth_zoo, so it runs in the Repo tests (CPU) job where both are installed and skips anywhere they are absent.