← all posts
September 13, 2026·boracoder01

Mapping Honeykrisp decode: +27%, and where the other half lives

An M2 Ultra decodes a 4B model at half the rate of a machine with a third of its bandwidth. Two environment variables recover 27%, and six further experiments close every conventional explanation — the remaining 2x is provably below the kernel source and the compiler schedule.

38.5 → 48.9 t/sdecode, Qwen3-4B Q4_0
+21.3%DMMV_WG=1 alone
34 µs + bytes/290 GB/sthe fixed-cost fit
−17.5%Metal algebra on winning geometry
+14.4%ALU-ablation ceiling
75.6 W / 88.9 W peakdecode power
vulkanasahi-linuxapple-siliconllama-cppbenchmarking

An ⁠M2 ‌Ultra ​decodes ‌a ‌4B ​quantized ‌model ​at ‌38.5 ‌tokens ​per ‌second ‌through Vulkan. ​A ‌Strix ‌Halo ​— ‌a ‌third ‌of ​the ​memory ‌bandwidth ​on ‌paper ​— ‌decodes ​the same ​file ​at ​78. ‌Every ​optimization ‌avenue ‌on ‌that ‌gap ‌was ‌measured, ‌one ​closed per ⁠experiment, ⁠and ‌the ​result ‌is ‌a ​map: ‌two ​environment ‌variables ‌recover +27%, ​and ‌the ‌rest ​of ‌the ‌gap ​provably ‌does ‌not ‌live ​in ​the ‌kernel ​source, ‌the arithmetic, ​or ‌the ​compiler's ​instruction ​schedule. ​This ‌is ​the ‌whole ‌map, ‌with every ‌number.

Test bench

host     Apple Mac Studio, M2 Ultra (G14D B1)
         24 cores, 190.0 GiB unified memory (measured)
         integrated AGX GPU, 60 cores, ~800 GB/s LPDDR5

OS       Fedora Asahi Remix 44
         kernel 7.1.13-usb4gpu (locally built)

Vulkan   Mesa 26.1.5-1.fc44, driver Honeykrisp (release build)

runtime  llama.cpp f5919bf + env-knob layer
         (the pristine control build is never modified)

model    Qwen3-4B Q4_0, 2.21 GiB, md5 verified every run
         loaded with -lm dio (weights bypass the page cache — a separate story)

reference AMD Strix Halo, Radeon 8060S, RADV, ~256 GB/s
         same file, same flags, same day

Two lines of upstream default, half the decode

The ‌kernel ‌dispatch ‌code ​in ⁠ggml-vulkan.cpp ⁠tunes ‌two ​things ‌per ‌driver: rows ​per ‌workgroup ​(AMD ‌GCN ‌and ​Intel ‌get ‌raised ​values) ‌and ‌a shader_core_count ​that ‌several ‌memory ‌heuristics ​consume. ​Mesa's ‌Honeykrisp reports ​VK_VENDOR_ID_MESA ‌— ​matching ‌neither ​vendor ​branch ​— ​so ‌it ​receives 32-thread ‌workgroups ‌and ‌a ‌core ‌count ‌of ‌zero, ​and ⁠every ⁠heuristic ‌downstream of ​those ‌two ‌values ​runs ‌at ​its ‌floor.

GGML_VK_DMMV_WG=1 GGML_VK_SHADER_CORE_COUNT=60 \
  llama-bench -m model.gguf -ngl 99 -lm dio

control (upstream defaults)      38.17-38.47 t/s floor
DMMV_WG=1  (128-thread groups)  46.29   +21.3%
+ SHADER_CORE_COUNT=60          48.38   +25.8%

The ‌two ​knobs ‌compose ‌multiplicatively ​— ‌1.213 ‌× ​1.038 ‌= ‌1.259, ‌measured 1.258. ​The ​direction ‌is ​monotone ‌in ​parallelism ‌per ​row: ​raising ​rows ​per workgroup ‌loses ​7–34% ‌(fewer ‌groups), ‌doubling ‌the ‌groups ‌gains ‌7%, ​and ⁠the four-subgroup ⁠cooperative ‌form ​wins ‌outright. ‌Strix ​Halo's ‌driver ​makes ‌the equivalent ‌choice ​for ‌its ‌hardware; ​Honeykrisp ‌never ‌got ​the ‌branch.

What the machine was actually doing

With ‌the ‌two ​knobs ​set, ‌a ​perf-logger ‌decomposition ​of ‌one ​decode ​token ​shows the ​whole ‌token ​in ‌matrix-vector ‌kernels, ‌and ‌the ‌per-shape ‌effective bandwidth ‌explains ​the ⁠rest ⁠of ‌the ​gap:

