Round 4 Track C: Prefill MoE Plan (mm_clear + mm_hist + mm_scan + mm_place) & Epilogue / Gather Audit #
- Patch:
/tmp/argon_prefill_plan/argon_prefill_plan.patch(verified clean withgit -C vendor/strata apply --check) - Target Device: Apple M5 (10-core GPU, 24 GB Unified Memory, Metal 4
matmul2dK4 right-input layout)
1. Executive Summary #
We audited and benchmarked the MoE prefill plan pipeline (mm_clear, mm_hist, mm_scan, mm_place, mm_gather) in metal/kernels/moe_mm.metal and all elementwise epilogue / conversion kernels (swiglu_h, act_f16, moe_out, gr_epi_down, gr_norm_h, mm_combine) in Window::prompt_chunk (metal/src/engine/window.cpp & prompt_chain.inc).
Key Findings & Optimizations Implemented #
-
100% Elimination of
mm_clearvia Exact Padding-Row Initialization inmm_scan:mm_histinitializesthreadgroup atomic_uint h[1024]to0and unconditionally writes every elementbcnt[tg.x * a.n_expert + e](e < a.n_expert), sobcntnever needed external clearing.mm_clearwas dispatching(p.maxrows + 255) / 256threadgroups (36,864ints forC = 2048) on every layer solely to initializep.stokto-1for the padded tail rows[r0 + tot, r0 + rows)of each expert thatmm_placedoes not touch.- In
mm_scan(1 threadgroup ofn_expert = 512threads,e = tid), each threadealready computesr0,tot, androws = nt * a.tm. Having threadewritesrt_tok[r0 + r] = -1forr = tot .. rows - 1(0..15ints per expert) completely eliminates themm_cleardispatch across all 48 MoE layers.
-
Discovery & Fix of the Real Plan Bottleneck: $O(1024^2)$ Serial Threadgroup Scan in
mm_place(5.3x–6.0x Plan Speedup):- Profiling individual plan kernels revealed that
mm_hist(1.1–1.5 us) andmm_scan(2.9–4.8 us) were tiny, whereasmm_placealone took50.4–81.5 us/layer(91% of total plan time)! - Root cause:
for (uint j = 0; j < tid; ++j) rank += s_id[j] == e;inmm_placeforced threads in the last warp (tid = 992..1023) to execute up to1,023serial threadgroup memory loads and comparisons per block. - Fix: Replaced the $O(1024)$ serial loop in
mm_placewith a two-level deterministic 32-lane sub-block nibble-packed counter (p_cnt[4096]) +short4intra-warp scan (ltid = tid & 31, at most 7short4loads per thread). Becausetopk = 10per token guarantees at most $\lceil 32/10 ceil = 4 \le 15$ occurrences of any expert within a 32-entry sub-block, 8 sub-block counts pack into a single 32-bitatomic_uintwithout overflow, and the resultingrank,pos[i],srt_tok[row], andsrt_w[row]are 100% bitwise identical to the original loop. - Result:
mm_placedrops from50.4–81.5 usto3.9–6.2 us(12.9x faster), and the full MoE plan (mm_hist + mm_scan + mm_place) drops from46.9–77.2 us/layerto8.6–13.1 us/layer(5.4x–5.9x faster) inmoe_mm_parity --bench.
- Profiling individual plan kernels revealed that
-
Why Single-Threadgroup
mm_hist + mm_scanFusion Was Rejected:- We also prototyped and measured a single-threadgroup fused
mm_hist_scankernel (1 TG of 1024 threads looping overb = 0 .. n_blocks - 1). Because 1 TG cannot occupy all 10 GPU cores on M5 acrossn_blocks = 10..20,mm_hist_scantook8.27–11.59 usvs5.60 usfor separate multi-TGmm_hist+ 1-TGmm_scan_pad. Keepingmm_histmulti-TG and fusing padding-clear intomm_scanis strictly faster.
- We also prototyped and measured a single-threadgroup fused
-
Halving
mm_gatherMemory Traffic viamm_gather_h(p.mixhFP16 Source +half4Loads/Stores):- In
Window::prompt_chunk(line 3332),gemm_hc_up_64_64_4already outputs both FP32p.mixedand FP16p.mixh(mixh[i] = half(mixed[i])). - Previously,
mm_gatherread FP32p.mixed(4bytes/elem) and converted it tohalfagain (half(x[...])) for every routed token row (24,576 x 2560 = 251.6 MBread +125.8 MBwrite per layer atT = 2048). - Added
mm_gather_h, which reads directly from FP16p.mixhusing vectorized 64-bithalf4loads and stores (125.8 MBread +125.8 MBwrite, bit-for-bit identical tomm_gathersincemixhis alreadyhalf(mixed)). - Result:
mm_gathertime inmoe_mm_parity --benchatT = 1024drops from762.2 usto495.2 us(-267.0 us/layer, 1.54x faster) and atT = 2048drops from2028.3 usto1190.5 us(-837.8 us/layer, 1.70x faster, saving40.2 msacross 48 layers).
- In
-
Audit of
swiglu_h,act_f16, and Other Elementwise Kernels + Barrier Placement:- Audited all barriers in
Window::prompt_chunk:- Removed the redundant
mm_cleardispatch and fixed the barrier flag beforemm_hist: previouslymm_clearhadnb_ = trueandmm_histALSO hadnb_ = true, which only worked whenstream_ && pred_on && l + 1 < 48inserted a barrier inside the read-ahead loop. Nownb_ = stream_ && pred_on && l + 1 < 48;beforemm_hist, guaranteeing a barrier afterroute_topk(p.ids)when read-ahead is off or on layer 47, while overlappingmm_histwith the read-aheadroute_topk(predicted_ids)when read-ahead is active. - Verified
swiglu_h(line 3567): the three shared-expert GEMMs (Wshg -> p.gsh,Wshu -> p.ush,Wsg -> p.sgv) run concurrently in one barrier group (nb_ = trueon lines 3562, 3564);swiglu_hmust wait forp.gshandp.ush, andgemm(p.hh, p.Wshd, p.ys)must wait forp.hh. - Benchmarked
swiglu_h(9.6–68.8 us) vsfloat4-vectorizedswiglu_h_v4(17.2–75.0 us) vs fusedgemm_swiglu_f16(113.6–1294.6 us), andact_f16vsact_f16_v4. On M5, scalar coalesced 256-thread dispatches saturate the memory controller and hardware coalescer slightly better thanfloat4-strided threads (64.8 usvs75.0 usforswiglu_hatT=2048;546.1 usvs558.5 usforact_f16), and fusingWshg+Wshuinto one double-GEMM kernel hurts occupancy (1042 us->1295 usatT=2048). Thusswiglu_handact_f16are already optimal as-is.
- Removed the redundant
- Audited all barriers in
2. Benchmark Results on Apple M5 (moe_mm_parity --bench --v3 --gu IQ2_XS --dn IQ4_NL) #
2.1 Microbenchmark: Isolated Plan Kernels (48-layer run_graph median per layer) #
T |
nblk |
Padded Rows | Baseline mm_clear |
Baseline mm_hist |
Baseline mm_scan |
New mm_scan (with pad clear) |
Baseline mm_place |
New mm_place (32-sub + short4) |
Full Plan Baseline | Full Plan New | Plan Speedup |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 128 | 2 | 7,568 | 4.15 us | 1.56 us | 2.90 us | 4.02 us | 50.46 us | 3.92 us | 54.93 us | 8.95 us | 6.14x |
| 512 | 5 | 8,352 | 0.93 us | 1.13 us | 3.27 us | 4.12 us | 50.82 us | 4.02 us | 55.54 us | 9.18 us | 6.05x |
| 1024 | 10 | 14,256 | 0.88 us | 1.10 us | 3.81 us | 4.69 us | 50.97 us | 4.14 us | 56.29 us | 9.63 us | 5.85x |
| 2048 | 20 | 24,160 | 0.93 us | 1.33 us | 4.77 us | 5.60 us | 81.41 us | 6.19 us | 88.08 us | 13.20 us | 6.67x |
2.2 End-to-End moe_mm_parity --bench (512 Experts, IQ2_XS Gate/Up 16,32,32,4, IQ4_NL Down 16,64,32,2) #
T |
Metric | Baseline (Before) | Optimized (After) | Delta / Speedup |
|---|---|---|---|---|
| 128 | Plan (hist + scan + place) |
46.9 us |
8.7 us |
-38.2 us (5.39x faster) |
| 128 | Gather (mm_gather -> mm_gather_h) |
236.7 us |
235.3 us |
-1.4 us |
| 128 | Total per layer (plan + GEMMs + combine) |
13,123.8 us |
12,965.0 us |
-158.8 us/layer (-7.6 ms / 48L) |
| 512 | Plan (hist + scan + place) |
47.2 us |
8.6 us |
-38.6 us (5.49x faster) |
| 512 | Gather (mm_gather -> mm_gather_h) |
278.8 us |
274.2 us |
-4.6 us |
| 512 | Total per layer (plan + GEMMs + combine) |
14,689.1 us |
14,603.8 us |
-85.3 us/layer (-4.1 ms / 48L) |
| 1024 | Plan (hist + scan + place) |
48.0 us |
9.7 us |
-38.3 us (4.95x faster) |
| 1024 | Gather (mm_gather -> mm_gather_h) |
762.2 us |
495.2 us |
-267.0 us (1.54x faster) |
| 1024 | Total per layer (plan + GEMMs + combine) |
22,201.9 us |
21,900.3 us |
-301.6 us/layer (-14.5 ms / 48L) |
| 2048 | Plan (hist + scan + place) |
77.2 us |
13.1 us |
-64.1 us (5.89x faster) |
| 2048 | Gather (mm_gather -> mm_gather_h) |
2,028.3 us |
1,190.5 us |
-837.8 us (1.70x faster) |
| 2048 | Total per layer (plan + GEMMs + combine) |
37,210.6 us |
36,216.1 us |
-994.5 us/layer (-47.7 ms / 48L, +2.7% tok/s) |
3. Bitwise & Numerical Parity Verification #
- Bitwise Plan & Gather Parity (
plan_epi_bench):- Tested across
T = 128, 512, 1024, 2048withstokandbcntpre-polluted with0xDEADBEEF/777777and with skipped experts (ids = -1from MoCE tail skipping): std::memcmpacrossmeta[0..10],boff[0..nblk*NE-1],eoff[0..NE-1],tiles[0..nt-1],stok[0..nr-1],sw[0..nr-1],pos[0..nent-1], andXS[0..nr*N-1]: 100% bitwise identical (0byte differences).
- Tested across
moe_mm_parity(--v3 --gu IQ2_XS --dn IQ4_NL --tile 16,32,32,4 --dtile 64,32,2):T = 128:plan ok; y vs fp16-operand reference rel L2 2.02e-05, vs exact float reference 4.93e-04 PASST = 256:plan ok; y vs fp16-operand reference rel L2 1.80e-05, vs exact float reference 4.90e-04 PASS(identical1.80e-05/4.90e-04to baseline)
prompt_parity:ALL PASS(route_topk,HC read as GEMMs,swiglu_h,moe_out)dense_mm_parity:ALL PASSacross all 13 quantization formats (F32,BF16,Q8_0,Q4_0,Q5_0,IQ4_NL,IQ4_XS,Q2_0,IQ3_S,Q3_K,Q4_K,Q5_K,Q6_K).