feat(tools): add a hardware oracle for RDNA instruction semantics

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Adrià Franch 2026-10-06 13:54:28 +02:00
parent 5895ea721a
commit f617b40a45
4 changed files with 405 additions and 0 deletions

64
docs/dev/HW_ORACLE.md Normal file
View File

@ -0,0 +1,64 @@
# Hardware oracle
`tools/hw-oracle/` runs a few lines of RDNA assembly on a local AMD GPU, one input row per lane, and returns what the
hardware computed. Use it to settle instruction semantics that the ISA documentation, LLVM or Mesa leave open, then pin the
measured rows as expected values in `tests/execution/`.
The PS5 GPU is gfx1013. Other RDNA2 GPUs (gfx103x) share the VALU, SALU, DS, MUBUF and MIMG encodings, apart from ops that
exist only on gfx10.1 (`v_mad_legacy_f32`, `v_mac_legacy_f32`, `v_mul_lo_i32`, `s_dcache_discard`, ...). RDNA3 also runs
the tool, but differs from gfx1013 in more places. Say which GPU you measured on in the PR.
## Requirements
- Linux with the `amdgpu` driver and read/write access to `/dev/kfd` (usually the `render` group). Root is not needed.
- The ROCm HSA runtime with its headers: `libhsa-runtime-dev` (Debian, Ubuntu), `rocm-runtime-devel` (Fedora),
`hsa-rocr` (Arch), or AMD's ROCm packages. A runtime under `ROCM_PATH` or `/opt/rocm` is used first. The rest of ROCm
is not needed.
- A C compiler, and `clang` + `ld.lld` with the AMDGPU target (`llc --version` lists `amdgcn`). If they aren't in
`PATH`, they are taken from `$ROCM_PATH/llvm/bin`.
The first run builds `oracle.c` into `~/.cache/anyps5-hw-oracle/` (`HW_ORACLE_CACHE` overrides it). The target is read
from the GPU (`HW_ORACLE_TARGET` overrides it).
## Kernel
`template.s` wraps the body under test. Per lane:
- `v4`-`v7` hold the row's 4 input dwords. `v10`-`v25` start at 0 and are stored as the result (16 dwords).
- Leave `v0` (lane id), `v1` (input offset), `v2` (output offset) and `s[4:7]` (input and output addresses) unchanged.
- 4 KiB of LDS. Rows run in workgroups of up to 1024 lanes, padded to whole waves with zero rows.
- Bytes passed as `extra` follow the rows of each dispatch, at `s[4:5] + 16 * rows`: buffer contents, texels, etc.
Build buffer and image descriptors in SGPRs from that address.
The FLOAT_MODE of the dispatch is set with `ieee` (0/1), `denorm32` and `denorm16` (0-3, 3 = denormals kept). Float
atomics need `coarse`: on fine-grained system memory they do nothing.
## Use
```sh
cat > body.s <<'EOF'
v_add_f16 v10, v4, v5
EOF
printf '0x3c00 0x3c00 0 0\n0x7e01 0x3c00 0 0\n' > rows.txt
python3 tools/hw-oracle/hw_oracle.py body.s rows.txt --outs 1 --ieee 0
```
prints one line per row with the output dwords in hex. From Python:
```python
import sys
sys.path.insert(0, "tools/hw-oracle")
from hw_oracle import run
rows = [(a, b, 0, 0) for a in values for b in values]
results = run(body, rows, wave64=True, denorm32=0)
```
Usual workflow: run the instruction on rows that separate the candidate models (edge values, NaN payloads, rounding
ties), write a model that matches every row, implement it, and put measured rows in the execution test.
## Limits
Only compute kernels. Pixel-shader ops that read their inputs from LDS also work: `v_interp_*` reads the attribute
parameters from LDS at `M0`, so writing them there by hand is enough. Behaviour that depends on the graphics pipeline
(parameter cache, exports, rasterization, `SPI_*` state) can't be measured with it.

View File

