round 12 track c: gemm_f16 2d l2/slc tile swizzling & gemm_hc_up_reg tile + epilogue optimization #
- patch:
/tmp/argon_r12_gemm/argon_r12_gemm.patch(git -C vendor/strata apply --checkexits 0; touchesmetal/kernels/prompt.metalandmetal/src/engine/window.cpponly) - scratch checkout & standalone benchmarks:
/tmp/argon_r12_gemm/strata(build-metal),/tmp/argon_r12_gemm/bench_r12.cpp,/tmp/argon_r12_gemm/bench_swz2.cpp,/tmp/argon_r12_gemm/bench_swz_overhead.cpp,/tmp/argon_r12_gemm/bench_epi_ab.cpp,/tmp/argon_r12_gemm/window.o
1. executive summary & decision #
-
target 1 (
gemm_f162D L2/SLC tile swizzling inmetal/kernels/dense_mm.metal): REJECTED (0 changes todense_mm.metal).- All tested 2D threadgroup swizzle mappings (
GROUP_M = 2, 4, 8, 16,GROUP_N = 2, 4, 8, and pure grid transposemy = tg.x, nx = tg.y) are 100% bitwise identical (memcmp == 0) across all 12 production(N, K)shapes and allM = 256, 512, 1024, 1983, 2048. - However, on Apple M5 (
64_128_4,128_64_4,32_128_2selected in Round 11), the hardware threadgroup walker already schedules 2D(tg.x, tg.y)grids across the 10 GPU cores with strong L2/SLC locality. Any software swizzle either ties within< 0.2%noise (GN4,GN8,GM2atM = 2048) or severely degrades M5's hardware L2/SLC prefetching on64_128_4atM = 1983, 2048(e.g., pure transpose slows the 12-shape chunk from978.2 msto1,966.1 ms,GM16slows it to1,302.6 ms,GM8slows it to1,013.5 ms). - Even when compiled as zero-argument compile-time template constants (
bench_swz_overhead), no swizzle variant delivers a repeatable speedup acrossM = 256..2048. Following the falsifier rule ("Keep ONLY what delivers a clean, repeatable speedup across the production shapes"),dense_mm.metalis left untouched.
- All tested 2D threadgroup swizzle mappings (
-
target 2 (
gemm_hc_up_regtile shape & epilogue optimization inmetal/kernels/prompt.metal+metal/src/engine/window.cpp): SHIPPED (+6.9%to+7.8%faster, saving7.3–7.9 ms/chunkatT = 1983..2048).- Epilogue Unroll & Pointer Hoisting (
prompt.metal): Added#pragma clang loop unroll(full)to the outer cooperative-tensor loop (i += 4) and inner 4-stream reduction loop (c = 0..3), hoisteddevice const half* xnh_t = xnh + ulong(t) * 4 * n + d;andconst ulong out_idx = ulong(t) * n + d;, and replaceds = s / 4.0fwiths *= 0.25f. Isolated paired A/B testing (11interleaved rounds) shows the epilogue optimization alone yields a consistent 1.008x–1.019x (~1.0–1.9%) speedup across every tile (64_64_4,64_128_4,32_128_4) and everyT, while remaining 100% bitwise identical (memcmp == 0) on bothmixh(fp16) andmixed(fp32). - Tile Shape Sweep (
32_128_4across allTon M5k4): Sweeping 10 register tile instantiations (64_64_4,64_128_4,128_64_4,64_32_4,32_128_2,32_128_4,64_128_2,32_256_4,64_256_4,32_64_2,64_64_2,16_128_4,16_128_2,32_64_4,16_64_2) revealed thatgemm_hc_up_reg_32_128_4(TM = 32, TN = 128, SG = 4,0bytes threadgroup memory) is the fastest tile across every singleTfrom1to2048:- Why
32_128_4wins: HalvingTMfrom64to32while keepingTN = 128, SG = 4cuts each thread's cooperative-tensor register footprint in half (acc.get_capacity()drops from64floats to32floats per lane =8outputs x4streams). Lower register pressure boosts M5 occupancy while keeping 4 simdgroups cooperating across theK = 320reduction and 32 coalesced output columns per threadgroup. By contrast,SG = 2variants (32_128_2,64_128_2) increase per-lane accumulator capacity or halve K-reduction parallelism. - Furthermore, at
T = 1983,TM = 64pads to31 * 64 = 1984(1wasted row), whereas at common non-multiple-of-64 lengthsTM = 32also reduces tail padding.
- Why
- Combined (
gemm_hc_up_reg_32_128_4+ unrolled/hoisted epilogue), the 96-call per-chunk time drops from112.39 ms -> 105.10 ms(1.069x,-7.29 ms/chunk) atT = 1983and116.52 ms -> 108.76 ms(1.071x,-7.76 ms/chunk) atT = 2048.
- Epilogue Unroll & Pointer Hoisting (
2. target 1 evidence: gemm_f16 2d l2/slc tile swizzling #
2.1 tested swizzle mappings (bench_swz_overhead.cpp & bench_swz2.cpp) #
All variants were compiled as zero-overhead template specializations (orig, GM4, GM8, GM16, GN4, GN8, Transp) and verified against orig (memcmp == 0 on 0xFF-poisoned output buffer Y across all 12 production shapes at M = 256, 512, 1024, 1983, 2048, fails = 0).
2.2 paired 12-shape chunk totals on quiet M5 (7 interleaved rounds per shape) #
M |
orig (ms/chunk) |
GM4 (ms/chunk) |
GM8 (ms/chunk) |
GM16 (ms/chunk) |
GN4 (ms/chunk) |
GN8 (ms/chunk) |
Transp (M-major, ms/chunk) |
Bitwise memcmp == 0 |
|---|---|---|---|---|---|---|---|---|
256 |
192.05 | 192.40 | 192.63 | 192.90 | 191.97 | 192.76 | 192.13 | PASS (0 fails) |
512 |
251.60 | 250.60 | 253.35 | 250.84 | 249.88 | 251.14 | 252.23 | PASS (0 fails) |
1024 |
512.02 | 515.03 | 523.92 | 511.98 | 510.58 | 512.69 | 512.60 | PASS (0 fails) |
1983 |
978.22 | 981.79 | 1013.48 | 1302.59 | 976.02 | 976.60 | 1966.07 | PASS (0 fails) |
2048 |
1011.13 | 1004.93 | 1024.29 | 1652.16 | 1011.77 | 1012.36 | 1856.75 | PASS (0 fails) |
2.3 per-shape breakdown at M = 1983 (us/call, median of 7 interleaved rounds) #
| Shape | N x K |
Calls | Tile | orig (us) |
GM4 (us) |
GM8 (us) |
GM16 (us) |
GN4 (us) |
GN8 (us) |
Transp (us) |
|---|---|---|---|---|---|---|---|---|---|---|
hc_read |
324 x 10240 |
96 | 32_128_2 |
1164.0 | 1160.6 | 1172.6 | 1170.2 | 1161.6 | 1164.5 | 1161.9 |
Wqkv |
10240 x 2560 |
37 | 64_128_4 |
7200.0 | 7223.1 | 7446.8 | 8167.7 | 7191.7 | 7198.1 | 17324.6 |
Wz |
6144 x 2560 |
36 | 64_128_4 |
4329.4 | 4345.2 | 4444.9 | 4874.5 | 4329.7 | 4325.7 | 9248.7 |
Wout |
2560 x 6144 |
48 | 64_128_4 |
4450.2 | 4439.6 | 4753.0 | 9708.2 | 4408.6 | 4414.2 | 10608.4 |
Wq |
12288 x 2560 |
12 | 64_128_4 |
8627.3 | 8682.5 | 8913.1 | 9759.4 | 8638.0 | 8635.9 | 20167.9 |
Wpv |
2560 x 2560 |
1 | 64_128_4 |
1806.3 | 1821.4 | 1897.5 | 2640.2 | 1807.1 | 1817.4 | 2226.2 |
Wshgu |
640 x 2560 |
96 | 64_128_4 |
468.2 | 478.8 | 487.8 | 479.6 | 467.2 | 467.6 | 474.5 |
Wshd |
2560 x 640 |
48 | 64_128_4 |
458.6 | 460.8 | 462.4 | 460.9 | 458.6 | 460.0 | 460.7 |
W512 |
512 x 2560 |
119 | 32_128_2 |
401.7 | 410.8 | 409.6 | 404.3 | 403.7 | 402.7 | 410.5 |
Wik |
128 x 2560 |
12 | 32_64_2 |
105.0 | 106.0 | 106.3 | 104.3 | 105.5 | 104.2 | 105.7 |
Walbe |
48 x 2560 |
72 | 64_32_2 |
80.9 | 82.0 | 81.8 | 81.4 | 81.4 | 81.3 | 82.3 |
Wsg |
1 x 2560 |
48 | 64_32_2 |
71.4 | 71.5 | 71.6 | 71.5 | 71.4 | 71.4 | 71.3 |
Analysis: On M5, gemm_f16_64_128_4 reads 64 rows of A (320 KB at K = 2560) and 128 rows of B (640 KB at K = 2560) per threadgroup. When Metal dispatches (tg.x, tg.y) in its native order, simultaneously active threadgroups across the 10 GPU cores share the 320 KB mA slice in L2 while streaming consecutive mB slices sequentially. Remapping tg into M-major strips (GM >= 8 or Transp) forces concurrent threadgroups to read disjoint mA rows (31 * 320 KB = 9.9 MB) while hammering the same mB slice, thrashing L2 and causing up to a 2.4x slowdown on Wqkv/Wout/Wq.
3. target 2 evidence: gemm_hc_up_reg tile sweep & epilogue optimization #
3.1 full tile + swizzle sweep (bench_r12.cpp & bench_hcup2.cpp, 96 calls/chunk, unbound mixed) #
All candidates verified memcmp == 0 on both mixh (fp16) and mixed (fp32, when bound) across T = 1, 4, 7, 8, 16, 24, 32, 48, 64, 128, 256, 384, 512, 768, 1024, 1536, 1983, 2048 (0 failures).
Candidate (N = 10240, K = 320, 96 calls) |
T = 256 (ms) |
T = 512 (ms) |
T = 1024 (ms) |
T = 1983 (ms) |
T = 2048 (ms) |
|---|---|---|---|---|---|
gemm_hc_up_reg_64_64_4 (old T < 1024) |
15.55 | 30.22 | 60.84 | 116.95 | 125.29 |
gemm_hc_up_reg_64_128_4 (old T >= 1024) |
16.39 | 30.61 | 58.92 | 113.47 | 123.54 |
gemm_hc_up_reg_32_128_2 (opt epilogue) |
17.12 | 31.78 | 61.36 | 117.49 | 126.48 |
gemm_hc_up_reg_32_128_4 (opt epilogue, SHIPPED) |
14.82 | 28.41 | 55.22 | 106.65 | 115.31 |
gemm_hc_up_reg_64_128_2 (opt epilogue) |
32.80 | 67.52 | 134.08 | 258.16 | 267.07 |
gemm_hc_up_reg_32_256_4 (opt epilogue) |
17.09 | 31.05 | 59.58 | 114.06 | 123.90 |
gemm_hc_up_reg_64_256_4 (opt epilogue) |
35.40 | 71.75 | 140.89 | 266.12 | 277.68 |
gemm_hc_up_reg_32_64_2 (opt epilogue) |
15.93 | 31.29 | 62.00 | 120.51 | 125.26 |
gemm_hc_up_reg_64_64_2 (opt epilogue) |
19.27 | 36.55 | 70.58 | 135.13 | 149.28 |
gemm_hc_up_reg_16_128_4 (opt epilogue) |
16.95 | 33.08 | 62.82 | 118.92 | 122.81 |
gemm_hc_up_reg_32_64_4 (opt epilogue) |
17.48 | 34.72 | 71.65 | 138.51 | 143.11 |
3.2 isolated paired A/B: baseline epilogue vs unrolled/hoisted epilogue (bench_epi_ab.cpp, 11 interleaved rounds, 96 calls/chunk) #
T |
mixed |
64_64_4 base -> opt (ms) |
64_128_4 base -> opt (ms) |
32_128_4 base -> opt (ms) |
Old Shipped -> New Shipped (opt_32_128_4) |
Saved (ms/chunk) |
|---|---|---|---|---|---|---|
16 |
unbound | 4.67 -> 4.64 (1.006x) |
5.39 -> 5.42 (0.994x) |
4.29 -> 4.29 (1.000x) |
4.67 -> 4.29 ms (1.089x) |
-0.38 ms |
64 |
unbound | 5.18 -> 5.16 (1.004x) |
6.19 -> 6.16 (1.005x) |
5.01 -> 4.99 (1.004x) |
5.18 -> 4.99 ms (1.039x) |
-0.19 ms |
256 |
unbound | 15.40 -> 15.28 (1.008x) |
16.11 -> 15.91 (1.013x) |
14.60 -> 14.46 (1.009x) |
15.40 -> 14.46 ms (1.065x) |
-0.94 ms |
256 |
bound | 16.17 -> 16.02 (1.009x) |
16.71 -> 16.59 (1.007x) |
15.24 -> 15.09 (1.010x) |
16.17 -> 15.09 ms (1.072x) |
-1.08 ms |
512 |
unbound | 30.09 -> 29.84 (1.008x) |
30.34 -> 30.03 (1.011x) |
28.44 -> 28.15 (1.010x) |
30.09 -> 28.15 ms (1.069x) |
-1.94 ms |
512 |
bound | 31.45 -> 31.15 (1.010x) |
31.27 -> 30.98 (1.009x) |
29.52 -> 29.18 (1.011x) |
31.45 -> 29.18 ms (1.078x) |
-2.27 ms |
1024 |
unbound | 60.22 -> 59.46 (1.013x) |
58.59 -> 57.94 (1.011x) |
55.36 -> 54.79 (1.010x) |
58.59 -> 54.79 ms (1.069x) |
-3.80 ms |
1024 |
bound | 62.31 -> 61.73 (1.009x) |
60.11 -> 59.46 (1.011x) |
56.97 -> 56.41 (1.010x) |
60.11 -> 56.41 ms (1.066x) |
-3.70 ms |
1983 |
unbound | 117.70 -> 115.56 (1.019x) |
112.39 -> 111.05 (1.012x) |
106.18 -> 105.10 (1.010x) |
112.39 -> 105.10 ms (1.069x) |
-7.29 ms |
1983 |
bound | 120.43 -> 118.95 (1.012x) |
115.50 -> 114.34 (1.010x) |
109.08 -> 108.00 (1.010x) |
115.50 -> 108.00 ms (1.069x) |
-7.50 ms |
2048 |
unbound | 120.95 -> 119.88 (1.009x) |
116.52 -> 115.16 (1.012x) |
109.82 -> 108.76 (1.010x) |
116.52 -> 108.76 ms (1.071x) |
-7.76 ms |
2048 |
bound | 125.55 -> 124.51 (1.008x) |
119.80 -> 118.50 (1.011x) |
112.86 -> 111.80 (1.009x) |
119.80 -> 111.80 ms (1.072x) |
-8.00 ms |
4. verification & parity results #
dense_mm_parity:/tmp/argon_r12_gemm/strata/build-metal/dense_mm_parity: ALL PASS (3.05s, all 13 quant formats x 6 shapes,memcmp(wh)=0, memcmp(xh)=0)./tmp/argon_r12_gemm/strata/build-metal/dense_mm_parity --tiles M(M = 256, 512, 1024) and--ab M(M = 1983, 2048): ALL BITWISE (< 3sper invocation).
comb_fold_parity:/tmp/argon_r12_gemm/strata/build-metal/comb_fold_parity 64: ALL PASS (4.67s,T = 1, 7, 64)./tmp/argon_r12_gemm/strata/build-metal/comb_fold_parity T hcup confirm(T = 64, 512, 1024, 1983, 2048): ALL PASS (< 2.1sper invocation,tgmem = 0on allregkernels).
- Engine compilation & patch check:
window.cppcompiled to/tmp/argon_r12_gemm/window.ousingstrata_metal_kernelsrelease flags: exit 0 (0warnings/errors).git -C vendor/strata apply --check /tmp/argon_r12_gemm/argon_r12_gemm.patch: exit 0.