Repository navigation
simd_qmm on Metal: X's group sums and half operands from a prepass, with the tiles' bits (#2089) - #3215
Open
jschmied wants to merge 1 commit into
Open
simd_qmm on Metal: X's group sums and half operands from a prepass, with the tiles' bits (#2089)#3215jschmied wants to merge 1 commit into
jschmied wants to merge 1 commit into
Conversation
…ith the tiles' bits (ashhart#2089) tf_sq_prep writes each row's 64-input group sums by the tiles' own tree, and with them X as half, rows paired. The half tiles (tf_sq_mmh) read both after each norm instead of redoing them in every column threadgroup; prompt rows' chunk pairs (tf_sq_mmx) read the sums. Each path is checked once per shape against the fp32 tiles.
This branch has not been deployed
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.
What this changes
Before, each column threadgroup of simd_qmm's prompt tiles redid X's 64-input group sums (for the affine bias) and, on the half path, X's conversion to half. Both depend only on X, not on the column. Now a prepass,
tf_sq_prep, writes them once per call. Each row's group sums use the tiles' own tree (((c0+c1)+(c2+c3))+((c4+c5)+(c6+c7)), each c a left-to-right fma sum of 8), so they're the same floats. With the half variant, it also writes X as half, rows interleaved in pairs, so a tile reads twouint4per row and step instead of eight floats.tf_sq_mmh(the half tiles, n past 6,144) reads both. The prepass runs once after each norm, besidetf_sq_fits. The fp32 fallback, for tiles whose inputs don't fit half, still reads X itself.tf_sq_mmxistf_sq_mmp(the paired-chunk tiles) reading the sums. The paired body is one inline function shared by both kernels. Prompt rows of 64 or more take it after a per-shape check (summedExact, run once per shape on the chip) has shown its bits equal the fp32 tiles'.The bits are main's. Prompt prefill is faster.
Issue: Refs Qwen3.8-27B on M1-M4: prompt GEMM behind MLX's rate, and plain decode behind 0.6.6 #2089 (the bit-equal prepass: v72 for mmh, v8 for mmp, in the bench series).
Stacked on: main (223da76). Commit: b3692aa. CHANGELOG line, for whoever merges:
- simd_qmm on Metal: a prepass writes X's group sums and half operands once per call; prompt prefill on the 27B is about 4 % faster on chips without tensor units, with the same bits (#2089).Overlap (checked with
ghon all open PRs, thengit merge-treeagainst this head): 12 open PRs touchzig/src/families/qwen27/quant_gpu.zig(train 1113121437: #2851 #2765 #2883 #2889 #2891 #2892 #2878 (CI only) #3202, qwen27 Metal (M5): fused norm and RoPE write K and V into the cache and Q into qa directly (#2940 step 2b) #2960, qwen27 Metal: one q27_qk_norm_rope dispatch for Q and K head norm and RoPE (#2940 step 2a) #2955, qwen27 Metal: K and V read in place from the mixed projection, no unstack copies (#2940 step 1) #2945, qwen27 Metal (M5): out_proj group sums written by the GDN post and attention merge kernels (#2931) #2944, qwen27 Metal (M1-M4): projections no longer barrier internally, and XS sums compile out before tensor units (#2900) #2913, Metal M5: prompt lane projections at every width take a chunk's 16-row blocks in one dispatch #2851, qwen27 metal: a prompt chunk's last layer runs q, o, the MLP and the final norm over the read row's 128-row block #2826, qwen27 metal: qk projections, flat glue and Metal 4 verify passes, each bit-identical and switchable #2342, Metal: serve the dense Qwen3.8-27B straight from a GGUF file (Q4_K_M, Q8_0) (#1920) #2078, Implement GLP control vectors with a fused Metal kernel for Qwen3.8-27B #1835) or alsoquant_rows_test.zig(Qwen3.5-9B prompt rows in one product in Zig engine #1897). train 1113121437: #2851 #2765 #2883 #2889 #2891 #2892 #2878 (CI only) #3202, Metal M5: prompt lane projections at every width take a chunk's 16-row blocks in one dispatch #2851, qwen27 metal: a prompt chunk's last layer runs q, o, the MLP and the final norm over the read row's 128-row block #2826 and Implement GLP control vectors with a fused Metal kernel for Qwen3.8-27B #1835 merge with this head cleanly; the others conflict, most inquant_gpu.zig. No other open PR touchessimd_qmm.metal,simd_qmm.zigor the two contract record files. Whichever lands second rebases.Checks on this head
python3 tools/zig/lean_check.py, on main 223da76 and on this head: base 341, head 341.lean_new.py: 0 new. The 341 are inherited from main.zig build test --summary all: 844/846 steps succeeded, 13,204/13,308 tests passed, 103 skipped, 1 failed. The failure isdashboard_test("dashboard page polls the public stats snapshot without private details"). It fails the same way on main 223da76, and nothing in it is touched here.zig build test-qwen27: 78/78 steps, 185/185 tests passed.zig build native -Dcpu=apple_m1: 285/285 steps.zig fmt --checkon the changed files: clean.git diff --check 223da7659 HEAD: clean. Every changed file is 535 lines or fewer.tf_sq_prep_hinto its own buffer and stays bit-equal.Receipt
Environment:
iogpu.wired_limit_mb29517.zig build native -Dcpu=apple_m1; served withtensorfold-native serve.Paths and platforms the change reaches, which I ran and which I could not:
!device.tensorUnits().Kernel time,
tf-sq-benchat gate_up's shape, 128 rows (fresh process each, md5 of the output equal to main's):The prepass itself takes about 0.02 ms. Main's fp32 tiles (mma) run 12.69 ms on the same shape.
Exactness:
bench_concurrent.py --prompts long.json --tokens 128 --reps 1 --temperatures 0,0.7 --levels 1,3 --alone --serial, on four distinct ~1.2K-token code prompts, served--parallel 3 --context 2048 --prompt-cache-gib 0with mtp-linked copy drafts:"draft": falseat each level and temperature.--drafter qwen38-27b-dflash2, one stream (the drafter at--parallel 2or more isn't admitted beside the 27B on this Mac):tf-qwen27-reuse(turn 1 of 1,500/5,000 tokens, turn 2 of 2,600/7,003; drafted and plain):bench_cache.py(1,536-token prompt, 3 repeats): the replies are equal across repeats and between builds (token_sha256 91df979a…). It found no cache reuse on this Mac (cached 0) on either build.Main against this branch, prompt speed, same machine and checkpoint. Served prefill at one stream, greedy,
--no-drafts --no-thinking --prompt-cache-gib 0 --context 20480, a fresh server per run, cold, A-B-B-A over two rounds:Per run, prefill s:
Every branch run is faster than every main run, and each round alone gains 3.9-5.2 %. 32K and 64K weren't run, because this Mac doesn't admit them beside the 27B.
Decode speed before and after:
--no-drafts, 256 greedy tokens, a fresh server per run, A-B-B-A over two rounds. Main decodes in 30.48, 31.57, 31.74 and 31.68 s; the branch in 30.91, 31.82, 31.75 and 31.67 s. The medians are 31.62 s and 31.71 s, 8.1 tok/s on each. All 8 replies have token_sha256 c157e66f….bench_concurrent.py --prompts long.json --tokens 256 --reps 1 --temperatures 0 --levels 1,3, served--parallel 3 --context 2048 --prompt-cache-gib 0with mtp-linked copy drafts, a fresh server per run, A-B-B-A over one round. Per-stream decode, main then branch:--alone --serialexactness runs above) read 7.7 against 8.0 tok/s for the branch. The paired runs don't repeat that.Licence of any new weights or drafter, or "n/a": n/a, no new weights.
Kernel contract, only when a GPU kernel is new or changed
tf_sq_mmx(the "metal simd_qmm pairs prepass sums q4 g64" entry) replaces simd_qmm's fp32 tiles and is gated bit-equal to them.tf_sq_prep_sandtf_sq_prep_hare the prepass. The existing half entry now runstf_sq_prep_hbeforetf_sq_mmhand stays bit-equal.tf_sq_mmx_16_4_2_2in simd_pairs_records.zig (256 threads, 16,384 threadgroup bytes; M1 and M2 take 224 threads, which it adapts to). It also recordstf_sq_prep_sandtf_sq_prep_h(32 threads each, 0 bytes; M1 and M2 1,024).SUMMARYline and exit status,tf-kernel-contract --only simd_qmm:SUMMARY 6 kernels: 6 pass gates 1-4, 0 fail, 0 known findings; 2 controls: 2 red as planted, 0 missed; 0 limit problems; gates 45211 ms, limits 64 ms, exit 0.SUMMARY 5 kernels: 5 pass gates 1-4, 0 fail, 0 known findings; 1 controls: 1 red as planted, 0 missed; 0 limit problems, exit 0.zig build kernel-contractrefuses before any gate withUnrecordedRotaryCompileron main as on this head, so that's inherited.Checklist
"draft": false, a resumed prompt equals a fresh one, and concurrent equals solo. Drafted and concurrent:--alone --serialabove, with copy drafts and DFlash2. Resumed: every resumed state is equal to the fresh one; the tool's resume-point check fails on main and on the branch alike under this Mac's memory pressure (above).