Skip to content

Add RDNA 2/3/4 ROCm routing tests via a CPU-only torch spoof - #6935

Merged
danielhanchen merged 2 commits into
mainfrom
rocm-rdna-routing-tests
Jul 7, 2026
Merged

danielhanchen merged 2 commits into
mainfrom
rocm-rdna-routing-tests

Conversation

@danielhanchen

Copy link
Copy Markdown
Member

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.py fakes 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-op torch.cuda machinery and overlays an AMD Radeon identity (torch.version.hip, gcnArchName, capability, marketing name) for a given gfx target. Apply it before importing unsloth/unsloth_zoo, since DEVICE_TYPE is 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_type resolves to hip, the llama.cpp GPU target resolves to ("rocm", gfx), and the correct per-family ROCm bundle suffix is picked.

gfx gen example card bundle family
gfx1030/1031/1032/1034 RDNA2 RX 6900/6700/6600/6400 gfx103X
gfx1100/1101/1102 RDNA3 RX 7900 XTX / 7800 XT / 7600 gfx110X
gfx1150/1151 RDNA3.5 Strix Point / Strix Halo self
gfx1200/1201 RDNA4 RX 9060 XT / 9070 XT gfx120X

Notes

  • Test only, no production changes. The spoof makes device_type resolve to hip naturally, so no code change is needed to exercise the routing.
  • The torch-facing checks run in a subprocess so the spoof never leaks into sibling tests, and DEVICE_TYPE (cached at import) is read from a clean process. The pure gfx-family mapping runs in-process.
  • Guarded by pytest.importorskip on torch and unsloth_zoo, so it runs in the Repo tests (CPU) job where both are installed and skips anywhere they are absent.
  • Local run: 23 passed.

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.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Comment thread tests/_zoo_rocm_spoof.py
Comment on lines +41 to +43
spec = importlib.util.spec_from_file_location("_zoo_aggressive_cuda_spoof", path)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

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.

Suggested change
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)

Comment on lines +43 to +57
_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}}))
"""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

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.

Suggested change
_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}}))
"""

Comment on lines +60 to +66
@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 "):])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

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.

Suggested change
@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
  1. 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.

@danielhanchen
danielhanchen merged commit d79495d into main Jul 7, 2026
13 of 21 checks passed
@danielhanchen
danielhanchen deleted the rocm-rdna-routing-tests branch July 7, 2026 11:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant