Local LLM/VLM + image/video generation + NPU inference for AMD Strix Halo APU (Ryzen AI MAX+ 395, Radeon 8060S iGPU, XDNA2 NPU) with 128GB unified LPDDR5X (124GiB visible to Linux; the iGPU addresses up to 108GiB of it via GTT — capped on purpose, see boot parameters).
┌──────────────────────────────────────────────────────────────────┐
│ GPU (Vulkan RADV + ROCm toolboxes — GTT capped at 108GiB) │
│ ├─ llama-server (Vulkan, llama.cpp) port 8001 │
│ │ └─ Qwen3.6-35B-A3B MTP (UD-Q4_K_XL, primary; 256k-capable, │
│ │ run at 128k ctx). Switchable: Qwen3.6-27B / Gemma / DS4 │
│ ├─ llama-server-qwen38 (Vulkan) port 8022 │
│ │ └─ Qwen3.8-27B VL + native MTP (UD-Q4_K_XL + mmproj-F16) │
│ │ second resident: vision + writing. Replaced Muse-Glimmer │
│ ├─ ComfyUI (ROCm toolbox) port 7860 │
│ │ └─ Image/video gen (Wan 2.2, HunyuanVideo, Qwen Image) │
│ ├─ llama-surya2 (ROCm toolbox) port 8093 (on-demand) │
│ │ └─ Surya 2 OCR VLM 650M (document OCR) │
│ └─ surya-server (podman, dedicated) port 8090 (legacy) │
│ └─ Surya v1 layout+OCR │
│ │
│ NPU (XDNA2 — 51 TOPS, 47μs latency) │
│ └─ FastFlowLM (flm-asr.service) port 52625 (on-demand) │
│ └─ Whisper STT for Jarvis (off CPU) + small LLMs │
└──────────────────────────────────────────────────────────────────┘
All GPU services run as systemd user services; the two resident LLM servers (:8001, :8022) and ComfyUI auto-start on boot, the rest start on demand.
| Component | Details |
|---|---|
| Board | Sixunited AXB35 (BeyondMax Series) |
| CPU | AMD Ryzen AI MAX+ 395 (32 threads) |
| GPU | Radeon 8060S (RDNA 3.5, gfx1151) |
| VRAM | BIOS VGM carve: 1GB (deliberate — GPU allocates from GTT instead) |
| NPU | XDNA2 aie2p 6x8 (PCI c7:00.1, 1022:17f0 rev 11) |
| RAM | 128GB LPDDR5X-8000 physical; 124GiB visible to Linux |
| GTT | 124GiB window, pinned pages capped at 108GiB (amdgpu.gttsize=126976, ttm.pages_limit=28311552) — see Kernel boot parameters |
| BIOS | AMI v1.07 |
| Component | Version | Notes |
|---|---|---|
| Kernel | 7.2.0-rc3 (vanilla) | COPR @kernel-vanilla/mainline-wo-mergew |
| Mesa | 25.3.6 (Vulkan 1.4.341) | Vulkan RADV driver |
| llama.cpp | fresh upstream ~/llama.cpp @ 69bf643 (2026-08-08, Vulkan build) |
Native MTP speculative decoding (--spec-type draft-mtp). The old ~/llama-cpp-turboquant fork is retired for the LLM path. |
| ROCm | 7.2 (kyuz0 toolbox) | For Surya OCR + ComfyUI containers |
Vulkan users: pin the date-stamped kyuz0 tag. A later build on the floating tag cost 2.3x on token generation here. See docs/vulkan-pinned-build.md. | XRT | 2.23.0 | NPU runtime, built from
~/xdna-driver| | amdxdna | 2.23.0 (DKMS) | NPU kernel module | | FastFlowLM | v0.9.36 | NPU inference server | | Unsloth | 2026.7.4 (torch 2.11.0+rocm7.13.0) | Fine-tuning, isolated venv at~/.unsloth/studio. Seedocs/unsloth-rocm-gfx1151.md|
git clone https://github.com/sypherin/strix-halo-setup.git
cd strix-halo-setup
chmod +x setup.sh bin/*.sh patches/*.sh
./setup.sh
systemctl --user start llama-server comfyuiFor NPU setup, see NPU Setup below.
Five compute shaders implementing DSv4's custom GGML operations on Vulkan — the first
such implementations I can find, since upstream is adding them CPU-only and the fork
they target ships CPU + Metal. 1,153 insertions, 64 numerics tests, verified on gfx1151.
Applying them takes a fork that produced zero tokens in 30 minutes to coherent
generation at 12.3 t/s. Patch series + apply instructions:
patches/vulkan-dsv4-kernels/.
A 284B-parameter model at full 131k context on this box, GPU-offloaded via Vulkan,
no CUDA. 130 t/s prompt, 12 t/s generation, ~5% decay to 4k depth. Measured 2026-08-01.
Includes the traps that cost a day (the llama-cli interactive-EOF loop that mimics a
hung model, Vulkan's ~5GB single-allocation cap, reasoning-budget starvation, the
q8_0-KV throughput penalty) and an honest quality verdict against a strict spec
benchmark. Full writeup: docs/deepseek-v4-flash-284b.md.
llama-server -m DeepSeek-V4-Flash-0731-UD-IQ2_XXS-00001-of-00003.gguf \
-ngl 99 -c 131072 -ub 1024 -fa on -np 1 --cache-prompt --reasoning-budget 4096Run the Claude Code CLI against the local Qwen3.6-35B-A3B model — fully offline,
zero API cost, 256k context — for real coding on large codebases. Built + verified
2026-07-10. Full writeup: docs/claude-code-local-qwen3.6-mtp.md.
ccr code # from any project dir → Claude Code driving local Qwen3.6
ccr code -p "refactor foo" # one-shot / headless
ccr status # is the router up?
ccr restart # after any model/config changeClaude Code CLI ──► claude-code-router (ccr, :3456) ──► llama-server (:8001)
Qwen3.6-35B-A3B MTP, Vulkan/RADV, 256k
- Model: unsloth
Qwen3.6-35B-A3B-MTP-GGUF(UD-Q4_K_XL, ~22.85 GB) with native multi-token-prediction speculative decoding — the MTP layers are grafted into the GGUF. - The MTP win:
--spec-type draft-mtpgives ~75–86 t/s on coding prompts (draft acceptance 65–80%) vs ~61 t/s no-MTP. A classic separate 0.8B draft model backfired (~27 t/s, ~20% acceptance) — don't use it. No penalty at 256k (~74–80 t/s). - ccr config:
configs/claude-code-router.config.json(lives at~/.claude-code-router/config.json;ccr restartafter edits).
:8001 runs one model at a time as a systemd user service. Flip between them with
bin/strix-llm-switch.sh:
~/bin/strix-llm-switch.sh qwen # → Qwen3.6-35B-A3B MTP (llama-server.service, the default)
~/bin/strix-llm-switch.sh qwen27 # → Qwen3.6-27B dense (llama-server-qwen27b.service)
~/bin/strix-llm-switch.sh gemma # → Gemma 4 26B-A4B (llama-server-gemma.service)
~/bin/strix-llm-switch.sh ds4 # → DeepSeek-V4-Flash 284B (llama-server-ds4.service)It writes ~/.config/strix-llm-unit as the single source of truth so a liveness watchdog
revives the currently-selected unit instead of fighting the swap.
Thinking is now OFF at the server (2026-08-14). The unit runs --reasoning off, so
Qwen3.6's hybrid thinking is opt-in per request with
chat_template_kwargs: {"enable_thinking": true}. This inverts the earlier setup, where
thinking was on by default and every small-max_tokens caller had to remember to send
enable_thinking: false or reasoning silently ate the budget and content came back
empty — off-by-default turned a recurring caller-side foot-gun into an explicit opt-in.
Since 2026-08-08 the box keeps a second model resident alongside the :8001 primary, for vision + writing work (the 35B-A3B is text-only). Two models held that seat:
- Muse-Glimmer-30B (2026-08-08 → 08-14): vision 30B on a custom ROCm-FP4 build,
~26.5 t/s — FP4-on-ROCm beats Q4_K-on-Vulkan on this bandwidth-bound part. Full
writeup:
docs/muse-glimmer-30b-strix.md. The unit (llama-server-glimmer-fpx) is disabled but kept on disk as a rollback. - Qwen3.8-27B (2026-08-14 →, current): native vision (mmproj) + native MTP in one
model, unsloth UD-Q4_K_XL + mmproj-F16, served by the same upstream llama.cpp Vulkan
build as :8001 — one stack instead of a bespoke FP4 build. ~22 t/s tg with 61% MTP
draft acceptance, 256k ctx,
--parallel 2. Recommended sampling (temp 0.7, top-p 0.8, top-k 20, presence 1.5) is baked into the unit, reasoning off by default. Unit:systemd/llama-server-qwen38.service.
vLLM runs on Strix Halo. Serving and generating, verified 2026-08-03 via the
lemonade-sdk/vllm-rocm qualified bundle with
Qwen3.6-27B-AWQ-INT4. This is the workload llama.cpp with --parallel 1 does not cover:
111 t/s aggregate across 32 concurrent users (19.6x scaling over single-stream), on one
desktop-class box. Full writeup: docs/vllm-gfx1151.md.
Muse-Glimmer-30B (vision, 30B): ~26.5 t/s on a custom ROCm-FP4 build; FP4-on-ROCm beats Q4_K-on-Vulkan (bandwidth-bound). Full writeup: docs/muse-glimmer-30b-strix.md.
bin/vllm-serve-strix.sh # defaults: AWQ 27B, :8107, eager, max_num_seqs 256
MEM_FRAC=0.5 PORT=8107 bin/vllm-serve-strix.sh /path/to/hf-model| concurrency | aggregate t/s | per-user t/s |
|---|---|---|
| 1 | 5.66 | 5.66 |
| 8 | 38.6 | 4.84 |
| 16 | 71.3 | 4.46 |
| 32 | 111.2 | 3.48 |
- Do not use it as a daily driver.
:8001(35B-A3B MoE + MTP) does 66–86 t/s single-stream — roughly 10x faster for one developer. vLLM's win is concurrency only. - These are floor numbers — measured with
--enforce-eager, so torch.compile and HIP graph capture were disabled. Graph capture is untried on purpose: vllm-project#32180 reports it hanging the gfx1151 driver, which would take every other GPU service down with it. - The one rule: launch through the bundle's own
bin/vllm-servershim. Every "packaging bug" worth hours —Failed to infer device type, a CUDAflash_attnimport error,EngineDeadErrorinkernel_paged_attention_2d— is really that shim's missing env (CC, the real amdsmi,LD_LIBRARY_PATH), not a gfx1151 defect.
Both drivers run Q4 with native MTP (--spec-type draft-mtp) on a fresh upstream llama.cpp
Vulkan build. Numbers are the server's own timings on real chat-completion requests (250-word
generation, warm), i.e. actual end-user throughput including the jinja chat template.
| Model | Quant + MTP | Sustained tg | Peak | Draft accept | Role |
|---|---|---|---|---|---|
| Qwen3.6-35B-A3B (3B active) | UD-Q4_K_XL, MTP n=3, q8_0 KV | ~66-78 t/s | 86 | 44-80% | PRIMARY :8001 driver (strix-llm-switch qwen) — MoE, proven agent driver, 256k ctx. ~66 on general text, up to ~78 on code (higher accept) |
| Qwen3.6-27B (DENSE, 27B active) | UD-Q4_K_XL, MTP n=5, q8_0 KV | ~20-22 t/s | 25 | 30-58% | experimental alternate (not default): dense, vision, stronger coder, 256k ctx. MTP only ~1.7x here (dense) |
| Qwen3.8-27B VL (DENSE, 27B active) | UD-Q4_K_XL + mmproj-F16, MTP n=2, q8_0 KV | ~22 t/s | — | ~61% | SECOND RESIDENT :8022 — native vision + native MTP in one model; the box's vision/writing driver since 2026-08-14 |
| Gemma-4-26B-A4B (4B active) | UD-Q4_K_XL, MTP n=3, f16 KV | ~78 t/s | 82 | 66-71% | switchable: vision-capable, strong extraction/structured-output |
Optimal MTP settings (tuned 2026-07-14):
--spec-draft-n-max 3is the sweet spot for both. Going higher (5/6/8) lowers throughput: the MTP head's draft acceptance craters past ~3 tokens (n=8 dropped Gemma to ~40 t/s).- KV cache: use
f16for Gemma MTP (about 10% overq8_0: 82 vs 74 peak). Qwen keepsq8_0(its 256k context needs the smaller KV, and f16's gain there is marginal). - Always:
-fa 1,--ubatch-size 1024,-ngl 99, and-ngld 99to offload the draft head too.
Tuned Gemma Q4+MTP launch:
llama-server -m gemma-4-26B-A4B-it-UD-Q4_K_XL.gguf \
-md mtp-gemma-4-26B-A4B-it.gguf --spec-type draft-mtp --spec-draft-n-max 3 -ngld 99 \
-ngl 99 -fa 1 --ubatch-size 1024 -ctk f16 -ctv f16 --parallel 1 --no-warmup --jinja --port 8001The MTP draft head mtp-gemma-4-26B-A4B-it.gguf (~0.46GB) lives inside the regular
unsloth/gemma-4-26B-A4B-it-GGUF repo (a separate head file, not a single fused gguf). Load it with
--spec-type draft-mtp; draft-simple tries to load the head as a full model and fails with
"failed to create llama_context".
Cross-machine (2026-07-14): against a reported HP Zbook Ultra G1a laptop running the same models via Lemonade/Vulkan, this desktop wins both: Gemma-4-26B-A4B MTP 78-82 vs 72, Qwen3.6-35B-A3B MTP 78 vs 65. For a 4B-active MoE the lever is MTP, not quant (Q4 non-MTP was only ~48 t/s; Q4+MTP ~78).
⚠ Qwen3.6-27B DENSE + MTP (EXPERIMENTAL ALTERNATE — not the default). The primary :8001 driver is
the Qwen3.6-35B-A3B MoE above; the 27B is a documented option you can flip to, not the standing driver.
The dense 27B is bandwidth-bound at ~12 t/s non-MTP (it reads all 17.9 GB of Q4 weights per token
against ~256 GB/s), and its built-in MTP head only lifts it to ~20-22 t/s at 256k (measured across 9
runs, draft acceptance ~30-58%). That is about 1.7x, NOT the ~6x a MoE gets. On a dense model MTP is
hard-capped at (draft_len + 1) x the base rate, and acceptance is low, so a dense 27B simply cannot reach
MoE-class throughput on this box. Correction: an earlier revision of this file listed the 27B at
~77 t/s; that figure was the 35B-A3B MoE mislabeled during a messy benchmarking session (the MoE has only
3B active per token, so its base rate is far higher). It is corrected here after a clean re-bench, with
thanks to the r/LocalLLaMA reader who flagged it. MTP is still lossless (the main model verifies every
token). The 27B is vision-capable (ships an mmproj) and a stronger coder, so it can be worth the flip if
you want vision + coding and can live with ~20 t/s. Flip to it with strix-llm-switch.sh qwen27; revert
to the 35B with strix-llm-switch.sh qwen.
Launch (see systemd/llama-server-qwen27b.service):
llama-server -m Qwen3.6-27B-UD-Q4_K_XL.gguf --spec-type draft-mtp --spec-draft-n-max 5 -ngld 99 \
--ctx-size 262144 -ngl 99 -fa 1 --ubatch-size 1024 -ctk q8_0 -ctv q8_0 --parallel 1 --port 8001Note: keep --reasoning-budget/--reasoning-format off the 27B unit and suppress thinking with
enable_thinking:false per request; letting a slow model think just burns the token budget.
(A previous version of this note claimed those flags "tanked" the 27B from ~77 to ~22 t/s. That was the
same 35B-vs-27B mixup as above, not a real reasoning-flag effect: the dense 27B runs ~20-22 t/s either
way.) The 27B-MTP gguf (single file, embedded head) is unsloth/Qwen3.6-27B-MTP-GGUF. Still
validating: watch for monologuing / degraded tool-following in real agent loops before trusting it
over the 35B MoE.
The Q8 / 128k tables below are the earlier (May) baseline, kept for host-config and kernel reference.
Tested 2026-05-23 against the live llama-server on :8001. Host: kernel
7.0.0-261 vanilla, Mesa 25.3.6, Vulkan RADV, llama-cpp-turboquant build.
| Metric | Value | Notes |
|---|---|---|
| Prompt processing (pp) | ~720 t/s | 10K-token prompt, 3-run avg (warm runs: 698, 741 t/s) |
| Token generation (tg) | ~41 t/s | 64-token generation, 3-run avg, hot |
| Time to first token | ~269ms | Short prompt, streamed, 3-run avg (257, 275, 274 ms) |
| Context size | 131072 | KV cache: q8_0 |
Tested 2026-04-29 against the live llama-server on :8001. Host: kernel
7.0.0-261 vanilla, Mesa 25.3.6, Vulkan RADV, llama-cpp-turboquant build.
| Metric | Value | Notes |
|---|---|---|
| Prompt processing (pp) | ~839 t/s | 10,223-token prompt |
| Token generation (tg) | ~44 t/s | 64-token generation, no reasoning |
| Time to first token | ~254ms | 23-token prompt, --no-warmup hot |
| Context size | 131072 | KV cache: q8_0 |
Numbers in both tables are taken from the server's own timings field on real
OpenAI-compatible chat-completion requests, not synthetic llama-bench runs —
i.e. they reflect actual end-user latency including the chat template + jinja
rendering.
Trade quantified. Moving from Qwen3.6-35B-A3B (3B active) to Gemma 4 26B-A4B (4B active) costs ~14% on pp (839 → 720), ~7% on tg (44 → 41), and adds ~15ms on TTFT (254 → 269). All within the expected ~33% active-param ratio. Decode quality on long-form extraction + structured-output workloads improved enough to justify the throughput cost for this box's workload mix; your mileage will depend on what you're shipping.
Note on long-generation throughput. Streaming a 512-token reply with the
default --reasoning-budget 500 and the model's built-in thinking mode produced
~7 t/s wall-clock on a single request. The slowdown is not a Vulkan/host issue
— Qwen3.6 silently emits thinking tokens that don't get counted in predicted_n,
so the t/s reported is artificially low. For "real" tg comparisons against
non-thinking models, set --reasoning-budget 0 or use a no-reasoning system
prompt.
Retained for host-config reference. Tested kernel 7.0-rc6, Mesa 25.3.6.
| Metric | Value | Notes |
|---|---|---|
| Prompt processing (pp) | 393 t/s | ~2K token prompt |
| Token generation (tg) | 22 t/s | Stable across runs |
| Time to first token | ~430ms | Short prompts |
| Context size | 65536 | KV cache: q8_0 |
| Metric | Kernel 6.19.9 | Kernel 7.0-rc6 | Change |
|---|---|---|---|
| pp | 287-351 t/s | 393 t/s | +12-37% |
| tg | 22-23 t/s | 22 t/s | No change |
Kernel 7.0 significantly improves prompt processing via RADV/Vulkan improvements, but token generation is memory-bandwidth bound and unchanged.
| Setting | Tested values | Winner | Notes |
|---|---|---|---|
--ubatch-size |
512, 1024, 2048 | 1024 | pp 320 vs 266 vs 320, diminishing returns at 2048 |
--kv-unified |
on, off | off | Hurt pp, broke prompt caching, no tg benefit |
-ctk/-ctv |
turbo2, q8_0 | q8_0 | turbo2 not supported on Vulkan (SET_ROWS op missing) |
-fa |
on, off | on | Required for good performance on Strix Halo |
| Service | Port | Backend | Startup | Description |
|---|---|---|---|---|
llama-server |
8001 | Vulkan | auto | Primary LLM — Qwen3.6-35B-A3B MTP (fresh upstream llama.cpp, Vulkan RADV) |
llama-server-qwen38 |
8022 | Vulkan | auto | Second resident — Qwen3.8-27B VL + MTP (vision + writing; replaced Muse-Glimmer 2026-08-14) |
llama-server-qwen27b |
8001 | Vulkan | via switch | Alternate — Qwen3.6-27B dense. Bind-conflicts with llama-server; flip with strix-llm-switch.sh qwen27 |
llama-server-gemma |
8001 | Vulkan | via switch | Alternate — Gemma 4 26B-A4B. Bind-conflicts with llama-server; flip with strix-llm-switch.sh gemma |
comfyui |
7860 | ROCm | auto | Image/video gen (kyuz0 toolbox container) |
llama-surya2 |
8093 | ROCm | on-demand | Surya 2 OCR VLM 650M (document OCR) |
flm-asr |
52625 | NPU | on-demand | Whisper STT for the Jarvis voice assistant (offloaded from CPU) + small LLMs |
lemonade |
8000 | Vulkan | manual | Web UI + sd-cpp (optional) |
# Status
systemctl --user status llama-server llama-server-qwen38 comfyui
# Start/stop
systemctl --user start llama-server
systemctl --user stop llama-server
# Logs
journalctl --user -u llama-server -f
# Health check
curl http://localhost:8001/healthRequired for optimal Strix Halo unified memory performance (current since 2026-08-09):
amd_iommu=off amdgpu.gttsize=126976 ttm.pages_limit=28311552
amd_iommu=off— replaces the earlieriommu=pt. Benchmarked on Strix Halo as 5-12% faster than either IOMMU-enabled mode (Lars Urban's numbers in kyuz0's toolboxes repo, issue #66); fine on a box that needs no VFIO/SR-IOV passthroughamdgpu.gttsize=126976— GTT window of 124GiB (126976 MiB) so the iGPU can address nearly all system RAMttm.pages_limit=28311552— pinned-pages cap of 108GiB (28311552 × 4KiB), deliberately ~16GiB below the GTT window. An earlier revision matched the cap to the window (124GiB == all of RAM); that let GPU allocations starve the host — a second model load OOM-killed the whole desktop with "nothing killable" (2026-08-08) because every large process was pinned. Capping TTM at 108GiB keeps a floor of host-reclaimable RAM no model load can take.
⚠ The TTM cap can be silently clamped — verify it at runtime, not just on the cmdline. Setting
ttm.pages_limit=on the kernel cmdline is necessary but not always sufficient: on some boots the live value is clamped back toward the ~96GiB default, socat /sys/module/ttm/parameters/pages_limitreads something other than what the cmdline asked for. When that happens, any model whose weights exceed the live cap (e.g. the 90.9GiB DeepSeek-V4-Flash mixed q2/q4/q8 quants) fails to load or thrashes — even though the cmdline looks correct. The symptom is a "needs a smaller base model / exceeds the box" style failure that disappears once the live cap is actually applied.Fix: enforce it at runtime and persist it with a tiny boot service. The service must write the SAME value as the cmdline — a service still carrying an old value silently re-widens (or re-narrows) the cap on every boot.
cat /sys/module/ttm/parameters/pages_limit # check the LIVE value first (pages) echo 28311552 | sudo tee /sys/module/ttm/parameters/pages_limit # 28311552 x 4KiB = 108GiB # persist across reboots — see systemd/ttm-pages-limit.service sudo cp systemd/ttm-pages-limit.service /etc/systemd/system/ sudo systemctl enable --now ttm-pages-limit.service cat /sys/module/ttm/parameters/pages_limit # must now read 28311552The service only writes one sysfs number, depends on nothing, and no-ops on failure, so it can never delay or block boot.
Pairs with the BIOS: VGM/UMA set to the 1GB minimum, NOT a big dedicated carve. The GPU then allocates from GTT on demand — RAM stays flexible between CPU and GPU instead of being hard-partitioned at boot. (An earlier revision of this setup used a 96GB VGM carve, which left Linux only ~31GB of system RAM; that approach is retired and this README previously described it.)
Set via grubby or /etc/default/grub.
| Container | Image | Status | Purpose |
|---|---|---|---|
llama-rocm-7.2 |
kyuz0/amd-strix-halo-toolboxes:rocm-7.2 |
running | ROCm inference (Surya 2 OCR; formerly the VLM) |
strix-halo-comfyui |
kyuz0/amd-strix-halo-comfyui:latest |
running | ComfyUI image/video gen |
strix-halo-image-video |
kyuz0/amd-strix-halo-image-video:latest |
available | Qwen Image Studio + Wan 2.2 |
llama-vulkan-radv |
kyuz0/amd-strix-halo-toolboxes:vulkan-radv |
available | Vulkan LLM (backup) |
| Toolbox | Docker Tag | Purpose |
|---|---|---|
| Vulkan RADV | kyuz0/amd-strix-halo-toolboxes:vulkan-radv |
llama.cpp Vulkan (most stable) |
| ROCm 7.2 | kyuz0/amd-strix-halo-toolboxes:rocm-7.2 |
llama.cpp ROCm (long context) |
| ComfyUI | kyuz0/amd-strix-halo-comfyui:latest |
Image/video gen (ROCm TheRock) |
| Image/Video | kyuz0/amd-strix-halo-image-video:latest |
Qwen Image + Wan 2.2 + ComfyUI |
| vLLM | kyuz0/vllm-therock-gfx1151:latest |
vLLM serving (ROCm TheRock) |
| Finetuning | kyuz0/amd-strix-halo-llm-finetuning:latest |
LoRA/QLoRA training (ROCm) |
| Voice | kyuz0/amd-strix-halo-voice:latest |
VibeVoice TTS + voice cloning |
The XDNA2 NPU requires an out-of-tree driver build — the kernel's built-in amdxdna v0.6.0 has a version mismatch with newer XRT. The COPR xanderlent/amd-npu-driver packages (April 2025) are also outdated.
# Clone AMD's xdna-driver repo
git clone --depth 1 https://github.com/amd/xdna-driver.git ~/xdna-driver
cd ~/xdna-driver
git submodule update --init --recursive
# Install dependencies
sudo dnf install -y ninja-build jq
# Build XRT base + NPU packages
cd xrt/build
bash build.sh -npu -opt
sudo rpm -Uvh --force Release/xrt_*-base.rpm Release/xrt_*-npu.rpm
# Build and install the xdna driver plugin (includes DKMS kernel module)
cd ../../build
bash build.sh -release -install_prefix /opt/xilinx/xrt
sudo rpm -Uvh --force Release/xrt_plugin.*-amdxdna.rpm
# Verify NPU is detected
source /opt/xilinx/xrt/setup.sh
xrt-smi examine
# Should show: RyzenAI-npu5, aie2p, 6x8
# Validate NPU compute
xrt-smi validate
# GEMM and latency tests should passTest 1: gemm → PASSED (51.0 TOPS)
Test 2: latency → PASSED (47.0 μs average)
Test 3: throughput → FAILED (runlist abort — known issue, non-critical)
- Driver: amdxdna v2.23.0 (DKMS, built from
amd/xdna-drivermain branch) - XRT: v2.23.0 (built from submodule, installed at
/opt/xilinx/xrt/) - Firmware:
npu.sbin.1.0.0.166at/lib/firmware/amdnpu/17f0_11/ - Device:
[0000:c7:00.1]RyzenAI-npu5, aie2p architecture, 6x8 topology - memlock: unlimited (
/etc/security/limits.d/99-amdxdna.conf) - BIOS note: No NPU/IPU toggle in Sixunited AXB35 BIOS — NPU is enabled by default
The NPU (XDNA2, ~50 TOPS INT8) is best for small always-on models, freeing the GPU for large models:
| Use Case | Tool | Status |
|---|---|---|
| Voice assistant STT (Jarvis) | FastFlowLM whisper-v3:turbo (flm-asr.service) |
Working — ~6x realtime, off the CPU (service on-demand) |
| Small LLM (1-4B) | FastFlowLM (flm serve <model>) |
28-89 tok/s |
| Embeddings | embed-gemma:300m via flm |
Low latency |
The "Hey Jarvis" assistant's Whisper STT runs on the NPU instead of the CPU, freeing the CPU that the :8001 MTP draft-verify contends for. Wake-word (openWakeWord) and Piper TTS stay on the CPU — both are tiny with no NPU path.
- NPU endpoint:
flm serve lfm2:2.6b --asr 1exposes an OpenAI-compatiblePOST /v1/audio/transcriptionson:52625, backed bywhisper-v3:turboon the NPU. Persisted asflm-asr.service(seesystemd/).- Gotcha: do NOT set
FLM_CONFIG_PATHto the~/.config/flmdir — flm reads it as a model-list file and crashes (basic_filebuf::underflow ... Is a directory). Leave it unset (flm finds~/.config/flm/modelsfrom$HOME); the service just sources/opt/xilinx/xrt/setup.shfor the XRT libs.
- Gotcha: do NOT set
- Assistant wiring:
~/bin/voice-assistant/assistant.pytranscribe()posts the recorded-command WAV to the NPU endpoint whenconfig.jsonhas"stt_backend": "npu"(+"npu_stt_url"). Default"cpu"keeps the openai-whisper path — fully revertible by flipping that one key. - Not movable: MTP/speculative decoding stays GPU-only (integrated MTP heads = no separable draft; a cross-hardware NPU draft would add per-step latency that eats the speedup).
The old advice here ("--mmap is REQUIRED for Vulkan") dated from the 96GB-VGM-carve era, when Linux only saw ~31GB of RAM and --no-mmap would swap-thrash. That no longer applies. With the current 1GB carve + 124GiB GTT, model weights live in host RAM either way, so mmap is now a per-unit tuning choice — the two deployed Vulkan LLM units differ deliberately:
| Unit | mmap | Env |
|---|---|---|
llama-server (Qwen3.6 MTP, primary) |
--mmap |
GGML_VK_PREFER_HOST_MEMORY=ON |
llama-server-gemma (Gemma 4 Q4+MTP, alternate) |
--mmap |
GGML_VK_PREFER_HOST_MEMORY=ON, RADV_PERFTEST=nogttspill |
--no-mmap loads weights into host memory up front (no first-token page-fault stalls); --mmap is fine here too since weights stay resident under the GTT regime. If you change either, benchmark on that specific model — don't cargo-cult the flag across units.
ROCm toolbox containers use --no-mmap because ROCm's mmap path above 64GB is very slow on gfx1151. The GPU has direct access to system memory in ROCm mode, so --no-mmap loads into GPU-accessible memory correctly.
ComfyUI also requires --disable-mmap --cache-none --bf16-vae for the same reason.
FP8 is a software limitation on Strix Halo (RDNA 3.5). Always use BF16 models for image/video generation.
LLM inference: ROCm doesn't reliably detect gfx1151 for all workloads. Vulkan via RADV works perfectly and uses the full unified pool (~124GiB GTT).
Image/video generation: ComfyUI and PyTorch-based pipelines require ROCm. The kyuz0 toolbox containers include patched ROCm (TheRock nightlies) that work on gfx1151 with HSA_OVERRIDE_GFX_VERSION=11.5.1.
| Vulkan (RADV) | ROCm (kyuz0 toolbox) | |
|---|---|---|
| LLM (llama.cpp) | ~22 t/s gen, ~393 t/s pp | ~21 t/s gen, ~268 t/s pp |
| Image gen (ComfyUI) | N/A | ~198 it/s |
| Stability | Excellent | Good (needs toolbox) |
| Model | Type | Active Params | Quant | Speed | Use case |
|---|---|---|---|---|---|
| Qwen3.6-35B-A3B MTP | MoE | 3B | UD-Q4_K_XL | ~75–86 t/s gen (native MTP), 256k-capable (run at 128k) | Default (2026-07-10+) — Claude Code / coding, fast decode via --spec-type draft-mtp |
| Qwen3.8-27B VL (:8022, second resident) | Dense | 27B | UD-Q4_K_XL + mmproj-F16 | ~22 t/s gen (native MTP, 61% accept), 256k ctx | Vision + writing (2026-08-14+) — replaced Muse-Glimmer-30B on :8022 |
| Gemma 4 26B-A4B-it | MoE | 4B | UD-Q4_K_XL + MTP | ~78 t/s gen (draft-mtp n=3, f16 KV peak 82), 128k ctx | Switchable alternate: vision, extraction quality, structured output, tool use |
| Qwen3.6-35B-A3B (Q8, no MTP) | MoE | 3B | UD-Q8_K_XL | ~44 t/s gen, ~839 t/s pp | Prior primary — higher-fidelity quant without MTP |
| Qwen3.5-122B-A10B | MoE | 10B | UD-Q4_K_XL | ~22 t/s gen, ~393 t/s pp | Legacy, SOTA quality but slower |
To flip the live :8001 model between the Qwen3.6 MTP default and Gemma:
~/bin/strix-llm-switch.sh qwen # or: gemmaTo change quant/path, edit the -m line in the relevant unit (llama-server.service for
Qwen3.6, llama-server-gemma.service for Gemma) and systemctl --user restart it.
The dedicated Qwen3-VL-32B server (llama-vlm-bom, :8080, ROCm toolbox) is retired:
the always-on Qwen3.8-27B VL on :8022 handles vision now — one resident model instead of
a windowed 32B that had to be started for every vision batch. Every former :8080 consumer
points at :8022. Surya 2 (:8093) remains the dedicated document-OCR path.
Toolbox: kyuz0/amd-strix-halo-comfyui:latest (ROCm TheRock nightlies)
| Workflow | Type | Notes |
|---|---|---|
| HunyuanVideo 1.5 | I2V / T2V | 4-step LoRA, 720p |
| Qwen Image 2512 | T2I | Must use BF16 (not FP8) |
| Qwen Image Edit | Image Editing | Lightning LoRA |
| Wan 2.2 | I2V / T2V | 14B model with 4-step Lightning LoRA |
The LLM path now uses a fresh upstream ~/llama.cpp build (verified @ fb30ba9,
2026-07-09) — it exposes the native MTP flag (--spec-type draft-mtp) the Qwen3.6
primary needs. The old ~/llama-cpp-turboquant fork is retired for the LLM (its turbo
KV cache types were CPU-only anyway; on Vulkan we use q8_0 KV cache).
git clone --depth 1 https://github.com/ggml-org/llama.cpp ~/llama.cpp
cd ~/llama.cpp
cmake -B build -G Ninja -DGGML_VULKAN=ON -DCMAKE_BUILD_TYPE=Release -DLLAMA_CURL=ON
cmake --build build -j16 --target llama-server
# Update service
systemctl --user restart llama-server
curl http://localhost:8001/health # wait for model to load (~35s)Build deps: glslc, cmake, ninja, Vulkan headers (mesa 1.4.x). The resulting
build/bin/llama-server finds its .sos via rpath — no LD_LIBRARY_PATH wrapper needed.
├── setup.sh # Main setup script
├── systemd/
│ ├── llama-server.service # PRIMARY LLM — Qwen3.6-35B-A3B MTP (Vulkan, auto)
│ ├── llama-server-qwen38.service # SECOND RESIDENT — Qwen3.8-27B VL + MTP on :8022 (auto)
│ ├── llama-server-qwen27b.service # Alternate LLM — Qwen3.6-27B dense (via switch)
│ ├── llama-server-gemma.service # Alternate LLM — Gemma 4 26B-A4B (via switch)
│ ├── llama-surya2.service # Surya 2 OCR VLM (document OCR, ROCm, on-demand)
│ ├── ttm-pages-limit.service # Enforce the 108GiB TTM cap at boot
│ ├── comfyui.service # ComfyUI (ROCm toolbox, auto-enabled)
│ └── lemonade.service # Lemonade router (optional, disabled by default)
├── bin/
│ ├── llama-server-wrapper.sh # LD_LIBRARY_PATH wrapper for Vulkan binary
│ ├── sd-server-wrapper.sh # LD_LIBRARY_PATH wrapper for sd-cpp binary
│ ├── strix-llm-switch.sh # Flip :8001 between Qwen3.6 MTP and Gemma
│ └── vllm-serve-strix.sh # vLLM via the lemonade bundle (concurrent serving)
├── configs/
│ └── claude-code-router.config.json # ccr config → local :8001 (secrets redacted)
├── docs/
│ ├── claude-code-local-qwen3.6-mtp.md # Claude Code on local Qwen3.6 (full writeup)
│ ├── comfyui-qwen-image.md # Qwen-Image GGUF workflow notes
│ ├── deepseek-v4-flash-284b.md # DeepSeek V4 Flash 284B: Vulkan config, numbers, traps
│ ├── diffusion-lora-training-gfx1151.md # Image gen (sd.cpp Vulkan) + LoRA training on gfx1151
│ ├── muse-glimmer-30b-strix.md # Muse-Glimmer-30B: FP4-ROCm vs Q4-Vulkan A/B
│ ├── strix-guard.md # Remote kill-switch / stack guard
│ ├── unsloth-rocm-gfx1151.md # Fine-tuning: Unsloth on ROCm, multimodal LoRA, GGUF
│ ├── vllm-gfx1151.md # vLLM on gfx1151: what blocks it, measured throughput
│ └── vulkan-pinned-build.md # Why the kyuz0 Vulkan tag must be date-pinned
├── tools/
│ └── cc-qwen-vs-opus.sh # Head-to-head test harness (local Qwen3.6 vs Opus)
├── workflows/
│ └── qwen-image-2512-gguf-lightning.json # ComfyUI image workflow
├── patches/
│ ├── lemonade-provider-reasoning.patch # VS Code extension patches (human-readable)
│ └── apply-lemonade-patches.sh # Auto-apply patches
└── vscode/
└── continue-config.yaml # Continue extension config
| Component | Version | Date | Notes |
|---|---|---|---|
| Qwen3.8-27B VL on :8022 | unsloth UD-Q4_K_XL + mmproj-F16 | 2026-08-14 | Native VL + native MTP in one model; replaced Muse-Glimmer as the second resident |
| Kernel + boot params | 7.2.0-rc3 vanilla; amd_iommu=off, TTM cap 108GiB |
2026-08-09 | IOMMU off (5-12% on this platform); TTM capped below GTT so the GPU can't starve the host |
| Muse-Glimmer-30B on :8022 | custom ROCm-FP4 build | 2026-08-08 | Second resident (vision), ~26.5 t/s; retired 08-14, unit kept as rollback |
| llama.cpp | fresh upstream ~/llama.cpp @ 69bf643 (Vulkan) |
2026-08-08 | Current build for :8001 and :8022 |
| llama.cpp | fresh upstream ~/llama.cpp @ fb30ba9 (Vulkan) |
2026-07-10 | Native MTP (--spec-type draft-mtp), Qwen3.6 primary, ~75–86 t/s tg |
| Qwen3.6-35B-A3B MTP model | unsloth UD-Q4_K_XL (~22.85 GB) | 2026-07-10 | MTP layers grafted into GGUF; 256k ctx |
| llama.cpp | 8793 (Vulkan build from turboquant fork) | 2026-04-03 | 393 t/s pp, 22 t/s tg |
| llama-server | b8461 (kyuz0 Vulkan RADV) | 2026-03 | 351 t/s pp, 19 t/s tg (replaced) |
| llama-server | b8299 (official release) | 2026-03-13 | +40% prompt speed over b8119 |
| llama-server | b8119 (kyuz0 custom) | 2026-02 | Initial build |
| Kernel | 7.1.0-rc4 (vanilla) | 2026-04-04 | +12-37% pp over 6.19.9 |
| Kernel | 6.19.9 (Fedora 43) | 2026-03 | Previous stable |
| XRT | 2.23.0 | 2026-03-22 | Built from amd/xdna-driver submodule |
| amdxdna driver | 2.23.0 (DKMS) | 2026-03-22 | Out-of-tree, replaces kernel v0.6.0 |
| NPU firmware | 1.0.0.166 | 2026-03 | Protocol v6.x |
Check the mmap flag on the active unit:
systemctl --user cat llama-server | grep mmapThe Qwen3.6 primary intentionally runs --mmap; the Gemma alternate runs --no-mmap. Both are correct under the current GTT regime — see mmap notes. If a unit was hand-edited to the wrong flag for its model, laggy load is the symptom.
journalctl --user -u llama-server --no-pager -n 50Common causes:
- "no usable GPU found" — Vulkan backend libs missing. Check build output.
- Model path changed — update
-min service file - Esuna not mounted — check
mount | grep Esuna
- Check if DKMS module is loaded:
lsmod | grep amdxdna - Check for errors:
journalctl -k -b | grep amdxdna - Verify device:
ls /dev/accel/(should showaccel0) - If "0 devices found" but accel0 exists → XRT/driver version mismatch. Rebuild both from
~/xdna-driver.
All ROCm containers use HSA_OVERRIDE_GFX_VERSION=11.5.1 internally. If you need to run ROCm commands outside a toolbox, set this env var first.
Notes from running local LLMs on AMD Strix Halo in production. Maintained by Zachary Aw · altronis.sg · Singapore. Issues and PRs welcome.