Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
4 changes: 4 additions & 0 deletions dnn-providers/hip-kernel-provider/rocke/platform/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,10 @@ GPU node.
- **Default arch is `gfx950`** (the byte-identity baseline). Do not change the
codegen default; for on-GPU runs, prefer the local device via
`rocke.runtime.hip_module.get_device_arch()` and fall back to `gfx950`.
*Note*: We are transitioning away from the `gfx950` fallback. Currently,
`rocke/platform/python/rocke/helpers/fusion_lowering.py` does not fall back to
`gfx950`; an exception is raised if a known device can't be detected. Future
work will remove remaining `gfx950` fallbacks.

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.

maybe we can remove this and just add a line saying pass target_architecture for non-gfx950 targets and beware of the silent fallbacks on non-gpus?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Sounds good. Done.

- **torch is optional, never import it from a library to gain a side effect.**
numpy is the only hard dep. torch is needed only for the fusion frontend,
torch-tensor launch, and on-GPU torch-eager numeric checks; gate it behind
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
arch_from_isa,
known_arches,
normalize_dtype,
validate_arch,
)

__all__ = [
Expand All @@ -28,4 +29,5 @@
"arch_from_isa",
"known_arches",
"normalize_dtype",
"validate_arch",
]
Original file line number Diff line number Diff line change
Expand Up @@ -1021,3 +1021,14 @@ def known_arches() -> Tuple[str, ...]:
def arch_from_isa(isa: str) -> str:
"""Extract the gfx token from an isa triple like ``amdgcn-amd-amdhsa--gfx942``."""
return isa.rsplit("-", 1)[-1] if "-" in isa else isa


def validate_arch(arch: Optional[str]) -> None:
if arch is None:
raise ValueError(
"Could not detect a GPU architecture. Pass in an explicit architecture instead."
)
if arch not in known_arches():
raise ValueError(
f"Unknown GPU architecture detected. Known architectures include: {known_arches()}"
)
Original file line number Diff line number Diff line change
Expand Up @@ -166,23 +166,21 @@ def _build_region_hsaco(kernel, *, explicit_arch: Optional[str] = None) -> bytes
Fusion regions are built to be launched immediately on the local device,
so this targets the active device's gfx (via
:func:`rocke.runtime.hip_module.get_device_arch`) instead of defaulting
to a fixed architecture. Falls back to ``gfx950`` when no device/arch can
be detected (e.g. IR-only/no-GPU contexts), preserving the historical
default. Pass ``explicit_arch`` to override (reproducible builds).
to a fixed architecture. Raises an error when no device/arch can
be detected (e.g. IR-only/no-GPU contexts). Pass ``explicit_arch`` to override (reproducible builds).

