The benchmark was talking to the previous server

Strata v0.1.42 is an inference engine for one model family. Its loader accepts exactly one architecture, qwen4exp: Qwen3.8-Flash-Next and the models built on it. On the A9 its own setup.sh compiled the engine against ROCm 7.14 nightly wheels in about seven minutes, no root, and reused the GGUFs already sitting in ~/models. No new model download.

So far, a short and happy engine review. Then I asked it to compare its own modes, and the second mode's result was measured on the first mode's server.

Strata: one engine, one model family, three modes#

Flash-Next has attracted purpose-built engines on Strix Halo: Halogen (closed source), Gufo (MIT, whose own speed claim part 5 quotes) and Strata (MIT). Strata got the first slot here for a first-party reason. On an RTX 3090 with 32 GB of RAM, an earlier study of mine had it at 90–100 tok/s decode and 1.7–2.4k prefill on Flash-Next Q2_0 at 128k, against llama.cpp's 8.5–10 and 323–359 on the same box. Different hardware, different quant: that's why Strata got tried, not what it would do here.

Strata exposes speed as environment switches, and the A9 runs got three modes:

  • default: the gfx1151 defaults the engine sets itself, which the maintainers describe as bit-exact to Strata's normal path.
  • hcq8: default plus STRATA_HC_Q8=1 and nothing else.
  • fast: all nine switches from section 5 of Strata's STRIX_HALO.md, which the docs say "round differently from the default path".

All three come from one script, serve-check.sh: export the mode's switches, start Strata's server on port 8099, wait for /health, run the greedy output check, run measure.py, stop the server. The job queue runs the modes back to back.

The hcq8 result that looked fine#

Default finished. hcq8 ran next, and the queue logged it like this:

{"ts": "2026-10-11 17:59:28", "job": "66-gpu-a4b-strata-iq4xs-hcq8.job", "event": "done", "rc": 0, "verified": true}

Output check 10/10, text byte-identical to default on all 11 prompts. measure.py had warm medians too, at 4k / 32k / 100k prompts:

runprompt tok/sgen tok/s
default696.65 / 745.15 / 678.3544.3 / 41.8 / 41.5
"hcq8" (first run)669.55 / 720.15 / 655.744.25 / 41.8 / 41.55

Prompt 3–4 % slower, generation unchanged, identical text. For a switch that reads one set of projections at Q8_0 instead of BF16, that's a believable result. Nothing about it asks to be checked.

What gave it away was a log line that wasn't there. Strata's engine announces STRATA_HC_Q8=1 when the switch is live, in its own log file, separate from the server log serve-check.sh keeps. Neither the hcq8 run nor the fast run that had just started had printed it. The running engine's /proc/<pid>/environ held one STRATA_ variable, a hipBLASLt tuning path. None of the nine switches the fast run had exported.

The server on port 8099 belonged to the default run, whose job had ended at 17:42:08.

Why $! pointed at the wrong process#

This is the line that started each server:

cd "$S" && .venv/bin/python -m serve.server --engine strata --config "$CFG" --port "$PORT" >"$ARCH/server.log" 2>&1 &
SP=$!; trap 'kill $SP 2>/dev/null; sleep 10; kill -9 $SP 2>/dev/null; pkill -9 -x strata 2>/dev/null' EXIT

cd dir && server &, then kill $! on exit. You may already see where this goes.

 run 1 (default), exiting                  run 2 (hcq8), starting

 serve-check.sh                            serve-check.sh
  └─ bash subshell     <- $!, killed        └─ bash subshell     <- $!
      └─ python serve.server  SURVIVES          └─ python serve.server
          │  still bound to :8099                   "port 8099 is already in use"
          └─ strata engine  <- pkill -9               exits

 next request: serve.server respawns        curl :8099/health -> "ok"
 strata with run 1's env (no switches)      (that's run 1's serve.server)

The & applies to the whole cd && python list, and bash runs a backgrounded command in a subshell. For a single program that subshell becomes the program. For a list it stays, as the parent of the last command. $! is the subshell's PID, and the Python server is its child. A few lines of bash show it:

$ cat ps-demo.sh
#!/bin/bash
cd /tmp && sleep 302 &
SP=$!
sleep 0.5
ps -o pid,ppid,comm --pid $SP --ppid $SP
kill $SP; sleep 0.5
echo "after kill \$!:"
ps -o pid,ppid,args -C sleep | command grep 302
$ bash ps-demo.sh
    PID    PPID COMMAND
 856979  856977 bash
 856981  856979 sleep
after kill $!:
 856981    1634 sleep 302

$! is the bash. Kill it, and the sleep carries on under a new parent. Run 1's exit trap killed the subshell, and pkill -x strata killed the engine binary, but nothing signalled serve.server. It stayed up, still bound to 8099, and when the next request arrived it started a fresh engine with the environment it had been born with.

Run 2's server then died on start. Its server.log ends:

python -m serve.server: error: port 8099 is already in use - is Strata (or another server) already running? Close it, or start this one with a different --port

Strata's server says exactly what is wrong. Nobody was reading it, because the wait loop polls the port, not the process:

for i in $(seq 900); do
  curl -sf "http://127.0.0.1:$PORT/health" >/dev/null && break
  kill -0 $SP 2>/dev/null || break; sleep 2
done
if ! curl -sf "http://127.0.0.1:$PORT/health" >/dev/null; then echo "server failed; tail:"; tail -30 "$ARCH/server.log"; exit 2; fi

curl succeeded on the first pass, so the loop broke before kill -0 ever ran. The "server failed" branch tests the port too. And the body that came back is the same for every mode:

{"status": "ok", "max_context": 131072, "model": "qwen3.8-flash-next-unsloth-ud-iq4_xs", "images": false, "api_key": false, "loaded": true, "service": "strata"}

Right model, right context, loaded: true. Nothing in it says which switches the engine got. The health check was honest. It answered "is something serving this model on 8099", and something was.

Reproducing the stale server with a toy server#

The rest of the mechanism doesn't need Strata either. srv.py is a few lines of Python's http.server that answer any GET with the process's MODE environment variable and PID. run.sh starts it the way serve-check.sh did:

#!/bin/bash
# run.sh MODE -- start the server, check /health, stop it (the buggy way)
D=$(dirname "$(readlink -f "$0")")
cd "$D" && MODE=$1 python3 srv.py 8099 2>server-$1.log &
SP=$!
sleep 1
curl -s http://127.0.0.1:8099/health
kill $SP

A driver script runs it twice and echoes each step (bash 5.3.9, Fedora 44):

$ ./run.sh default
ok mode=default pid=848412
$ ./run.sh fast
ok mode=default pid=848412
./run.sh: line 8: kill: (848594) - No such process
$ tail -1 server-fast.log
OSError: [Errno 98] Address already in use

The "fast" run reports mode=default, from the first run's PID. The No such process is the second subshell, already gone because its Python died on the bind. The real script sent that complaint to 2>/dev/null.

The serve-check fix: refuse, then verify the environ#

The fix, a9-agent-bench ac1fb1e, does four things. Stop anything already on the port, and refuse to start if it still answers:

stop_all() { pkill -f "[s]erve.server --engine strata" 2>/dev/null; sleep 5; pkill -9 -f "[s]erve.server --engine strata" 2>/dev/null; pkill -9 -x strata 2>/dev/null; }
if curl -sf "http://127.0.0.1:$PORT/health" >/dev/null; then echo "port $PORT busy: stopping a stale Strata server"; stop_all; sleep 3; fi
curl -sf "http://127.0.0.1:$PORT/health" >/dev/null && { echo "port $PORT still answers: refusing"; exit 2; }

Background the server itself, so $! is the server:

cd "$S" || exit 2
.venv/bin/python -m serve.server --engine strata --config "$CFG" --port "$PORT" >"$ARCH/server.log" 2>&1 &
SP=$!; trap 'kill $SP 2>/dev/null; stop_all' EXIT   # $SP is the python server itself (no subshell)

Then, once /health answers, check that the switches reached the process doing the work:

EP=$(pgrep -P $SP -x strata | head -1)
[ -n "$EP" ] && tr '\0' '\n' < /proc/$EP/environ | command grep '^STRATA_' | sort > "$ARCH/engine-env.txt"
for v in $(cut -d= -f1 "$ARCH/env.txt"); do
  command grep -q "^$v=" "$ARCH/engine-env.txt" 2>/dev/null || { echo "engine (pid ${EP:-none}) lacks $v: refusing"; exit 2; }
done

pgrep -P $SP only finds an engine whose parent is $SP, so for any mode that exports switches, a returning subshell bug means no engine found and a refusal. Default exports none, so the loop has nothing to check there. The toy fixed.sh refuses a busy port and reads its own server's environ. Run while the first orphan is still up, then again after killing it:

$ ./fixed.sh fast
port 8099 busy: refusing
rc=2
$ ./fixed.sh fast
MODE=fast
ok mode=fast pid=848615

One more check costs nothing and would have caught this run on its own: look for the mode's own log line. The hcq8 rerun's engine log has it:

strata: STRATA_HC_Q8=1: 0.63 GiB of Q8_0 hyper-connection projections for the verify read

The two bad runs went to a9-results/invalid-20261011-stale-strata-server/ with a README saying why. The hcq8 rerun started at 18:01:49.

Output checks before speed: 10/10 in every mode#

With each mode actually running, the greedy output check passed 10/10 on Flash-Next UD-IQ4_XS in all three: an exact prime list, a 350-word passage echoed back, four planted codes retrieved one at a time and four at once. RAM peaked at 74 GiB. ISTA Coder IQ1_M passed 10/10 in default and fast at 60 GiB. Swift 1.5 IQ3_S, default only, passed 10/10 at 107 GiB of the box's 124.9.

The text was identical across modes on all 11 prompts. That's the same result the stale-server run printed, so byte-identical output was never evidence either way: a mode that rounds differently can still pick the same greedy tokens on these 11 prompts. The log line and the environ are the evidence.

Default, hcq8 and fast, side by side#

Flash-Next UD-IQ4_XS, measure.py warm medians, prompt / gen tok/s:

mode4k32k100k
default697 / 44.3745 / 41.8678 / 41.5
hcq8667 / 49.4708 / 45.1646 / 45.8
fast892 / 49.81172 / 45.41104 / 46.0

The real hcq8 costs about what the fake one did at prompt, but generates 8–11 % faster, which the stale server never showed. Its point is quality. Strata's docs say its --compat-bf16 packs, this UD-IQ4_XS among them, round the file's Q8_0 hyper-connection projections to BF16, and "on the Strix Halo that rounding put perplexity 6-9% above llama.cpp's on the same file". Reading the Q8_0 values "closes most of that gap", with the caveat in the same sentence that it "is measured on AMD only and not in this release". That's the vendor's measurement. The v0.1.42 build here did load the switch, per the engine log line above; whether that is the path their AMD measurement used, the doc doesn't say. Perplexity hasn't been measured here.

Fast buys prompt speed: at 100k a warm prompt takes 90.67 s to first token against 147.55 s for default. The maintainers' own Strix Halo table for this file, with the same switches and their own engine flags, reads 1,293 and 1,320 prompt tok/s at 8K and 128K for this release. The A9's 32k and 100k land 9–16 % under that, 4k well under. Their no-switches numbers are 1,159 and 1,299, so the A9's fast mode reaches roughly what their default does, and the A9's default runs at 52–60 % of theirs.

ISTA Coder IQ1_M tells the same story: default 729/46.3, 732/42.7, 665/41.3; fast 945/47.0, 1168/43.5, 1095/43.4.

Which mode serves is a quality question, settled on agent tasks. The rule set for this study: a faster mode that's slightly less exact is acceptable if the speed buys more agent iterations at the same pass rate, and the exact mode stays measured next to it.

Strata vs llama.cpp, same harness, same file#

Part 5's llama.cpp curves are llama-bench marginal rates at depth, a different metric, so they stay out of this. The like-for-like comparison is measure.py against llama.cpp b11538 on Vulkan, same file, same prompts, one slot, with and without llama.cpp's own MTP (n-max 2):

engine4k32k100k
llama.cpp, MTP off463 / 25.7371 / 24.7186 / 22.2
llama.cpp, MTP n2426 / 36.6345 / 34.3173 / 28.2
Strata hcq8667 / 49.4708 / 45.1646 / 45.8
Strata fast892 / 49.81172 / 45.41104 / 46.0

MTP is llama.cpp's in-tree answer for this model: +42 / +39 / +27 % decode for 7–8 % of prefill, draft acceptance 0.67–0.74 over the nine timed generations, greedy text byte-identical to MTP off. n-max 3 accepted 0.54–0.62 and was no faster. Strata's runs use its own MTP as well, so its decode compares with the MTP n2 row: drafting against drafting.

Prefill is where they part. Against llama.cpp with MTP off, Strata reads prompts 1.4–5.9× faster, and the gap widens with length: 1.44× at 4k for hcq8, 5.9× at 100k for fast. A 100k-token prompt took llama.cpp 537.84 s to first token.

Serving Flash-Next: Strata in, llama.cpp MTP as the reference#

Each model gets one engine for the whole matrix, since a session's cache can't follow it into another process.

For Flash-Next the agent matrix gets Strata in hcq8 and fast, with llama.cpp Vulkan at MTP n2 running the same tasks as the reference. Strata's quality claim is the vendor's, so it has to beat that reference on the tasks. The matrix starts its servers from a different script, a9-serve.sh, which refuses to start while anything answers on 8099. It still wraps Strata in a subshell, with one word added:

(cd "$ST" && exec choom -n 1000 -- .venv/bin/python -m serve.server --engine strata --config "$CFG" --port "$PORT") >"$LOG" 2>&1 &

exec turns the subshell into choom, and choom execs the server, so $! ends up as the server's PID.

The first hcq8 number this engine gave me was 3–4 % slower than default at prompt and identical at decode. It was a measurement of default.

Filed under: amd, linux, local-llm, benchmarks, bash

Sources#

  • Strata, docs/STRIX_HALO.md: section 5 (the opt-in switches, "round differently"), section 6 (the maintainers' Strix Halo table).
  • Strata, docs/UNSLOTH_Q4.md: the --compat-bf16 perplexity note and the Q8_0 read that closes most of it.
  • Strata: the engine, MIT.
  • GNU Bash manual, Lists of Commands: "If a command is terminated by the control operator '&', the shell executes the command asynchronously in a subshell."
  • llama.cpp #29761: the Flash-Next MTP support used in the comparison.
  • a9-agent-bench ac1fb1e (private repo): the serve-check.sh fix quoted above.