Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -1689,9 +1689,14 @@ def _load_harness_with_fake_aiter():
from pathlib import Path
from unittest import mock

# Import torch BEFORE patching sys.modules so torch stays in the
import pytest

# The harness's reference implementation imports torch at module scope,
# so it cannot be loaded without torch; skip torch-free instead of
# erroring. Import torch BEFORE patching sys.modules so it stays in the
# parent process's module table after ``mock.patch.dict`` exits.
pytest.importorskip("torch")
import torch # noqa: F401

# The harness moved into the library tree (builders/); resolve it via the
# package system (editable-installed) rather than a hardcoded path, then
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,15 @@ class TestExampleAttentionShapes(unittest.TestCase):
"""Keep non-GPU CI anchored to attention shapes shipped with examples."""

def test_gfx950_attention_trace_samples_decode_and_prefill(self):
rows = _read_jsonl(
files("builders.gfx950.attention").joinpath("aiter_ua_shapes.json")
)
shapes = files("builders.gfx950.attention").joinpath("aiter_ua_shapes.json")
# TODO: PR #9098 ("test(rocke): attention benchmarks refactor") deleted
# aiter_ua_shapes.json from builders/gfx950/attention/ without removing or
# updating this test, so it errors on any develop-based checkout. Skip
# until the shapes fixture is restored (or this test is repointed at its
# replacement); re-enable once #9098's fallout is resolved upstream.
if not shapes.is_file():
self.skipTest("aiter_ua_shapes.json missing (removed by PR #9098)")
rows = _read_jsonl(shapes)
kinds = {row["kind"] for row in rows}
head_sizes = {int(row["head_size"]) for row in rows}
max_q = {int(row["max_seqlen_q"]) for row in rows}
Expand Down
40 changes: 40 additions & 0 deletions dnn-providers/hip-kernel-provider/rocke/platform/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,39 @@ PY
same virtualenv if it is missing. Do **not** use the default PyPI CPU torch for
GPU/numeric lanes. On-GPU lanes also need a HIP-visible device + ROCm `comgr`.

### torch is OPTIONAL

rocke is consumed downstream by PyTorch, so a hard dependency on torch would be
circular. torch is therefore **optional** and the only hard dep is `numpy`
(`pyproject.toml` / `requirements.txt`). torch is needed only for three lanes:

1. the `torch.fx` fusion frontend (`torch_backend.py`, `helpers/fuse.py`
graph capture);
2. torch-tensor launch integration (`runtime/torch_module.py`);
3. on-GPU numeric verification against torch eager.

Everything else runs torch-free: IR build, lower, `comgr` compile, numpy launch,
the byte-identity gate, and the CPU test suite (torch-only tests `importorskip`;
GPU tests skip via a torch-free `/dev/kfd` probe). `tests/run_all.py` is GREEN
with neither torch nor a GPU installed.

### ROCm library discovery (libamd_comgr / libamdhip64)

The runtime resolves the ROCm shared libs WITHOUT importing torch
(`runtime/hip_module._candidate_lib_paths` / `_rocm_root_libdirs`), in priority
order:

1. explicit full-path override env var (`ROCKE_COMGR_LIB`, `ROCKE_HIP_LIB`);
2. torch-bundled `<torch>/lib/lib*.so` — opportunistic fast-path **only if torch
is already imported** (never imports torch to get it);
3. a real ROCm install discovered without torch: `$ROCM_PATH` / `$ROCM_HOME` →
`<root>/lib`, then globbed `/opt/rocm*/core-*/lib` and `/opt/rocm*/lib`,
newest version first;
4. bare `lib<name>.so` on the dynamic linker's search path (last resort).

If you hit `cannot load libamd_comgr.so` in a torch-less process, set
`ROCM_PATH` (or `ROCKE_COMGR_LIB`) to point at your ROCm install.

## Hardware requirements for numeric tests