The comgr ISA always matches the resolved arch; the LLVM lowering uses the
matching ISA backend when the arch is one rocke models, else the gfx950
backend (the device still gets a code object built for its own ISA).
matching ISA backend when the arch is one rocke models.
"""
from ..core.arch import known_arches
from ..core.arch import validate_arch
from ..core.lower_llvm import lower_kernel_to_llvm
from ..runtime.comgr import build_hsaco_from_llvm_ir
from ..runtime.hip_module import get_device_arch

arch = explicit_arch or get_device_arch() or "gfx950"
arch = explicit_arch or get_device_arch()
validate_arch(arch)
isa = f"amdgcn-amd-amdhsa--{arch}"
lower_arch = arch if arch in known_arches() else "gfx950"
ir = lower_kernel_to_llvm(kernel, arch=lower_arch)
ir = lower_kernel_to_llvm(kernel, arch=arch)
hsaco, _ = build_hsaco_from_llvm_ir(ir, isa=isa)
return hsaco

Expand Down Expand Up @@ -349,7 +347,7 @@ def candidates(
)
from .fuse import dtype_to_ir

from ..core.arch import ArchTarget, known_arches
from ..core.arch import ArchTarget, validate_arch
from ..instances.common.gemm_universal import is_valid_spec
from ..runtime.hip_module import get_device_arch

Expand All @@ -371,9 +369,9 @@ def candidates(
# filtered through the arch-aware ``is_valid_spec`` so configs that don't
# fit (e.g. an LDS tile too large for gfx942's 64 KB, or a non-WMMA-legal
# tile/pipeline on gfx1151) are dropped instead of crashing comgr.
arch = get_device_arch() or "gfx950"
target_gfx = arch if arch in known_arches() else "gfx950"
target = ArchTarget.from_gfx(target_gfx)
arch = get_device_arch()
validate_arch(arch)
target = ArchTarget.from_gfx(arch)
wmma = target.wave_size == 32
family = "wmma" if wmma else "mma"
wave_size = target.wave_size
Expand Down Expand Up @@ -433,7 +431,7 @@ def candidates(
object.__setattr__(spec, "_fused_epilogue", epilogue)
object.__setattr__(spec, "_bias_arg", bias_name)
object.__setattr__(spec, "_residual_args", residual_names)
if not is_valid_spec(spec, arch=target_gfx)[0]:
if not is_valid_spec(spec, arch=arch)[0]:
continue
configs.append(
AutotuneConfig(
Expand All @@ -445,7 +443,7 @@ def candidates(
return configs

def build(self, config: AutotuneConfig) -> BuiltRegion:
from ..core.arch import ArchTarget, known_arches
from ..core.arch import ArchTarget, known_arches, validate_arch
from ..instances.common.gemm_universal import build_universal_gemm
from ..runtime.hip_module import get_device_arch
from ..runtime.launcher import KernelLauncher
Expand All @@ -463,15 +461,15 @@ def build(self, config: AutotuneConfig) -> BuiltRegion:
# when it matches the spec's wave size (the common case); otherwise fall
# back to the first known arch with the matching wave size so a wave32
# WMMA candidate still builds on a wave64 host (and vice versa).
dev = get_device_arch() or "gfx950"
build_gfx = dev if dev in known_arches() else "gfx950"
if ArchTarget.from_gfx(build_gfx).wave_size != spec.wave_size:
arch = get_device_arch()
validate_arch(arch)
if ArchTarget.from_gfx(arch).wave_size != spec.wave_size:
for cand in known_arches():
if ArchTarget.from_gfx(cand).wave_size == spec.wave_size:
build_gfx = cand
arch = cand
break
kernel = build_universal_gemm(spec, arch=build_gfx)
hsaco = _build_region_hsaco(kernel, explicit_arch=build_gfx)
kernel = build_universal_gemm(spec, arch=arch)
hsaco = _build_region_hsaco(kernel, explicit_arch=arch)
ptr_ty = f"ptr<{ir_dtype.name}, global>"
sig: List[Mapping[str, Any]] = [
{"name": "A", "type": ptr_ty, "size_bytes": 8},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import pathlib
import unittest

from rocke.core.arch import ArchTarget, known_arches, normalize_dtype
from rocke.core.arch import ArchTarget, known_arches, normalize_dtype, validate_arch
from rocke.core.isa import Gfx9MfmaBackend, Gfx950Backend, backend_for

_ROCKE_ROOT = pathlib.Path(__file__).resolve().parents[2] / "python" / "rocke"
Expand Down Expand Up @@ -113,6 +113,26 @@ def test_dtype_normalization(self):
self.assertEqual(normalize_dtype("bf16"), "bf16")
self.assertEqual(normalize_dtype("f32"), "fp32")

def test_validate_arch_none(self):
"""`validate_arch` should raise ValueError when arch is None."""
with self.assertRaises(ValueError) as ctx:
validate_arch(None)
self.assertIn("Could not detect", str(ctx.exception))
self.assertIn("Pass in an explicit", str(ctx.exception))

def test_validate_arch_unknown(self):
"""`validate_arch` should raise ValueError for unknown arch."""
with self.assertRaises(ValueError) as ctx:
validate_arch("gfx9999")
self.assertIn("Unknown GPU architecture", str(ctx.exception))
self.assertIn("Known architectures include", str(ctx.exception))

def test_validate_arch_valid(self):
"""`validate_arch` should not raise for known arches."""
for arch in known_arches():
# Should not raise
validate_arch(arch)


class TestMmaCatalog(unittest.TestCase):
def test_f16_atom_divergence(self):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from __future__ import annotations

import unittest
from unittest.mock import patch, Mock

import pytest

Expand Down Expand Up @@ -82,12 +83,6 @@
# A handful of tests below exercise torch-facing features (the torch.fx
# fusion planner, torch-eager validation baselines) and are skipped when
# torch is absent — torch-full CI lanes still run them.
#
# A separate handful drive the lowerer all the way through
# ``hipModuleLoadData``, which blocks indefinitely on a host with no ROCm
# GPU (no ``/dev/kfd``). Those are skipped when no GPU device node is
# present. The probe is deliberately torch-free (the point of this suite is
# torch-independence) and avoids issuing any HIP call that could itself hang.

try: # torch is optional; gate torch-facing tests on its presence.
import torch as _torch # noqa: F401
Expand All @@ -96,17 +91,7 @@
except Exception: # pragma: no cover - depends on the environment
_HAVE_TORCH = False

import os as _os

# A ROCm GPU exposes the kernel-fusion device node at /dev/kfd. Its absence
# means launches/module loads cannot succeed and would hang; skip then.
_HAVE_GPU = _os.path.exists("/dev/kfd")

_requires_torch = unittest.skipUnless(_HAVE_TORCH, "requires torch")
_requires_gpu = unittest.skipUnless(
_HAVE_GPU, "requires a ROCm GPU (no /dev/kfd device node present)"
)


# ---------------------------------------------------------------------
# Core IR
Expand Down Expand Up @@ -3080,14 +3065,47 @@ def fn(A):


class TestLoweringRegistryBuild(unittest.TestCase):
"""End-to-end ``can_lower`` + ``candidates`` + ``build`` smoke tests.
"""Lowerer tests: candidate generation phase (GPU-agnostic).

These tests cover the path from a normalized fusion graph all the
way through HSACO build for each concrete lowerer. They do NOT
launch the kernels (no GPU); the goal is to confirm the lowerers
wire up a real launcher object on every supported region kind.
Tests that validate lowerers can recognize regions and generate
spec configurations without requiring GPU hardware. Architecture
is mocked to gfx950 so that tests can still run without a GPU.
"""

