Repository navigation
Recover ARM F16 GEMM speed with blocked F32 accumulation (#174) - #179
Merged
Merged
Conversation
Patch 0006 kept every llamafile F16 tile in F32, which halved F16-weight prefill on Apple CPUs. The tile now multiplies and adds in F16 again, adds each lane into F32 every 64 products, and redoes any tile with a nonfinite result in the F32 tile. Finite inputs still never overflow; non-overflowing tiles match the unpatched tile for k <= 512 and are more accurate beyond. Qwen2.5-0.5B F16 pp512 on M4 Max goes from 0.44 to 0.91 (FA off) and 0.94 (FA on) of v0.8.0-3. Tests add worst-case block sums, block-length and tail checks, and a partial-tile overflow away from the origin.
leehack
marked this pull request as ready for review
November 7, 2026 17:04
This was referenced Nov 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Replaces patch 0006 with a faster overflow-safe F16 GEMM. The first 0006 (#177) accumulated every llamafile F16 tile in F32, which cut F16-weight prefill on Apple CPUs to 0.44× of
v0.8.0-3. This version gets back to 0.91× (FA off) and 0.94× (FA on). Finite inputs still never overflow.Refs #174 (keep open). Upstream: ggml-org/llama.cpp#31734.
Design
The FP16
tinyBLAS<8, float16x8_t, ...>tile keeps its F16 FMAs and upstream order, with two changes:inf/NaN, and that value survives every later F16 and F32 add. After the tile is stored, any nonfinite output causes the whole tile to be recomputed by the F32 tile from the first 0006 (tinyBLAS<4, float32x4_t, float32x4_t, ggml_fp16_t, ggml_fp16_t, float>, called through afrienddeclaration). The recomputed results overwrite the tile.Patch: one
sgemm.cpphunk addsgemm_bloc_f16and a dispatch fromgemm_blocwhenDisfloat16x8_t. A second hunk is the unchangedk >= 0marker guard. Both apply strictly to the new pinv0.9.0(cd959bae, from #175) and to everyLLAMADART_UPSTREAMSrow (v0.7.0620d2225, v0.8.08642541b, v0.9.0).Correctness contract
k≤ 512k> 512No magnitude bound is needed for safety: the block length only sets precision and how often tiles are redone. An over-read past
kthat reaches only NaN padding would be hidden by the redo. The attention case catches an over-read that reaches the next row.Choosing the design
M4 Max,
GGML_NATIVE=OFF, F16 GEMM 4096×4096×512, time against the unpatched tile (median of 2):FMLAL and FMLA.8H have the same throughput on M4: about 3.4 per cycle in a microbenchmark. A tile that widens on every FMA can therefore get at most half the F16 tile's speed. The earlier
tinyBLAS<8, float32x4x2_t>attempt was slower still (+270%) because it spilled. So the speed comes from F16 FMAs, and safety comes from detecting overflow. End to end, pp512 was 0.79 with 16 products per lane, 0.87 with 32 and 0.92 with 64 (FA off). 64 keepsk≤ 512 bit-identical to upstream and gives the conversion path (linux_armv8.2_2) the most headroom.Performance
M4 Max, CPU only, 8 threads, Qwen2.5-0.5B-Instruct, F16 KV. Runtimes: shipped
v0.8.0-3macOS dylib, the 0006 dylib built froma073d08, and this head (macos-arm64-fullpreset). 3 interleaved rounds; each ratio is the median of the per-round ratios (thermal drift moved absolute numbers by about 10%):An earlier run of the same fast path, which differed only in an unoptimized redo, gave F16 pp512 at 0.92 / 0.94. Decode never reaches the tile (n < 8), so tg differences are noise. All 216 result rows reported 0 nonfinite logits.
Qwen3-TTS cancel latency (core validation
ttspack,--backend cpu, Mac,q174/harness/mac_tts.sh), all samples pooled:Every pack exited 0 within the 500 ms budget. Neither the 4.4× of the first 0006 nor this PR's ratio has been measured on
linux_armv8.2_2hardware. That variant runs the conversion flush, which costs +12–14% GEMM time on M4 in aarmv8.2-a+fp16+dotprodbuild.Validation (code at
6f419890; head6c9ad9c8mergesmainatb1ce76c8(#175, v0.9.0 pin))The merges were clean. #175's manual-dispatch wording in
docs/native174_f16_dot_accumulation.mdis kept. Benchmarks below were run on the v0.8.0 pin; the 0006 hunks and their context are identical on v0.9.0.llamadart_f16_dot_accumulation_test, default Apple flags (FMLAL, 306fmlalin test binary)GGML_CPU_ARM_ARCH=armv8.2-a+fp16+dotprod(0fmlal)v0.9.0(cd959bae) after merging #175batch.cpp(1–32 columns) andattn.cpp(ordinary + flash, 1–32 queries), both builds, v0.8.0 and v0.9.0apply --stricton the v0.9.0 pin; 0006 applies to the v0.7.0, v0.8.0 and v0.9.0 rows6c9ad9c8macos-exit-teardownrelease and ASan for v0.7.0, v0.8.0 and v0.9.0 (FMLAL and conversion F16 test)40b526e7(before #175)python3 -m unittest discover -s tests -p 'test_*.py'6f419890; 446 OK (1 skipped) at6c9ad9c8New test cases for the bound:
gemm-block-maximum: every lane of every block sums exactly to 65504.gemm-block-overflow: every lane sums to 65536, so every tile is redone.gemm-block-exact: sums are exact in F16 within one block but not across two. Widths are 8, 504, 512, 520 and 2056, so tails and a longer block are covered.gemm-tile: an overflow covers rows 5–6 × columns 8–9 of the 4×6 tile at (4, 6). Exact integers are compared with tolerance 0.Mutants, each built and run on both paths. Every mutant that applies to a path fails at least one check there:
kin F16)gemm-block-exact)gemm-tile)jjk(over-read)tests/test_llama_cpp_patches.pynow uses the dispatch hunk's context,D Cv[RN][RM] = {};, to check exact-source application. The old context,tinyBLAS<8, float16x8_t, is no longer in the patch, and the test fails with it.Not covered / gates
linux_armv8.2_2.Sequencing
Branched from
a073d08.mainwas merged in at #178 (f3961b7f) and at #175 (b1ce76c8), with no force-push. The change touches only 0006, the patch README, its tests anddocs/native174*. It must land before the manual v0.9.0 release dispatch. Independent audit: ACCEPT at6c9ad9c8againstb1ce76c8, with no blocking findings. The auditor built six configurations (FMLAL, conversion, ASan+UBSan, GCC 16 conversion, unpatched, first 0006). Results:Non-blocking test gaps go to a follow-up:
gemm-tilenever overflows the tile's last row or column, so a finiteness check that skips them survives.