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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ jobs:
laya_encoder_test http_guard_test moe_gguf_index_test laya_bounds_test \
npu_model_bounds_test npu_forward_loader_test npu_design_grid_test npu_sampling_test \
npu_flm_head_import_test npu_elf_args_test \
tokenizer_linear_test hrx_placement_test hrx_cwsr_test npu_device_lock_test -j
tokenizer_linear_test hrx_placement_test hrx_cwsr_test hrx_runtime_test npu_device_lock_test -j
- name: Test
# One hung test must not hold the job for hours: each test gets 10 minutes.
run: ctest --test-dir build --output-on-failure --timeout 600
10 changes: 10 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,9 @@ add_test(NAME laya_gguf_route COMMAND ${CMAKE_SOURCE_DIR}/tests/laya_gguf_route.
add_test(NAME mem_admit_route COMMAND ${CMAKE_SOURCE_DIR}/tests/mem_admit_route.sh $<TARGET_FILE:onebit>)
# engine#315: what --hrx-cwsr-placement puts in an HRX backend's environment (app/hrx_cwsr.h); fake backend, no GPU.
add_test(NAME hrx_cwsr_route COMMAND ${CMAKE_SOURCE_DIR}/tests/hrx_cwsr_route.sh $<TARGET_FILE:onebit>)
# engine#713 criterion 4 end to end without a GPU: a fake runtime file and a fake backend, the line
# serve prints and the IREE_HAL_AMDGPU_LIBHSA_PATH it hands over, sha256 included.
add_test(NAME hrx_runtime_route COMMAND ${CMAKE_SOURCE_DIR}/tests/hrx_runtime_route.sh $<TARGET_FILE:onebit>)
# --device hybrid (docs/hybrid.md): the handoff with a fake backend and a fake provider; and, with a model,
# end to end on the CPU llama-server (ONEBIT_SERVE_TEST_LLAMA_SERVER, else the build's own; skips without one)
add_test(NAME hybrid_route COMMAND ${CMAKE_SOURCE_DIR}/tests/hybrid_route.sh $<TARGET_FILE:onebit>)
Expand Down Expand Up @@ -242,6 +245,12 @@ add_test(NAME hrx_placement COMMAND hrx_placement_test)
add_executable(hrx_cwsr_test tests/hrx_cwsr_test.cpp)
target_include_directories(hrx_cwsr_test PRIVATE app)
add_test(NAME hrx_cwsr COMMAND hrx_cwsr_test)
# engine#713 criterion 4's runtime identity (app/hrx_runtime.h, docs/hrx.md "The HSA runtime"):
# sha256 of a buffer and of a file, and the line that names the runtime HRX will dlopen; FIPS 180-4
# vectors, no GPU, no runtime.
add_executable(hrx_runtime_test tests/hrx_runtime_test.cpp)
target_include_directories(hrx_runtime_test PRIVATE app)
add_test(NAME hrx_runtime COMMAND hrx_runtime_test)
# --pm-router's class -> expert table and its flag values (app/pm.cpp, docs/pm.md "Routing with
# Laya"); no GPU, no checkpoint, no Lemonade: config in, mapping out.
add_executable(pm_routes_test tests/pm_routes_test.cpp app/pm.cpp)
Expand Down Expand Up @@ -556,6 +565,7 @@ endif()

# Q4NX chunk decoders against 1bit-MONSTER's verified decoders, on committed real tiles
add_executable(npu_q4nx_test tests/npu_q4nx_test.cpp)
target_include_directories(npu_q4nx_test PRIVATE app)
target_link_libraries(npu_q4nx_test PRIVATE onebit_npu)
add_test(NAME npu_q4nx COMMAND npu_q4nx_test ${CMAKE_SOURCE_DIR}/tests/golden/q4nx)
add_executable(npu_pack_import_test tests/npu_pack_import_test.cpp)
Expand Down
95 changes: 95 additions & 0 deletions app/hrx_runtime.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Copyright 2026 bong-water-water-bong
// SPDX-License-Identifier: Apache-2.0
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// engine#713 criterion 4 (docs/hrx.md, "The HSA runtime"): the HRX init line names the HSA runtime
// HRX will dlopen - its path and its sha256 - because three libhsa-runtime64.so.1 exist on a box
// and they are not interchangeable: the release-shipped one does not register HRX0 for a dev
// build, the pinned toolchain's does, and a distro one is untested. Without naming it, a run can
// pass while a silent fallback to another copy executes, which is how the A/B behind engine#315's
// residual was only trustworthy - every arm named its runtime.
//
// Anything that needs the same identity (llama-server's log, a test) gets it from here, so the
// format is written once.
#pragma once

#include <cstdint>
#include <cstdio>
#include <cstring>
#include <fstream>
#include <iterator>
#include <string>
#include <vector>

namespace onebit {

// SHA-256 (FIPS 180-4) of one buffer, as lower-case hex.
inline std::string sha256_hex(const uint8_t* data, size_t len) {
static const uint32_t K[64] = {
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2};
uint32_t h[8] = {0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19};
std::vector<uint8_t> m(data, data + len);
m.push_back(0x80);
while (m.size() % 64 != 56) m.push_back(0);
for (int i = 7; i >= 0; i--) m.push_back(uint8_t((uint64_t(len) * 8) >> (8 * i)));
auto rotr = [](uint32_t x, int n) { return (x >> n) | (x << (32 - n)); };
for (size_t off = 0; off < m.size(); off += 64) {
uint32_t w[64];
for (int i = 0; i < 16; i++)
w[i] = uint32_t(m[off + 4 * i]) << 24 | uint32_t(m[off + 4 * i + 1]) << 16 |
uint32_t(m[off + 4 * i + 2]) << 8 | m[off + 4 * i + 3];
for (int i = 16; i < 64; i++) {
const uint32_t s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ (w[i - 15] >> 3);
const uint32_t s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ (w[i - 2] >> 10);
w[i] = w[i - 16] + s0 + w[i - 7] + s1;
}
uint32_t a = h[0], b = h[1], c = h[2], d = h[3], e = h[4], f = h[5], g = h[6], hh = h[7];
for (int i = 0; i < 64; i++) {
const uint32_t t1 = hh + (rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) + ((e & f) ^ (~e & g)) + K[i] + w[i];
const uint32_t t2 = (rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + ((a & b) ^ (a & c) ^ (b & c));
hh = g; g = f; f = e; e = d + t1; d = c; c = b; b = a; a = t1 + t2;
}
h[0] += a; h[1] += b; h[2] += c; h[3] += d; h[4] += e; h[5] += f; h[6] += g; h[7] += hh;
}
char hex[65];
for (int i = 0; i < 8; i++) std::snprintf(hex + 8 * i, 9, "%08x", h[i]);
return hex;
}

// SHA-256 of a whole file, or "" when it cannot be read: the HSA runtime is a few MiB, and one
// read per serve costs less than a wrong runtime costs a run.
inline std::string sha256_file(const std::string& path) {
std::ifstream f(path, std::ios::binary);
if (!f) return "";
const std::vector<uint8_t> bytes{std::istreambuf_iterator<char>(f), std::istreambuf_iterator<char>()};
if (!f.good() && !f.eof()) return "";
return sha256_hex(bytes.data(), bytes.size());
}

// The one line serve prints when HRX first resolves its runtime. `sha256` empty means the file could
// not be read, which is itself worth saying: HRX will still try to dlopen it.
inline std::string hrx_runtime_line(const std::string& path, const std::string& sha256) {
if (sha256.empty())
return "HRX: HSA runtime " + path + " (unreadable: no sha256, HRX may fail to open it)";
return "HRX: HSA runtime " + path + " sha256 " + sha256;
}

} // namespace onebit
25 changes: 24 additions & 1 deletion app/serve.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
#include "gguf_meta.h"
#include "hrx_archs.h"
#include "hrx_cwsr.h"
#include "hrx_runtime.h"
#include "hrx_strict.h"
#include "mem_admit.h"
#include "pm.h"
Expand Down Expand Up @@ -235,6 +236,24 @@ const CwsrDecision& hrx_cwsr_decision(const Options& o) {

const std::vector<std::string>& hrx_cwsr_env(const Options& o) { return hrx_cwsr_decision(o).env; }

// engine#713 criterion 4 (app/hrx_runtime.h, docs/hrx.md "The HSA runtime"): name the HSA runtime HRX will dlopen -
// path and sha256 - once per serve, at the first HRX backend. Three libhsa-runtime64.so.1 exist on a box and they are
// not interchangeable, so a run that silently fell back to another copy must be visible in the log. Printed before the
// CWSR decision line so the runtime is identified first.
void announce_hrx_runtime(const Options& o) {
static bool announced = false;
if (announced) return;
announced = true;
const std::string path = hrx_libhsa(o.hrx_libhsa);
if (path.empty()) {
std::fprintf(stderr,
"1bit serve: HRX: no HSA runtime found (--hrx-libhsa, the build's pinned toolchain, "
"/opt/rocm-therock); HRX will not register a device (docs/hrx.md)\n");
return;
}
std::fprintf(stderr, "1bit serve: %s\n", hrx_runtime_line(path, sha256_file(path)).c_str());
}

// The decision line, printed at the first HRX backend that uses it. Deliberately NOT printed by hrx_cwsr_env(): that
// would also print on paths that must emit exactly one line (the memory refusal, tests/mem_admit_route.sh counts
// lines) and on routes that never touch HRX at all (engine#715).
Expand Down Expand Up @@ -1022,6 +1041,7 @@ Launch launch_for(const Options& o, const std::string& device, int child_port) {
if (device == "hrx") {
const std::string hsa = hrx_libhsa(o.hrx_libhsa);
if (!hsa.empty()) env.push_back("IREE_HAL_AMDGPU_LIBHSA_PATH=" + hsa);
announce_hrx_runtime(o);
announce_cwsr_env(o);
for (const auto& e : hrx_cwsr_env(o)) env.push_back(e); // engine#315 (app/hrx_cwsr.h)
// engine #390 (docs/serve.md, "Off-HRX placement"): the backend's placement observer
Expand Down Expand Up @@ -1177,6 +1197,7 @@ Launch rag_launch(const Options& o, const std::string& model, const char* role,
// the same ROCr the chat server gets, or the RAG server cannot open HRX0 ("invalid device: HRX0")
if (const std::string hsa = hrx_libhsa(o.hrx_libhsa); !hsa.empty())
l.env.push_back("IREE_HAL_AMDGPU_LIBHSA_PATH=" + hsa);
announce_hrx_runtime(o);
announce_cwsr_env(o);
for (const auto& e : hrx_cwsr_env(o)) l.env.push_back(e); // engine#315 (app/hrx_cwsr.h)
// strict placement, as the model's own backend (app/hrx_strict.h); no report, no -lv 4
Expand Down Expand Up @@ -1692,8 +1713,10 @@ int serve_child(const Options& given) {
// the GGUF scorer, with the HSA runtime HRX needs (docs/hrx.md)
std::vector<std::string> env;
if (const std::string hsa = hrx_libhsa(o.hrx_libhsa); !hsa.empty()) env.push_back("IREE_HAL_AMDGPU_LIBHSA_PATH=" + hsa);
if (!dev || !*dev || std::string(dev) == "hrx")
if (!dev || !*dev || std::string(dev) == "hrx") {
announce_hrx_runtime(o);
announce_cwsr_env(o);
}
for (const auto& e : hrx_cwsr_env(o)) env.push_back(e); // engine#315 (app/hrx_cwsr.h)
laya_daemon = std::make_unique<LayaDaemon>(std::vector<std::string>{
bin, "daemon", gguf, "--family", "typed-decisions", "--device", dev && *dev ? dev : "hrx"}, env);
Expand Down
14 changes: 14 additions & 0 deletions docs/hrx.md
Original file line number Diff line number Diff line change
Expand Up @@ -936,6 +936,20 @@ cmake --build build --target onebit
llama-server as `IREE_HAL_AMDGPU_LIBHSA_PATH`, unless it is already set. Without a
build path it searches the pinned toolchain, then `/opt/rocm-therock`. To run
llama-server or llama-bench by hand, export that variable yourself.
- **The runtime is named at init (engine#713).** Three `libhsa-runtime64.so.1` exist on a
box (the release-shipped one, the pinned toolchain's, the distro's) and they are not
interchangeable: the release-shipped one does not register `HRX0` for a dev build. So the
first HRX backend `serve` starts logs the file HRX will dlopen and its digest, once:

```
1bit serve: HRX: HSA runtime /home/you/engine/third_party/toolchain/rocm-sdk-10.3.0a20261031-gfx1151/lib/python3.14/site-packages/_rocm_sdk_core/lib/libhsa-runtime64.so.1 sha256 21a6682452e159cfcdc6bdc84676f6b7f9c1236f3a2957a97a2e7170889cdfa3
```

Check it against `sha256sum <path>`, and against the digest the release table records
([releases.md](releases.md), "Pins") — a run that fell back to another copy is visible
there instead of passing silently. The line comes from `app/hrx_runtime.h`; `--hrx-libhsa
<file>` overrides the path, and a file that cannot be read is still named, without a
digest.

### Private HIP kernels

Expand Down
14 changes: 13 additions & 1 deletion docs/releases.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,23 @@ DeepSeek V4.x, GLM 5.x and Qwen3.8-Flash-Next run on **HRX** here, and the catal
| engine | `bcb28cc8f57387865ecdbe744a7e3cdcf65eb7ad` |
| HRX (llama.cpp fork, ggml-hrx) | `4a58da9b087d` |
| hrx-system | `82804197bcbe` |
| HRX toolchain (ROCm/TheRock SDK) | `10.3.0a20261031`, `gfx1151` (`config/hrx-toolchain.config`) |
| HRX toolchain (ROCm/TheRock SDK) | `10.3.0a20261031`, `gfx1151` (`config/hrx-toolchain.config`), HSA runtime `21a6682452e159cf…` |
| Lemonade Server | `2026.46.0~1.ef0f3b49` (our fork, the `onebit` recipe) |
| kernel in the image | `7.3.0-rc4-1bit-perfopt` (IOMMU PerfOpt backports, MOK-signed) |
| NPU kernel sets | 3 sealed sets, 30 kernels, every manifest signed |

The pinned toolchain's HSA runtime, so a bump is a diff here and a wrong runtime is visible
without a GPU (`scripts/fetch-rocm-sdk.sh --check` prints the tree; the engine's first HRX backend
logs this same file and digest — `docs/hrx.md`, "The HSA runtime"):

| what | value |
|---|---|
| `libhsa-runtime64.so.1` in `rocm-sdk-10.3.0a20261031-gfx1151` | 5,141,585 B, sha256 `21a6682452e159cfcdc6bdc84676f6b7f9c1236f3a2957a97a2e7170889cdfa3` |
| measured capabilities, Strix Halo `gfx1151` (rocminfo from the pinned tree, 2026-11-03) | `XNACK enabled: NO`, `Coherent Host Access: FALSE`, `Memory Properties: APU`, Runtime Version 1.22, System Timestamp Freq. 1000 MHz |

That runtime is the one a dev tree must use: the release-shipped copy does not register `HRX0` for a
dev build, and a distro `libhsa-runtime64` is untested (engine#713's acceptance table).

The Lemonade pin is the sha (`ef0f3b49`), not the string around it: `tools/version.py` derives the dev version as
`<week>.0~<commit count>.<sha>`, and that count is the number of commits in the **checkout it was built in** —
`third_party/lemonade` is a `shallow = true` submodule, so a depth-1 clone reports `~1` for the same commit while a
Expand Down
Loading
Loading