@ -0,0 +1,106 @@
import argparse
import os
import shutil
import struct
import subprocess
import sys
import tempfile
from functools import cache
from pathlib import Path
HERE = Path(__file__).resolve().parent
CACHE = Path(os.environ.get("HW_ORACLE_CACHE") or Path(os.environ.get("XDG_CACHE_HOME") or Path.home() / ".cache") / "anyps5-hw-oracle")
ROWS_PER_DISPATCH = 1024
def rocm_root():
for root in (os.environ.get("ROCM_PATH"), "/opt/rocm"):
if root and (Path(root) / "include" / "hsa" / "hsa.h").exists():
return Path(root)
return None
def tool(name):
found = shutil.which(name)
if found:
return found
root = rocm_root()
if root and (root / "llvm" / "bin" / name).exists():
return str(root / "llvm" / "bin" / name)
sys.exit(f"{name} not found: install LLVM with the AMDGPU target or set ROCM_PATH")
def oracle():
binary = CACHE / "oracle"
source = HERE / "oracle.c"
if binary.exists() and binary.stat().st_mtime >= source.stat().st_mtime:
return binary
CACHE.mkdir(parents=True, exist_ok=True)
command = [os.environ.get("CC", "cc"), "-O1", str(source), "-o", str(binary)]
root = rocm_root()
if root:
lib = root / "lib"
command += [f"-I{root / 'include'}", f"-L{lib}", f"-Wl,-rpath,{lib}"]
subprocess.run(command + ["-lhsa-runtime64"], check=True)
return binary
@cache
def target():
return os.environ.get("HW_ORACLE_TARGET") or subprocess.run([oracle(), "--target"], capture_output=True, text=True, check=True).stdout.strip()
def assemble(body, work, wave64, ieee, denorm32, denorm16):
text = (HERE / "template.s").read_text()
for key, value in (("@TARGET@", target()), ("@WAVE32@", "0" if wave64 else "1"), ("@WAVESIZE@", "64" if wave64 else "32"),
("@DENORM@", str(denorm32)), ("@DENORM16@", str(denorm16)), ("@IEEE@", str(ieee)), ("@BODY@", body)):
text = text.replace(key, value)
(work / "k.s").write_text(text)
subprocess.run([tool("clang"), "-x", "assembler", "-target", "amdgcn-amd-amdhsa", f"-mcpu={target()}", "-c", str(work / "k.s"), "-o", str(work / "k.o")], check=True)
subprocess.run([tool("ld.lld"), "-shared", str(work / "k.o"), "-o", str(work / "k.co")], check=True)
return work / "k.co"
def run(body, rows, extra=b"", wave64=False, ieee=1, denorm32=3, denorm16=3, coarse=False):
rows = [tuple(r) for r in rows]
wave = 64 if wave64 else 32
padded = rows + [(0, 0, 0, 0)] * (-len(rows) % wave)
env = dict(os.environ)
env.pop("COARSE", None)
if coarse:
env["COARSE"] = "1"
out = []
with tempfile.TemporaryDirectory() as tmp:
work = Path(tmp)
code = assemble(body, work, wave64, ieee, denorm32, denorm16)
for start in range(0, len(padded), ROWS_PER_DISPATCH):
chunk = padded[start:start + ROWS_PER_DISPATCH]
(work / "in.bin").write_bytes(b"".join(struct.pack("<4I", *r) for r in chunk) + extra)
subprocess.run([oracle(), str(code), str(len(chunk)), str(work / "in.bin"), str(work / "out.bin"), "16"], check=True, env=env)
data = (work / "out.bin").read_bytes()
out += [struct.unpack_from("<16I", data, lane * 64) for lane in range(len(chunk))]
return out[:len(rows)]
def main():
parser = argparse.ArgumentParser(description="Run an RDNA kernel body on the local AMD GPU, one input row per lane.")
parser.add_argument("body", type=Path, help="assembly inserted into template.s")
parser.add_argument("rows", type=Path, help="one row per line: 4 u32 values (decimal or 0x hex)")
parser.add_argument("--wave64", action="store_true")
parser.add_argument("--ieee", type=int, choices=(0, 1), default=1)
parser.add_argument("--denorm32", type=int, choices=range(4), default=3)
parser.add_argument("--denorm16", type=int, choices=range(4), default=3)
parser.add_argument("--coarse", action="store_true", help="input and output in coarse-grained GPU memory")
parser.add_argument("--extra", type=Path, help="bytes appended after the rows")
parser.add_argument("--outs", type=int, choices=range(1, 17), default=16, metavar="N", help="print v10..v(10+N-1)")
args = parser.parse_args()
rows = [tuple(int(v, 0) for v in line.split()) for line in args.rows.read_text().splitlines() if line.strip()]
if any(len(r) != 4 for r in rows):
sys.exit("every row needs 4 values")
extra = args.extra.read_bytes() if args.extra else b""
for result in run(args.body.read_text(), rows, extra, args.wave64, args.ieee, args.denorm32, args.denorm16, args.coarse):
print(" ".join(f"{v:08x}" for v in result[:args.outs]))
if __name__ == "__main__":
main()