shapeper dispatcheffective
gate+up 9728×256081.9 µs171 GB/s · 4864 workgroups
down 2560×972884.6 µs165 GB/s
q-proj 4096×256055.0 µs107 GB/s
kv 1024×256038.7 µs38 GB/s · 512 workgroups
output head q6_K1582 µs202 GB/s
— machine peak800 GB/s

Useful ‌FLOPs ‌run ​at ‌0.5–2.1% ​of ‌the ‌27 ​TFLOPS ‌peak. ‌Issued ​instructions ‌— counting ‌the ​full ‌dequantization ‌bloat, ‌~12.8 ​scalar ​operations ‌per ​weight ‌— sit ​at ‌~13% ​of ​issue ​capacity. ​An ‌ALU-ablation ​run ‌(same ‌loads, ‌checksum instead ‌of ‌the ‌arithmetic) ‌buys ​exactly ⁠+14.4%, ⁠matching ‌that ​ceiling. Neither ‌pipe ‌is ​saturated; ‌the ​kernel ‌is ‌latency-bound.

Every dispatch pays 34 µs before the first byte streamsper-dispatch time vs weight bytes — M2 Ultra, warm, one harness02550751004 MB8 MB12 MB16 MBµs / dispatchkv39 µs · 0 GB/sq55 µs · 0 GB/so62 µs · 0 GB/sgate/up82 µs · 0 GB/sdown85 µs · 0 GB/s34 µs fixed
The fit t ≈ 34 µs + bytes/290 GB/s predicts every shape within ~15%. The kv projection is 89% fixed cost — the smallest shape pays the full toll to move 1.47 MB. 250 such dispatches make the fixed term 41% of every decode token.

Fitting ​the ‌six ‌shapes ​gives ‌t ‌≈ ​34 ‌µs ‌+ ‌bytes/290 ​GB/s: ​every ‌dispatch pays ​a ‌fixed ​cost ‌before ​the ​first ​byte ​streams. ‌At ​250 ‌matvec ‌dispatches ‌per token ‌that ‌fixed ‌cost ‌is ​~8.5 ⁠ms ⁠of ‌a ​20.6 ‌ms ‌token ​— ‌41% ​of ‌every ‌token ​is dispatch ‌overhead, ‌not ​work.

Closed by measurement

Eight levers, each measuredgreen shipped · grey closed by measurement · orange openwide workgroups + core count+27% — shippedMetal kernel geometry−10% · closedMetal kernel arithmetic−17.5% · closedALU reduction (all of it)+14% ceiling · closedper-shape workgroup countflat on kv · closedsubmit batchingflat above default · closedgate/up fusion+1.1% measured · shippedfinal lowering + driverthe remaining ~2×
Every closed row is a measurement, not an argument — the two Metal ports were correctness-gated (991/991) before timing, and the ALU ceiling comes from an ablation run that keeps the loads and deletes the math.

Each ‌row ‌of ​that ‌ladder ‌is ‌an ​experiment, ​not ‌an ​argument. ‌Two ​deserve ‌detail.

First: ​porting ​Apple's ​own ​Metal ‌kernel ​for ‌this ‌exact ‌operation ‌— ‌its geometry, ‌then ‌separately ​its ⁠arithmetic ⁠— ‌loses ​on ‌Honeykrisp ‌(−10% ​and −17.5% ‌respectively, ​991/991 ‌correct ‌both ​times). ‌The ‌generic ​kernel's structure ‌is ‌already ​right ‌for ‌this ‌machine; ​Apple's ​source ‌is ​right ‌for Apple's ​compiler.

Second: ‌gate/up ​pair ​fusion. ​The ​host ‌patch ​fuses ‌all ‌36 ‌layers ‌— ‌the ‌perf logger ‌shows ​MUL_MAT_PAIR_GATE_UP ⁠at ⁠36 ‌dispatches ​replacing ‌72, ‌and ​the pair ‌itself ​runs ‌19% ‌faster ​— ‌but ‌end-to-end ​gains ‌only ‌+1.1% ​where ‌the fixed-cost ‌model ‌predicted ​+6%. ​The ‌fixed ​cost ‌was ​partly ‌overlapped ​with other ​work; ​removing ​dispatches ‌converts ​to ‌wall-clock ‌at ‌only ‌~18% ‌of ‌the naive ‌rate. ​Every ⁠future ⁠dispatch-count ‌estimate ​gets ‌priced ‌at ​the ‌measured conversion, ​not ‌the ‌nominal ​saving.

Apple compiles the same scalar kernel

