HRX + Vulkan#
One llama.cpp build with two GPU backends, ggml-hrx (AMD's HRX runtime) and
ggml-vulkan. Its single llama-server exposes both devices on Strix Halo:
HRX0: AMD Radeon 8060S Graphics (Node 1) (gfx1151)
Vulkan0: AMD Radeon 8060S Graphics (RADV STRIX_HALO)
1bit serve runs GGUF models on either device with it (1bit serve):
--device |
Device | Serves |
|---|---|---|
hrx |
HRX0 |
AMD's ggml-hrx |
vulkan (and auto for GGUF) |
Vulkan0 |
standard GGUF quants, the fastest measured device for them |
Pinned sources, kept current#
| Submodule | Source | Pinned to |
|---|---|---|
third_party/llama.cpp |
1bit-MONSTER/llama.cpp, branch 1bit/hrx-vulkan-patched |
AMD's hrx-graph-develop-v2 (ggml-hrx on ggml-org llama.cpp) plus our commits ("Our patches") |
third_party/hrx-system |
ROCm/hrx-system | libhrx, loomc and the Loom tools |
- Where the pair comes from. The two pins are the pair AMD's integration repo, ROCm/ggml-staging-automation, builds and tests together.
- How it stays current.
.github/workflows/bump-hrx.ymlruns daily. When AMD moves its pair, it:- syncs the fork (
masterto ggml-org,1bit/hrx-vulkanto AMD's pin); - rebases our commits onto AMD's pin and moves
1bit/hrx-vulkan-patchedthere, after tagging the previous tippatched-<sha>so every commit the engine has pinned stays reachable (a rebase conflict fails the run: rebase by hand); - opens a PR here moving both submodules, listing the rebased commits.
- syncs the fork (
- The token it needs. The secret
HRX_BUMP_TOKENis a fine-grained token with Contents read/write on1bit-MONSTER/llama.cppand1bit-MONSTER/engine, and Pull requests read/write on1bit-MONSTER/engine. - Validation. CI builds that PR without HRX, so run the checks below on Strix Halo before merging.
Prefill on HRX, decode on Vulkan#
HRX prefills faster than Vulkan and Vulkan decodes faster, on the same GPU.
1bit serve --device vulkan --prefill-device hrx uses both: the HRX build's
llama-server decodes on Vulkan0, and a second copy of the model on HRX0 runs
the prompt prefix. The two contexts share one KV cache with no copy: HRX owns it
in its device memory and exports it as a dma-buf (HSA), Vulkan maps the same
memory, and only the KV cell metadata (positions, sequences) moves between them.
- What runs on HRX: the prompt prefix in whole ubatches (HRX prefill halves on
a partial one), from
--prefill-min-tokenstokens (default 1024; below that the split does not pay). The rest, and all decoding, run on Vulkan. - Needs: flash attention on both sides (
servepasses-fa on), no LoRA, no multimodal. The weights are loaded twice (once per device). - Layout: the shared region is cut into chunks of at most 1 GiB, each its own dma-buf: Vulkan reads garbage from one buffer past about 4 GiB (a 40960-token Qwen3-0.6B cache), and both sides must compute the same layout.
Measured on Strix Halo, llama-server on Vulkan0, prefill / whole request:
| Model | Prompt | Vulkan alone | HRX prefill + Vulkan |
|---|---|---|---|
| Qwen2.5-7B Q4_K_M | 2048 | 1541 / 4314 ms | 1078 / 3890 ms (-10%) |
| Qwen2.5-7B Q4_K_M | 8192 | 7347 / 10376 ms | 4646 / 7668 ms (-26%) |
| Qwen3-0.6B Q4_K_M | 8192 | 1289 / 2267 ms | 1089 / 2032 ms (-10%) |
Decode on the shared KV is within 3% of Vulkan with its own KV. Against Vulkan alone, teacher-forced over 64 tokens, the split's mean KL is 0.0002-0.0006 nats (max 0.007) and the top token agrees on 64-65 of 65 positions; Q4_K_M against BF16 is 0.063.
Our patches#
1bit/hrx-vulkan is AMD's commit unchanged; 1bit/hrx-vulkan-patched adds:
- Zero-copy KV sharing (the section above): dma-buf export in ggml-hrx
(
ggml-hrx-dmabuf.cpp), dma-buf import in ggml-vulkan (ggml-vulkan-dmabuf.inc),llama_kv_share_next/llama_kv_share_from/llama_kv_cells_copyin llama (llama-kv-share.cpp), andONEBIT_PREFILL_DEVICEin llama-server (server-prefill.cpp). - IQ3_XXS matmul on HRX. A matrix-vector kernel (
kernels/hrx/mul_mat_vec_iq3xxs_f32.loom) and its matcher (common/dispatch-mul-mat-iq3-xxs.cpp), ported from thefeat/hrx-port-ae91949work. - Honest op claims. ggml-hrx used to claim every op of a declared type, so
ggml's scheduler handed it nodes it could not dispatch and the graph failed
(
unsupported HRX node) with no CPU fallback. It now claims a node only when its dispatcher can execute it, except nodes already in HRX memory (KV cache views), which cannot move. Leaf (NONE) nodes count as covered. AMD's IQ4_NL and IQ4_XS matmul kernels, and GET_ROWS for IQ4_XS and batched IQ3_S, give wrong values, so those nodes are left to the CPU.
Measured on Strix Halo (Qwen3-0.6B, perplexity over 8 x 512 wikitext tokens):
| File | Vulkan0 | HRX0 on AMD's commit | HRX0 with our patches |
|---|---|---|---|
| Q4_K_M | 22.53 | 22.52 | 22.52, same speed (21691 / 324 tok/s) |
| UD-Q4_K_XL | 22.43 | fails | 22.41 |
| UD-Q2_K_XL | 36.26 | fails | 36.46 |
| UD-IQ2_M | 55.44 | fails | 55.58 |
test-backend-ops -b HRX0: 791 OK, 0 failed (on AMD's commit, about 1150 fail).
Correct is not fast: the UD files run their IQ4_XS and sub-4-bit layers on the CPU
(UD-Q4_K_XL 6303 / 246 tok/s, UD-Q2_K_XL 974 / 76), so they belong on Vulkan0.
1bit-MONSTER/llama.cpp also holds
1bit/hrx2-archive, the previous build (AMD's abandoned ggml-hrx2 plus our Q4NX
kernels and the zaya architecture). It is kept for a later port of that work to
ggml-hrx.
The Vulkan route has its own upstream llama.cpp pin (latest release), so new
architectures do not wait for AMD's pair: see Vulkan (upstream). This build
still has a Vulkan backend, which --device vulkan uses when ONEBIT_VULKAN is
off.
Build#
git submodule update --init --depth 1 third_party/hrx-system third_party/llama.cpp
cmake -B build -G Ninja -DONEBIT_HRX=ON # needs TheRock at /opt/rocm-therock (ONEBIT_HRX_TOOLCHAIN)
cmake --build build --target onebit
- One external project. llama.cpp's ggml-hrx builds hrx-system itself
(
HRX_SOURCE_DIR) with TheRock'samdclang. IREE_ROCM_PATHis not passed. It would switch hrx-system to "package" mode, which needs TheRock's aqlprofile-sdk headers, and our/opt/rocm-therockdoes not ship them. Left unset, hrx-system fetches its pinned HSA/AQL headers.- The HSA runtime. HRX dlopens it at run time, and the distro's
libhsa-runtime64rejects theHSA_AMD_AGENT_INFO_PM4_EMULATIONprobe on gfx1151, so HRX then registers no device. CMake finds TheRock'slibhsa-runtime64.so.1(ONEBIT_HRX_LIBHSA), and1bit servepasses it to llama-server asIREE_HAL_AMDGPU_LIBHSA_PATH, unless it is already set. Without a build path it searches/opt/rocm-therock. To run llama-server or llama-bench by hand, export that variable yourself.
Verified (2026-09-23, llama.cpp f1a0aca, hrx-system 51b1739)#
ctest passed, re-run after the first daily bump (#13). At the time the check
was tests/hrx_lemonade_e2e.sh, through the engine's former embedded Lemonade:
unsloth/Qwen3-0.6B-GGUF:Q4_0 answered "Paris" on Vulkan0 and on HRX0. The
same build now passes tests/serve_e2e.sh on both devices through 1bit serve.
llama-bench, Qwen3-0.6B, pp512 / tg128 tok/s, against the previous build (April
llama.cpp with ggml-hrx2 on HRX20):
| Device, file | previous build | this build |
|---|---|---|
| Vulkan0, Q4_K_M | 10627 / 306 | 14097 / 349 |
| Vulkan0, UD-Q4_K_XL | 10668 / ~300 | 13943 / 324 |
| HRX, Q4_K_M | 1046 / 68 | 21990 / 314 |
| HRX, UD-Q4_K_XL | 837 / 83 | 17906 / 293 |
| Vulkan0, BF16 | n/a / 115 | 4504 / 144 |
| HRX, BF16 | n/a / 40 | 7646 / 101 |
HRX prefill is now faster than Vulkan's.
Not yet#
- Fast sub-4-bit and IQ4 kernels on HRX. With our patches the UD files are
correct on
HRX0but slow, since IQ4_XS, IQ2 and IQ3_S matmuls fall back to the CPU. AMD's IQ4_XS / IQ4_NL kernels need fixing upstream. - Q4NX on HRX. Our Q4NX kernels lived in ggml-hrx2 and are not in ggml-hrx.
They are kept on
1bit/hrx2-archiveuntil they are ported. - First-request cost. Lemonade's telemetry for the first short HRX chat shows 18 tok/s against llama-bench's 303. It is probably first-use kernel compilation, not steady state, but this is not yet separated.
- CI. CI has no AMD GPU and no TheRock, so
ONEBIT_HRXis exercised on Strix Halo only.