Benchmarks — Fucina vs the reference implementations, on CPU¶
This file is the benchmark record for Fucina's runners: the measurement
protocol, and results against each family's reference implementation on the
same machine, same weights, same thread count, CPU-only on both sides —
llama.cpp for the LLM runners (the bulk of this file), parakeet.cpp for
ASR, omnivoice.cpp for TTS. The NAM example is parity-oriented
(NeuralAmpModelerCore); its record lives in examples/nam/README.md.
The record is one snapshot, taken as of 2026-07-04 (llama.cpp build
30af6e2 throughout; every reference is pinned to its exact commit in
tools/fetch_refs.sh).
Two ground rules for reading it:
- Every number carries its hardware and measurement conditions. CPU benchmarks on laptops are thermally and page-cache sensitive and shape-specific; a number without its conditions is not a result.
- Losses are recorded as plainly as wins. Where llama.cpp is faster, this file says so, with the measured ratio.
Scoreboard¶
Condensed from the records below. "Ratio" is always Fucina / llama.cpp throughput (>1 = Fucina faster).
Apple M1 Max (arm64, 8 threads, macOS — llama.cpp runs with its Accelerate BLAS backend, its default on this platform):
Fucina wins:
- Dense prefill across the board (Qwen3 0.6B all seven formats + 1.7B, 20 prompt lengths each): per-format geomeans 1.18–1.81x. llama.cpp switches to its Accelerate/AMX path at batch >= 32, and that transition costs it heavily at pp32–129 (up to 7.2x in Fucina's favor at f16 pp32); 1.7B pp256 is 1.07x under the prewarmed paired gate.
- Large-batch prefill on the 0.6B (pp256): 1.26–1.42x across the quantized formats and 2.46x on f16.
- Large MoE prefill (pp64–256): Qwen3-30B-A3B 1.44–2.09x; Gemma-4-26B-A4B 1.66–1.84x measured cool.
- Gemma-4-26B mid-batch prefill (pp4–33): ahead on all 11 measured lengths same-session cool, 1.17–2.15x (the pp4–9 cells await one pristine-machine confirmation — see that section's disclosure).
- Dense decode: Q4_K_M/Q4_K_S +15%, Q8_0 +14%; f16/Q5_K/Q6_K parity or better cool (Q6_K paired gate 1.06x).
- 30B MoE decode and large prefill (Q5_K_M, prewarmed page cache both sides): decode tg32 1.10–1.36x, pp256 1.43–1.87x.
- 30B MoE mid-batch prefill (pp15–33, after the 2026-07-10 scheduler fix): M1 self-A/B +27–37% (68→87 tok/s at pp15, 76→105 at pp32, interleaved same-day); the x86 band flipped from 0.36–0.52x behind to 0.97–1.22x (decomposition in the x86 section).
- Lossless speculative decoding (no draft model): up to 2.3x on
retrieval-structured tasks, with a cost gate whose worst measured case is
0.98–0.99x (see
SPECULATIVE.md). - Batch-N multi-stream decode: 3.19–3.25x aggregate throughput at N=8 streams vs running the streams sequentially.
llama.cpp wins:
- Qwen3.5-0.8B Q8_0 pp32: 0.86x (3 interleaved rounds, both orders). The loss is confined to this shape — pp128 1.09x, pp512 1.17x, and decode 1.37x all win. Fucina's pp32 samples are bimodal across processes (408–683 tok/s, best samples at llama parity or above) while llama's are tight; profiling shows the slow processes uniformly inflated across every parallel phase. Open item (see Recorded negatives for what has been ruled out).
- 30B Q6_K decode: 0.88x — recorded without page-cache prewarming; its Q5_K_M sibling measures 1.36x under the stricter prewarmed protocol, so this cell is likely conditions-bound, but it stands until re-measured.
- 30B/26B MoE prefill at pp256 with Q4_K-transcoded experts: Fucina ~15–17% behind in the same-session comparison (the transcoded GGUFs were not kept, so this row awaits a rebuild to re-measure).
Intel i9-13950HX (x86-64, Raptor Lake 8P+16E, AVX2 + AVX-VNNI, no AVX-512, Linux, no BLAS on either side — verified, see below):
Fucina wins:
- Dense Qwen3-0.6B, all quantized formats: paired-gate median ratios 1.32–1.95 per format, maxima up to 2.56 (Q5_K_S pp128). Q8_0 pp256 1.45x, Q4_K decode 1.21–1.25x.
- f16 model: no llama.cpp pairing recorded, but the f32-accumulate f16 GEMM took pp1024 from 17.9 to 354 tok/s on this box (Fucina-only A/B).
llama.cpp wins:
- Qwen3-30B-A3B MoE mid-batch prefill — resolved 2026-07-10. The pp15–33 band went from 0.36–0.52x to Q5_K_M 1.15–1.22x and Q6_K 0.97–1.05x (same-day paired reruns; a reverted-gate control first reproduced 0.375–0.509, so the llama side is directly comparable). The loss was scheduling, not kernels: the band ran monolithic per-expert tasks below the old 512-pair phased-chain gate, so the chain machinery never executed there. Gating the chain at 64 pairs recovers most of the band; small-m column chunking lifts Q6_K over parity at pp15–17. Residual: Q6_K pp31–33 at 0.965–0.987x (open, small); pp1–7 stays on the monolithic path below the 64-pair gate.
- MoE decode: 0.90–0.95x (weight-bandwidth-bound at m=1).
- Q5_K dense decode: 0.90x (the m=1 GEMV path; open item). Q6_K dense decode is 0.987 — coin-flip parity.
- Gemma-4-26B small-batch prefill (pp1–9): 0.85–0.99x; Fucina wins pp15+ (up to 1.31x) and decode is parity (0.996–1.000).
Results age: llama.cpp advances continuously. Treat everything here as "measured as of the snapshot date, on that machine".
Methodology¶
Principles¶
- Build Fucina on the benchmarking machine itself, with no
-Dtarget: the default native target compiles in the host's full ISA features. A cross-built baseline binary benchmarks the wrong kernels. - Run one benchmark process at a time. Never run Fucina and llama.cpp in parallel.
- CPU-only llama.cpp runs:
-ngl 0. - Same model file (same quantization) on both sides.
- Treat prompt length as a benchmark parameter. Never quote
pp4alone. - Report both latency and tokens/sec. For Fucina, tokens/sec is
prompt_tokens * 1000 / avg_ms. - Keep correctness separate from throughput. Throughput may use synthetic
token IDs; correctness compares logits for a fixed token sequence and
requires top-token alignment.
tools/llama_logits.cppis the reference-side half of that check: compile it against a local llama.cpp checkout to dump per-position logits for a token sequence, then compare against the Fucina runners'--logits-out. - Re-run after thermal or scheduler-sensitive changes. Back-to-back runs on laptops move by several percent — or much more (see thermal discipline).
Why prompt length matters¶
CPU kernels have tile sizes (e.g. 4-row packing for SIMD lanes); models do
not. A correct benchmark matrix includes arbitrary lengths around SIMD/tile
and parallelism thresholds, not only powers of two: tail-only paths
(pp1..pp3), one exact tile (pp4), tile-plus-tail (pp5), near-tile tails
(pp6, 7, 9, 15, 17), boundary checks (pp31, 33, 64, 127, 129), and
multi-tile lengths (pp8, 16, 32, 128, 256). The routine matrix:
1,2,3,4,5,6,7,8,9,15,16,17,31,32,33,64,127,128,129,256
The paired benchmark gate¶
tools/bench_gate.py is the tool behind any "parity-or-faster" claim. It is
deliberately conservative:
- every row runs in both process orders (
Fucina -> llama, thenllama -> Fucina), so order effects and thermal drift show up in the samples; - rows whose cross-sample coefficient of variation exceeds
--max-cv(default 8%) are reported NOISY, not counted as results — cool down and rerun; - raw stdout/stderr and exact command lines are saved for every subprocess
(under
compare/bench-gate-<timestamp>/:SUMMARY.md,results.json,results.tsv,raw/); - it compares median tok/s and exits nonzero when Fucina is below
--min-ratio(default 1.0).
zig build -Doptimize=ReleaseFast
python3 tools/bench_gate.py \
--models qwen3-0.6b-q6_k \
--tasks prefill,decode \
--lengths 1,2,3,4,5,6,7,8,9,15,16,17,31,32,33,64,127,128,129,256 \
--rounds 1 --fucina-reps 3 --llama-reps 3 --cooldown-s 30
# or, after build args:
zig build bench-gate -- --models qwen3-0.6b-q6_k --tasks prefill,decode
--list-models prints the model catalog; --llama-bench points at your
llama-bench binary (default refs/llama.cpp/build-cpu/bin/llama-bench);
--prewarm-model reads the GGUF before each subprocess to stabilize the
page cache (important for 20+ GB models).
Standing fairness note (cuts in Fucina's favor): Fucina's prefill/decode
benchmark paths still perform the final logits/sampler work that
llama-bench skips inside its pp/tg loops. Passing rows are therefore
conservative for Fucina; failing rows deserve a no-logits A/B before being
called true kernel regressions.
The op-level regression gate¶
tools/opbench_gate.py covers what the paired gate cannot: dispatch
latency, autograd tracking overhead, and training forward/backward
throughput have no llama.cpp counterpart, so they gate against a locally
recorded per-machine baseline instead of an external reference.
It drives the bench* build steps (facade, mlp, backward-diamond,
attention-backward, einsum, ce, scatter, optim) in ReleaseFast with the
production allocator and applies three rules:
- timings gate on the median of N repeats within
--tol(default 10%); rows whose cross-repeat coefficient of variation exceeds 12% report NOISY instead of failing, as in the paired gate, and every timing exceedance is confirmed by re-running its suite after a cooldown (--retry-cooldown-s, default 30 s) — transient background load or a heat-soaked SoC must not fail the gate, and a real kernel regression reproduces; allocs_per_opgates exactly — allocation counts are deterministic, so any increase fails and any decrease asks for a re-record;- checksums gate exactly — a checksum change is numerical drift, never noise.
python3 tools/opbench_gate.py record # once per machine/toolchain
python3 tools/opbench_gate.py check # nonzero exit on regression
python3 tools/opbench_gate.py check --suites facade,mlp --repeats 5
Baselines land in bench/baselines/opbench-<host>.json keyed to hostname,
arch, and Zig version; check refuses a mismatched environment without
--force. zig build bench-check is the compile-only companion: it builds
every bench executable without running one, so bench mains can no longer
rot unnoticed (five of them did exactly that before this step existed).
The DRAM roofline probe¶
Decode-shaped quantized matmuls are weight-stream-bound, so an achieved
"GB/s" figure means nothing without the ceiling this machine actually
sustains. bench/membw.zig measures it:
zig build bench-membw -Doptimize=ReleaseFast # single-thread + all-core
zig build bench-membw -Doptimize=ReleaseFast -- --threads 4 # explicit sweep point
The ceiling is defined as what portable kernel-class code can read —
plain @Vector streaming loads, the same instruction class as the quant
kernels — not a spec-sheet or exotic-pattern number. One shared
larger-than-any-cache region is split into 8 MiB chunks claimed from an
atomic cursor (with fixed per-thread slices the slowest core sets the wall
time and the aggregate under-reads ~2x on heterogeneous parts); each chunk
is streamed as four interleaved lines-in-flight with wrapping u64
accumulators; workers take the same macOS performance-core QoS pin as the
engine's team; a round is one wall-clocked pass over the region and the
best of five rounds is the ceiling, in the decimal GB/s the bench tables
use. Alternative patterns (line-touch-only reads, single-stream, software
prefetch at 4–128-line distances) measure equal or worse on M1 Max, so the
figure is a genuine ceiling for this code class, not an artifact of one
loop shape. Thermal discipline applies as for every other number in this
file.
Reference figures, M1 Max (macOS 14.6): ~60 GB/s single-thread, ~115-120 GB/s all-core (10 threads, best-of-5, ranges across repeated cool-state runs).
bench-ternary runs the single-thread probe at startup (--no-roofline
skips it) and prints a %ceil column: hot-kernel weight-stream GB/s as a
fraction of the single-thread ceiling — the bench is single-threaded, so
the per-core ceiling is the honest denominator. A near-100% row is
memory-bound: only a smaller format can make it faster, and (per the
LUT-kernel record in TERNARY.md) DRAM-bound is the one regime
where table kernels deserve re-examination. Low percentages point at
compute or dispatch.
Current M1 Max reading: the TQ2_0 hot kernel at m=1 streams weights at
~16-17 GB/s, ~26-28% of the single-thread ceiling — single-thread
ternary decode is compute-bound, with ~3.7x of memory headroom, while
8-thread decode at that per-core rate sits right at the ~120 GB/s
aggregate wall.
q8_0 skinny-m GEMV¶
bench/q8gemv.zig measures the three live q8_0 decode paths
single-threaded (zig build bench-q8gemv -Doptimize=ReleaseFast): the
per-row column-major tile (the MoE-expert / unpacked route), the
x4-interleaved-rhs kernel PackedRhs(.q8_0) serves for generic linears,
and the lane-packed-lhs kernel behind the fused split-SwiGLU projection.
Reference figures, M1 Max at m=1, n=k=4096: x4 ~58 GB/s weight stream
(~96% of the single-thread ceiling — memory-bound), per-row tile
~35 GB/s, lane-packed ~32 GB/s. The lane-packed kernel pays all four
sdot lanes regardless of m — a compute ceiling, which is why the
split-SwiGLU route sends m == 1 through the plain-lhs x4 kernel instead —
and from two row groups up both packed tiles go column-outer, streaming
the rhs once instead of once per row group.
Manual commands¶
Fucina (comma-separated token IDs; the values are fixtures, the length is the variable):
zig build -Doptimize=ReleaseFast qwen3 -- models/Qwen3-0.6B-Q6_K.gguf TOKEN_IDS --repeat 100
zig build -Doptimize=ReleaseFast qwen3 -- models/Qwen3-0.6B-Q6_K.gguf TOKEN_IDS --repeat 500 --profile
llama.cpp, matching lengths:
refs/llama.cpp/build-cpu/bin/llama-bench -m models/Qwen3-0.6B-Q6_K.gguf -ngl 0 -t 8 -p N -n 0 -r 20 -o md
Correctness check¶
Compare final logits against llama.cpp's debug logits for an explicit token-id prompt:
refs/llama.cpp/build-cpu/bin/llama-debug \
-m models/Qwen3-0.6B-Q6_K.gguf \
-p ids:9707,847,829,374 \
-ngl 0 -t 8 -tb 8 -b 4 -ub 4 \
--save-logits --logits-output-dir compare/llama-q6-f32kv
zig build -Doptimize=ReleaseFast qwen3 -- \
models/Qwen3-0.6B-Q6_K.gguf \
9707,847,829,374 \
--compare-logits compare/llama-q6-f32kv/llamacpp-Qwen3-0.6B-Q6_K.bin
Expected signal for quantized formats: top-token alignment plus bounded logit drift. Exact bit equality is not expected.
Hardware and reference builds¶
- Apple M1 Max, macOS, 8 threads both sides (
-t 8for llama-bench; Fucina's build default is 8 workers). Fucina built-Doptimize=ReleaseFast. The llama.cpp reference for the M1 records is build 30af6e2, CPU-only at run time (-ngl 0), with its Accelerate BLAS backend active — that is llama.cpp's production configuration on macOS and it is kept deliberately (Accelerate drives Apple's AMX units; beating it is part of the job). - Intel i9-13950HX (Raptor Lake, 8 P-cores + 16 E-cores, AVX2 +
AVX-VNNI, no AVX-512), Linux, ReleaseFast. The llama.cpp comparison build
was verified — checked, not assumed — as
GGML_BLAS=OFF(no BLAS linkage),GGML_NATIVE=ON(-march=native, its AVX-VNNI kernels live),GGML_LLAMAFILE=ON(vendored tinyBLAS sgemm),GGML_CPU_REPACK=ON,GGML_OPENMP=ON. Both engines run their own CPU kernels with no BLAS on either side. The llama-bench binary did not embed a git commit (build_commit: unknownin its JSON output); the build flags above are the recorded provenance. llama-bench ran-t 8unpinned (the scheduler places threads on P-cores); Fucina ran its default 8 workers. Single-core rows usedtasksetto pin one P-core on both sides.
Thermal discipline (Apple Silicon)¶
The single largest source of wrong conclusions in this file's history was chip temperature:
- Heat soak inverts thread scaling (a throttled chip favors fewer active cores) and inflates per-phase profile wall times. Two documented dead-end investigations came from hot-chip profiles.
- Long sweeps (~40 min) systematically depress the rows measured late. Several apparent "llama.cpp wins decode" readings evaporated when re-measured cool and prewarmed — the discipline distinguishes real gaps from artifacts, and the record keeps only what survives it.
- Authoritative comparisons are cool, isolated, best-of A/B pairs with pre-cooldowns (30–240 s), ideally interleaved (ABBA) so drift hits both sides.
- For 20+ GB models, prewarm the page cache before decode A/Bs — a fresh process faults the weights cold and depresses whichever side runs first.
- Confirm the binary is ReleaseFast and the machine is not swapping. One recorded bad run traced to an accidentally-Debug binary plus swap pressure from concurrent model loads.
Model files¶
The GGUF weights are not in this repository. Download them (e.g. from
Hugging Face) and place them under models/ using the file names in the
tools/bench_gate.py catalog (--list-models), e.g.
models/Qwen3-0.6B-Q6_K.gguf, models/Qwen3-30B-A3B-Instruct-2507-Q5_K_M.gguf.
The reference implementations live under refs/ — untracked local clones,
never vendored. tools/fetch_refs.sh clones every reference at the exact
commit this snapshot was measured against, and tools/fetch_refs.sh
--build additionally builds llama.cpp CPU-only into
refs/llama.cpp/build-cpu/ (Accelerate stays on under macOS — llama.cpp's
production configuration there, kept deliberately; plain native CPU
elsewhere). bench_gate.py --llama-bench overrides the binary path.
tools/fetch_refs.sh --patch additionally applies the instrumentation
patches under tools/ref-patches/ (currently: parakeet.cpp tensor-dump
hooks, needed only to regenerate parity dumps). Benchmarks always run stock
pinned references — never benchmark a patched reference.
Results¶
M1 Max — full CPU sweep (11 model/format combinations)¶
-t 8 both sides, ReleaseFast Fucina vs llama-bench -ngl 0, serial runs,
20 prefill lengths + decode per model. Parity band ±3%. Fucina
prefill/decode for the qwen3 models is mean over reps; gemma4/qwen3.5 rows
were best-of only (favorable to Fucina — flagged). Rows sensitive to
measurement conditions — giant-model decode, sweep-tail lengths — use the
stricter prewarmed paired gate (tools/bench_gate.py: both engine orders,
per-subprocess cooldowns, --prewarm-model page-cache reads); prewarming
is not optional there. A fresh process on a 20 GB model faults the weights
and measures the SSD, not the kernels, and a single cold-cache sample once
read 0.81 on a row that measures 1.06–1.15x prewarmed.
Headline: of 236 cells — Fucina faster 221, parity 13, llama.cpp faster 2 (prefill: Fucina 215 / parity 8 / llama 1; decode: 6 / 5 / 1).
Per-model summary (decode is tg64 for dense, tg32 for MoE/Gemma):
| model | prefill geomean (20 lengths) | decode Fucina | decode llama | decode ratio |
|---|---|---|---|---|
| Qwen3-0.6B f16 | 1.81x | 83.0 | 80.7 | 1.03 (parity) |
| Qwen3-0.6B Q8_0 | 1.27x | 151.7 | 133.6 | 1.14 |
| Qwen3-0.6B Q6_K | 1.39x | 140.0 | 132.0 | 1.06 |
| Qwen3-0.6B Q5_K_M | 1.65x | 145.5 | 145.8 | 1.00 (parity) |
| Qwen3-0.6B Q5_K_S | 1.47x | 148.4 | 151.7 | 0.98 (parity) |
| Qwen3-0.6B Q4_K_M | 1.45x | 190.9 | 166.3 | 1.15 |
| Qwen3-0.6B Q4_K_S | 1.45x | 198.9 | 173.5 | 1.15 |
| Qwen3-1.7B Q4_K_M | 1.18x | 81.5 | 80.0 | 1.02 (parity) |
| Qwen3.5-0.8B Q8_0 | 1.07x (4 lengths) | 98.6 | 71.9 | 1.37 |
| Qwen3-30B-A3B Q5_K_M (MoE) | 1.77x | 26.3 | 19.4 | 1.36 |
| Qwen3-30B-A3B Q6_K (MoE) | 1.62x | 29.8 | 33.8 | 0.88 (llama; pre-prewarming) |
| Gemma-4-26B-A4B Q6_K (MoE) | 1.47x | 24.1 | 24.7 | 0.98 (parity) |
Protocol notes on the summary values: the Q6_K-0.6B, Qwen3.5, and 30B Q5_K_M decode cells and the 1.7B/Gemma prefill geomeans use the cool/prewarmed paired protocol where the plain serial sweep proved conditions-sensitive; dense decode on M1 is compute- rather than bandwidth-bound (Q6_K, fewer bytes per weight, read slower hot while Q8_0, more bytes, read faster — bandwidth would predict the opposite).
The two llama.cpp-win cells, exhaustively: Qwen3.5-0.8B pp32 (0.86) and the 30B Q6_K decode (0.88 — recorded without page-cache prewarming; its Q5_K_M sibling measures 1.36x under the prewarmed gate). Q6_K-0.6B pp4 and pp7 measure 1.43x and 2.24x under the prewarmed gate (single paired round each); Qwen3-1.7B pp256 is 1.07x with pp31 confirmed over three prewarmed rounds (1.06–1.15x).
Representative full tables (all tok/s; ratio = Fucina/llama):
Qwen3-0.6B Q4_K_S (a typical dense-format win):
| pp | Fucina | llama | ratio |
|---|---|---|---|
| 1 | 226.4 | 150.3 | 1.51x |
| 2 | 295.7 | 261.7 | 1.13x |
| 3 | 398.6 | 329.4 | 1.21x |
| 4 | 626.4 | 340.6 | 1.84x |
| 5 | 543.7 | 366.2 | 1.48x |
| 6 | 611.6 | 390.0 | 1.57x |
| 7 | 675.0 | 426.1 | 1.58x |
| 8 | 730.3 | 478.7 | 1.53x |
| 9 | 646.3 | 509.7 | 1.27x |
| 15 | 841.8 | 633.7 | 1.33x |
| 16 | 979.3 | 726.9 | 1.35x |
| 17 | 892.7 | 709.5 | 1.26x |
| 31 | 1025.6 | 856.1 | 1.20x |
| 32 | 1077.3 | 500.4 | 2.15x |
| 33 | 910.7 | 505.7 | 1.80x |
| 64 | 1109.2 | 683.1 | 1.62x |
| 127 | 1129.5 | 839.9 | 1.34x |
| 128 | 1248.8 | 864.4 | 1.44x |
| 129 | 1212.3 | 852.2 | 1.42x |
| 256 | 1219.2 | 936.0 | 1.30x |
decode (tg64): Fucina 198.9 vs llama 173.5 — 1.15x.
Note the llama.cpp discontinuity at pp32 across every dense table: that is its switch to the Accelerate/AMX BLAS path at batch >= 32, which only re-amortizes at larger batches.
Qwen3-30B-A3B MoE Q6_K (prefill win, decode loss):
| pp | Fucina | llama | ratio |
|---|---|---|---|
| 1 | 34.3 | 30.2 | 1.14x |
| 2 | 30.5 | 15.6 | 1.96x |
| 3 | 42.4 | 22.1 | 1.92x |
| 4 | 46.8 | 32.0 | 1.46x |
| 5 | 53.2 | 36.1 | 1.47x |
| 6 | 55.2 | 25.9 | 2.13x |
| 7 | 56.1 | 35.8 | 1.57x |
| 8 | 60.4 | 33.9 | 1.78x |
| 9 | 62.3 | 41.2 | 1.51x |
| 15 | 66.5 | 54.4 | 1.22x |
| 16 | 69.4 | 52.2 | 1.33x |
| 17 | 68.9 | 58.8 | 1.17x |
| 31 | 73.3 | 54.4 | 1.35x |
| 32 | 73.2 | 50.5 | 1.45x |
| 33 | 72.1 | 51.0 | 1.41x |
| 64 | 125.7 | 63.8 | 1.97x |
| 127 | 132.8 | 66.1 | 2.01x |
| 128 | 134.8 | 64.6 | 2.09x |
| 129 | 132.4 | 64.1 | 2.07x |
| 256 | 137.1 | 65.7 | 2.09x |
decode (tg32): Fucina 29.8 vs llama 33.8 — 0.88x, llama.cpp.
Gemma-4-26B-A4B MoE Q6_K — measured with the cool protocol (Fucina: fresh process per length, best-of, pre-cooldowns; llama.cpp: one combined warm process per session — fresh per-length llama processes measure far below llama's real speed on this model and are not used):
| pp | Fucina | llama | ratio |
|---|---|---|---|
| 4 | 44.6 | 20.8 | 2.15x |
| 6 | 45.1 | 32.6 | 1.38x |
| 7 | 53.4 | 41.2 | 1.30x |
| 8 | 65.5 | 47.9 | 1.37x |
| 9 | 57.2 | 48.8 | 1.17x |
| 15 | 79.3 | 56.3 | 1.41x |
| 16 | 86.2 | 58.4 | 1.48x |
| 17 | 87.0 | 48.8 | 1.78x |
| 31 | 115.2 | 66.5 | 1.73x |
| 32 | 121.6 | 64.1 | 1.90x |
| 33 | 116.0 | 58.8 | 1.97x |
| 64 | 154.1 | 93.0 | 1.66x |
| 128 | 166.4 | 96.9 | 1.72x |
| 256 | 170.2 | 92.5 | 1.84x |
decode (tg32): Fucina 24.1 vs llama 24.7 — 0.98x, parity. pp1–3 are parity-to-win (1.01–1.19x). The pp64–256 rows come from a separate cool session in which llama measured at its strongest recorded values.
Disclosure, so the mid-batch rows are not over-claimed: llama's absolute
numbers in the pp4–33 session ran below its strongest recorded values on
this model (e.g. pp16 58.4 vs 82.1 in another session; same binary, model,
and flags — llama's qwen rows reproduced their expected values in the same
session, so binary and machine are fine). The depression has a warming
gradient (earliest lengths hit hardest), consistent with page-cache
pressure penalizing llama's mmap-resident expert access; Fucina is
structurally less exposed because it copies experts into resident packed
buffers at load. Same-session paired rows are the valid comparison and
Fucina wins all of them; conservatively cross-checking Fucina's numbers
against llama's strongest recorded values still gives wins at pp15–33
(86–116 vs 81–97) and leaves pp4–9 dependent on llama numbers that session
could not reproduce. A pristine-machine session (long idle, no prior model
reads) should re-confirm the pp4–9 band before anyone leans on those
specific cells. Ruled out as causes of the mid-batch shape being hard for
both engines: Accelerate/AMX (dequant-sgemm cannot amortize at 1–3 tokens
per expert, and llama.cpp's mul_mat_id is int8 there too) and thread
over-subscription (an apparent "4 threads beats 8" was a heat-soak
artifact; cool, 8 > 6 > 4).
30B/26B MoE with experts transcoded to Q4_K (a behind-record)¶
Experts-only Q4_K transcode via the exporter (zig build export-gguf --
--experts-dtype q4_k): Qwen3-30B-A3B 25.1→17.6 GB, Gemma-4-26B 23.2→19.2 GB.
Decode follows expert bytes-per-weight (weight-bandwidth-bound): Qwen3-30B
decode Q5_K_M (0.69 B/w) 32.3 tok/s, Q6_K (0.82) 28.5–30.1, Q4_K-experts
(0.56) 34.1 (+20% mean vs same-session Q6_K). Quality: argmax aligned on the
parity prompts, top-5 order approximately preserved.
Same-session llama.cpp comparison (build 30af6e2, -ngl 0 -t 8 -p 256 -n 32
-r 3, heat-soaked session — treat as lower bounds both sides):
| model (Q4_K experts) | side | pp256 tok/s | tg32 tok/s |
|---|---|---|---|
| Qwen3-30B-A3B | llama.cpp | 107.44 ± 0.57 | 25.84 ± 0.25 |
| Qwen3-30B-A3B | Fucina | 89.02 ± 2.01 | 26.41 ± 0.84 |
| Gemma-4-26B | llama.cpp | 116.18 ± 3.57 | 19.51 ± 0.34 |
| Gemma-4-26B | Fucina | 98.1 (best-of-3) | 18.7 (best-of-3) |
Net: Fucina behind llama.cpp on pp256 by ~15–17% with Q4_K experts (L2-bound Q4_K prefill), roughly tied on Qwen decode, slightly behind on Gemma decode in this run.
Lossless speculative decoding¶
M1 Max, ReleaseFast, Qwen3-0.6B-Q4_K_S, greedy unless noted. Speedup =
--spec vs plain decode tok/s on the same prompt; the committed token
stream is identical by construction (lossless), so this is pure throughput.
Design record and break-even math: SPECULATIVE.md.
| Task | Speedup | Notes |
|---|---|---|
| Grounded copy | 1.47x | 70% acceptance, 6.6 tok/step |
| Same prompt, re-encoded post tokenizer fix | 1.04x | 41% acceptance — generation itself diverged |
| Verbatim-repetition microcase | 2.3x | 100% acceptance, ~11 tok/step |
| Code edit | 1.12x | 84.6% acceptance; was 0.79x before the tokenizer parity fix |
| Free-form generation | 0.99x | cost gate auto-off; was 0.83x ungated |
RAG-injected reference (--spec-ref) |
0.98x | 33/68 draft tokens accepted from the reference |
| Grounded copy, sampled t=0.7 | 1.20x | same single-RNG-draw path as greedy |
The cost gate turns speculation off when its rolling speedup estimate drops
below 1.0: worst observed case 0.98–0.99x, vs 14–21% losses ungated.
Tokenizer parity is a precondition for acceptance numbers — the qwen2
pretokenizer is token-ID-exact vs llama-tokenize on 14 fixtures.
Context only (not a controlled A/B): llama.cpp's llama-lookup reports 93%
acceptance at max-draft 3 on the same grounded text — a shorter-draft
operating point.
Text-diffusion step time (DiffusionGemma)¶
M1 Max, 8 threads, ReleaseFast, cool chip, same Q6_K GGUF (22.65 GB) and
threads on both sides. Per denoise step (one 256-token bidirectional canvas
forward + sampler pass): Fucina ~3.5 s vs llama-diffusion-cli (llama.cpp
PR 24423) 4.84 s/step — ~1.4x. Step counts vary with RNG/template, so
compare seconds/step, not totals. Logit parity sits inside llama.cpp's own
cached-vs-unified numeric spread on this model (the model is numerically
chaotic; small kernel differences amplify).
x86-64 — Intel i9-13950HX (AVX2 + AVX-VNNI)¶
First x86 hardware record. No BLAS on either side (verified — see "Hardware and reference builds"); both engines run their own AVX2/AVX-VNNI CPU kernels. Fucina numbers are after the packed-VNNI/AVX2 quantized kernels and the f32-accumulate f16 GEMM landed; "before" is the same tree without them (the prior kernels fell back to scalar on x86).
Targeted campaign runs (Qwen3-0.6B unless noted; tok/s):
| format | metric | before | after | llama.cpp | verdict |
|---|---|---|---|---|---|
| q8_0 | pp256 | 105 | 761 | 558 | 7.2x self; 1.36x llama |
| q8_0 | pp256, single P-core | 19.1 | 151.9 | — | 8.0x self |
| q4_k_s | pp256 | 164.6 | 801.1 | 631 | 1.27x |
| q4_k_m | pp256 | 174.8 | 713.9 | 607 | 1.18x |
| q4_k_s | decode64 | 54.3 | 92.8 | ~76 | win (was 0.72x) |
| q4_k_m | decode64 | 55.7 | 87.1 | ~76 | win |
| q6_k | pp256 | 120.7 | 531.0 | 385 | 1.38x (was 0.31x) |
| q6_k | decode | 63.2 | 67.4 | 68.4 | 0.986x — parity |
| q5_k | decode | — | ~64 | 79.8 | 0.80–0.90x — llama.cpp wins |
| Qwen3-30B-A3B Q6_K (MoE) | pp256 | 18.07 | 24.06 | 53 | still behind pre-rerun; see matrix below |
| f16 model | pp1024 | 17.9 | 354 | — | ~20x self |
| f16 model | tg | 9.3 | 28.5 | — | 3.1x self |
The full paired-gate matrix run the same day gives the durable medians (paired orders, median-of-samples — generally lower than the targeted rows above on both sides; e.g. q8_0 pp256 681.7 vs 468.9, ratio 1.45; Q5_K_M pp256 677.7 vs 298.2, ratio 2.27; Q5_K_S pp256 729.6 vs 294.1, ratio 2.48). Matrix verdicts per model (PASS = ratio >= 1.0 with CV <= 8% both sides; NOISY = excessive variance, not a result; FAIL = Fucina behind):
| model | pass/noisy/fail | ratio min/med/max | residual FAILs |
|---|---|---|---|
| qwen3-0.6b q8_0 | 8/13/0 | 1.04/1.32/1.95 | — |
| qwen3-0.6b q4_k_m / q4_k_s | 13/8/0 · 8/13/0 | 1.21/1.46/1.93 · 1.04/1.40/1.82 | — |
| qwen3-0.6b q6_k | 14/6/1 | 0.99/1.41/1.66 | decode 0.987 (coin-flip parity) |
| qwen3-0.6b q5_k_m / q5_k_s | 10/10/1 · 14/6/1 | 0.85/1.70/2.29 · 0.90/1.95/2.56 | decode 0.90–0.91 (m=1 GEMV path, open) |
| qwen3moe-30b q5_k_m | 5/0/16 | 0.46/0.80/1.30 | mid-batch pp15–33 (0.46–0.52) + decode 0.90 |
| qwen3moe-30b q6_k | 2/0/19 | 0.36/0.76/1.11 | mid-batch pp15–33 (0.36–0.43) + decode 0.95 |
| gemma4-26b q6_k | 12/1/8 | 0.85/1.04/1.31 | small-batch pp2–9 (0.85–0.98) |
Reading: the dense 0.6B is swept (large-batch ratios 1.3–2.6x). The 30B MoE
wins or holds parity at pp64+ on Q5_K_M (1.24–1.30x) but loses mid-batch
prefill decisively — at 128 experts / top-8, pp8–33 gives each selected
expert only ~1–3 rows, so each expert's full weight matrix streams from
memory for almost no work (a weight-bandwidth shape llama.cpp's
mul_mat_id handles better) — and m=1 decode stays bandwidth-bound at
0.90–0.95x. Gemma on x86: llama.cpp wins pp1–9 (0.85–0.99x), Fucina wins
pp15+ (up to 1.31x), decode parity.
Numerics: all quantized x86 kernels are i32-bit-exact vs their scalar reference arms; the f16 change (f32 accumulation) is strictly more accurate than the old per-op rounding, proven bit-identical on a Q8_0 model forward.
Update 2026-07-10 — the MoE mid-batch band is resolved. Two coupled
scheduler changes (same llama build 30af6e2 reference, same flags, -t 8,
prewarmed, paired orders, reps 3): the batched-MoE phased chain now engages
at 64 routed pairs instead of 512 (at top-8 the old gate excluded everything
below seq 64, so the pp15–33 band ran one monolithic task per expert), and
small-m (m < 16) projection phases column-chunk at 256 when the layer has
fewer than workers x 16 active experts. Decomposition on the same day, same
box (tok/s Fucina vs llama, ratio):
| variant | Q5_K_M pp15 / pp17 / pp32 | Q6_K pp15 / pp17 / pp32 |
|---|---|---|
| old gate (512 pairs) | 0.509 / 0.500 / 0.461 | 0.465 / 0.441 / 0.375 |
| chain at 64 pairs only | 1.131 / 1.162 / 1.207 | 0.940 / 0.958 / 0.963 |
| + small-m column chunking | 1.152 / 1.175 / 1.218 | 1.050 / 1.021 / 0.987 |
The chain gate does the heavy lifting; chunking adds ~2% on Q5_K_M and +2.5–11% on Q6_K — Q6_K needs both to cross parity at pp15–17. Full-band run (pp15,16,17,31,32,33): Q5_K_M 6/6 PASS at 1.15–1.22x; Q6_K passes pp15–17 (1.02–1.05x) and sits at 0.965–0.987x at pp31–33 (residual gap, recorded under Open gaps). On the M1 the same change is +27–37% self-A/B across the band (68.3→86.5 tok/s at pp15, 75.7→103.5 at pp32; interleaved OLD/NEW, 2 passes x 3 reps, sigma under 3.3). Routing skew observation from the profile counters: at pp256 only ~52 of 128 experts are active per batch (uniform routing would predict ~124), so the small-m heuristic also engages beyond the target band.
Update 2026-07-10 — Q5_K dense decode resolved (measured; landing queued
behind in-flight tree work). The 0.90x loss was layout bytes, not
kernels: every dense Q5_K weight is resident twice (WeightQ5_K
{value, packed_rhs}), and decode streamed the prefill-favorable
byte-expanded Q5_Kx8 pack at 8.625 bpw where the GGUF-native blocks in
value are 5.5 bpw — 1.57x the necessary bytes at a DRAM-bound m=1 GEMV
(llama.cpp's x86 arm does not even repack Q5_K; it wins on density alone).
Routing m < 4 to the already-resident compact blocks through the existing
tensor-RHS kernels — a ~30-line dispatch gate, zero added memory, proven
bitwise-identical cross-layout at m=1..3 by a new kernel test — measured on
the i9: decode Q5_K_M 0.90 → 1.016 PASS, Q5_K_S 0.91 → 1.055 PASS
(self-A/B 66.1 → 79.0 tok/s, +19.5%), prefill 7/7 PASS including the
compact-routed pp1–3 (1.10–1.32), 30B MoE decode ride-along 0.90 → 0.972
(dense tensors route compact; the expert residual remains). The Q6_K twin
gate (1.30x expansion) measured the proportional result: dense decode
0.987 → 1.057 PASS (self-A/B 66.7 → 74.0 tok/s, +10.9%), 30B MoE Q6_K
decode 0.93 → 0.971. Both gates are bitwise-proven (kernel cross-layout
tests are on the tree) and are landed.
Knob sweep and regression legs (same day, same protocol): raising the task-budget multiplier to 32 does NOT move the Q6_K pp31–33 residual (0.952–0.979 vs 0.965–0.987 at 16 — noise-indistinguishable), so the shipped constants stay gate=64 / chunk=256 / budget=workers x 16 and the residual is attributed to the Q6_K packed layout's 1.30x byte expansion at small m, not scheduling. Broad sweep pp1–256 + decode on both 30B quants: no regressions (Q5_K_M pp64–256 wins hold at 1.24–1.28x; Q6_K pp64–256 now 0.93–0.97x, up from a 0.76 median / 0.45x targeted pp256 record; pp1–7 stays the known monolithic-path band; decode unchanged as expected — the seq==1 path is untouched). Gemma-4-26B full leg: non-regressed (pp2–9 stays its pre-existing 0.91–0.97 band, pp15–64 wins hold, decode parity 0.996).
Batch-N multi-stream decode (M1 Max)¶
--streams N on the qwen3 runner: N lockstep decode streams (per-stream KV
cache and sampler, one m=N weight pass per step) vs the same binary running
the N generations back to back.
zig build qwen3 -Doptimize=ReleaseFast -- models/Qwen3-0.6B-Q4_K_S.gguf \
<8-token-ids> --gen 65 --bench 3 --streams 4
M1 Max, ReleaseFast, 8 threads, cool machine, Qwen3-0.6B, 8-token prompt, 64 decode steps per stream:
| weights | N | batch tok/s | sequential tok/s | speedup | outputs |
|---|---|---|---|---|---|
| Q4_K_S | 2 | 152.6 | 136.2 | 1.12x | identical |
| Q4_K_S | 4 | 313.5 | 121.6 | 2.58x | ~1e-6 logit drift (packed 4-row kernels) |
| Q4_K_S | 8 | 369.7 | 116.0 | 3.19x | ~1e-6 drift |
| Q6_K | 2 | 139.8 | 101.2 | 1.38x | identical |
| Q6_K | 4 | 254.1 | 92.8 | 2.74x | identical |
| Q6_K | 8 | 358.3 | 110.2 | 3.25x | identical |
| f16 | 4 | 205.5 | 66.4 | 3.09x | identical |
Decode is weight-bandwidth-bound, so batching N streams into one m=N pass reads the weights once instead of N times. The modest N=2 gain is the per-row (m<4) quantized kernels re-streaming the packed weights; the weights-read-once kernels engage at m>=4 — which is also the documented ~1e-6 numerics boundary for quantized weights (f32/f16 stay bitwise, verified to m=12). Single-stream non-regression was verified with interleaved cool A/B pairs (prefill neutral; decode differences within thermal-order noise, both signs observed).
Parakeet ASR vs parakeet.cpp (M1 Max)¶
(Parity-dump regeneration for this family needs the instrumentation patch:
tools/fetch_refs.sh --patch parakeet.cpp. The benchmark rows below were
measured against the stock pinned binary.)
A different reference engine (ggml-based, Accelerate), same discipline. NeMo Parakeet FastConformer models; transcripts are byte-identical to parakeet.cpp in every mode below, so these are pure-speed comparisons. Fixture: speech.wav, 7.435 s. The port started ~7–8x slower than parakeet.cpp and closed the gap in stages; final states:
Cold one-shot (full process incl. load, best/median of 7, tdt_ctc-110m, measured cool):
| format | Fucina best/median | parakeet.cpp best/median | result |
|---|---|---|---|
| f16 | 142.6 / 143.9 ms | 156.2 / 159.4 ms | 1.09x faster |
| q8_0 | 136.9 / 137.6 ms | 143.9 / 148.7 ms | 1.05x faster |
| q6_k | 173.1 / 177.2 ms | 188.3 / 190.5 ms | 1.09x faster |
| q5_k | 167.1 / 169.1 ms | 182.1 / 185.2 ms | 1.09x faster |
| q4_k | 148.0 / 150.8 ms | 162.0 / 164.5 ms | 1.09x faster |
Warmed steady-state session (--f32-cache --fast-mel, 20 reps, best; vs
parakeet-cli bench): q8_0 97.5 vs 102.230 ms (1.05x), q6_k
99.5 vs 144.659 (1.45x), q5_k 103.3 vs 144.615 (1.40x), q4_k 102.4 vs
123.898 (1.21x). f16 is not a win in this mode (Fucina 101.6 vs 93.404
ms) — the big dense f16 GEMM is where ggml's Accelerate/AMX path is
strongest.
Streaming head-to-head (per-invocation wall incl. load, warm page cache, best-of-6, 8 threads): realtime_eou-120m 667 vs 846 ms (Fucina 1.27x faster); nemotron-0.6b multilingual 1309 vs 1675 ms (1.28x faster); offline tdt-0.6b-v3 567 vs 532 ms (0.94x — parakeet.cpp edges it, same AMX-favored dense-GEMM story).
OmniVoice TTS vs omnivoice.cpp (M1 Max)¶
The OmniVoice port is validated and benchmarked against omnivoice.cpp, the
C++ reference it ports: MaskGIT token streams and RVQ codes are
byte-exact at F32 with a fixed seed, decoded audio cosine >= 0.99999,
and synthesis runs 2.3–4.6x faster than omnivoice.cpp's CPU backend at
equal dtypes (32x on BF16, where ggml's CPU BF16 matmul path is
pathological — use Fucina's BF16, not the reference's, if you want that
dtype). Setup, per-dtype guidance, and the determinism contract:
examples/omnivoice/README.md.
LocateAnything-3B detection vs locate-anything.cpp (M1 Max, 2026-07-07)¶
Open-vocabulary detection VLM (MoonViT + Qwen2.5-3B + MTP parallel box
decoding) vs the ggml-based reference it ports (mudler/locate-anything.cpp @
92c1682, CPU backend). Detections are byte-identical in every cell below
(the JSON outputs cmp equal), and the underlying token streams are id-exact
(the compare gate battery), so these are pure-speed comparisons.
Protocol mirrors the reference's own benchmarks/BENCHMARK.md: the 448x448
parity fixture, prompt "catremote", greedy, full streams (early-stop off
on both sides), 8 threads, warm, wall of detect with load subtracted via
info (ref ~3.9 s f32 / ~1.0 s q8; ours ~2.7 / ~1.2 s). Two interleaved
passes each; both shown:
| mode | dtype | locate-anything.cpp | Fucina | inference speedup |
|---|---|---|---|---|
| slow (pure AR) | f32 | 22.9 / 24.7 s | 10.6 / 11.8 s | ~2.3x |
| hybrid (PBD, default) | f32 | 44.3 / 45.4 s | 29.4 / 35.5 s | ~1.4x |
| fast (MTP-only) | f32 | 41.5 / 40.6 s | 26.3 / 26.7 s | ~1.6x |
| slow | q8_0 | 10.9 / 11.2 s | 5.5 / 5.4 s | ~2.4x |
| hybrid | q8_0 | 18.4 / 18.6 s | 9.3 / 8.9 s | ~2.2x |
| fast | q8_0 | 18.2 / 17.9 s | 9.5 / 8.6 s | ~2.2x |
q8_0 boxes are additionally byte-identical to each engine's own f32 output — stronger than the cross-implementation guarantee the tolerance policy promises for quantized paths, recorded as a fixture-level observation, not a contract.
The hybrid/fast gap was closed by the gemmNTCols loop interchange (see
Recorded negatives below and the commit): the MTP block rounds are
m = n_recompute + 6 ≈ 8-row NT GEMMs, squarely in the f32 column-kernel
window whose row-outer loop re-streamed the whole RHS per row. Routing those
shapes to Accelerate instead was measured slower (37.8 s vs 32.5 s wall,
fast mode) — the fixed vector kernel wins tall-skinny small-m on this
machine.
x86-64 leg (i9-13950HX, 8P+16E, no BLAS either side, 2026-07-07). Same
fixture/protocol, both engines compiled on the box (gcc-12 / zig native).
The full parity battery re-ran green on x86 (39 gates, ReleaseFast — the
AVX2/AVX-VNNI kernel arms included); the remote zig build test suite
passed natively first. At 8 threads pinned to the P-cores (taskset -c
0-15, median of 3, wall; f32 boxes byte-identical per mode):
| mode | dtype | locate-anything.cpp | Fucina | inference speedup |
|---|---|---|---|---|
| slow | f32 | 26.1 s | 19.1 s | ~1.4x |
| hybrid | f32 | 40.3 s | 30.0 s | ~1.4x |
| fast | f32 | 37.5 s | 28.4 s | ~1.3x |
| slow | q8_0 | 13.7 s | 11.2 s | ~1.3x |
| hybrid | q8_0 | 22.3 s | 19.1 s | ~1.2x |
| fast | q8_0 | 21.8 s | 18.5 s | ~1.2x |
Honest reads on the x86 leg:
- 16 threads is parity-to-behind, and HT-pinned is a Fucina pathology.
Unpinned t16 (E-cores in play) is noisy parity on f32 and ~0.9x on q8
hybrid/fast (ggml slightly ahead). Pinning 16 Fucina workers onto the 8
P-cores' 16 hyperthreads collapses our side (slow 19 -> 43 s, hybrid
30 -> 72 s) while ggml holds its t8 level — the spin-then-park worker
team degrades under HT oversubscription where ggml's threading does not.
The 8-thread numbers above are the intended operating point.
Resolved 2026-07-10:
cpuThreadCountnow min()s in the physical-core count (Linux:thread_siblings_listdedup intersected with the affinity mask; macOS:hw.physicalcpu, which equals the logical count on all Apple Silicon so no mac config changes). Verified on this box: unpinned sizing 32→24,taskset -c 0-15(this collapse scenario) → 8, a mixed 2P+4E mask → 6;FUCINA_MAX_THREADS/setMaxThreadssemantics preserved (setMaxThreadsstill deliberately oversubscribes past physical). - q8_0 boxes on x86 are not cross-engine identical (they are on the M1): activation-quantization rounding differs per kernel arm. Real detections match in every mode; slow mode drifts two coordinates by one coordinate token (~0.45 px), hybrid only inside the degenerate repeated-box tail; fast is byte-identical. Reference-q8-vs-its-own-f32 is 0 fields on this fixture, ours-q8-vs-our-f32 is 2 (slow) / 25-in-tail (hybrid) / 0 (fast) — within the tolerance policy's expectations for quantized paths, and the f32 streams stay byte-identical cross-engine on both ISAs.
Fucina-only kernel context (no llama.cpp pairing)¶
Two internal records that frame the numbers above:
- Query-tiled online-softmax attention forward: M1, Qwen3-0.6B — attention phase ~2.1x; end-to-end prefill pp1024 +24.5%, pp2048 +56%, pp4096 2.19x; no regressions across the routine matrix.
- Blocked packed f32 GEMM for the no-BLAS build (
-Dblas=none, the default off macOS): 2048^3 109 → 608 GFLOP/s (5.6x), reaching ~26–35% of Accelerate/AMX on the same machine — what makes the no-BLAS builds credible at training shapes.
CUDA Q5_K addendum (2026-07-12; no llama.cpp pairing). RTX 5000 Ada
Laptop + i9-13950HX, -Dmax-threads=32 -Doptimize=ReleaseFast, committed PTX,
resident weights, 31 workers plus caller. The paired bench-gpu-formats
CPU contender is Fucina's real compact Q5_K kernel for rows 1–3 and packed
Q5_Kx8 kernel otherwise. Host-visible eager latency was 63.0→46.4 µs at
32×1024×512, 263.7→134.8 µs at 32×4096×1024, and 5.578→1.410 ms at
128×4096². The measured decode gate keeps 1×4096² on CPU (95.8 versus
108.6 µs) but admits 1×6144×4096 (212.5→159.4 µs) and rows 2–8 at 4096²;
Q5_K uses GEMV below row four and tiled MMA above it. Restoring residency when
mixed-format fusion declines removed all 84 repeated streamed-weight calls
(330 resident async submissions in the traced pp32 pass). Qwen3-0.6B-Q5_K_M
warm prefill improved 503.3→770.3 tok/s at 32 tokens and 620.3→1167.1 at 128;
opt-in decode improved 62.85→92.30 tok/s and reproduced the same 32-token
greedy continuation. CPU/GPU quant arithmetic is tolerance-equivalent, not
bit-identical; the Q5_K_S close-margin argmax counterexample and direct-op
error bounds are recorded in GPU-OFFLOAD.md.
Recorded negatives¶
Kept on record so they are not re-tried or over-claimed:
-
BLAS at small-m tall-skinny f32 NT shapes loses to the fixed vector column kernel (M1 Max). Lowering
shouldUseBlasto m >= 2 routed the LocateAnything MTP-round GEMMs (m ≈ 8, n up to 11008/152681, k = 2048) to Accelerate and measured 37.8 s fast-mode wall vs 32.5 s for the interchange-fixedgemmNTCols(2026-07-07). The m >= 16 BLAS gate stays; do not re-lower it without new evidence on these shapes. -
Dispatcher-QoS pin did not fix the qwen3.5 pp32 variance. The barrier workers already elevate to
QOS_CLASS_USER_INTERACTIVE; the dispatcher thread (which computes chunk 0 of every parallel op) and the dot-backward worker did not. Pinning them too is kept — it restores QoS parity across all compute threads, matches llama.cpp's practice, and measured no regression — but it did NOT collapse the qwen3.5 pp32 process-to-process bimodality (still 446–667 tok/s best-of-3 across six fresh processes afterwards), so the root cause of that spread remains open. - Q5_K decode repack: tried and reverted. A repack that helped decode regressed prefill; the tree keeps the prefill-favorable layout.
- q8_0 KV cache is a capacity option, not a speed option: 1.88x context in the same cache budget, but decode is slower with it on M1 — decode attention there is compute-bound, and the dequant adds ~2.3x to the attention phase at 2048 ctx.
- bf16-resident weights buy memory, not speed, on M1: decode 54.7 tok/s vs f16's 65.5; the M1 has no bf16 FMA, so the kernel widens in-register.
- Residual-add epilogues: measured and declined (2026-07-10). The profile counters at the qwen3 residual sites put the entire opportunity at 0.7% (decode) / 1.2% (pp256) of forward time on the 0.6B and 0.1% / 0.34% on the 30B MoE — the capturable slice of an in-place/consuming-add rewrite (roughly half) sits inside paired-gate noise, and a true beta=1 GEMM epilogue cannot be proven bit-exact for BLAS (the CBLAS contract fixes the value, not the summation order). Do not re-try without a workload where the residual share is measured above ~2%.
- trans_ab contraction kind: measured and declined (2026-07-10). The role-pinned double-transposed GEMM — the only case where the einsum engine materializes an operand — has zero reachable sites (every production dot/einsum call and every training-backward einsum verified to resolve to plain/trans_a/trans_b), and the measured recovery ceiling is 1.13-2.8x (M1, Accelerate) in a case nothing hits. The equation-level role-swap workaround is faster than the trans_ab ceiling itself (Accelerate's NN beats its TT at every measured shape), and on no-BLAS builds at most 1.06-1.32x is recoverable — likely unattainable with a TT access pattern. Re-open trigger: a port whose profile shows the tagged.zig materialize arm hot with genuinely pinned output; the recorded BLAS-arm-only design (~250 LOC) then applies, and the cheaper first lever is a blocked transpose in the materialize pass (currently ~3-8 GB/s).
- Open llama.cpp gaps as of the latest records: Gemma-4-26B mid-batch prefill on M1 (0.72–0.91x cool, cause unresolved); Qwen3-30B MoE Q6_K pp31–33 on x86 (0.965–0.987x residual of the resolved mid-batch band — byte-expansion-attributed, see the two 2026-07-10 updates) and MoE decode (x86 0.971–0.972 after the compact ride-alongs; M1 0.87–0.95); pp256 with Q4_K experts on M1 (~0.85x). x86 dense decode on Q5_K and Q6_K is resolved (1.016/1.055 and 1.057; gates landed).
Limitations¶
These results are shape- and machine-specific: two CPUs (Apple M1 Max, Intel i9-13950HX), one thread count each, specific GGUF files, specific prompt lengths. On Apple Silicon, thermal state changes outcomes by tens of percent — several conclusions in this file flipped between hot and cool measurements, and only cool, isolated, best-of A/B pairs proved trustworthy. Fucina's benchmark loops still include final logits/sampler work that llama-bench skips, which biases every ratio here against Fucina by a small amount. And the comparison target moves: llama.cpp advances continuously, so a dated ratio is a statement about two specific builds on one day, not a standing property. Re-run the paired gate on your own hardware before relying on any row.