Most rocKE tests are CPU-only lowering, serialization, byte-identity, or static
Expand Down Expand Up @@ -153,6 +186,11 @@ 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`.
- **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
`importorskip` in tests and lazy/guarded imports in code. The ROCm-lib
resolver must keep discovering `comgr`/HIP without importing torch.

## dsl_docs

Expand Down Expand Up @@ -215,3 +253,5 @@ dual-engine vs Python-only split.
| `ROCKE_BACKEND` | `cpp` (default) \| `python` \| `both` (differential assert) |
| `ROCKE_CPP_STRICT` | `1` = raise instead of silently falling back to Python when `rocke_engine` isn't built |
| `ROCKE_LLVM_FLAVOR` | `llvm20` \| `llvm22` (must match the ROCm `comgr` in use) |
| `ROCM_PATH` / `ROCM_HOME` | ROCm install root; `<root>/lib` is searched for `comgr`/HIP before the globbed `/opt/rocm*` trees |
| `ROCKE_COMGR_LIB` / `ROCKE_HIP_LIB` | explicit full path to `libamd_comgr` / `libamdhip64`; wins over all discovery |
22 changes: 22 additions & 0 deletions dnn-providers/hip-kernel-provider/rocke/platform/BUILD.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,28 @@ Optional sanitizer build for diagnostics (not for shipping): `-DROCKE_SANITIZE=O
> The two engines must stay byte-identical — see the parity rule in
> [`dsl_docs/development/engine_parity.md`](dsl_docs/development/engine_parity.md).

## Runtime: finding ROCm (libamd_comgr / libamdhip64)

In-process compile + launch needs the ROCm shared libs at runtime. The Python
runtime resolves them WITHOUT importing torch
(`Python/rocke/runtime/hip_module._candidate_lib_paths` / `_rocm_root_libdirs`),
in priority order:

1. explicit full-path override env var: `ROCKE_COMGR_LIB`, `ROCKE_HIP_LIB`;
2. torch-bundled `<torch>/lib/lib*.so` — only if torch is already imported (the
resolver never imports torch to obtain it);
3. a real ROCm install discovered without torch: `$ROCM_PATH` / `$ROCM_HOME` ->
`<root>/lib`, then globbed `/opt/rocm*/core-*/lib` and `/opt/rocm*/lib`,
newest version first (a packaged ROCm 7.2 keeps the runtime under a versioned
`core-*/lib`, not always plain `/opt/rocm/lib`);
4. bare `lib<name>.so` on the dynamic linker's search path (last resort).

torch is therefore **optional** — required only for the `torch.fx` fusion
frontend, torch-tensor launch (`runtime/torch_module.py`), and on-GPU torch-eager
numeric checks. Building the engine, lowering, `comgr` compile, numpy launch, and
the byte-identity gate need no torch. If a torch-less process reports `cannot load
libamd_comgr.so`, set `ROCM_PATH` (or `ROCKE_COMGR_LIB`) to your ROCm install.

## The rocke_engine Python binding (optional)

The `cpp` backend of the Python frontend reaches the engine through the
Expand Down
Comment thread
shumway marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,9 @@ def resolved_lib_rocm_version() -> Optional[Tuple[int, int]]:

