From 3cdd69c833d52aa2f1f20d2a8044f0edc8b5e17d Mon Sep 17 00:00:00 2001 From: AaronStGeorge Date: Wed, 30 Sep 2026 23:01:17 +0000 Subject: [PATCH 1/3] ggml-hrx: enforce a minimum HRX source revision Assisted-by: Codex --- ggml/src/ggml-hrx/CMakeLists.txt | 3 + ggml/src/ggml-hrx/README.md | 29 ++++ ggml/src/ggml-hrx/cmake/MinimumRevision.cmake | 61 +++++++ ggml/src/ggml-hrx/hrx-minimum-commit.txt | 1 + tests/CMakeLists.txt | 4 + tests/test-hrx-minimum-commit.py | 156 ++++++++++++++++++ 6 files changed, 254 insertions(+) create mode 100644 ggml/src/ggml-hrx/README.md create mode 100644 ggml/src/ggml-hrx/cmake/MinimumRevision.cmake create mode 100644 ggml/src/ggml-hrx/hrx-minimum-commit.txt create mode 100644 tests/test-hrx-minimum-commit.py diff --git a/ggml/src/ggml-hrx/CMakeLists.txt b/ggml/src/ggml-hrx/CMakeLists.txt index 6e15ce13931e..2626beb46b11 100644 --- a/ggml/src/ggml-hrx/CMakeLists.txt +++ b/ggml/src/ggml-hrx/CMakeLists.txt @@ -9,6 +9,9 @@ if(HRX_SOURCE_DIR) message(FATAL_ERROR "HRX_SOURCE_DIR does not contain a CMakeLists.txt: ${HRX_SOURCE_DIR}") endif() + include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/MinimumRevision.cmake") + ggml_hrx_check_minimum_revision("${HRX_SOURCE_DIR}" "${CMAKE_CURRENT_SOURCE_DIR}/hrx-minimum-commit.txt") + set(GGML_HRX_PREFIX "${CMAKE_CURRENT_BINARY_DIR}/hrx") set(GGML_HRX_BUILD_DIR "${GGML_HRX_PREFIX}/src/ggml-hrx-deps-build") set(GGML_HRX_LIB "${GGML_HRX_BUILD_DIR}/libhrx/src/libhrx/${CMAKE_SHARED_LIBRARY_PREFIX}hrx${CMAKE_SHARED_LIBRARY_SUFFIX}") diff --git a/ggml/src/ggml-hrx/README.md b/ggml/src/ggml-hrx/README.md new file mode 100644 index 000000000000..3a159ac03caf --- /dev/null +++ b/ggml/src/ggml-hrx/README.md @@ -0,0 +1,29 @@ +# HRX backend compatibility + +`hrx-minimum-commit.txt` records the minimum required commit in ROCm/hrx-system. It contains one full Git commit SHA. The backend's C++ APIs, Loom syntax, and numerical behavior can all depend on that revision. + +When `GGML_HRX=ON` and `HRX_SOURCE_DIR` is set, CMake requires the HRX checkout's HEAD to equal or descend from the recorded commit. The check uses local Git ancestry, not dates or version strings. It supports submodules and worktrees. Local edits are allowed but are not verified by the ancestry check. + +A missing commit, insufficient shallow history, or a source archive cannot establish compatibility and causes configuration to fail. Supply a Git checkout with sufficient history and rerun CMake. Configuration never fetches history or changes the checkout. Installed HRX/Loom packages remain outside this check. + +## Updating the minimum + +Raise the minimum in the same llama.cpp commit that introduces a newer HRX dependency. This includes new API calls, new Loom syntax, and removing a numerical workaround after an HRX compiler or runtime fix. Unrelated repairs do not require a bump. + +Choose the earliest demonstrated required commit that retains the previous floor in its ancestry. Do not replace the floor with the latest tested revision merely because it is available. Record the dependency and its HRX commit or PR in the change's validation evidence. A minimum is not a promise that every future HRX revision is compatible. + +The initial minimum, `40b7cc13717d9d4c7f7eb0e6885563042153566b` ([HRX #1054](https://github.com/ROCm/hrx-system/pull/1054)), preserves CFG guard proofs against constant bounds. The Q6 prefill JIT case with 512 tokens, 5120 inputs, and 1984 outputs needs that proof to compile its guarded fragment stores. The preceding commit fails with `SUBRANGE/010`; this revision passes the JIT compile suite. + +This minimum also contains `cba4f16b2902c218be16f5394fad94563eb68173`, which corrects packed AMDGPU table lookup ordering. The IQ4 decoders now use logical table indices without the old XOR-12 compensation and require that correction. + +For builds using installed packages, check the source revision used to produce both HRX and Loom against this floor before validating a repair. From the staging checkout: + +```sh +git -C hrx-system merge-base --is-ancestor "$(cat llama.cpp/ggml/src/ggml-hrx/hrx-minimum-commit.txt)" HEAD +``` + +This source check is useful only when the tested libraries were built from that checkout; record the actual artifact provenance as well. + +## Configure-check tests + +Run `python3 tests/test-hrx-minimum-commit.py` from the llama.cpp root. The tests use temporary Git repositories and CMake projects and require no HRX build or GPU. diff --git a/ggml/src/ggml-hrx/cmake/MinimumRevision.cmake b/ggml/src/ggml-hrx/cmake/MinimumRevision.cmake new file mode 100644 index 000000000000..c7e3d1e2a802 --- /dev/null +++ b/ggml/src/ggml-hrx/cmake/MinimumRevision.cmake @@ -0,0 +1,61 @@ +# The minimum revision records HRX behavior required by the backend and its Loom kernels. +# Source builds must prove that revision is in HEAD's ancestry before configuring dependencies. +# Only local Git history is read; missing history is an error, never a reason to fetch or skip the check. + +function(ggml_hrx_check_minimum_revision SOURCE_DIR PIN_FILE) + file(READ "${PIN_FILE}" GGML_HRX_MINIMUM_REVISION) + string(STRIP "${GGML_HRX_MINIMUM_REVISION}" GGML_HRX_MINIMUM_REVISION) + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${PIN_FILE}") + + set(GGML_HRX_REVISION_HELP + "HRX_SOURCE_DIR: ${SOURCE_DIR}\nRequired HRX commit: ${GGML_HRX_MINIMUM_REVISION}\nUse an HRX Git checkout at this commit or a descendant. Fetch missing history manually (unshallow a shallow clone if needed), then rerun CMake.") + find_package(Git QUIET) + if(NOT GIT_FOUND) + message(FATAL_ERROR "Git is required to verify the HRX minimum revision. Install Git and rerun CMake.\n${GGML_HRX_REVISION_HELP}") + endif() + + execute_process( + COMMAND "${GIT_EXECUTABLE}" -C "${SOURCE_DIR}" rev-parse --show-toplevel + RESULT_VARIABLE GGML_HRX_ROOT_RESULT + OUTPUT_VARIABLE GGML_HRX_GIT_ROOT + ERROR_VARIABLE GGML_HRX_GIT_ERROR + OUTPUT_STRIP_TRAILING_WHITESPACE) + if(NOT GGML_HRX_ROOT_RESULT EQUAL 0) + message(FATAL_ERROR "Cannot identify the HRX Git checkout: ${GGML_HRX_GIT_ERROR}\n${GGML_HRX_REVISION_HELP}") + endif() + + # Git also discovers enclosing repositories; an unpacked HRX tree inside one is not an HRX checkout. + get_filename_component(GGML_HRX_SOURCE_REAL "${SOURCE_DIR}" REALPATH) + get_filename_component(GGML_HRX_GIT_ROOT_REAL "${GGML_HRX_GIT_ROOT}" REALPATH) + if(NOT GGML_HRX_SOURCE_REAL STREQUAL GGML_HRX_GIT_ROOT_REAL) + message(FATAL_ERROR "HRX_SOURCE_DIR must be the root of its own Git checkout, not a directory inside ${GGML_HRX_GIT_ROOT}.\n${GGML_HRX_REVISION_HELP}") + endif() + + execute_process( + COMMAND "${GIT_EXECUTABLE}" -C "${SOURCE_DIR}" rev-parse --verify "HEAD^{commit}" + RESULT_VARIABLE GGML_HRX_HEAD_RESULT + OUTPUT_VARIABLE GGML_HRX_HEAD + ERROR_VARIABLE GGML_HRX_GIT_ERROR + OUTPUT_STRIP_TRAILING_WHITESPACE) + if(NOT GGML_HRX_HEAD_RESULT EQUAL 0) + message(FATAL_ERROR "Cannot resolve HRX HEAD: ${GGML_HRX_GIT_ERROR}\n${GGML_HRX_REVISION_HELP}") + endif() + string(APPEND GGML_HRX_REVISION_HELP "\nActual HRX HEAD: ${GGML_HRX_HEAD}") + + execute_process( + COMMAND "${GIT_EXECUTABLE}" -C "${SOURCE_DIR}" cat-file -e "${GGML_HRX_MINIMUM_REVISION}^{commit}" + RESULT_VARIABLE GGML_HRX_PIN_RESULT + ERROR_VARIABLE GGML_HRX_GIT_ERROR) + if(NOT GGML_HRX_PIN_RESULT EQUAL 0) + message(FATAL_ERROR "Required HRX commit is unavailable locally: ${GGML_HRX_GIT_ERROR}\n${GGML_HRX_REVISION_HELP}") + endif() + + execute_process( + COMMAND "${GIT_EXECUTABLE}" -C "${SOURCE_DIR}" merge-base --is-ancestor "${GGML_HRX_MINIMUM_REVISION}" "${GGML_HRX_HEAD}" + RESULT_VARIABLE GGML_HRX_ANCESTRY_RESULT + ERROR_VARIABLE GGML_HRX_GIT_ERROR) + if(NOT GGML_HRX_ANCESTRY_RESULT EQUAL 0) + message(FATAL_ERROR "HRX HEAD does not have the required commit in its available ancestry: ${GGML_HRX_GIT_ERROR}\n${GGML_HRX_REVISION_HELP}") + endif() + message(STATUS "HRX revision ${GGML_HRX_HEAD} satisfies minimum ${GGML_HRX_MINIMUM_REVISION}") +endfunction() diff --git a/ggml/src/ggml-hrx/hrx-minimum-commit.txt b/ggml/src/ggml-hrx/hrx-minimum-commit.txt new file mode 100644 index 000000000000..82d19141ff2b --- /dev/null +++ b/ggml/src/ggml-hrx/hrx-minimum-commit.txt @@ -0,0 +1 @@ +40b7cc13717d9d4c7f7eb0e6885563042153566b diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index b53be5e8a127..359ea666bc4a 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -348,6 +348,10 @@ if (TARGET ggml-hrx) target_include_directories(hrx-backend-test PRIVATE ../ggml/include ../ggml/src ../ggml/src/ggml-hrx) add_test(NAME hrx-backend-test COMMAND hrx-backend-test) + find_package(Python3 REQUIRED COMPONENTS Interpreter) + add_test(NAME test-hrx-minimum-commit + COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test-hrx-minimum-commit.py) + add_executable(test-hrx-loom-jit test-hrx-loom-jit.cpp) target_link_libraries(test-hrx-loom-jit PRIVATE ggml-hrx ggml-hrx-kernel-corpus hrx::hrx loomc::loomc) target_include_directories(test-hrx-loom-jit PRIVATE ../ggml/include ../ggml/src ../ggml/src/ggml-hrx) diff --git a/tests/test-hrx-minimum-commit.py b/tests/test-hrx-minimum-commit.py new file mode 100644 index 000000000000..665495d2747c --- /dev/null +++ b/tests/test-hrx-minimum-commit.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python3 +"""Exercise the HRX configure gate with local Git histories; no GPU is needed.""" + +import os +from pathlib import Path +import shutil +import subprocess +import tempfile +import time +import unittest + + +CHECK = Path(__file__).resolve().parents[1] / "ggml/src/ggml-hrx/cmake/MinimumRevision.cmake" + + +class MinimumRevisionTests(unittest.TestCase): + def setUp(self): + self.temporary = tempfile.TemporaryDirectory(prefix="hrx minimum ") + self.addCleanup(self.temporary.cleanup) + self.root = Path(self.temporary.name) + self.hrx = self.root / "hrx" + self.git("init", "--quiet", str(self.hrx)) + self.git("config", "user.name", "HRX test", cwd=self.hrx) + self.git("config", "user.email", "hrx-test@example.invalid", cwd=self.hrx) + self.git("config", "commit.gpgsign", "false", cwd=self.hrx) + self.old = self.commit("old") + self.minimum = self.commit("minimum") + self.new = self.commit("new") + self.project = self.root / "project" + self.project.mkdir() + self.pin = self.project / "minimum.txt" + self.pin.write_text(self.minimum + "\n", encoding="utf-8") + (self.project / "CMakeLists.txt").write_text( + 'cmake_minimum_required(VERSION 3.14)\n' + 'project(hrx_revision_test NONE)\n' + f'include("{CHECK.as_posix()}")\n' + 'ggml_hrx_check_minimum_revision("${HRX_SOURCE_DIR}" "${CMAKE_CURRENT_SOURCE_DIR}/minimum.txt")\n', + encoding="utf-8", + ) + self.build = self.root / "build" + + def git(self, *args, cwd=None): + return subprocess.run( + ["git", *args], cwd=cwd, check=True, text=True, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + ).stdout.strip() + + def commit(self, name): + self.git("commit", "--quiet", "--allow-empty", "-m", name, cwd=self.hrx) + return self.git("rev-parse", "HEAD", cwd=self.hrx) + + def configure(self, source=None, *extra): + return subprocess.run( + ["cmake", "-S", str(self.project), "-B", str(self.build), + f"-DHRX_SOURCE_DIR={source or self.hrx}", *extra], + text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + ) + + def assert_passes(self, result): + self.assertEqual(result.returncode, 0, result.stdout) + self.assertIn(f"satisfies minimum {self.minimum}", result.stdout) + + def assert_fails(self, result, reason): + self.assertNotEqual(result.returncode, 0, result.stdout) + self.assertIn(reason, result.stdout) + self.assertIn(self.minimum, result.stdout) + self.assertIn("HRX_SOURCE_DIR:", result.stdout) + + def test_exact_minimum(self): + self.git("checkout", "--quiet", self.minimum, cwd=self.hrx) + self.assert_passes(self.configure()) + + def test_descendant_with_local_edits(self): + (self.hrx / "local.txt").write_text("local edit\n", encoding="utf-8") + self.assert_passes(self.configure()) + + def test_older_revision(self): + self.git("checkout", "--quiet", self.old, cwd=self.hrx) + result = self.configure() + self.assert_fails(result, "does not have the required commit") + self.assertIn(self.old, result.stdout) + + def test_divergent_revision(self): + self.git("checkout", "--quiet", self.old, cwd=self.hrx) + divergent = self.commit("divergent") + result = self.configure() + self.assert_fails(result, "does not have the required commit") + self.assertIn(divergent, result.stdout) + + def test_unknown_revision(self): + self.minimum = "1" * 40 + self.pin.write_text(self.minimum + "\n", encoding="utf-8") + self.assert_fails(self.configure(), "Required HRX commit is unavailable") + + def test_source_archive(self): + archive = self.root / "archive" + archive.mkdir() + self.assert_fails(self.configure(archive), "Cannot identify the HRX Git checkout") + + def test_enclosing_repository(self): + archive = self.hrx / "unpacked-hrx" + archive.mkdir() + self.assert_fails(self.configure(archive), "must be the root of its own Git checkout") + + def test_worktree(self): + worktree = self.root / "worktree" + self.git("worktree", "add", "--detach", str(worktree), self.minimum, cwd=self.hrx) + self.assert_passes(self.configure(worktree)) + + def test_submodule(self): + parent = self.root / "parent" + self.git("init", "--quiet", str(parent)) + self.git("-c", "protocol.file.allow=always", "submodule", "add", str(self.hrx), "hrx", cwd=parent) + self.assert_passes(self.configure(parent / "hrx")) + + def test_shallow_history(self): + shallow = self.root / "shallow" + self.git("clone", "--depth=1", self.hrx.as_uri(), str(shallow)) + self.assert_fails(self.configure(shallow), "Required HRX commit is unavailable") + # An available pin object does not prove ancestry through a missing parent chain. + self.git("fetch", "--depth=1", "origin", self.minimum, cwd=shallow) + self.assert_fails(self.configure(shallow), "does not have the required commit") + self.git("checkout", "--quiet", self.minimum, cwd=shallow) + self.assert_passes(self.configure(shallow)) + + def test_git_missing(self): + self.assert_fails( + self.configure(self.hrx, "-DCMAKE_DISABLE_FIND_PACKAGE_Git=TRUE"), + "Git is required", + ) + + def test_reconfigure_after_checkout_changes(self): + self.assert_passes(self.configure()) + self.git("checkout", "--quiet", self.old, cwd=self.hrx) + self.assert_fails(self.configure(), "does not have the required commit") + + def test_pin_change_triggers_reconfigure(self): + earlier = time.time() - 10 + os.utime(self.pin, (earlier, earlier)) + self.git("checkout", "--quiet", self.minimum, cwd=self.hrx) + self.assert_passes(self.configure()) + self.minimum = self.new + self.pin.write_text(self.minimum + "\n", encoding="utf-8") + result = subprocess.run( + ["cmake", "--build", str(self.build)], + text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + ) + self.assert_fails(result, "does not have the required commit") + + +if __name__ == "__main__": + if shutil.which("cmake") is None: + raise SystemExit("cmake is required") + if shutil.which("git") is None: + raise SystemExit("git is required") + unittest.main() From 142cbe8064ef86c881e82fd3a4f6979fefb2c795 Mon Sep 17 00:00:00 2001 From: AaronStGeorge Date: Wed, 30 Sep 2026 23:16:31 +0000 Subject: [PATCH 2/3] ggml-hrx: share the revision check through Python Assisted-by: Codex --- ggml/src/ggml-hrx/CMakeLists.txt | 18 +- ggml/src/ggml-hrx/README.md | 26 +-- ggml/src/ggml-hrx/cmake/MinimumRevision.cmake | 61 ------- ggml/src/ggml-hrx/tools/check_hrx_revision.py | 58 +++++++ tests/CMakeLists.txt | 4 - tests/test-hrx-minimum-commit.py | 156 ------------------ 6 files changed, 79 insertions(+), 244 deletions(-) delete mode 100644 ggml/src/ggml-hrx/cmake/MinimumRevision.cmake create mode 100644 ggml/src/ggml-hrx/tools/check_hrx_revision.py delete mode 100644 tests/test-hrx-minimum-commit.py diff --git a/ggml/src/ggml-hrx/CMakeLists.txt b/ggml/src/ggml-hrx/CMakeLists.txt index 2626beb46b11..57893f3519ab 100644 --- a/ggml/src/ggml-hrx/CMakeLists.txt +++ b/ggml/src/ggml-hrx/CMakeLists.txt @@ -1,4 +1,5 @@ set(HRX_SOURCE_DIR "" CACHE PATH "Optional HRX source tree to build instead of using installed hrx and loomc packages") +find_package(Python3 REQUIRED COMPONENTS Interpreter) if(HRX_SOURCE_DIR) include(ExternalProject) @@ -9,8 +10,19 @@ if(HRX_SOURCE_DIR) message(FATAL_ERROR "HRX_SOURCE_DIR does not contain a CMakeLists.txt: ${HRX_SOURCE_DIR}") endif() - include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/MinimumRevision.cmake") - ggml_hrx_check_minimum_revision("${HRX_SOURCE_DIR}" "${CMAKE_CURRENT_SOURCE_DIR}/hrx-minimum-commit.txt") + set(GGML_HRX_REVISION_CHECK "${CMAKE_CURRENT_SOURCE_DIR}/tools/check_hrx_revision.py") + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS + "${CMAKE_CURRENT_SOURCE_DIR}/hrx-minimum-commit.txt" "${GGML_HRX_REVISION_CHECK}") + execute_process( + COMMAND "${Python3_EXECUTABLE}" "${GGML_HRX_REVISION_CHECK}" "${HRX_SOURCE_DIR}" + RESULT_VARIABLE GGML_HRX_REVISION_RESULT + OUTPUT_VARIABLE GGML_HRX_REVISION_OUTPUT + ERROR_VARIABLE GGML_HRX_REVISION_ERROR + OUTPUT_STRIP_TRAILING_WHITESPACE) + if(NOT GGML_HRX_REVISION_RESULT EQUAL 0) + message(FATAL_ERROR "${GGML_HRX_REVISION_ERROR}") + endif() + message(STATUS "${GGML_HRX_REVISION_OUTPUT}") set(GGML_HRX_PREFIX "${CMAKE_CURRENT_BINARY_DIR}/hrx") set(GGML_HRX_BUILD_DIR "${GGML_HRX_PREFIX}/src/ggml-hrx-deps-build") @@ -72,8 +84,6 @@ else() find_program(GGML_HRX_IREE_BENCHMARK_LOOM NAMES iree-benchmark-loom) endif() -find_package(Python3 REQUIRED COMPONENTS Interpreter) - set(GGML_HRX_KERNEL_CORPUS_SOURCE_FORMAT "binary" CACHE STRING "Embedded Loom corpus source format: text or binary") set_property(CACHE GGML_HRX_KERNEL_CORPUS_SOURCE_FORMAT PROPERTY STRINGS text binary) if(GGML_HRX_KERNEL_CORPUS_SOURCE_FORMAT STREQUAL "binary") diff --git a/ggml/src/ggml-hrx/README.md b/ggml/src/ggml-hrx/README.md index 3a159ac03caf..cf00e3131c6f 100644 --- a/ggml/src/ggml-hrx/README.md +++ b/ggml/src/ggml-hrx/README.md @@ -1,29 +1,17 @@ -# HRX backend compatibility +# HRX Backend -`hrx-minimum-commit.txt` records the minimum required commit in ROCm/hrx-system. It contains one full Git commit SHA. The backend's C++ APIs, Loom syntax, and numerical behavior can all depend on that revision. +## `hrx-system` backend compatibility -When `GGML_HRX=ON` and `HRX_SOURCE_DIR` is set, CMake requires the HRX checkout's HEAD to equal or descend from the recorded commit. The check uses local Git ancestry, not dates or version strings. It supports submodules and worktrees. Local edits are allowed but are not verified by the ancestry check. - -A missing commit, insufficient shallow history, or a source archive cannot establish compatibility and causes configuration to fail. Supply a Git checkout with sufficient history and rerun CMake. Configuration never fetches history or changes the checkout. Installed HRX/Loom packages remain outside this check. - -## Updating the minimum - -Raise the minimum in the same llama.cpp commit that introduces a newer HRX dependency. This includes new API calls, new Loom syntax, and removing a numerical workaround after an HRX compiler or runtime fix. Unrelated repairs do not require a bump. - -Choose the earliest demonstrated required commit that retains the previous floor in its ancestry. Do not replace the floor with the latest tested revision merely because it is available. Record the dependency and its HRX commit or PR in the change's validation evidence. A minimum is not a promise that every future HRX revision is compatible. - -The initial minimum, `40b7cc13717d9d4c7f7eb0e6885563042153566b` ([HRX #1054](https://github.com/ROCm/hrx-system/pull/1054)), preserves CFG guard proofs against constant bounds. The Q6 prefill JIT case with 512 tokens, 5120 inputs, and 1984 outputs needs that proof to compile its guarded fragment stores. The preceding commit fails with `SUBRANGE/010`; this revision passes the JIT compile suite. - -This minimum also contains `cba4f16b2902c218be16f5394fad94563eb68173`, which corrects packed AMDGPU table lookup ordering. The IQ4 decoders now use logical table indices without the old XOR-12 compensation and require that correction. +`hrx-minimum-commit.txt` records the minimum required commit in ROCm/hrx-system. It contains one full Git commit SHA. The backend's C++ APIs, Loom syntax, and numerical behavior can all depend on that revision. When `GGML_HRX=ON` and `HRX_SOURCE_DIR` is set, CMake requires the HRX checkout's HEAD to equal or descend from the recorded commit. For builds using installed packages, check the source revision used to produce both HRX and Loom against this floor before validating a repair. From the staging checkout: ```sh -git -C hrx-system merge-base --is-ancestor "$(cat llama.cpp/ggml/src/ggml-hrx/hrx-minimum-commit.txt)" HEAD +python3 llama.cpp/ggml/src/ggml-hrx/tools/check_hrx_revision.py hrx-system ``` -This source check is useful only when the tested libraries were built from that checkout; record the actual artifact provenance as well. +### Updating the minimum -## Configure-check tests +Raise the minimum in the same llama.cpp commit that introduces a newer HRX dependency. This includes new API calls, new Loom syntax, and removing a numerical workaround after an HRX compiler or runtime fix. Unrelated repairs do not require a bump. -Run `python3 tests/test-hrx-minimum-commit.py` from the llama.cpp root. The tests use temporary Git repositories and CMake projects and require no HRX build or GPU. +Choose the earliest demonstrated required commit that retains the previous floor in its ancestry. Do not replace the floor with the latest tested revision merely because it is available. Record the dependency and its HRX commit or PR in the change's validation evidence. A minimum is not a promise that every future HRX revision is compatible. diff --git a/ggml/src/ggml-hrx/cmake/MinimumRevision.cmake b/ggml/src/ggml-hrx/cmake/MinimumRevision.cmake deleted file mode 100644 index c7e3d1e2a802..000000000000 --- a/ggml/src/ggml-hrx/cmake/MinimumRevision.cmake +++ /dev/null @@ -1,61 +0,0 @@ -# The minimum revision records HRX behavior required by the backend and its Loom kernels. -# Source builds must prove that revision is in HEAD's ancestry before configuring dependencies. -# Only local Git history is read; missing history is an error, never a reason to fetch or skip the check. - -function(ggml_hrx_check_minimum_revision SOURCE_DIR PIN_FILE) - file(READ "${PIN_FILE}" GGML_HRX_MINIMUM_REVISION) - string(STRIP "${GGML_HRX_MINIMUM_REVISION}" GGML_HRX_MINIMUM_REVISION) - set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${PIN_FILE}") - - set(GGML_HRX_REVISION_HELP - "HRX_SOURCE_DIR: ${SOURCE_DIR}\nRequired HRX commit: ${GGML_HRX_MINIMUM_REVISION}\nUse an HRX Git checkout at this commit or a descendant. Fetch missing history manually (unshallow a shallow clone if needed), then rerun CMake.") - find_package(Git QUIET) - if(NOT GIT_FOUND) - message(FATAL_ERROR "Git is required to verify the HRX minimum revision. Install Git and rerun CMake.\n${GGML_HRX_REVISION_HELP}") - endif() - - execute_process( - COMMAND "${GIT_EXECUTABLE}" -C "${SOURCE_DIR}" rev-parse --show-toplevel - RESULT_VARIABLE GGML_HRX_ROOT_RESULT - OUTPUT_VARIABLE GGML_HRX_GIT_ROOT - ERROR_VARIABLE GGML_HRX_GIT_ERROR - OUTPUT_STRIP_TRAILING_WHITESPACE) - if(NOT GGML_HRX_ROOT_RESULT EQUAL 0) - message(FATAL_ERROR "Cannot identify the HRX Git checkout: ${GGML_HRX_GIT_ERROR}\n${GGML_HRX_REVISION_HELP}") - endif() - - # Git also discovers enclosing repositories; an unpacked HRX tree inside one is not an HRX checkout. - get_filename_component(GGML_HRX_SOURCE_REAL "${SOURCE_DIR}" REALPATH) - get_filename_component(GGML_HRX_GIT_ROOT_REAL "${GGML_HRX_GIT_ROOT}" REALPATH) - if(NOT GGML_HRX_SOURCE_REAL STREQUAL GGML_HRX_GIT_ROOT_REAL) - message(FATAL_ERROR "HRX_SOURCE_DIR must be the root of its own Git checkout, not a directory inside ${GGML_HRX_GIT_ROOT}.\n${GGML_HRX_REVISION_HELP}") - endif() - - execute_process( - COMMAND "${GIT_EXECUTABLE}" -C "${SOURCE_DIR}" rev-parse --verify "HEAD^{commit}" - RESULT_VARIABLE GGML_HRX_HEAD_RESULT - OUTPUT_VARIABLE GGML_HRX_HEAD - ERROR_VARIABLE GGML_HRX_GIT_ERROR - OUTPUT_STRIP_TRAILING_WHITESPACE) - if(NOT GGML_HRX_HEAD_RESULT EQUAL 0) - message(FATAL_ERROR "Cannot resolve HRX HEAD: ${GGML_HRX_GIT_ERROR}\n${GGML_HRX_REVISION_HELP}") - endif() - string(APPEND GGML_HRX_REVISION_HELP "\nActual HRX HEAD: ${GGML_HRX_HEAD}") - - execute_process( - COMMAND "${GIT_EXECUTABLE}" -C "${SOURCE_DIR}" cat-file -e "${GGML_HRX_MINIMUM_REVISION}^{commit}" - RESULT_VARIABLE GGML_HRX_PIN_RESULT - ERROR_VARIABLE GGML_HRX_GIT_ERROR) - if(NOT GGML_HRX_PIN_RESULT EQUAL 0) - message(FATAL_ERROR "Required HRX commit is unavailable locally: ${GGML_HRX_GIT_ERROR}\n${GGML_HRX_REVISION_HELP}") - endif() - - execute_process( - COMMAND "${GIT_EXECUTABLE}" -C "${SOURCE_DIR}" merge-base --is-ancestor "${GGML_HRX_MINIMUM_REVISION}" "${GGML_HRX_HEAD}" - RESULT_VARIABLE GGML_HRX_ANCESTRY_RESULT - ERROR_VARIABLE GGML_HRX_GIT_ERROR) - if(NOT GGML_HRX_ANCESTRY_RESULT EQUAL 0) - message(FATAL_ERROR "HRX HEAD does not have the required commit in its available ancestry: ${GGML_HRX_GIT_ERROR}\n${GGML_HRX_REVISION_HELP}") - endif() - message(STATUS "HRX revision ${GGML_HRX_HEAD} satisfies minimum ${GGML_HRX_MINIMUM_REVISION}") -endfunction() diff --git a/ggml/src/ggml-hrx/tools/check_hrx_revision.py b/ggml/src/ggml-hrx/tools/check_hrx_revision.py new file mode 100644 index 000000000000..5f9293bcb8ac --- /dev/null +++ b/ggml/src/ggml-hrx/tools/check_hrx_revision.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +"""Check an HRX checkout against the backend's recorded minimum commit. + +CMake and callers using installed packages share this local ancestry check. +Success prints the accepted revision; failure prints context and exits nonzero. +No history is fetched and no source files are changed. +""" + +import argparse +from pathlib import Path +import subprocess +import sys + + +def check_revision(source: Path) -> int: + minimum = (Path(__file__).resolve().parents[1] / "hrx-minimum-commit.txt").read_text(encoding="utf-8").strip() + head = "unknown" + + def git(*args: str) -> str: + return subprocess.run( + ["git", "-C", str(source), *args], check=True, text=True, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + ).stdout.strip() + + try: + # Git can discover an enclosing repository around an unpacked source archive. + root = Path(git("rev-parse", "--show-toplevel")).resolve() + if root != source: + raise ValueError(f"HRX_SOURCE_DIR must be the root of its own Git checkout, not a directory inside {root}.") + head = git("rev-parse", "--verify", "HEAD^{commit}") + git("cat-file", "-e", f"{minimum}^{{commit}}") + git("merge-base", "--is-ancestor", minimum, head) + except (OSError, ValueError, subprocess.CalledProcessError) as error: + if isinstance(error, subprocess.CalledProcessError): + reason = error.stderr.strip() or "HRX HEAD does not have the required commit in its available ancestry." + else: + reason = str(error) + print( + f"{reason}\nHRX_SOURCE_DIR: {source}\nRequired HRX commit: {minimum}\nActual HRX HEAD: {head}\n" + "Use an HRX Git checkout at the required commit or a descendant. Ensure Git is installed, " + "fetch missing history manually (unshallow a shallow clone if needed), then rerun the check.", + file=sys.stderr, + ) + return 1 + + print(f"HRX revision {head} satisfies minimum {minimum}") + return 0 + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("source", type=Path, help="HRX source checkout") + args = parser.parse_args() + return check_revision(args.source.resolve()) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 359ea666bc4a..b53be5e8a127 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -348,10 +348,6 @@ if (TARGET ggml-hrx) target_include_directories(hrx-backend-test PRIVATE ../ggml/include ../ggml/src ../ggml/src/ggml-hrx) add_test(NAME hrx-backend-test COMMAND hrx-backend-test) - find_package(Python3 REQUIRED COMPONENTS Interpreter) - add_test(NAME test-hrx-minimum-commit - COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test-hrx-minimum-commit.py) - add_executable(test-hrx-loom-jit test-hrx-loom-jit.cpp) target_link_libraries(test-hrx-loom-jit PRIVATE ggml-hrx ggml-hrx-kernel-corpus hrx::hrx loomc::loomc) target_include_directories(test-hrx-loom-jit PRIVATE ../ggml/include ../ggml/src ../ggml/src/ggml-hrx) diff --git a/tests/test-hrx-minimum-commit.py b/tests/test-hrx-minimum-commit.py deleted file mode 100644 index 665495d2747c..000000000000 --- a/tests/test-hrx-minimum-commit.py +++ /dev/null @@ -1,156 +0,0 @@ -#!/usr/bin/env python3 -"""Exercise the HRX configure gate with local Git histories; no GPU is needed.""" - -import os -from pathlib import Path -import shutil -import subprocess -import tempfile -import time -import unittest - - -CHECK = Path(__file__).resolve().parents[1] / "ggml/src/ggml-hrx/cmake/MinimumRevision.cmake" - - -class MinimumRevisionTests(unittest.TestCase): - def setUp(self): - self.temporary = tempfile.TemporaryDirectory(prefix="hrx minimum ") - self.addCleanup(self.temporary.cleanup) - self.root = Path(self.temporary.name) - self.hrx = self.root / "hrx" - self.git("init", "--quiet", str(self.hrx)) - self.git("config", "user.name", "HRX test", cwd=self.hrx) - self.git("config", "user.email", "hrx-test@example.invalid", cwd=self.hrx) - self.git("config", "commit.gpgsign", "false", cwd=self.hrx) - self.old = self.commit("old") - self.minimum = self.commit("minimum") - self.new = self.commit("new") - self.project = self.root / "project" - self.project.mkdir() - self.pin = self.project / "minimum.txt" - self.pin.write_text(self.minimum + "\n", encoding="utf-8") - (self.project / "CMakeLists.txt").write_text( - 'cmake_minimum_required(VERSION 3.14)\n' - 'project(hrx_revision_test NONE)\n' - f'include("{CHECK.as_posix()}")\n' - 'ggml_hrx_check_minimum_revision("${HRX_SOURCE_DIR}" "${CMAKE_CURRENT_SOURCE_DIR}/minimum.txt")\n', - encoding="utf-8", - ) - self.build = self.root / "build" - - def git(self, *args, cwd=None): - return subprocess.run( - ["git", *args], cwd=cwd, check=True, text=True, - stdout=subprocess.PIPE, stderr=subprocess.PIPE, - ).stdout.strip() - - def commit(self, name): - self.git("commit", "--quiet", "--allow-empty", "-m", name, cwd=self.hrx) - return self.git("rev-parse", "HEAD", cwd=self.hrx) - - def configure(self, source=None, *extra): - return subprocess.run( - ["cmake", "-S", str(self.project), "-B", str(self.build), - f"-DHRX_SOURCE_DIR={source or self.hrx}", *extra], - text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, - ) - - def assert_passes(self, result): - self.assertEqual(result.returncode, 0, result.stdout) - self.assertIn(f"satisfies minimum {self.minimum}", result.stdout) - - def assert_fails(self, result, reason): - self.assertNotEqual(result.returncode, 0, result.stdout) - self.assertIn(reason, result.stdout) - self.assertIn(self.minimum, result.stdout) - self.assertIn("HRX_SOURCE_DIR:", result.stdout) - - def test_exact_minimum(self): - self.git("checkout", "--quiet", self.minimum, cwd=self.hrx) - self.assert_passes(self.configure()) - - def test_descendant_with_local_edits(self): - (self.hrx / "local.txt").write_text("local edit\n", encoding="utf-8") - self.assert_passes(self.configure()) - - def test_older_revision(self): - self.git("checkout", "--quiet", self.old, cwd=self.hrx) - result = self.configure() - self.assert_fails(result, "does not have the required commit") - self.assertIn(self.old, result.stdout) - - def test_divergent_revision(self): - self.git("checkout", "--quiet", self.old, cwd=self.hrx) - divergent = self.commit("divergent") - result = self.configure() - self.assert_fails(result, "does not have the required commit") - self.assertIn(divergent, result.stdout) - - def test_unknown_revision(self): - self.minimum = "1" * 40 - self.pin.write_text(self.minimum + "\n", encoding="utf-8") - self.assert_fails(self.configure(), "Required HRX commit is unavailable") - - def test_source_archive(self): - archive = self.root / "archive" - archive.mkdir() - self.assert_fails(self.configure(archive), "Cannot identify the HRX Git checkout") - - def test_enclosing_repository(self): - archive = self.hrx / "unpacked-hrx" - archive.mkdir() - self.assert_fails(self.configure(archive), "must be the root of its own Git checkout") - - def test_worktree(self): - worktree = self.root / "worktree" - self.git("worktree", "add", "--detach", str(worktree), self.minimum, cwd=self.hrx) - self.assert_passes(self.configure(worktree)) - - def test_submodule(self): - parent = self.root / "parent" - self.git("init", "--quiet", str(parent)) - self.git("-c", "protocol.file.allow=always", "submodule", "add", str(self.hrx), "hrx", cwd=parent) - self.assert_passes(self.configure(parent / "hrx")) - - def test_shallow_history(self): - shallow = self.root / "shallow" - self.git("clone", "--depth=1", self.hrx.as_uri(), str(shallow)) - self.assert_fails(self.configure(shallow), "Required HRX commit is unavailable") - # An available pin object does not prove ancestry through a missing parent chain. - self.git("fetch", "--depth=1", "origin", self.minimum, cwd=shallow) - self.assert_fails(self.configure(shallow), "does not have the required commit") - self.git("checkout", "--quiet", self.minimum, cwd=shallow) - self.assert_passes(self.configure(shallow)) - - def test_git_missing(self): - self.assert_fails( - self.configure(self.hrx, "-DCMAKE_DISABLE_FIND_PACKAGE_Git=TRUE"), - "Git is required", - ) - - def test_reconfigure_after_checkout_changes(self): - self.assert_passes(self.configure()) - self.git("checkout", "--quiet", self.old, cwd=self.hrx) - self.assert_fails(self.configure(), "does not have the required commit") - - def test_pin_change_triggers_reconfigure(self): - earlier = time.time() - 10 - os.utime(self.pin, (earlier, earlier)) - self.git("checkout", "--quiet", self.minimum, cwd=self.hrx) - self.assert_passes(self.configure()) - self.minimum = self.new - self.pin.write_text(self.minimum + "\n", encoding="utf-8") - result = subprocess.run( - ["cmake", "--build", str(self.build)], - text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, - ) - self.assert_fails(result, "does not have the required commit") - - -if __name__ == "__main__": - if shutil.which("cmake") is None: - raise SystemExit("cmake is required") - if shutil.which("git") is None: - raise SystemExit("git is required") - unittest.main() From f0be33cd896d5a69fa335443627a80e5aa2ac757 Mon Sep 17 00:00:00 2001 From: AaronStGeorge Date: Wed, 30 Sep 2026 23:24:13 +0000 Subject: [PATCH 3/3] ggml-hrx: simplify revision checking Assisted-by: Codex --- ggml/src/ggml-hrx/CMakeLists.txt | 1 + ggml/src/ggml-hrx/tools/check_hrx_revision.py | 6 +----- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/ggml/src/ggml-hrx/CMakeLists.txt b/ggml/src/ggml-hrx/CMakeLists.txt index 57893f3519ab..16e77e8ad2f3 100644 --- a/ggml/src/ggml-hrx/CMakeLists.txt +++ b/ggml/src/ggml-hrx/CMakeLists.txt @@ -10,6 +10,7 @@ if(HRX_SOURCE_DIR) message(FATAL_ERROR "HRX_SOURCE_DIR does not contain a CMakeLists.txt: ${HRX_SOURCE_DIR}") endif() + # Enforce the HRX compatibility floor before building dependencies; rerun configure when the pin or checker changes. set(GGML_HRX_REVISION_CHECK "${CMAKE_CURRENT_SOURCE_DIR}/tools/check_hrx_revision.py") set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/hrx-minimum-commit.txt" "${GGML_HRX_REVISION_CHECK}") diff --git a/ggml/src/ggml-hrx/tools/check_hrx_revision.py b/ggml/src/ggml-hrx/tools/check_hrx_revision.py index 5f9293bcb8ac..9ce6a441e2da 100644 --- a/ggml/src/ggml-hrx/tools/check_hrx_revision.py +++ b/ggml/src/ggml-hrx/tools/check_hrx_revision.py @@ -23,14 +23,10 @@ def git(*args: str) -> str: ).stdout.strip() try: - # Git can discover an enclosing repository around an unpacked source archive. - root = Path(git("rev-parse", "--show-toplevel")).resolve() - if root != source: - raise ValueError(f"HRX_SOURCE_DIR must be the root of its own Git checkout, not a directory inside {root}.") head = git("rev-parse", "--verify", "HEAD^{commit}") git("cat-file", "-e", f"{minimum}^{{commit}}") git("merge-base", "--is-ancestor", minimum, head) - except (OSError, ValueError, subprocess.CalledProcessError) as error: + except (OSError, subprocess.CalledProcessError) as error: if isinstance(error, subprocess.CalledProcessError): reason = error.stderr.strip() or "HRX HEAD does not have the required commit in its available ancestry." else: