Repository navigation
Benchmark Standards and Methodology
Every performance number in this wiki — and every claim in code review, commit messages, or chat — must come from the standardized benchmark below. No number from memory, extrapolation, a different model, or a different code path. This page is the canonical procedure; each Experiment Log entry carries a benchmark run to this standard.
The rule was written after repeated over-optimistic claims that didn't survive a fair test. Two failure modes recur:
- Mixed configs (apples-to-oranges). A claimed "ggml-ork prefill 280 beats librkllmrt's 181" was actually a smaller 0.5B model on ggml-ork compared against a 1.7B model on librkllmrt — and the "decode match" was the standalone kernel loop, not the integration. Same-model, same-path measurement showed the closed runtime was 2.6× / 3.9× faster. See Open vs Closed.
-
Measurement artifacts. Short prompts (10 tokens → avgM≈6) are nothing like real prefill (M=512). And the lazy one-time weight-pack lands inside the timed window unless warmup exercises the same M as the measured op. Both produced false prefill numbers until
llama-bench(real warmup + repetition) was used. See Hybrid M-Gate.
-
Always compare against the closed runtime — rkllm or rknn, whichever is applicable. This is mandatory, not optional: an open-stack number alone is meaningless without the closed reference on the same work. Pick the one that matches what you're measuring:
-
Full-model throughput (tok/s, an end-to-end experiment) → librkllmrt on the matching
.rkllmviarknpu_bench. Same family + size + quant. -
A single matmul / kernel-level cost (submit latency, tiling, a per-op question) → the RKNN matmul API (
librknnrt,rknn_matmul_*) on the same (M,K,N) + dtype, viatools/rknn_vs_ork. rkllm has no matmul API, so this is the only true kernel-level apples-to-apples. - If neither closed path can run the exact thing (e.g. a probe the closed runtime doesn't expose), say so explicitly and give the closest closed reference + why it's not exact.
-
Full-model throughput (tok/s, an end-to-end experiment) → librkllmrt on the matching
-
Same model / same shape on both sides. ggml-ork: a GGUF (Q8_0). librkllmrt: the matching
.rkllm. RKNN: identical (M,K,N) + dtype to the ork matmul. Same family + size + quant. Never compare across model sizes or paths. -
Use
llama-bench, NOT shortllama-cliprompts — it warms with a realpp512batch and repeats. Shortllama-cliprompts lie on prefill (unrepresentative M + lazy pack in the timed window). For matmul-level work userknn_vs_ork(warm, N iterations). - Verify governors at max BEFORE timing. Decode is memory-bandwidth-bound; a parked DDR governor ~halves it.
- Report BOTH prefill and decode (or, for matmul, the relevant M=1 and M≫1 shapes), for every arm, with ≥2 repetitions (mean ± stddev).
- Match the core count when comparing kernels. The RKNN matmul API is single-core (rejects 3-core mask) — pin ork to 1 core for the clean comparison and show multi-core separately.
- State the model, the path (integration vs standalone), and warm vs cold — never conflate.
-
Board: RK3588 @
10.3.0.236. Model: Qwen3-1.7B-w8a8 (/tmp/gguf/qwen3_1.7b.ggufQ8_0 for ggml-ork; matching.rkllmv1.2.3 for librkllmrt at/var/lib/orkllm/models/...w8a8...). -
Closed-runtime references (mandatory comparison):
-
Full-model: librkllmrt v1.2.3 — prefill 183.9 / decode 11.71 tok/s (
rknpu_bench, same.rkllm, 2026-06-13). -
Per-matmul: RKNN matmul API (
librknnrt, headers + lib in/tmp/rknn) — see Matmul apples-to-apples.
-
Full-model: librkllmrt v1.2.3 — prefill 183.9 / decode 11.71 tok/s (
cat /sys/class/devfreq/dmc/governor # -> performance
cat /sys/class/devfreq/dmc/cur_freq # -> 2112000000
cat /sys/devices/system/cpu/cpu4/cpufreq/scaling_governor # -> performance
cat /sys/devices/system/cpu/cpu4/cpufreq/scaling_cur_freq # -> 2400000If not pinned (oRKLLM's perf_governor does this automatically while a model is loaded):
echo performance | sudo tee /sys/class/devfreq/dmc/governor
for c in 4 5 6 7; do echo performance | sudo tee /sys/devices/system/cpu/cpu$c/cpufreq/scaling_governor; donesudo build/bin/llama-bench -m /tmp/gguf/qwen3_1.7b.gguf -p 512 -n 128 -t 8 -r 2sudo /tmp/rknn/rknpu_bench /var/lib/orkllm/runtimes/librkllmrt-aarch64-v1.2.3.so \
/var/lib/orkllm/models/Qwen3-1.7B-Base-rk3588-w8a8-normal-v1.2.3-RKLLM/Qwen3-1.7B-Base-rk3588-w8a8-normal-v1.2.3-RKLLM.rkllm 128# ork-driver tools/rknn_vs_ork: same int8 (M,K,N) via ork_mm_run_i8 vs rknn_matmul_run
make rknn_vs_ork RKNN_DIR=/tmp/rknn
sudo env LD_LIBRARY_PATH=/tmp/rknn ./rknn_vs_ork 200The ggml-ork backend exposes knobs so each experiment's "on" and "off" arms can be measured on the same reference model. Hold everything else fixed; change one knob.
| Knob | Effect | Used by |
|---|---|---|
ORK_OFF=1 |
Force every matmul to CPU (pure-CPU baseline) | all (baseline) |
ORK_MINM=<n> |
Offload gate: matmuls with M≥n go to NPU (default 32). 1 = all-NPU; 99999 ≈ off |
Hybrid M-Gate |
ORK_QUANT=4 |
int4: compact int4 STORAGE + W8A8-inflate compute (route B — coherent, no wedge; native W4A4 is opt-in via ORK_MIXED_W4A4/ORK_HADAMARD). RECOMMENDED: build the .orkpack from an F16/F32/BF16 source so the NF4 codebook auto-selects |
BR-7 (int4 P=128: NF4-from-F16 215/6.77 vs uniform-from-Q8 172/2.96), int4 W4A4 |
ORK_FUSE=1 |
Fuse adjacent same-input matmuls (QKV/gate-up) | Fusion |
ORK_NOREUSE=1 |
Disable activation-quant reuse | Activation reuse |
ORK_NOCACHE=1 |
Re-pack the weight every matmul (no NPU-resident cache) | Weight caching |
ORK_PROFILE=1 |
Print per-section timing (quant / NPU run / dequant; decode vs prefill; run_multicore phase split) | Submit-floor, run_multicore |
Note on
ORK_NOCACHE: re-packing every matmul is pathologically slow (the pre-cache regime), so that one arm is benchmarked with reduced-p/-nand labelled as such — it demonstrates the order-of-magnitude effect, not a serving rate.
M=/tmp/gguf/qwen3_1.7b.gguf
run() { echo "### $1"; shift; sudo env "$@" build/bin/llama-bench -m $M -p 512 -n 128 -t 8 -r 2 | grep -E "pp512|tg128"; }
run CPU_baseline ORK_OFF=1
run hybrid_default ORK_MINM=32
run all_NPU ORK_MINM=1
run all_NPU_fuse ORK_MINM=1 ORK_FUSE=1
# ... one row per arm; change exactly one knob vs the controlEach experiment page reports a table with the arms compared and the control, always including prefill + decode:
| Config | pp512 (prefill) | tg128 (decode) |
|---|---|---|
| arm under test | … ± … | … ± … |
| control | … ± … | … ± … |
Profiling-only experiments (no tok/s comparison) report the ORK_PROFILE breakdown on the reference model instead, plus the standardized end-to-end numbers for context.
tests/sbc_attest.txt used to carry one CORE_SHA and nothing else, so it said a board validated the
tree, not which. The two validated boards are not interchangeable — different DRAM type and clock,
different kernel, different int4 dispatch envelope — so "ALL TESTS PASSED" on one is not validation on the
other. 99771bc landed with "make test ALL TESTS PASSED on .235" in its message and broke every W4A4
inference on the ROCK 5B for six commits.
Each attesting board now adds a line keyed on /proc/device-tree/compatible[0] — product-level, stable,
and the same file ork_soc_detect reads. IP address is environmental and the SoC id (rockchip,rk3588)
is identical on both:
| board | compatible[0] | DRAM | kernel (2026-10-13) |
|---|---|---|---|
10.3.0.236 Radxa ROCK 5B |
radxa,rock-5b |
LPDDR4X, DMC max 2112 | 6.1.115-vendor-rk35xx-sram |
10.3.0.235 Radxa ROCK 5B+ |
radxa,rock-5b-plus |
LPDDR5, DMC max 2400 | 6.1.115-vendor-rk35xx-fix |
make test rewrites only its own BOARD= line and carries every other board's through; make check-attest
(CI, no NPU) is unchanged — it still compares CORE_SHA — and now also prints which boards hold that hash
and which are behind. make check-attest-all is the opt-in full-coverage gate, with an expiring
WAIVE=<board> sha=<hash> escape (make attest-waive). It is deliberately not in CI: the suite is ~75 s
but the boards are shared and serialise agents, and a gate that cannot be satisfied is a gate nobody runs.
One constant could not serve both. At M=512 K=N=2048 int8 the ROCK 5B runs ~2.8 ms 3-core; the ROCK 5B+
runs slower despite LPDDR5, with a clean bimodal, per-process distribution — best-of-3 reps inside
one run does not collapse it, so roughly half of all runs land in the slow mode. The 5B's 3600 µs limit sat
inside that band.
ROCK 5B+ (radxa,rock-5b-plus), 6.1.115-vendor-rk35xx-fix, governors at performance (DMC 2400 MHz, A76
2400 MHz), n = 41 runs across four contexts:
| context | n | 3-core min | 3-core max | 1-core min | 1-core max | fails vs 3600 µs |
|---|---|---|---|---|---|---|
| standalone, first batch after idle | 8 | 3408 | 3652 | 5767 | 5866 | 6/8 |
| standalone | 20 | 3357 | 3616 | 5706 | 5880 | 2/20 |
after model 12 (the suite's predecessor) |
10 | 3377 | 3613 | 5753 | 5845 | 1/10 |
inside a full make test
|
3 | 3314 | 3533 | 5756 | 5781 | 0/3 |
Modes: fast ≈ 3310–3420 µs, slow ≈ 3530–3655 µs. Worst observed 3652 µs 3-core / 5880 µs 1-core. Thermals are not the cause (51.8–55.5 °C through a whole suite, A76 never below 2400 MHz), and it is pre-existing — it reproduces on a pristine control tree.
Limits are each board's worst observed value +15%:
| board | 3-core limit | 1-core limit |
|---|---|---|
radxa,rock-5b |
3600 µs (unchanged) | 6500 µs (unchanged) |
radxa,rock-5b-plus |
4200 µs | 6800 µs |
| uncalibrated | widest row, with a printed NOT CALIBRATED warning |
What the 5B+ guard still catches. The regression class it exists for is the weight-DMA M-tile cap
reverting from mg_max*64 to R-1 (2026-06-30), which was 1.63× on 3-core and 2.3× on 1-core. From
the 5B+'s slow mode that lands at ~5900 µs / ~13.3 ms — caught with 40% and 95% of headroom. In general the
4200 µs limit catches any ≥1.15× slowdown from the slow mode, ≥1.26× from the fast mode.
What it would miss. A regression smaller than ~15% on the 5B+ (the 5B's 3600 µs limit, against a ~2.8 ms
base, caught ≥1.29×). A regression that only shows in the fast mode, because the slow mode already sets the
limit. And — unchanged from before — anything that is not multi-core int8 M=512 K=N=2048: test_speed
times exactly one shape. 12/12 runs pass under the new limit.
At cb0bf06 — a tree whose committed attest says ALL TESTS PASSED — make test on the ROCK 5B+ fails two
tests deterministically (3/3 suite runs, and standalone):
-
i4: W4A4 public-API casesM=8 K=512 N=256,M=16 K=2048 N={64,256},M=32 K=1024 N=128and the newsatcollision case all reportmaxerr≈ 34 200–36 200, i.e. ≈ 32767 — output cells still carrying the0x7fffint16 doorbell seed. -
test_slice_rescue: threefnv64golden mismatches.
Same tree, same governors; the two boards differ in DRAM clock and kernel. This is the exact failure class
two-board attest exists to make visible, and it was invisible in the single-CORE_SHA format.