Round 10 Track C Audit: mm_gather_h 128-Bit uint4 Vectorization + mm_scan/mm_place Plan Optimization #
- Agent:
argon-mm-gather-and-plan(opencode-go/gemini-4-argon-contributor) - Scratch workspace:
/tmp/argon_r10_gather/strata - Unified patch:
/tmp/argon_r10_gather/argon_r10_gather.patch(verified clean withgit -C vendor/strata apply --check) - Files touched in patch:
metal/kernels/moe_mm.metal,metal/tests/moe_mm_parity.cpp(metal/src/engine/window.cppline 3540g.tg[0] = 256kept unchanged; compatible withprompt_chain.incandprompt_chain.cpptoo).
1. mm_gather_h Investigation (half4 vs uint4, Threadgroup Sizing, Unrolling & Multi-Row-per-TG) #
1.1 Roofline & Geometry Analysis on Apple M5 (24 GB Unified Memory, 153.6 GB/s Peak DRAM Bandwidth) #
For N_EMBD = 2560 (5,120 bytes per row = 640 half4s = 320 uint4s):
- At
T = 512(512experts,TM = 16):tot_rows = 8,432->XSwrite footprint is43.17 MBper layer (2.07 GBacross 48 layers) plus gathered reads fromxh(2.62 MB). - At
T = 1024:tot_rows = 14,352->XSwrite footprint is73.48 MBper layer (3.53 GBacross 48 layers) plus gathered reads fromxh(5.24 MB). - At
T = 2048:tot_rows = 24,576->XSwrite footprint is125.83 MBper layer (6.04 GBacross 48 layers) plus scattered reads fromxh(10.49 MB). Because125.83 MBexceeds the M5 System Level Cache (SLC), every layer'sXSwrite and subsequent read stream through DRAM at ~130–153 GB/s (85–99% of hardware DRAM peak).
1.2 Paired 48-Layer Sweep Results (mm_gather_h Variants) #
Measured via paired alternating 48-dispatch command buffers (7–9 pairs after GPU warmup) on Apple M5:
| Variant | Vector Type | tg[0] |
Grid (T=2048) |
T=512 (48L ms / us/L) |
T=1024 (48L ms / us/L) |
T=2048 (48L ms / us/L) |
XS memcmp |
|---|---|---|---|---|---|---|---|
Baseline (half4, fixed stride 256) |
half4 (64-bit) |
256 | 24,576 | 13.153 ms (274.0 us) | 23.455 ms (488.6 us) | 55.543 ms (1157.1 us) | Ref |
half4_tg32 (nth stride) |
half4 (64-bit) |
32 | 24,576 | 13.555 ms (282.4 us) | 24.314 ms (506.5 us) | 56.517 ms (1177.4 us) | PASS (0) |
half4_tg64 (nth stride) |
half4 (64-bit) |
64 | 24,576 | 13.446 ms (280.1 us) | 23.932 ms (498.6 us) | 56.172 ms (1170.3 us) | PASS (0) |
half4_tg128 (nth stride) |
half4 (64-bit) |
128 | 24,576 | 13.307 ms (277.2 us) | 23.611 ms (491.9 us) | 55.931 ms (1165.2 us) | PASS (0) |
uint4_tg32 (nth stride) |
uint4 (128-bit) |
32 | 24,576 | 13.265 ms (276.4 us) | 23.372 ms (486.9 us) | 56.978 ms (1187.1 us) | PASS (0) |
uint4_tg64 (nth stride, 5 iters) |
uint4 (128-bit) |
64 | 24,576 | 13.080 ms (272.5 us) | 23.038 ms (480.0 us) | 56.063 ms (1168.0 us) | PASS (0) |
uint4_u64_tg64 (unrolled 5x uint4) |
uint4 (128-bit) |
64 | 24,576 | 13.212 ms (275.2 us) | 23.061 ms (480.4 us) | 56.177 ms (1170.3 us) | PASS (0) |
uint4_tg128 (nth stride) |
uint4 (128-bit) |
128 | 24,576 | 13.022 ms (271.3 us) | 22.979 ms (478.7 us) | 55.820 ms (1162.9 us) | PASS (0) |
uint4_tg256 (nth stride, selected) |
uint4 (128-bit) |
256 | 24,576 | 12.992 ms (270.7 us) | 22.955 ms (478.2 us) | 55.508 ms (1156.4 us) | PASS (0) |
mr64_R2_tg128 (2 rows/TG, 64 thr/row) |
uint4 (128-bit) |
128 | 12,288 | 13.104 ms (273.0 us) | 23.156 ms (482.4 us) | 56.628 ms (1179.8 us) | PASS (0) |
mr64_R4_tg256 (4 rows/TG, 64 thr/row) |
uint4 (128-bit) |
256 | 6,144 | 13.080 ms (272.5 us) | 23.167 ms (482.6 us) | 56.563 ms (1178.4 us) | PASS (0) |
mr32_R4_tg128 (4 rows/TG, 32 thr/row) |
uint4 (128-bit) |
128 | 6,144 | 13.189 ms (274.8 us) | 23.393 ms (487.4 us) | 57.298 ms (1193.7 us) | PASS (0) |
1.3 Key Findings for mm_gather_h #
- 128-bit
uint4copies beat 64-bithalf4copies across all threadgroup sizes (32, 64, 128, 256) atT=512(-0.16 ms/-1.2%) andT=1024(-0.50 ms/-2.1%), and tie/edge out baseline atT=2048(55.508 msvs55.543 ms), because 128-bit loads/stores halve the number of loop iterations (320uint4s vs640half4s per row). - Why
tg[0] = 256withuint4beatstg[0] = 64atT = 2048(55.51 msvs56.06 ms):- With
uint4andtg[0] = 256, iteration 0 issues256 * 16B = 4,096B(32cache lines of128B) of the5,120Brow concurrently across 8 simdgroups, and iteration 1 issues the remaining64 * 16B = 1,024B(8cache lines) on the first 2 simdgroups (tid < 64). Because64is an exact multiple of the 32-wide SIMD width, simdgroups2..7exit cleanly at the SIMD level without intra-warp divergence, while launching 8 simdgroups per row maximizes memory-level parallelism (MLP) whenxh(10.49 MBatT=2048) spills out of L1/L2 into SLC/DRAM. - Making
mm_gather_htakeushort nth [[threads_per_threadgroup]](with scalaruint tg [[threadgroup_position_in_grid]]per MSL scalar/vector attribute rules) supports anytg[0] = 32, 64, 128, 256while keepingg.tg[0] = 256inwindow.cpp,prompt_chain.inc, andprompt_chain.cpp.
- With
2. Target 2 Investigation: mm_scan and mm_place Plan Kernels #
We also inspected and benchmarked mm_hist, mm_scan, and mm_place in metal/kernels/moe_mm.metal:
mm_scanbarrier + dead-slot elimination:- Previously,
mm_scanzeroeds_rows[0..31]ands_tiles[0..31], executed an extrathreadgroup_barrier(mem_flags::mem_threadgroup), and then loopedfor (int i = 0; i < 32; ++i)after the prefix sum barrier because 512 threads only populate16of the32simdgroup slots. - By taking
ushort nsg [[simdgroups_per_threadgroup]]and loopingfor (uint i = 0; i < nsg; ++i), we eliminate both the initial zero-fill and the firstthreadgroup_barrier(cuttingmm_scan's barriers in half from 2 to 1).
- Previously,
mm_placethreadgroup-memory & barrier reduction (s_id[1024]-> registersimd_broadcast):- Previously,
mm_placeallocatedthreadgroup short s_id[1024](2 KiB), wrotes_id[tid] = short(e)after zeroingp_cnt, and after the second barrier did a threadgroup-memoryshort4scan over the caller's 32-lane sub-block. - Because each 32-lane sub-block (
sub = tid >> 5) is an exact 32-lane Metal simdgroup, each lane can compute its intra-simdgroup rank directly in registers viasimd_broadcast(e, l)while waiting for thep_cntatomic updates, eliminatingthreadgroup short s_id[1024]completely.
- Previously,
2.1 Paired 48-Layer Plan Timing Results (mm_hist + mm_scan + mm_place) #
| Kernel | T=512 Before -> After (48L) |
T=1024 Before -> After (48L) |
T=2048 Before -> After (48L) |
Bitwise memcmp (bcnt, boff, eoff, tiles, meta, stok, sw, pos) |
|---|---|---|---|---|
mm_hist |
0.111 ms -> 0.109 ms | 0.049 ms -> 0.048 ms | 0.060 ms -> 0.052 ms | PASS (0) |
mm_scan |
0.354 ms -> 0.322 ms (-9.0%) |
0.214 ms -> 0.200 ms (-6.5%) |
0.265 ms -> 0.250 ms (-5.7%) |
PASS (0) |
mm_place |
0.309 ms -> 0.281 ms (-9.1%) |
0.192 ms -> 0.165 ms (-14.1%) |
0.290 ms -> 0.271 ms (-6.6%) |
PASS (0) |
| Full Plan (48L) | 1.663 ms -> 1.160 ms | 0.445 ms -> 0.408 ms (-8.3%) |
0.611 ms -> 0.573 ms (-6.2%) |
PASS (0) |
3. Verification (memcmp == 0 & moe_mm_parity) #
- Added fast (
1.40stotal runtime)--gather-benchmode tometal/tests/moe_mm_parity.cpp(using#ifdef MM_GATHER_ONLYto skip compiling the heavy GEMM template matrix when testing/benchmarking plan + gather):- Verifies 100% bitwise identity (
memcmp == 0) onXS(across alltg[0] in {32, 64, 128, 256}againstmm_gather) and exact plan invariants acrossT = 1, 7, 64, 256, 512, 1024, 2048. - Benchmarks 48-layer command buffers for both
planandmm_gather_hacrossT = 512, 1024, 2048.
- Verifies 100% bitwise identity (
- Full end-to-end
moe_mm_parity --v3 --tile 16,32,32,4 --dtile 64,32,2verifiedPASS(rel L2 1.11e-05atT=1,3.20e-06atT=7,2.65e-06atT=64).