Radeon 8060S · gfx1151 · ROCm
Three layers of a local inference stack, and what is happening in each
Strix Halo is an unusual machine to run large models on: a lot of unified memory, modest bandwidth, and an iGPU that most of the software stack still treats as a discrete card. This page is a working log of three layers — a HIP runtime change, ROCm work in llama.cpp, and two Qwen3.8 models — and of where each of them currently stands.
Part one · the runtime
Retained PM4 command lists for HIP graphs
A change to the ROCm userspace runtimes, not to any inference engine. It is the most general piece of this work: nothing about it is specific to llama.cpp or to a particular model.
The problem
A HIP graph is meant to amortise launch cost, but the runtime still re-encodes the PM4 packets for every node on every launch. On a workload made of many small dispatches — which is what a quantized MoE forward pass looks like — that host-side encode is a measurable share of the wall clock, and on an APU there is no spare CPU to hide it behind.
The change
Let a graph materialise its packets once and replay the retained command list. ROCr gains a
vendor API — hsa_ven_amd_graph_command_list_create,
…_materialize_packet, …_get_capabilities and friends — and HIP grows
retained command lists on top of it, with batch merging for collapsed packet runs.
Where it stands
Five commits on a branch of rocm-systems,
rebased on current develop: the command lists themselves, packet-batch merging, a
fix for quadratic growth in the update path, gfx1201 register programming, and a build fix for
header selection. There is a contribution document and a rocrtst case alongside
the implementation.
Status Experimental and enabled by an environment variable. It is not a proposal to AMD yet; the register programming is only validated on the two targets to hand.
Honest caveat
For the Qwen3.8-Next-Flash prefill work below, HIP graphs never actually engage: graph capture needs two consecutive graphs with unchanged properties, and each prefill chunk shape occurs once per request. The retained-PM4 path pays off in decode, where the same graph repeats. It is included here because it is the layer that generalises, not because it carries the prefill numbers.
Part two · the engine
Three places ROCm work on llama.cpp is happening
These are not competing efforts so much as different tolerances for risk and different review latencies. It is worth being clear about which is which, because the right place to send a given change depends on it.
Mainline — slow and stable
Upstream takes RDNA-specific work, but carefully and with broad-hardware review. Merged recently:
- RDNA3 MMA flash attention, a faster AMD transpose and tuning (#22880)
- RDNA3.5 and RDNA3 split into independently tunable MMQ configs (#26199), then tuned (#26284)
- Routed MoE MMQ N-tiles sized from typical expert width on RDNA3 (#28552)
- Flash-attention tuning for gfx1201 (#28102)
- A ROCm CI runner with gfx1151 fixes (#26544)
- iGPU-aware loading:
--load-mode autoavoiding mmap (#26081), lazy tensor loading off by default on iGPUs (#28326), virtual iGPU devices (#26953)
Vulkan is moving at least as fast on this hardware — mat-vec row tuning for Strix Halo (#27909), a dedicated IQ4_XS mat-vec shader (#28426), tiled transpose for permuted CONT (#26585), and fused hyper-connection ops for DeepSeek-V4 (#26578).
The community fork — fast and targeted
halo-box/strix-llama.cpp exists to get Strix Halo changes usable before they are upstreamable, and it is currently doing most of its work on the Vulkan backend:
- Quantized dense coopmat pipelines run at wave32
- An optional f16 B operand for quantized
MUL_MATon coopmat1, with an auto mode mul_mat_idtiles derived from the wave64 dense tiles; the Strix mmid tile gates on by default- Command buffers bounded by memory traffic rather than flops alone
- Four env-gated prefill fixes for delta-net MoE, and the transposed-concat path defaulted on
- A coopmat1 FA wave32 pin narrowed to multi-row dispatches
Several of these are the kind of thing that wants a wider hardware matrix before upstream will take it, which is exactly what a fork is for.
My branches — trying to reconcile the two
The work on pwilkin/llama.cpp aims at the middle: keep the fast path usable locally, and factor the pieces that can stand on their own into shapes mainline can review. Currently in flight:
sched-uma-ring— a graph-input ring buffer in the backend scheduler, which fixes the aliasing race that made upstream disableprop.integratedon HIP rather than work around ittopk-rocm-fix— wave32-native TOP_K kernelscuda-getrows-vec,cuda-q8-norm-fusion,hip-fattn-tile-hs256-tune,fix-igpu-input-layer— small, self-contained pieces aimed at upstreamstrix-halo— the integration branch these pages build fromstrix-halo-for-halobox— the graph-agnostic half of those kernels rebased onto the community fork, so the pieces that do not depend on this branch's model graph can live where the rest of the Strix work is
Position Where mainline already has a good answer, the branch takes mainline's. The head-size-256 attention tuning in the series below is a case where upstream's broader gate turned out to be the correct half and ours was the wrong one.
Why a fork at all
Some of what makes this machine fast is genuinely narrow: kernels that assume 48 value heads, a 2560-wide residual, or a particular expert count. Those are not upstreamable as written, and pretending otherwise wastes reviewers' time. Keeping them on a branch, behind gates, with the measurements written down, seems more useful than either hiding them or filing them.
Part three · the models
Two Qwen3.8 models, and what they do here
One is a dense-ish 27B that fits comfortably; the other is a 177B hybrid that barely fits at all. They stress completely different parts of the stack.
Qwen3.8-27B
Calibrated IQ4_XS weights with a Q8 output layer and an IQ4_XS DFlash2 drafter. This is the configuration the installer below builds, and the one that has been stable longest.
| Measurement | Value |
|---|---|
| Prompt processing | 256.84 t/s |
| Decode | 26.26 t/s |
| Speculative acceptance | 60.7% |
| Prompt length | 31 497 tokens |
Two-pass range 26.23–26.28 t/s across six generations, GPU 37–54 °C. Measured with the retained-PM4 runtime and a fixed-width DFlash2 policy.
Qwen3.8-Next-Flash
177 B parameters, 93 GiB of IQ4_NL, a gated delta-net recurrence in most layers, a lightning indexer selecting 2048 key blocks per query, and a 27.5 GB per-layer-embedding table that cannot be resident. It is the harder problem by some distance, and prefill is where the work went.
| Depth | pp16384 | tg128 |
|---|---|---|
| 0 | 1204.31 ± 2.31 | 26.28 ± 0.29 |
| 40 000 | 1086.29 ± 0.96 | 16.63 ± 0.14 |
Measured with a 16384-token batch and ubatch, which is what the installed launcher defaults to, so the whole prompt goes through as one batch. A 24576 ubatch measures the same within error (1199.63 ± 7.12 against 1204.31 ± 2.31 in the same session); 16384 is the default because it reserves smaller compute buffers, which on a unified memory part is 8 GB less competing with the weights. Prefill holds 90% of its depth-0 rate at 40 000 tokens of context, which is the sparse attention path doing its job. Decode is the weaker half — the journey's paired run put it about 8% short of what the same kernels reach out of tree — and that gap is open and written up rather than smoothed over.
On comparisons Earlier versions of this page ranked these numbers against other people's stacks. That framing was more combative than useful: the configurations differ in target model, draft policy and backend, and a single ordering hides more than it shows. The figures here are what this stack does on this machine, with the protocol stated so anyone can reproduce or contradict them.
Getting it running
Two installers, one per model
Both build the pinned ROCr, HIP and llama.cpp revisions into user-owned prefixes,
verify hardware first, and install launchers into ~/.local/bin. Neither touches
/opt/rocm or needs root.
Qwen3.8-27B — the stable configuration
bash <(curl -fsSL https://raw.githubusercontent.com/pwilkin/strix-halo/main/install.sh)
Needs about 30 GiB free. Models default to ~/.models.
Inspect before running if you would rather:
curl -fsSLO …/install.sh && ./install.sh --skip-packages --check-only.
Qwen3.8-Next-Flash — the experimental one
bash <(curl -fsSL https://raw.githubusercontent.com/pwilkin/strix-halo/main/install-flash-next.sh)
Needs about 110 GiB free for the model alone, and a
machine with 128 GB of unified memory. The launcher it installs sets
--load-mode none --lazy-mode on-direct and a 16384-token batch and ubatch, which is
what keeps the embedding table out of the resident set.
Expect rough edges This configuration is newer than the 27B one, the kernels are gated by environment variables, and decode is not yet where it should be.
Support
Useful? Help fund the next weird driver idea.
This runs on hardware, power, storage and a fair amount of patience with experimental drivers. If any of it saved you time, a coffee is a genuinely good way to keep it going.
☕ Buy me a coffee