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
7 changes: 5 additions & 2 deletions dnn-providers/hip-kernel-provider/rocke/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,12 @@ if(ROCKE_BUILD_PYENV)
# repack) is inert in exactly the case it exists for: someone editing a
# kernel. There are two wheels and they need separate globs -- the kernel
# bodies live under library/, so wiring only the platform wheel would leave
# the real case broken.
# the real case broken. The platform glob takes the package's JSON data too:
# the arch-domain columns ship in that wheel and drive the lowerer, so
# regenerating one has to rebuild it like any source edit.
file(GLOB_RECURSE _rocke_platform_sources CONFIGURE_DEPENDS
"${CMAKE_CURRENT_SOURCE_DIR}/platform/python/*.py")
"${CMAKE_CURRENT_SOURCE_DIR}/platform/python/*.py"
"${CMAKE_CURRENT_SOURCE_DIR}/platform/python/*.json")
file(GLOB_RECURSE _rocke_library_sources CONFIGURE_DEPENDS
"${CMAKE_CURRENT_SOURCE_DIR}/library/*.py")

Expand Down
81 changes: 55 additions & 26 deletions dnn-providers/hip-kernel-provider/rocke/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -364,39 +364,68 @@ fix cannot leave dead weight behind.

### 4.5 Can this target lower this intrinsic? (the arch-domain gate)

§4.4 asks a compiler. The **arch-domain gate**,
[`check_arch_domain.py`](platform/tools/check_arch_domain.py), asks *committed
measured data* instead: `tools/gen_arch_domain.py` probes, per (declaration key,
gfx target), whether that declaration actually links, and commits one JSON column
per LLVM flavor under `platform/python/rocke/core/arch/data/`. The lowerer's
`_need` chokepoint consults that table on every intrinsic demand and warns
(`ArchDomainWarning`) when the answer is a measured negative. This gate lowers
the same corpus §4.4 uses, harvests the warnings, and compares them against an
`EXPECTED_WARNINGS` allowlist that names, for each entry, the corpus cases allowed
to demand it.

The two gates overlap on purpose and neither subsumes the other:
§4.4 asks a compiler. The **arch-domain lane** asks *committed measured data*
instead: `tools/gen_arch_domain.py` probes, per (declaration key, gfx target),
whether that declaration actually links, and commits one JSON column per LLVM
flavor under `platform/python/rocke/core/arch/data/`. Both engines consult it at
their demand chokepoint — `_Lowerer._need` in Python, `rocke_ll_need` in C++ —
on every intrinsic a kernel demands. The C++ engine's copy of the table is
generated from the same columns at build time (`tools/gen_arch_domain_cpp.py`),
so there is one table and no hand-maintained mirror; regenerating a column needs
no other step.

What the lane does with a measured negative is `ROCKE_ARCH_DOMAIN`:

| mode | effect |
|---|---|
| `error` | refuse the lowering with `ArchDomainError` (a `NotImplementedError`), unless the cell is waived; both engines refuse with the same message |
| `warn` (default) | report it as `ArchDomainWarning` and keep lowering |
| `off` | skip the lookup |

Any other value raises in both engines rather than falling back to the
default. On the default `cpp` backend a refusal is raised, never "fallen back"
from: the Python lowerer reads the same table and would refuse the same kernel.
The C++ engine has no warning channel, so on that backend the reports come from
a pass over the IR it emitted, which also checks that it refused what it had to.

**Only a definite negative speaks.** `arch_absent` is the sole negative status.
A flavor with no committed column, an unswept target, a key absent from the
table, and the no-data statuses (`target_unsupported`, `toolchain_crash`,
`toolchain_timeout`, `probe_error`) are all silent — 14% of the llvm20 column is
no-data, and warning on those would train people to ignore the lane.
`name_absent` is deliberately *not* reported: §4.4 already catches a reachable
one with a real diagnostic.