165
tools/hw-oracle/oracle.c Normal file
View File

@ -0,0 +1,165 @@
#include <hsa/hsa.h>
#include <hsa/hsa_ext_amd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define CHECK(x) do { hsa_status_t s_ = (x); if (s_ != HSA_STATUS_SUCCESS && s_ != HSA_STATUS_INFO_BREAK) { const char* m_; hsa_status_string(s_, &m_); fprintf(stderr, "%s: %s\n", #x, m_); exit(1); } } while (0)
static hsa_agent_t gpu;
static int haveGpu;
static hsa_amd_memory_pool_t kernargPool, finePool;
static int haveKernarg, haveFine;
static hsa_amd_memory_pool_t coarsePool;
static int haveCoarse;
static hsa_status_t findCoarse(hsa_amd_memory_pool_t pool, void* data) {
hsa_amd_segment_t segment;
hsa_amd_memory_pool_get_info(pool, HSA_AMD_MEMORY_POOL_INFO_SEGMENT, &segment);
if (segment != HSA_AMD_SEGMENT_GLOBAL) return HSA_STATUS_SUCCESS;
uint32_t flags;
hsa_amd_memory_pool_get_info(pool, HSA_AMD_MEMORY_POOL_INFO_GLOBAL_FLAGS, &flags);
if ((flags & HSA_AMD_MEMORY_POOL_GLOBAL_FLAG_COARSE_GRAINED) && !haveCoarse) { coarsePool = pool; haveCoarse = 1; }
return HSA_STATUS_SUCCESS;
}
static hsa_status_t findGpu(hsa_agent_t agent, void* data) {
hsa_device_type_t type;
hsa_agent_get_info(agent, HSA_AGENT_INFO_DEVICE, &type);
if (type == HSA_DEVICE_TYPE_GPU) { gpu = agent; haveGpu = 1; return HSA_STATUS_INFO_BREAK; }
return HSA_STATUS_SUCCESS;
}
static hsa_status_t findPools(hsa_amd_memory_pool_t pool, void* data) {
hsa_amd_segment_t segment;
hsa_amd_memory_pool_get_info(pool, HSA_AMD_MEMORY_POOL_INFO_SEGMENT, &segment);
if (segment != HSA_AMD_SEGMENT_GLOBAL) return HSA_STATUS_SUCCESS;
uint32_t flags;
hsa_amd_memory_pool_get_info(pool, HSA_AMD_MEMORY_POOL_INFO_GLOBAL_FLAGS, &flags);
if ((flags & HSA_AMD_MEMORY_POOL_GLOBAL_FLAG_KERNARG_INIT) && !haveKernarg) { kernargPool = pool; haveKernarg = 1; }
if ((flags & HSA_AMD_MEMORY_POOL_GLOBAL_FLAG_FINE_GRAINED) && !haveFine) { finePool = pool; haveFine = 1; }
return HSA_STATUS_SUCCESS;
}
static hsa_status_t findCpu(hsa_agent_t agent, void* data) {
hsa_device_type_t type;
hsa_agent_get_info(agent, HSA_AGENT_INFO_DEVICE, &type);
if (type == HSA_DEVICE_TYPE_CPU) { hsa_amd_agent_iterate_memory_pools(agent, findPools, NULL); }
return HSA_STATUS_SUCCESS;
}
static void* readFile(const char* path, size_t* size) {
FILE* f = fopen(path, "rb");
if (!f) { perror(path); exit(1); }
fseek(f, 0, SEEK_END); *size = (size_t)ftell(f); fseek(f, 0, SEEK_SET);
void* data = malloc(*size);
if (fread(data, 1, *size, f) != *size) { perror(path); exit(1); }
fclose(f);
return data;
}
int main(int argc, char** argv) {
if (argc == 2 && strcmp(argv[1], "--target") == 0) {
CHECK(hsa_init());
CHECK(hsa_iterate_agents(findGpu, NULL));
if (!haveGpu) { fprintf(stderr, "no GPU agent\n"); return 1; }
char name[64] = {0};
CHECK(hsa_agent_get_info(gpu, HSA_AGENT_INFO_NAME, name));
printf("%s\n", name);
return 0;
}
if (argc != 6) { fprintf(stderr, "usage: oracle --target | oracle code.o threads in.bin out.bin out_dwords\n"); return 2; }
const uint32_t threads = (uint32_t)atoi(argv[2]);
const uint32_t outDwords = (uint32_t)atoi(argv[5]);
CHECK(hsa_init());
CHECK(hsa_iterate_agents(findGpu, NULL));
if (!haveGpu) { fprintf(stderr, "no GPU agent\n"); return 1; }
CHECK(hsa_iterate_agents(findCpu, NULL));
if (!haveKernarg || !haveFine) { fprintf(stderr, "no system memory pools\n"); return 1; }
size_t codeSize; void* code = readFile(argv[1], &codeSize);
hsa_code_object_reader_t reader;
CHECK(hsa_code_object_reader_create_from_memory(code, codeSize, &reader));
hsa_executable_t exe;
CHECK(hsa_executable_create_alt(HSA_PROFILE_FULL, HSA_DEFAULT_FLOAT_ROUNDING_MODE_DEFAULT, NULL, &exe));
CHECK(hsa_executable_load_agent_code_object(exe, gpu, reader, NULL, NULL));
CHECK(hsa_executable_freeze(exe, NULL));
hsa_executable_symbol_t sym;
CHECK(hsa_executable_get_symbol_by_name(exe, "probe.kd", &gpu, &sym));
uint64_t kernelObject; uint32_t kernargSize, groupSize, privateSize;
hsa_executable_symbol_get_info(sym, HSA_EXECUTABLE_SYMBOL_INFO_KERNEL_OBJECT, &kernelObject);
hsa_executable_symbol_get_info(sym, HSA_EXECUTABLE_SYMBOL_INFO_KERNEL_KERNARG_SEGMENT_SIZE, &kernargSize);
hsa_executable_symbol_get_info(sym, HSA_EXECUTABLE_SYMBOL_INFO_KERNEL_GROUP_SEGMENT_SIZE, &groupSize);
hsa_executable_symbol_get_info(sym, HSA_EXECUTABLE_SYMBOL_INFO_KERNEL_PRIVATE_SEGMENT_SIZE, &privateSize);
size_t inSize; void* inHost = readFile(argv[3], &inSize);
void *in, *out, *args;
const size_t outSize = (size_t)threads * outDwords * 4u;
const int coarse = getenv("COARSE") != NULL;
if (coarse) {
CHECK(hsa_amd_agent_iterate_memory_pools(gpu, findCoarse, NULL));
if (!haveCoarse) { fprintf(stderr, "no coarse-grained GPU pool\n"); return 1; }
}
void *inStage = NULL, *outStage = NULL;
CHECK(hsa_amd_memory_pool_allocate(coarse ? coarsePool : finePool, inSize, 0, &in));
CHECK(hsa_amd_memory_pool_allocate(coarse ? coarsePool : finePool, outSize, 0, &out));
if (coarse) {
CHECK(hsa_amd_memory_pool_allocate(finePool, inSize, 0, &inStage));
CHECK(hsa_amd_memory_pool_allocate(finePool, outSize, 0, &outStage));
CHECK(hsa_amd_agents_allow_access(1, &gpu, NULL, inStage));
CHECK(hsa_amd_agents_allow_access(1, &gpu, NULL, outStage));
}
CHECK(hsa_amd_memory_pool_allocate(kernargPool, 16, 0, &args));
if (!coarse) {
CHECK(hsa_amd_agents_allow_access(1, &gpu, NULL, in));
CHECK(hsa_amd_agents_allow_access(1, &gpu, NULL, out));
}
CHECK(hsa_amd_agents_allow_access(1, &gpu, NULL, args));
if (coarse) {
memcpy(inStage, inHost, inSize);
memset(outStage, 0xee, outSize);
CHECK(hsa_memory_copy(in, inStage, inSize));
CHECK(hsa_memory_copy(out, outStage, outSize));
} else {
memcpy(in, inHost, inSize);
memset(out, 0xee, outSize);
}
((void**)args)[0] = in;
((void**)args)[1] = out;
hsa_queue_t* queue;
CHECK(hsa_queue_create(gpu, 64, HSA_QUEUE_TYPE_SINGLE, NULL, NULL, UINT32_MAX, UINT32_MAX, &queue));
hsa_signal_t done;
CHECK(hsa_signal_create(1, 0, NULL, &done));
const uint64_t index = hsa_queue_add_write_index_relaxed(queue, 1);
hsa_kernel_dispatch_packet_t* packet = (hsa_kernel_dispatch_packet_t*)queue->base_address + (index & (queue->size - 1));
memset((uint8_t*)packet + 4, 0, sizeof(*packet) - 4);
packet->setup = 1u << HSA_KERNEL_DISPATCH_PACKET_SETUP_DIMENSIONS;
packet->workgroup_size_x = (uint16_t)(threads < 1024 ? threads : 1024);
packet->workgroup_size_y = 1;
packet->workgroup_size_z = 1;
packet->grid_size_x = threads;
packet->grid_size_y = 1;
packet->grid_size_z = 1;
packet->kernel_object = kernelObject;
packet->kernarg_address = args;
packet->private_segment_size = privateSize;
packet->group_segment_size = groupSize;
packet->completion_signal = done;
const uint16_t header = (HSA_PACKET_TYPE_KERNEL_DISPATCH << HSA_PACKET_HEADER_TYPE) |
(HSA_FENCE_SCOPE_SYSTEM << HSA_PACKET_HEADER_ACQUIRE_FENCE_SCOPE) | (HSA_FENCE_SCOPE_SYSTEM << HSA_PACKET_HEADER_RELEASE_FENCE_SCOPE);
__atomic_store_n((uint32_t*)packet, header | ((uint32_t)packet->setup << 16), __ATOMIC_RELEASE);
hsa_signal_store_screlease(queue->doorbell_signal, (hsa_signal_value_t)index);
if (hsa_signal_wait_scacquire(done, HSA_SIGNAL_CONDITION_LT, 1, 5000000000ull, HSA_WAIT_STATE_BLOCKED) != 0) {
fprintf(stderr, "kernel timed out\n");
return 1;
}
if (coarse) {
CHECK(hsa_memory_copy(outStage, out, outSize));
out = outStage;
}
FILE* f = fopen(argv[4], "wb");
if (!f || fwrite(out, 1, outSize, f) != outSize) { perror(argv[4]); return 1; }
fclose(f);
return 0;
}