* torch-bundled (path under the imported torch's package dir) ->
``torch.version.hip``;
* a ROCm tree (``<root>/lib/libamd_comgr.so``) -> ``<root>/.info/version``
(with ``/opt/rocm`` as a final fallback).
* a ROCm tree (``<root>/lib/libamd_comgr.so`` or, on a packaged install,
``<root>/core-<X>/lib/libamd_comgr.so``) -> the install root's
``.info/version`` (with ``/opt/rocm`` as a final fallback).

Returns ``None`` when the path or version cannot be determined.
"""
Expand All @@ -158,13 +159,36 @@ def resolved_lib_rocm_version() -> Optional[Tuple[int, int]]:
if rp == tdir or rp.startswith(tdir + os.sep):
ver = getattr(getattr(torch_mod, "version", None), "hip", None)
return _parse_rocm_version(ver) if ver else None
# ROCm tree: <root>/lib/libamd_comgr.so -> <root>/.info/version.
root = os.path.dirname(os.path.dirname(rp))
for verfile in (os.path.join(root, ".info", "version"), "/opt/rocm/.info/version"):
ver = _read_rocm_version_file(verfile)
# ROCm tree -> the install *root's* ``.info/version``. Climb from the lib
# dir collecting every ``.info/version`` we pass. This is deliberately a
# climb, not a fixed ``dirname(dirname(path))``: a packaged ROCm 7.2 keeps
# comgr in a versioned ``core-<X>/lib`` subdir (e.g.
# ``/opt/rocm-7.2.0/core-7.13/lib``) which has its OWN ``.info/version``
# recording the *component* version (7.13.0) -- NOT the ROCm release. The
# ROCm release (7.2.0) lives in the top-level ``/opt/rocm-7.2.0/.info/
# version``, and getting it right is load-bearing: it drives LLVM-flavor
# selection (:func:`_assert_ir_flavor_matches_lib`). So we prefer the
# ``.info/version`` sitting in a directory whose name looks like a ROCm
# install root (``rocm`` / ``rocm-X.Y.Z``); failing that, the highest
# (closest to ``/``) one we found.
found: List[Tuple[str, Tuple[int, int]]] = []
d = os.path.dirname(rp)
prev = None
while d and d != prev:
ver = _read_rocm_version_file(os.path.join(d, ".info", "version"))
if ver is not None:
found.append((d, ver))
prev = d
d = os.path.dirname(d)
for dirpath, ver in found:
base = os.path.basename(dirpath)
if base == "rocm" or base.startswith("rocm-") or base.startswith("rocm_"):
return ver
return None
if found:
# No obvious ``rocm`` root in the name -> the outermost match is the
# install root (component subdirs are always deeper than it).
return found[-1][1]
return _read_rocm_version_file("/opt/rocm/.info/version")


def prefer_bundled_lib() -> Optional[Tuple[int, int]]:
Expand Down Expand Up @@ -374,6 +398,13 @@ def build_hsaco_from_llvm_ir(

# Fail fast + clean on an IR-flavor / comgr-vintage mismatch rather than
# letting comgr SIGABRT in codegen (see _assert_ir_flavor_matches_lib).
# Load comgr *before* the gate: resolved_lib_path() is first-existing until
# _lib is set, but _load_lib() picks first-*loadable*. With many roots
# emitted newest-first, an existing-but-unloadable install would otherwise
# let the gate validate the wrong vintage and pass, then the real lib aborts
# in codegen. Loading first makes the gate read the lib actually used. Free:
# the compile below loads comgr anyway.
_resolve_lib()
_assert_ir_flavor_matches_lib(ir_text)

# Handles are created lazily below; declare them up front so the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import ctypes
import glob
import os
import re
import sys
from dataclasses import dataclass
from typing import Any, Callable, Dict, List, Optional, Tuple
Expand Down Expand Up @@ -122,14 +123,86 @@ def _rocm_sdk_dll(stem: str) -> Optional[str]:
return None


def _version_key(path: str) -> Any:
"""Sort key that orders ROCm install dirs newest-first.

A plain string sort puts ``rocm-7.10`` *before* ``rocm-7.2`` (because
``'1' < '2'`` lexically) -- wrong for picking the newest toolkit. Extract
every run of digits from the path and compare them as an integer tuple so
``7.10`` > ``7.2``. Non-numeric paths sort last. Callers reverse the result
to get descending (newest-first) order.
"""
nums = tuple(int(n) for n in re.findall(r"\d+", path))
return (len(nums) > 0, nums)


def _rocm_root_libdirs() -> List[str]:
"""Existing ``<rocm>/lib`` directories discovered WITHOUT importing torch.

This is the crux of removing rocke's accidental torch dependency. The ROCm
torch wheel bundles ``libamdhip64.so`` / ``libamd_comgr.so`` inside
``torch/lib`` and, as a side effect of ``import torch``, drops that
directory onto the process's loader search path -- which is the *only*
reason a bare ``ctypes.CDLL("libamd_comgr.so")`` used to succeed here. A
library must never ``import torch`` to obtain that side effect (it would
invert the dependency and drag a multi-hundred-MB wheel into a pure-IR
process), so we discover a real ROCm install directly instead.

Priority, newest-version-first within each tier:
1. ``$ROCM_PATH`` / ``$ROCM_HOME`` -> ``<root>/lib`` (operator override).
2. Globbed real install layouts. On a packaged ROCm 7.2 there is often no
``/opt/rocm/lib`` with the runtime in it; the libs live under a
versioned ``core-<X>/lib`` subdir (e.g.
``/opt/rocm-7.2.0/core-7.13/lib``). Cover both ``/opt/rocm*/lib`` and
``/opt/rocm*/core-*/lib``.

Returns directories that exist, de-duplicated, in resolution order.
"""
roots: List[str] = []
seen: set = set()

def _add(d: str) -> None:
# De-dupe on the resolved real path so a symlinked root and its target
# don't both get probed; keep the original string for readable candidate
# paths.
if not d:
return
rp = os.path.realpath(d)
if rp and rp not in seen and os.path.isdir(rp):
seen.add(rp)
roots.append(d)
Comment thread
shumway marked this conversation as resolved.

# Tier 1: explicit env roots win over any globbed install.
for env in ("ROCM_PATH", "ROCM_HOME"):
root = os.environ.get(env)
if root:
_add(os.path.join(root, "lib"))

# Tier 2: glob real install trees, newest version first. ``core-*/lib`` is
# listed ahead of plain ``lib`` because that is where a packaged install
# actually keeps the runtime .so's.
for pattern in ("/opt/rocm*/core-*/lib", "/opt/rocm*/lib"):
for d in sorted(glob.glob(pattern), key=_version_key, reverse=True):
_add(d)
return roots


def _candidate_lib_paths(stem: str, env_var: str, sonames: List[str]) -> List[str]:
"""Resolution order for the HIP runtime / COMGR shared libraries.

Order:
1. ``$ROCKE_HIP_LIB`` (explicit override, full path).
2. ``<torch>/lib/lib<stem>.so`` if torch is already imported.
3. ``/opt/rocm/lib/lib<stem>.so`` and the requested SONAME variants.
4. Bare ``lib<stem>.so`` for the dynamic linker's search path.
1. ``$ROCKE_HIP_LIB`` / ``$ROCKE_COMGR_LIB`` (explicit override, full path).
2. ``<torch>/lib/lib<stem>.so`` if torch is *already* imported -- an
opportunistic fast-path only (see :func:`_torch_bundled_lib`); we never
import torch to populate it.
3. A real ROCm install discovered without torch (see
:func:`_rocm_root_libdirs`): ``$ROCM_PATH``/``$ROCM_HOME`` then globbed
``/opt/rocm*`` trees, newest version first, each with the bare ``.so``
and the requested SONAME variants.
4. Bare ``lib<stem>.so`` for the dynamic linker's search path -- last
resort. Historically this was the *only* non-torch candidate, which is
why a torch-less process failed with ``cannot load libamd_comgr.so``:
nothing had put the lib on the loader path. Tier 3 fixes that.
"""
paths: List[str] = []
override = os.environ.get(env_var)
Expand All @@ -142,10 +215,11 @@ def _candidate_lib_paths(stem: str, env_var: str, sonames: List[str]) -> List[st
if sdk is not None:
paths.append(sdk)
if _IS_WINDOWS:
# ROCm-for-Windows / HIP SDK install: ``%HIP_PATH%\bin`` then the
# bare DLL name (resolved via the default DLL search path). The
# comgr DLL carries a version suffix, so glob it.
for root_env in ("HIP_PATH", "ROCM_PATH"):
# ROCm-for-Windows / HIP SDK install: ``%HIP_PATH%\bin`` /
# ``%ROCM_PATH%\bin`` / ``%ROCM_HOME%\bin`` then the bare DLL name
# (resolved via the default DLL search path). The comgr DLL carries a
# version suffix, so glob it.
for root_env in ("HIP_PATH", "ROCM_PATH", "ROCM_HOME"):
root = os.environ.get(root_env)
if not root:
continue
Expand All @@ -154,9 +228,12 @@ def _candidate_lib_paths(stem: str, env_var: str, sonames: List[str]) -> List[st
paths.extend(sorted(glob.glob(os.path.join(bindir, f"{stem}*.dll"))))
paths.append(f"{stem}.dll")
return paths
paths.append(f"/opt/rocm/lib/lib{stem}.so")
for soname in sonames:
paths.append(f"/opt/rocm/lib/lib{stem}.so.{soname}")
# POSIX: each discovered ROCm ``<root>/lib`` contributes the bare .so plus
# SONAME-suffixed variants, newest install first.
for libdir in _rocm_root_libdirs():
paths.append(os.path.join(libdir, f"lib{stem}.so"))
for soname in sonames:
paths.append(os.path.join(libdir, f"lib{stem}.so.{soname}"))
paths.append(f"lib{stem}.so")
return paths

Expand Down
Loading
Loading