← lazarev.cloud
Ryzen AI MAX+ 395 · gfx1151 128 GB unified llama.cpp b10431 · Vulkan 2026-08-15

Every number here was measured.
Five of my best guesses were wrong.

Tuning local LLM inference on an AMD Strix Halo box. The settings below are worth copying. The more useful part is the record of what sounded right and wasn't — each took one command to disprove, and one would have cost weeks. That includes this project's own benchmark, which spent two weeks reporting a tie that its scoring rule had manufactured.

Result

The configuration

--model Qwen3.8-27B-UD-Q4_K_XL.gguf -ngl 99 -fa on
--spec-type draft-mtp --spec-draft-n-max 3     +79%  generation
-b 2048 -ub 256                                +29%  prefill, vs the usual 1024 (dense; a MoE can invert it)
-ctk q8_0 -ctv q8_0                            free, halves the KV cache
-c 262144                                      full context costs 3%
readingvaluenote
generation20.3 t/s~38 t/s on code, where drafts land better
prefill167 t/sa 44k prompt waits ~4.5 min before word one
weights16.7 GBthe smallest of the five models benchmarked
qualitywithdrawnthe benchmark was measuring itself
Two speeds, and they behave nothing alike. Prefill is the model reading your prompt — parallel, compute-bound. Generation is it writing the reply — sequential, memory-bound. Paste a long document and the wait before the first word is prefill; no amount of generation tuning touches it.

Signature finding

The prediction gap

Before each experiment I wrote down what I expected. Hollow marker: the prediction. Solid marker: what the hardware actually did. The dashed span between them is the error.

Generation speed · higher is better →

Quant IQ4_XS — smaller file, so faster 23.0 predicted→19.63
Quant Q3_K_XL — smaller still, faster still 27.0 predicted→18.17
Quant Q4_K_XL — the biggest file, the control no prediction→20.06
0102030 t/s
predictedmeasured

Generation is memory-bound, so a smaller model should read fewer bytes and run faster. It doesn't: unpacking the more compressed formats costs more arithmetic than the bandwidth it saves. Speed runs backwards from file size. Below roughly Q4_K you spend compute to save bandwidth you were never short of.

Measured

Draft depth peaks at three, then falls off a cliff

Generation speed, tokens/sec · higher is better →

off11.33
depth 117.18
depth 219.11
depth 320.27
depth 416.53
depth 57.73

A built-in draft head guesses the next few tokens; the model verifies them in one pass. Correct guesses are free — but depth 5 is slower than switching speculation off entirely, because every rejected token still costs a full verification pass that yields nothing.

Smaller batches prefill faster

Prefill speed, tokens/sec · higher is better → · smaller -ub wins

ubatch 2048107.8
1024129.5
512 default159.0llama.cpp default
256167.4what we ship
128169.0fastest measured

The accented bar is not the longest one, deliberately. 128 measured fastest, by 0.9% — one run each, so this sweep cannot actually separate it from 256. The curve has gone flat by then: every config at 256 or below lands between 167.4 and 169.0. We ship -ub 256 as the knee of the curve, where essentially all of the gain has been collected, rather than chase a 1.6 t/s edge that a single run can't establish. If you want that edge, measure 128 against 256 on your own box with repeats — don't take it from this chart.

-ub 256 is 29% faster than the 1024 commonly recommended — including by an earlier version of this repo, which measured it on a different model architecture. -b makes no measurable difference at matched -ub. Re-running the 512 case in a separate session landed within 0.1%, so this is the flag and not thermal drift.

Scope, added 2026-09-16: this chart is a DENSE model, and a MoE inverts it. The same sweep on Qwen3-Coder-Next 80B/A3B (arch qwen3next, solo, build b11003) runs the other way: prefill of a 4096-token prompt at depth 32768 goes 297.8 (-ub 256) → 356.2 (512) → 401.3 (1024) → 339.2 (2048) t/s — so -ub 1024 is +34.8% on that model, and 2048 regresses. Generation speed is flat to four values across the whole 8× range (44.0/44.0/44.2/44.1), re-confirming that batch size does not move tg on this box. Neither number is withdrawn; they are both correct, for different architectures. The shipped default stays 256 and the coder model carries a per-model override. The lesson is the one this page already gave — sweep it — and now it applies per model class, not just per GPU.

The rest

Generation speed, tokens/sec · higher is better · baseline 20.06

changet/sverdict
-ctk/-ctv q8_020.23free — and halves the KV cache
-c 26214419.67full context costs 3%
-ctk/-ctv q4_018.99−5%, no reason to go below q8_0
draft-mtp,ngram-mod17.97stacking speculators loses
-fa off17.62−14%, keep flash attention on

Refuted

Five ideas measurement killed

Each was a sound inference from real evidence. Each took a single command to disprove.