View File

@ -0,0 +1,70 @@
.amdgcn_target "amdgcn-amd-amdhsa--@TARGET@"
.text
.globl probe
.p2align 8
.type probe,@function
probe:
s_load_dwordx4 s[4:7], s[0:1], 0x0
v_lshlrev_b32 v1, 4, v0
v_lshlrev_b32 v2, 6, v0
s_waitcnt lgkmcnt(0)
global_load_dwordx4 v[4:7], v1, s[4:5]
s_waitcnt vmcnt(0)
v_mov_b32 v10, 0
v_mov_b32 v11, 0
v_mov_b32 v12, 0
v_mov_b32 v13, 0
v_mov_b32 v14, 0
v_mov_b32 v15, 0
v_mov_b32 v16, 0
v_mov_b32 v17, 0
v_mov_b32 v18, 0
v_mov_b32 v19, 0
v_mov_b32 v20, 0
v_mov_b32 v21, 0
v_mov_b32 v22, 0
v_mov_b32 v23, 0
v_mov_b32 v24, 0
v_mov_b32 v25, 0
@BODY@
global_store_dwordx4 v2, v[10:13], s[6:7]
global_store_dwordx4 v2, v[14:17], s[6:7] offset:16
global_store_dwordx4 v2, v[18:21], s[6:7] offset:32
global_store_dwordx4 v2, v[22:25], s[6:7] offset:48
s_endpgm
.Lend:
.size probe, .Lend-probe
.rodata
.p2align 6
.amdhsa_kernel probe
.amdhsa_user_sgpr_kernarg_segment_ptr 1
.amdhsa_kernarg_size 16
.amdhsa_group_segment_fixed_size 4096
.amdhsa_next_free_vgpr 128
.amdhsa_next_free_sgpr 48
.amdhsa_wavefront_size32 @WAVE32@
.amdhsa_float_denorm_mode_32 @DENORM@
.amdhsa_float_denorm_mode_16_64 @DENORM16@
.amdhsa_ieee_mode @IEEE@
.end_amdhsa_kernel
.amdgpu_metadata
---
amdhsa.version: [ 1, 2 ]
amdhsa.kernels:
- .name: probe
.symbol: probe.kd
.kernarg_segment_size: 16
.group_segment_fixed_size: 4096
.private_segment_fixed_size: 0
.kernarg_segment_align: 8
.wavefront_size: @WAVESIZE@
.sgpr_count: 48
.vgpr_count: 64
.max_flat_workgroup_size: 1024
.args:
- { .size: 8, .offset: 0, .value_kind: global_buffer, .address_space: global }
- { .size: 8, .offset: 8, .value_kind: global_buffer, .address_space: global }
...
.end_amdgpu_metadata