Repository navigation
perf: GDN verify conv rows chosen per tap, outputs kept per lane on Apple7/8 - #25
Merged
paperniuk merged 1 commit intoOct 24, 2026
Conversation
…pple7/8 Two changes to the fused GDN decode's Table16F instances, which only Apple7/8 run; the bits are unchanged. The prologue's convolution chose between the carried rows, the pending rows' tape and the packed rows on every load (GdnConvHistory::at), though the row a tap reads is the same for all of a simdgroup's channels. It now chooses each tap's row once and loads the channels from it. On an M2 Max that takes 30% off the kernel at one lane, 20% at two and 32% at four. The scan stored every token's output from lane 0, a branch per token and batch. Lane t now keeps token t's outputs with a select and stores them once per batch: 5-6% less time at one to four lanes. The GDN decode test's replay compares the Table16F decode with the plain one bit for bit, for all three shapes. M2 Max, 27B MLX 4-bit (decode-profile, 2048-token prompt): verify_gdn_fused_table16f 4.25 -> 2.71 ms per B1 cycle, 6.15 -> 4.46 at B2, 8.14 -> 5.33 at B3, 11.99 -> 7.57 at B4; the cycle 80.8 -> 79.4 ms (-1.7%), 140.5 -> 138.6 (-1.4%), 192.4 -> 189.6 (-1.5%), 257.5 -> 253.4 (-1.6%). End to end on the 1.4 port (test-performance-real), decode at one, two and four lanes: MLX 4-bit 88.7 -> 90.8 tok/s (+2.4%), 104.1 -> 105.8 (+1.6%), 113.1 -> 115.1 (+1.8%); UD-Q4_K_M 79.4 -> 81.0 (+2.0%), 95.4 -> 97.2 (+1.9%), 108.9 -> 111.1 (+2.0%). Prefill is unchanged.
Author
|
@paperniuk BTW, I also pushed the same change on top of apple7-m1-1.4: https://github.com/ghisguth/splash/tree/apple7-m1-gdn-verify. It is one commit and cherry-picks without conflicts, so you can take it from there if it's easier. If you prefer a separate PR to apple7-m1-1.4, let me know and I will open it. |
This was referenced Oct 22, 2026
Owner
|
Thanks! Confirmed on M1 Max (32-core GPU).
Merged. |
paperniuk
added a commit
that referenced
this pull request
Oct 24, 2026
…port) into the 1.4 port Conflicts: - paged_attention.metal, rope.metal: 1.4's own versions, which already hold #24 and the precise RoPE tables. - LinearGguf.cpp, linear_plan_test.mm: #27's one-pass check for the F32 register tile next to 1.4's 16-row tile check; #27's one-pass scratch with LinearPlan::table(). - gguf_projection_test.mm: #27's register one-pass test where the device is not Apple9, before 1.4's prefill. - gguf_projection_benchmark.mm: register_gu rows next to 1.4's staged16 rows; the staged rows take one pass only from a staged policy.
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.
Base:
paperniuk/splashapple7-m1-1.3.2(branchapple7-m1-gdn-verify-1.3.2, fd01a92). The same commit is onapple7-m1-1.4asapple7-m1-gdn-verify; it cherry-picks without conflicts.This one speeds up the fused GDN verify decode (
verify_gdn_fused_table16f), which was 5.2% of a one-lane MLX 4-bitdecode cycle on my M2 Max. With it, the kernel takes 35% less time at one lane, and decode is 1.2–2.5% faster on
all four models I tried (MLX 4-bit, Q4_0, UD-Q4_K_M, UD-Q5_K_M). The bits are unchanged. As before, I used Claude Code to author it and tested it by hand.
What changed
Only the Table16F instances of
kernels/decode/gdn.metalchange, which only Apple7/8 run (two template flags setfrom
is_same_v<Table, Table16F>). The plain and Table16 kernels and the host code are untouched.it in
GdnConvHistory::at: every load of the four-tap convolution chose between the carried rows, the pending rows'tape and the packed rows. The row a tap reads is the same for all of a simdgroup's channels, so the prologue now
chooses each tap's row once (
GdnConvHistory::row) and loads the channels from it. The arithmetic is unchanged.batch. Lane t now keeps token t's outputs with a select and stores them once per batch.
gdn-decode's replay already compares the Table16F decode with the plain one bit for bit overchanging batches, for all three shapes (27B, vh32 and Flash-Next's), so it compares the new code with the old.
Results (M2 Max, 38-core GPU)
The kernel alone, the 27B's 48 GDN layers in one command with DRAM-cold states, µs per layer at 1 / 2 / 4 lanes,
ABBA order:
decode-profile, MLX 4-bit, 2048-token prompt, ms per cycle at 1 / 2 / 3 / 4 lanes, ABBA, onapple7-m1-1.3.2(on
apple7-m1-1.4the cycle gains are −2.1 / −1.7 / −1.4 / −1.4%):make test-performance-real(ANE share 0.35, MLX first and last), decode in tok/s at1 / 2 / 4 lanes, on
apple7-m1-1.3.2. Prefill doesn't change (14K cold, hit+4K and 2K within 0.2%), and both MLX runsof each build agree within 0.2%.
On
apple7-m1-1.4: MLX 4-bit +2.4 / +1.6 / +1.8%, UD-Q4_K_M +2.0 / +1.9 / +2.0%.Together with the decode norm PR (they touch different files and merge cleanly),
decode-profileonapple7-m1-1.3.2gives a cycle of 81.2 / 140.3 / 192.6 / 257.7 → 77.4 / 136.9 / 187.7 / 252.1 ms(−4.6 / −2.4 / −2.5 / −2.2%), about the sum of the two. I didn't run the full benchmark on both together.
What didn't help
More threadgroups per head (each scanning part of the value rows, prologue duplicated) and 16 scan simdgroups per
threadgroup were both slower, even at one lane: after the prologue fix the scan is bound by each core's throughput,
not by latency. Two and eight rows in flight stay worse than four.
Note on maintenance
To keep Apple9+ untouched, the new code sits behind two flags next to the old code in
gdn_decode_prologueandgdn_decode_scan, so if upstream changes those functions it may conflict there. Both changes are bit exact on everyfamily, so if you prefer a smaller diff, I have an ungated version too (just replaces the code). Your call.
The same two changes would apply to Apple9+'s Table16 and plain kernels, but I can't measure those here.
Testing
gdn-decodepasses on the M2 Max (the Table16F replay against the plain decode, bit for bit), with and withoutMTL_SHADER_VALIDATION.gguf-projectionpasses too.make checkpasses on the M2 Max onapple7-m1-1.3.2. (Onapple7-m1-1.4, the 1.4 port's own test failures,q8-chunked-prefillnot building, thengguf-moe,qwen4-kernels,paged-attention-planandmetal-backend,fail the same way with and without this change.)
make test-performance-realwith MLX 4-bit, Q4_0, UD-Q4_K_M and UD-Q5_K_M before and after onapple7-m1-1.3.2,and with MLX 4-bit and UD-Q4_K_M on
apple7-m1-1.4.