the ideawhy it was reasonablewhat happened
rewrite the serial kernelthe code really is token-by-token, and upstream fixed it for NVIDIA only1.1% of prefill
attention collapses at deptha 3.3× cliff in the profilerthat data was decode
smaller quant is fasterdense model, memory-boundbackwards
bigger batches are efficientfewer, larger operations29% slower
less reasoning saves timefewer thinking tokens74% slower
The habit worth stealing: price the operation before you optimise it. test-backend-ops perf -o <OP> costs about a minute and needs no downtime. It refuted a multi-week plan here in one command. Finding that a kernel is inefficient proves only that the kernel is inefficient — not that it matters.

Attribution

Where prefill time actually goes

Share of total prefill time · composition, not a score

MUL_MAT~93%
CONCAT1.7%
GATED_DELTA_NET1.1%
GLU1.0%
everything else~3%

Reading a 44k-token prompt costs about 2.45 PFLOP, and the weight matmuls measure 10.83 TFLOPS — close to this hardware's practical ceiling. Roughly half of prefill is irreducible arithmetic. That's why the kernel rewrite above was worth 1.1%, not the weeks it would have taken.

Quality · withdrawn

Every model passed. That was the bug.

Four models spanning 16.7 GB to 89 GB all scored 70/70 on the private coding suite and 27–29 of 29 on tool calling. That is not four tied models. That is a benchmark with no information in it — and chasing why turned up something worse.

Both suites ran at temperature 0, picked for reproducibility. For thinking models it is measurably the wrong choice. Auditing the saved transcripts found that every truncated turn was a repetition loop that emitted no answer at all — 9 of 9, across all four models, one line repeated up to 439 times until the token budget ran out.

It stayed invisible because the scorer excluded truncated turns — a rule meant to stop a harness limit being scored as a coding failure. It was quietly rescuing the turns where the model produced nothing. Re-scoring the same transcripts without that rescue:

Raw last-turn score, against what was published · higher is better

modeltaskrawreported
Qwen3.5-122B-A10Btoken_budget0/2070/70
Qwen3.5-122B-A10Bwindow_merge0/1870/70
Qwen3.8-27Bwindow_merge3/1870/70
Laguna-S-2.1window_mergeno code70/70
Ornith-1.0-35Bquant_pick15/1870/70
The lesson isn't "temperature 0 is bad". It's that a rule which discards inconvenient data has to report what it discarded. A scoring rule that decides its own audit is not an audit. Every task now reports its score twice — rescued, and strict with no rescue at all.

The replacement

A tier that actually bites

Three new multi-turn tasks, 89 hidden tests, where the tests probe what each spec implies rather than what it states: a token bucket that stays correct under clock skew and sub-millisecond refill, semver range semantics including prerelease exclusion, and a SQL WHERE evaluator in full three-valued logic.

The first model through it — Ornith-1.0-35B, which scores 70/70 on the easy tier — reached 49/89. This page said 16/89 for a day, and that number was mostly our own bug.

Hidden tests passed · higher is better

taskturn 1correctedas first published
hard_ratelimit10/1016/2516/25
hard_semver11/1111/270/27
hard_where14/1422/370/37
Bug 12: fragment detection was a substring test. The prompts say "add a Range class", so models replied with only the new code. The harness overwrites the file wholesale, so a partial reply loses the earlier turns and fails on import — which was anticipated and flagged, except the check asked whether the entry symbol appeared anywhere in the text. A fragment that merely calls it contains the string. Five turns were graded as complete solutions scoring zero.

The real finding is narrower, and better. It passes every first-turn test and gets real work passing on later turns, then loses it at the handoff by replying with a diff instead of a file. That is worth measuring. It is not the same claim as "it cannot write the code" — which is what 18% implied, and which we published.

The full re-run across all five models is still in progress, so there is deliberately no ranking on this page. Until it lands, pick on speed and size — those numbers never depended on the sampler or the scorer.

Reproduce it

Three steps, on any of three platforms

Nothing to compile. The first script downloads a prebuilt llama.cpp release; the second fetches a model and verifies its byte count; the third serves it as an OpenAI-compatible endpoint that Claude Code, Codex and any OpenAI SDK can point straight at.

git clone https://github.com/lazarevtill/strix-halo-llm
cd strix-halo-llm

.\scripts\windows\fetch-llamacpp.ps1 -Build b10431   1 · the engine
.\scripts\windows\fetch-models.ps1 -Only qwen38      2 · a model, 16.7 GB
.\scripts\windows\run-solo.ps1                       3 · serve it on :8080

Linux and macOS have ported equivalents under scripts/linux/ and scripts/macos/. They are honest drafts — written here, never run on their own platform — so start with --dry-run, which prints the exact invocation and launches nothing.

platformbackendstate
Windows 11Vulkanmeasured — every number here
LinuxVulkanported, unproven
macOS · Apple siliconMetalported, unproven
One flag not to copy. -ub 256 is the biggest win here (+29% prefill on a dense model) and the most architecture-specific: it works because a 256-row tile fits gfx1151's 32 KB of shared memory. It does not even hold across architectures on this GPU — a MoE measured +34.8% at -ub 1024. Sweep it rather than trusting this number, per GPU and per model class.

Full install guide for all three platforms →