From d7813563b2ac71b4e4318b1063bffdc09b94db57 Mon Sep 17 00:00:00 2001 From: powderluv Date: Wed, 9 Sep 2026 19:35:42 -0700 Subject: [PATCH] Add experimental Radeon 8065S gfx1151 custom-workload support --- examples/gfx1151-smoke/README.md | 50 +++++++++++++++ examples/gfx1151-smoke/benchmark.py | 63 +++++++++++++++++++ examples/gfx1151-smoke/custom_radeon8065s.sh | 6 ++ src/hyperloom/common/gpu_identity.py | 1 + .../inference_optimizer/gpu_types.py | 12 +++- .../tests/test_radeon_gpu_identity.py | 62 ++++++++++++++++++ src/kernelforge/fusion/gpu_arch.py | 1 + 7 files changed, 192 insertions(+), 3 deletions(-) create mode 100644 examples/gfx1151-smoke/README.md create mode 100644 examples/gfx1151-smoke/benchmark.py create mode 100644 examples/gfx1151-smoke/custom_radeon8065s.sh create mode 100644 src/hyperloom/inference_optimizer/tests/test_radeon_gpu_identity.py diff --git a/examples/gfx1151-smoke/README.md b/examples/gfx1151-smoke/README.md new file mode 100644 index 0000000000..dbb2778eea --- /dev/null +++ b/examples/gfx1151-smoke/README.md @@ -0,0 +1,50 @@ + + +# Experimental Halo (gfx1151) custom workloads + +Hyperloom recognizes the Radeon 8065S as `radeon8065s`, with ISA `gfx1151` +and 40 compute units. This is initial platform identification and custom-workload +bring-up, validated on a Ryzen AI Max+ PRO 495 with ROCm 10 and AMD PyTorch 2.13. +It does not establish support for every Halo SKU or a complete kernel optimizer. + +Pass `--gpu-type radeon8065s` when HIP reports only `AMD Radeon Graphics`. +The ISA alone cannot distinguish boards or their compute-unit counts, so generic +`gfx1151` does not automatically select a board. Explicit Radeon product names +are recognized from rocm-smi or PyTorch device properties. + +## Smoke test + +Use an existing virtual environment containing a ROCm PyTorch build compatible +with gfx1151. From the repository root: + +```bash +source .venv/bin/activate +RESULT_DIR=/tmp/hyperloom-gfx1151-smoke bash examples/gfx1151-smoke/custom_radeon8065s.sh +``` + +The benchmark checks GPU FP16 attention against an independent CPU FP32 +reference before emitting `inferencex_result.json`. It requires no model download +or LLM credentials. Its throughput unit is attention calls per second, not LLM +tokens per second. HIP's `multi_processor_count` is preserved as +`hip_multiprocessors`; it must not be interpreted as the board's CU count. + +## Custom workload integration + +Use `--framework custom`, `HYPERLOOM_BENCHMARK_BACKEND=bypass`, and a dedicated +workload checkout as described in the [custom workload guide](../../docs/how-to/optimize-custom-workload.md). +Supply `custom_radeon8065s.sh` through `--benchmark-scripts-dir` and keep TP=1. +Set `INFERENCE_OPTIMIZER_RAY_EXEC=0` when Ray does not discover the APU GPU. + +No Magpie serving runner or Radeon peak-performance constants are introduced. +Instinct serving recipes, profiler hotfixes, and kernel optimization paths need +separate platform validation. In particular, do not apply the ROCm 7.2 bare-metal +framework installer to an existing ROCm 10 environment. + +A local Qwen3-0.6B BF16 custom workload completed baseline and configuration +benchmarks. A 15-minute source/kernel trial reached its time limit without an +accepted change; the GEAK kernel lane did not execute because its phase budget +was exhausted. These trials establish benchmark execution, not an optimization +gain or validated end-to-end kernel rewriting on Halo. diff --git a/examples/gfx1151-smoke/benchmark.py b/examples/gfx1151-smoke/benchmark.py new file mode 100644 index 0000000000..abec938f23 --- /dev/null +++ b/examples/gfx1151-smoke/benchmark.py @@ -0,0 +1,63 @@ +# SPDX-FileCopyrightText: 2026 Advanced Micro Devices, Inc. +# SPDX-License-Identifier: MIT + +"""Small GPU attention smoke test with a CPU reference, not an LLM benchmark.""" + +from __future__ import annotations + +import json +import math +import os +import statistics +import time +from pathlib import Path + +import torch +import torch.nn.functional as F + + +def main() -> None: + if not torch.version.hip or not torch.cuda.is_available(): + raise RuntimeError("A ROCm PyTorch build and accessible GPU are required") + properties = torch.cuda.get_device_properties(0) + if properties.gcnArchName.split(":", 1)[0] != "gfx1151": + raise RuntimeError(f"Expected gfx1151, got {properties.gcnArchName}") + torch.manual_seed(42) + q, k, v = [torch.randn(1, 8, 128, 64) for _ in range(3)] + reference = (q @ k.transpose(-2, -1) / math.sqrt(64)).softmax(-1) @ v + q, k, v = [x.to(device="cuda", dtype=torch.float16) for x in (q, k, v)] + with torch.inference_mode(): + for _ in range(5): + actual = F.scaled_dot_product_attention(q, k, v) + torch.cuda.synchronize() + samples = [] + for _ in range(5): + start = time.perf_counter() + for _ in range(20): + actual = F.scaled_dot_product_attention(q, k, v) + torch.cuda.synchronize() + samples.append((time.perf_counter() - start) / 20) + error = (actual.float().cpu() - reference).abs() + passed = bool( + torch.isfinite(error).all() and torch.allclose(actual.float().cpu(), reference, atol=0.002, rtol=0.02) + ) + report = { + "framework": "custom", + "workload_kind": "scriptable", + "throughput_unit": "attention_calls/s", + "output_throughput": 1 / statistics.median(samples), + "quality_gate": {"passed": passed, "max_abs_error": error.max().item()}, + "gpu_arch": properties.gcnArchName, + "hip_multiprocessors": properties.multi_processor_count, + "torch_version": torch.__version__, + } + output = Path(os.environ["RESULT_DIR"]) + output.mkdir(parents=True, exist_ok=True) + (output / "inferencex_result.json").write_text(json.dumps(report, indent=2) + "\n") + print(json.dumps(report, indent=2)) + if not passed: + raise RuntimeError("GPU attention failed the CPU-reference correctness check") + + +if __name__ == "__main__": + main() diff --git a/examples/gfx1151-smoke/custom_radeon8065s.sh b/examples/gfx1151-smoke/custom_radeon8065s.sh new file mode 100644 index 0000000000..0c0f10469a --- /dev/null +++ b/examples/gfx1151-smoke/custom_radeon8065s.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: 2026 Advanced Micro Devices, Inc. +# SPDX-License-Identifier: MIT +set -euo pipefail +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +exec "${HYPERLOOM_FRAMEWORK_PYTHON:-python}" "$script_dir/benchmark.py" diff --git a/src/hyperloom/common/gpu_identity.py b/src/hyperloom/common/gpu_identity.py index 669f89cb43..285f1ef661 100644 --- a/src/hyperloom/common/gpu_identity.py +++ b/src/hyperloom/common/gpu_identity.py @@ -20,6 +20,7 @@ "mi308x": ("gfx942", 304), "mi325x": ("gfx942", 304), "mi355x": ("gfx950", 256), + "radeon8065s": ("gfx1151", 40), } diff --git a/src/hyperloom/inference_optimizer/gpu_types.py b/src/hyperloom/inference_optimizer/gpu_types.py index fcbf98fec8..5864d9310c 100644 --- a/src/hyperloom/inference_optimizer/gpu_types.py +++ b/src/hyperloom/inference_optimizer/gpu_types.py @@ -59,7 +59,7 @@ def _resolve_gpu_type( def _autodetect_gpu_type() -> str | None: - """Return mi300x|mi308x|mi325x|mi355x or None if undetectable.""" + """Return a known board type, or None when its identity is ambiguous.""" import subprocess try: @@ -69,8 +69,9 @@ def _autodetect_gpu_type() -> str | None: text=True, timeout=5, ).stdout.upper() + product_name = "".join(out.split()) for tag in _PRODUCT_TAGS: - if tag in out: + if tag in product_name: return tag.lower() except (FileNotFoundError, subprocess.TimeoutExpired, PermissionError, OSError): # rocm-smi missing / slow / not permitted; fall through to the torch @@ -79,7 +80,12 @@ def _autodetect_gpu_type() -> str | None: try: import torch - arch = torch.cuda.get_device_properties(0).gcnArchName + properties = torch.cuda.get_device_properties(0) + name = "".join(str(getattr(properties, "name", "")).upper().split()) + for tag in _PRODUCT_TAGS: + if tag in name: + return tag.lower() + arch = properties.gcnArchName gfx = arch.split(":", 1)[0].lower() return _GFX_TO_RUNNER.get(gfx) except Exception: # noqa: BLE001 diff --git a/src/hyperloom/inference_optimizer/tests/test_radeon_gpu_identity.py b/src/hyperloom/inference_optimizer/tests/test_radeon_gpu_identity.py new file mode 100644 index 0000000000..2bc3d1e506 --- /dev/null +++ b/src/hyperloom/inference_optimizer/tests/test_radeon_gpu_identity.py @@ -0,0 +1,62 @@ +# SPDX-FileCopyrightText: 2026 Advanced Micro Devices, Inc. +# SPDX-License-Identifier: MIT + +"""Experimental Radeon dispatch must never select an Instinct runner.""" + +from __future__ import annotations + +import subprocess +import sys +from types import SimpleNamespace + +import pytest + +from hyperloom.common.provenance import detect_gfx_arch +from hyperloom.inference_optimizer import gpu_types +from kernelforge.fusion.gpu_arch import canon_arch + + +def test_radeon_identity_and_runner(): + assert gpu_types.amd_gpu_dispatch_identity("radeon8065s") == ("gfx1151", 40) + assert gpu_types._gpu_runner_type("radeon8065s") == "radeon8065s" + assert detect_gfx_arch({}, gpu_type="radeon8065s") == "gfx1151" + assert canon_arch("radeon8065s") == "gfx1151" + + +def test_spaced_product_name(monkeypatch): + monkeypatch.setattr(subprocess, "run", lambda *a, **kw: SimpleNamespace(stdout="AMD Radeon 8065S")) + assert gpu_types._autodetect_gpu_type() == "radeon8065s" + + +def test_torch_product_name(monkeypatch): + monkeypatch.setattr(subprocess, "run", lambda *a, **kw: SimpleNamespace(stdout="")) + props = SimpleNamespace(name="AMD Radeon 8065S Graphics", gcnArchName="gfx1151:sramecc-:xnack-") + monkeypatch.setitem( + sys.modules, "torch", SimpleNamespace(cuda=SimpleNamespace(get_device_properties=lambda i: props)) + ) + assert gpu_types._autodetect_gpu_type() == "radeon8065s" + + +def test_generic_gfx1151_does_not_guess_board(monkeypatch): + monkeypatch.setattr(subprocess, "run", lambda *a, **kw: SimpleNamespace(stdout="")) + props = SimpleNamespace(name="AMD Radeon Graphics", gcnArchName="gfx1151:sramecc-:xnack-") + monkeypatch.setitem( + sys.modules, "torch", SimpleNamespace(cuda=SimpleNamespace(get_device_properties=lambda i: props)) + ) + assert gpu_types._autodetect_gpu_type() is None + assert gpu_types._resolve_gpu_type("radeon8065s", "")[0] == "radeon8065s" + + +@pytest.mark.parametrize( + ("name", "expected"), + [("AMD Instinct MI300X", "mi300x"), ("AMD Instinct MI325X", "mi325x"), ("AMD Instinct MI355X", "mi355x")], +) +def test_instinct_product_detection_preserved(monkeypatch, name, expected): + monkeypatch.setattr(subprocess, "run", lambda *a, **kw: SimpleNamespace(stdout=name)) + assert gpu_types._autodetect_gpu_type() == expected + + +def test_explicit_radeon_identity_overrides_environment(monkeypatch): + monkeypatch.setenv("GPU_TYPE", "mi300x") + assert gpu_types._resolve_amd_gpu_type("radeon8065s") == "radeon8065s" + assert gpu_types.amd_gpu_dispatch_identity("radeon8065s") == ("gfx1151", 40) diff --git a/src/kernelforge/fusion/gpu_arch.py b/src/kernelforge/fusion/gpu_arch.py index fc62d34c24..2812a29d15 100644 --- a/src/kernelforge/fusion/gpu_arch.py +++ b/src/kernelforge/fusion/gpu_arch.py @@ -27,6 +27,7 @@ "mi308x": "gfx942", "mi325x": "gfx942", "mi355x": "gfx950", + "radeon8065s": "gfx1151", } _GFX_RE = re.compile(r"\bgfx[0-9a-f]+\b", re.IGNORECASE)