@classmethod
def setUpClass(cls):
"""Mock HIP runtime dependencies for in-process testing without GPU.

These tests do not launch kernels or require a physical GPU. To achieve this, we mock:

- ``get_device_arch``: Returns a fixed architecture string (gfx950)
so the compiler targets a known ISA without querying the actual
device via ``hipGetDeviceProperties``.

- ``Runtime.load_module``: Bypasses ``hipModuleLoadData``. The
mock allows tests to validate that lowerers produce a well-formed
launcher object without requiring a GPU driver.

Individual tests can override these mocks (via nested ``patch``
context managers) to exercise error paths or architecture-specific
behavior.
"""
cls.arch_patcher = patch(
"rocke.runtime.hip_module.get_device_arch", return_value="gfx950"
)

cls.load_module_patcher = patch(
"rocke.runtime.hip_module.Runtime.load_module", return_value=Mock()
)
cls.arch_patcher.start()
cls.load_module_patcher.start()

@classmethod
def tearDownClass(cls):
"""Clean up HIP runtime mocks after all tests complete."""
cls.arch_patcher.stop()
cls.load_module_patcher.stop()

def _toy_gemm_graph(self, with_epilogue=True):
from rocke.helpers import FusionOp, FusionTensor, build_graph

Expand Down Expand Up @@ -3130,7 +3148,33 @@ def test_gemm_epilogue_can_lower_and_emits_candidates(self):
for cfg in cfgs:
self.assertTrue(hasattr(cfg.spec, "_fused_epilogue"))

@_requires_gpu
def test_gemm_epilogue_candidates_errors_on_invalid_arch(self):
from rocke.helpers import GemmEpilogueLowerer, GreedyFusionScheduler

graph = self._toy_gemm_graph(with_epilogue=True)
plan = GreedyFusionScheduler().schedule(graph)
region = plan.regions[0]
lowerer = GemmEpilogueLowerer()
# Override the original class mock to test the error case when get_device_arch returns None
with patch("rocke.runtime.hip_module.get_device_arch", return_value=None):
with self.assertRaises(ValueError) as ctx:
lowerer.candidates(graph, region)
self.assertIn("Could not detect", str(ctx.exception))

def test_gemm_epilogue_build_errors_on_invalid_arch(self):
from rocke.helpers import GemmEpilogueLowerer, GreedyFusionScheduler

graph = self._toy_gemm_graph(with_epilogue=True)
plan = GreedyFusionScheduler().schedule(graph)
region = plan.regions[0]
lowerer = GemmEpilogueLowerer()
cfgs = lowerer.candidates(graph, region)
# Override the original class mock to test the error case when get_device_arch returns None
with patch("rocke.runtime.hip_module.get_device_arch", return_value=None):
with self.assertRaises(ValueError) as ctx:
lowerer.build(cfgs[0])
self.assertIn("Could not detect", str(ctx.exception))

def test_gemm_epilogue_build_emits_kernel_launcher(self):
Comment thread
yraparti marked this conversation as resolved.
from rocke.helpers import GemmEpilogueLowerer, GreedyFusionScheduler

Expand All @@ -3146,7 +3190,6 @@ def test_gemm_epilogue_build_emits_kernel_launcher(self):
self.assertGreater(built.block_size, 0)
self.assertEqual(built.extra.get("bias"), "bias")

@_requires_gpu
def test_elementwise_lowerer_round_trips(self):
from rocke.helpers import (
ElementwiseLowerer,
Expand Down Expand Up @@ -3181,7 +3224,6 @@ def test_elementwise_lowerer_round_trips(self):
self.assertEqual(built.spec.op, "relu")
self.assertEqual(built.spec.dtype, "f16")

@_requires_gpu
def test_reduction_lowerer_round_trips(self):
from rocke.helpers import (
FusionOp,
Expand Down
Loading