Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions examples/gfx1151-smoke/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<!--
SPDX-FileCopyrightText: 2026 Advanced Micro Devices, Inc.
SPDX-License-Identifier: MIT
-->

# 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.
63 changes: 63 additions & 0 deletions examples/gfx1151-smoke/benchmark.py
Original file line number Diff line number Diff line change
@@ -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()
6 changes: 6 additions & 0 deletions examples/gfx1151-smoke/custom_radeon8065s.sh
Original file line number Diff line number Diff line change
@@ -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"
1 change: 1 addition & 0 deletions src/hyperloom/common/gpu_identity.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"mi308x": ("gfx942", 304),
"mi325x": ("gfx942", 304),
"mi355x": ("gfx950", 256),
"radeon8065s": ("gfx1151", 40),
}


Expand Down
12 changes: 9 additions & 3 deletions src/hyperloom/inference_optimizer/gpu_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
1 change: 1 addition & 0 deletions src/kernelforge/fusion/gpu_arch.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"mi308x": "gfx942",
"mi325x": "gfx942",
"mi355x": "gfx950",
"radeon8065s": "gfx1151",
}
_GFX_RE = re.compile(r"\bgfx[0-9a-f]+\b", re.IGNORECASE)

Expand Down