**The escape hatch is one cell.** A row can be measured wrong, and in `error`
mode a wrong `arch_absent` would block every kernel that needs that intrinsic
until the column is fixed. `platform/python/rocke/core/arch/data/arch_domain_waivers.json`
lists waived `(key, arch, flavor)` cells, each with the reason the measurement is
wrong; both engines read it through the same build step as the columns, and a
waived cell is reported instead of refused — never silenced. It is for a wrong
measurement, not a kernel bug: a kernel that demands an intrinsic its target
cannot lower is fixed in the kernel. `ROCKE_ARCH_DOMAIN=warn` reports every
negative instead of stopping at the first, which is a way to diagnose, not to
ship.

The gate, [`check_arch_domain.py`](platform/tools/check_arch_domain.py), lowers
the same corpus §4.4 uses at every committed flavor with the lane in `warn` mode
and goes red on:

- a negative with no waiver: a kernel is demanding an intrinsic its target
cannot lower;
- a stale waiver: its cell no longer measures negative, so the column was fixed
and the waiver is deleted in the same change;
- when the `rocke_engine` extension is importable, the C++ engine in `error`
mode refusing a different set of cases than the Python lowerer would.

This gate and §4.4 overlap on purpose and neither subsumes the other:

- **It needs no toolchain.** Lowering is pure Python and the table is committed
data, so this gate rules on *every* flavor on any host — where §4.4 can only
ever rule on the host's own and reports `UNVALIDATED` for the rest.
- **It names the intrinsic.** §4.4 reports whatever the backend chose to say,
often an unnamed fatal error. This reports `(key, arch, flavor)` plus the
diagnostic the probe recorded.
- **Only a definite negative speaks.** `arch_absent` is the sole negative
status. A flavor with no committed column, an unswept target, a key absent
from the table, and the no-data statuses (`target_unsupported`,
`toolchain_crash`, `toolchain_timeout`, `probe_error`) are all silent — 14% of
the llvm20 column is no-data, and warning on those would train people to
ignore the lane.
`name_absent` is deliberately *not* warned on: §4.4 already catches a reachable
one with a real diagnostic.

`EXPECTED_WARNINGS` carries the same rule as `KNOWN_BAD`: **it only shrinks**,
and an entry, or one of its cases, that stops firing is itself a failure. Naming
the cases is what stops it widening in silence: a different case that starts
demanding an allowlisted intrinsic is a new finding. `ROCKE_ARCH_DOMAIN=off`
disables the lookup.

### 4.6 Does the measured arch-domain artifact reproduce?

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,7 @@ dual-engine vs Python-only split.

| flag | meaning |
|---|---|
| `ROCKE_ARCH_DOMAIN` | `warn` (default) \| `off` — warn when a kernel demands an intrinsic the committed arch-domain table says the target cannot lower. `off` skips the lookup entirely. Any other value raises rather than falling back to `warn`: every way of misspelling this is an attempt to switch the lane *off*, and defaulting would leave it on |
| `ROCKE_ARCH_DOMAIN` | `error` \| `warn` (default) \| `off` — what both engines do when a kernel demands an intrinsic the committed arch-domain table says the target cannot lower. `error` refuses the lowering (`ArchDomainError`) unless the cell is waived in `arch_domain_waivers.json`; `warn` reports it; `off` skips the lookup. Any other value raises rather than falling back to the default, because a misspelling is always an attempt to change what the lane does. A wrong measurement is waived per cell, not worked around with this variable; see TESTING.md, the arch-domain gate |
| `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_DEBUG_LOC` | `1` = capture Python source locations while building and emit DWARF, so ATT traces map to source; off by default because it changes the emitted `.ll` |
Expand Down
11 changes: 10 additions & 1 deletion dnn-providers/hip-kernel-provider/rocke/platform/BUILD.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,13 @@ cmake --build <build> --target rocke_core -j$(nproc)
The top-level `<rocke/platform>/CMakeLists.txt` globs `cpp/**/*.cpp` (excluding
`cpp/bindings/`) into `rocke_core`, with the public ABI headers at `cpp/include`.

Configuring needs a Python 3 interpreter. The engine's copy of the intrinsic
arch-domain table is generated at build time from the JSON columns under
`python/rocke/core/arch/data/`, which the Python engine reads directly, by
`tools/gen_arch_domain_cpp.py` (standard library only). The output goes to
`<build>/generated/` and is never committed, so regenerating a column with
`tools/gen_arch_domain.py` reaches both engines with no other step.