ggml's ‌Metal ‌kernel ​compiles ‌on ‌a ​Mac ‌with ‌xcrun -sdk macosx metal -S -O3. The ‌optimized ​IR ​of ‌the ​reference ‌q4_0 ​matvec ‌is ​scalar ​throughout: ​2-byte nibble ​loads, ‌element-by-element ​activation ‌reads, ‌one ‌simd_sum, ‌no ‌vector loads, ‌no ‌exotic ​intrinsics. ⁠The ⁠same ‌structure ​our ‌Vulkan ‌shader ​uses.

And ‌the ​Mesa ‌compiler ‌flags ​tell ‌the ‌complementary ​story: ‌disabling ‌the backend ​optimizer ‌entirely ‌(AGX_MESA_DEBUG=noopt), ‌or ​the ​scheduler (nosched), ‌moves ​decode ‌by ​less ‌than ​half ​a ​percent. ​The ‌gap ​to ‌Apple's ‌~2x on ‌the ‌same ‌silicon ‌is ‌not ​in ⁠the ⁠kernel ‌source ​and ‌not ‌in ​the ‌instruction schedule.

What remains, and the oracle plan

Two ​layers ‌sit ‌below ​everything ‌eliminated ‌so ​far. ‌Apple's ‌final AIR-to-AGX ​lowering ‌— ‌invisible ‌from ​Linux, ​capturable ‌by ​running ‌macOS ​as ‌a guest ​under ​the ​m1n1 ​hypervisor ‌and ​tracing ‌what ‌the ‌driver ‌uploads, ‌then decoding ‌with ‌Asahi's ​own ⁠disassembler. ⁠And ‌the ​kernel-driver ‌submission pipeline, ‌where ​the ‌34 ​µs ‌fixed ‌cost ​lives ‌— ‌open ​source ‌on ‌this ​machine, profilable. ‌A ‌debug ‌Mesa ​build ​(26.1.5, ‌same ​commit ‌as ​the ‌release ​driver) provides ​the ​other ​half ‌of ​the ‌comparison: ‌this ‌machine's ‌own ‌compiled ‌machine code ‌for ​the ⁠same ⁠kernel, ‌dumpable ​with ‌AGX_MESA_DEBUG=shaders. ‌The ​plan ‌is one ​line: ‌disassemble ‌both, ​diff ‌the ‌structure, ​patch ‌what ‌the ​diff ‌names.

Reproducing the +27%

# integrity first: the model must read identically through the page cache
# and O_DIRECT on this host (a separate finding)
md5sum model.gguf
dd if=model.gguf bs=8M iflag=direct | md5sum

GGML_VK_DMMV_WG=1 GGML_VK_SHADER_CORE_COUNT=60 \
  llama-bench -m model.gguf -ngl 99 -p 512,2048 -n 128 -r 5 -lm dio

Power ‌and ‌thermals ​were ​monitored ‌throughout ​(system ‌draw, ​SoC ‌rail, ​fans, every ​temperature ​channel ​the ‌SMC ​exposes): ‌75.6 ‌W ‌mean ‌/ ‌88.9 ‌W ‌peak ​under full ⁠decode ⁠against ‌a ​~370 ‌W ‌envelope, ​fans ‌unmoved. ​Nothing ‌here ‌is ​power- or ‌thermally-limited. ‌The ​ceiling ‌is ‌structural, ​and ‌the ‌map ‌now ​says precisely ​which ‌two ​structures ‌hold ​it.


Update, ‌2026-09-14: ​the ​campaign ​this ​map ‌called ​for ‌ran ‌— ‌the ‌graph ‌chain went ‌first, ‌then ​the ⁠quant. ⁠Decode ‌reached ​7.7 ‌t/s ‌on ​this ‌machine, ​one imported ‌"optimization ‌table" ​turned ‌out ‌wrong ​in ‌a ‌lane ​only ‌the ‌get-rows gate ‌reads, ​and ​a ‌control ​arm ‌turned ​out ‌to ​be ​measuring ​a ​CPU ‌fallback rather ​than ‌the ‌best ‌existing ‌path. ‌The ‌full ‌story, ​with ⁠the ⁠attribution matrix ‌that ​cleared ‌the ‌kernels ​and ‌caught ​the ‌page ‌cache ​lying ‌again, ‌is ​in the ‌update ‌to ​Raising the Honeykrisp memory cap.

boracode.ai · Ronald Adonyo · honeykrisp-decode-map
content hash b5b6e542fe21
A wide horizon at dusk: layered hills in muted plum and ochre above a lake that holds the last light, a family of crested cranes at the water's edge, an acacia in silhouette and a single bright star above the ridge.
Mapping Honeykrisp decode: +27%, and where the other half lives — boracode