← 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
-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 predicted19.63
Quant Q3_K_XL — smaller still, faster still 27.0 predicted18.17
Quant Q4_K_XL — the biggest file, the control no prediction20.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.

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 — managed 16/89.

Hidden tests passed · higher is better

taskturn 1final
hard_ratelimit10/1016/25
hard_semver11/110/27
hard_where14/140/37
It passes every first-turn test, then collapses. Writing the code was never the hard part — revising it is. That is exactly the thing a multi-turn agentic benchmark exists to isolate, and exactly what a single-shot benchmark cannot see.

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.

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) and the most architecture-specific: it works because a 256-row tile fits gfx1151's 32 KB of shared memory. On another GPU, sweep it rather than trusting this number.

Full install guide for all three platforms →