Optional sanitizer build for diagnostics (not for shipping): `-DROCKE_SANITIZE=ON`.

### The shared engine (`librocke.so`)
Expand Down Expand Up @@ -198,7 +205,9 @@ can detect a mismatch rather than rely on rebuild discipline.
`cmake/rocke_build_id.cmake` computes a deterministic, git-independent content
hash over the repository-owned inputs to the shared engine and replay CLI: the
linked C++ sources and headers (excluding pybind-only sources), the replay CLI
source, and the CMake recipes that build and stamp both artifacts.
source, the CMake recipes that build and stamp both artifacts, and the inputs of
the generated arch-domain table (the committed columns, `arch_domain_waivers.json`,
`domain.py` and `tools/gen_arch_domain_cpp.py`).

CMake computes the ID while configuring for diagnostics, then
`cmake/generate_rocke_build_id.cmake` recomputes it on every build. A source or
Expand Down
43 changes: 35 additions & 8 deletions dnn-providers/hip-kernel-provider/rocke/platform/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ cmake_minimum_required(VERSION 3.16)
project(rocke LANGUAGES CXX)

include(GNUInstallDirs)
find_package(Python3 COMPONENTS Interpreter QUIET)
# Required: the engine's copy of the intrinsic arch-domain table is generated
# at build time from the committed JSON columns (see below).
find_package(Python3 COMPONENTS Interpreter REQUIRED)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
Expand Down Expand Up @@ -146,12 +148,8 @@ function(rocke_cxx_test_installed_name out_var target)
endif()
endfunction()

if(Python3_Interpreter_FOUND)
set(_ROCKE_DEFAULT_PYTHON_INSTALL_DIR
"${CMAKE_INSTALL_LIBDIR}/python${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR}/site-packages")
else()
set(_ROCKE_DEFAULT_PYTHON_INSTALL_DIR "${CMAKE_INSTALL_LIBDIR}/python/site-packages")
endif()
set(_ROCKE_DEFAULT_PYTHON_INSTALL_DIR
"${CMAKE_INSTALL_LIBDIR}/python${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR}/site-packages")
set(ROCKE_PYTHON_INSTALL_DIR "${_ROCKE_DEFAULT_PYTHON_INSTALL_DIR}"
CACHE STRING "Relative install destination for rocke and rocke_engine")

Expand All @@ -177,6 +175,36 @@ target_include_directories(rocke_core PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/cpp/include>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>)

# --- Intrinsic arch-domain table ---------------------------------------------
# Which intrinsic declarations each gfx target can lower is measured by
# tools/gen_arch_domain.py and committed as one JSON column per LLVM flavor,
# beside the reviewed waivers in arch_domain_waivers.json. Those files are the
# one source of truth. The Python engine reads them when it lowers; this engine
# gets a copy generated here, through the same Python reader, so there is no
# hand-maintained mirror to drift. Regenerating a column or editing a waiver
# needs no other step: the next build rewrites this file. It lives in the
# build tree and is never committed.
set(_rocke_arch_domain_inc
"${CMAKE_CURRENT_BINARY_DIR}/generated/rocke/arch_domain_table.inc")
file(GLOB _rocke_arch_domain_columns CONFIGURE_DEPENDS
"${CMAKE_CURRENT_SOURCE_DIR}/python/rocke/core/arch/data/intrinsic_arch_domain.*.json")
add_custom_command(
OUTPUT "${_rocke_arch_domain_inc}"
COMMAND "${Python3_EXECUTABLE}" -I
"${CMAKE_CURRENT_SOURCE_DIR}/tools/gen_arch_domain_cpp.py"
--out "${_rocke_arch_domain_inc}"
DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/tools/gen_arch_domain_cpp.py"
"${CMAKE_CURRENT_SOURCE_DIR}/python/rocke/core/arch/domain.py"
"${CMAKE_CURRENT_SOURCE_DIR}/python/rocke/core/arch/data/arch_domain_waivers.json"
${_rocke_arch_domain_columns}
COMMENT "Generating the C++ arch-domain table from the committed columns"
VERBATIM)
# The objects are compiled once in rocke_core_objects and shared by the static
# and shared libraries, so the table belongs to that target.
target_sources(rocke_core_objects PRIVATE "${_rocke_arch_domain_inc}")
target_include_directories(rocke_core_objects PRIVATE
"${CMAKE_CURRENT_BINARY_DIR}/generated")

# --- Portable-IR bundle freshness / provenance stamp -------------------------
# Generate a deterministic, git-independent provenance header from the engine
# and replay CLI sources plus their CMake build recipes. This common bundle ID is
Expand Down Expand Up @@ -398,7 +426,6 @@ if(ROCKE_INSTALL_TESTS)
PATTERN "test_wavescope_sidecar.py" EXCLUDE
PATTERN "test_lds_analyzer.py" EXCLUDE
PATTERN "test_arch_domain_artifact.py" EXCLUDE
# Configures source CMake modules; not an installed runtime test.
PATTERN "test_gpu_references.py" EXCLUDE)

endblock()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ two engines must stay byte-identical (see [`dsl_docs/development/engine_parity.m
compiler’s LLVM version; ROCm release metadata does not select it. ROCm is a
system dependency, not a pip package.
- **Python 3.10+**, a C++20 compiler (`amdclang++`/`clang++`/`g++`), CMake 3.16+
(3.18+ for the `cpp/bindings` pybind module).
(3.18+ for the `cpp/bindings` pybind module). The C++ build needs the Python
interpreter too: it generates the engine's intrinsic arch-domain table from the
committed JSON columns (`tools/gen_arch_domain_cpp.py`), standard library only.
- Python deps: `pip install -r requirements.txt` (adds `numpy`, plus `pytest` /
`pybind11` in the `dev` extra). **torch** must be the ROCm build for your
system, installed separately from the ROCm wheel index (not via this repo),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ function(rocke_build_id_inputs out_var root_dir)
"${root_dir}/cpp/*.hpp")
list(FILTER _rocke_all EXCLUDE REGEX "/cpp/bindings/")

# The engine's arch-domain table is generated from these at build time
# (tools/gen_arch_domain_cpp.py), so they change what the engine does as
# surely as its sources do. Keep this list mirrored in online.py.
file(GLOB _rocke_arch_domain
Comment thread
CongMa13 marked this conversation as resolved.
Resolved
"${root_dir}/python/rocke/core/arch/data/intrinsic_arch_domain.*.json")
list(APPEND _rocke_all ${_rocke_arch_domain}
"${root_dir}/python/rocke/core/arch/data/arch_domain_waivers.json"
"${root_dir}/python/rocke/core/arch/domain.py"
"${root_dir}/tools/gen_arch_domain_cpp.py")

# Include the CLI frontend and the build recipes that select, stamp, compile,
# and link the two native artifacts. Keep this list mirrored in online.py.
foreach(_rocke_input
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
#include <stdexcept>
#include <string>

#include "rocke/ir.h" /* rocke_status_t */

/* The gfx target every binding lowers or builds for. Required: there is no
* default target on either engine. An empty string used to stand for each
* family's "usual" arch (gfx950, gfx942, gfx1151 or gfx1201), so a caller that
Expand All @@ -45,6 +47,24 @@ inline const char* require_arch(const std::string& arch)
return arch.c_str();
}

/* A lowering the arch-domain lane refused (ROCKE_ERR_ARCH_DOMAIN): a verdict
* about the kernel, not an engine failure. The module registers it as
* rocke_engine.ArchDomainError, a NotImplementedError carrying the engine's
* message verbatim -- the same bytes the Python lowerer raises -- so the cpp
* backend can re-raise it rather than fall back to the Python lowerer. */
struct ArchDomainRefusal : std::runtime_error
{
using std::runtime_error::runtime_error;
};

inline void throw_if_arch_domain(rocke_status_t st, const char* err)
{
if(st == ROCKE_ERR_ARCH_DOMAIN)
{
throw ArchDomainRefusal(err != nullptr ? err : "");
}
}

/* serialize-IR body for a family whose kernel is built with a local IRBuilder
* `b`. FN is the fully-qualified entry name used in diagnostics, SPECT is the
* spec struct type, SPECBUILD is the spec-builder symbol called as
Expand Down
Loading
Loading