opus_r18_moe — Round 18 Track A: moe_gu / moe_down / moe_route NE>1 reuse, xprep float4, swiglu_prep, combines #
Patch: /tmp/opus_r18_moe/opus_r18_moe.patch (185 lines; git -C vendor/strata apply --check = OK).
Touches only metal/kernels/{moe,units,glue}.metal and metal/tests/expert_parity.cpp.
A/B harness (scratch, not in the patch since it would be a new file): /tmp/opus_r18_moe/moe_ab.cpp
(compiles the untouched kernels from /tmp/opus_r18_moe/base_kernels as A and the scratch tree as B in one process,
memcmp of every output buffer, paired alternating timing, median of 11 B/A ratios). Raw logs:
/tmp/opus_r18_moe/ab_logs.txt, parity log /tmp/opus_r18_moe/parity_log.txt.
Device: Apple M5 (the 24 GB Air), live :8080 server up the whole time, so absolute µs drift between runs; the
paired ratio is the number to trust. gu/dn bench: 2560 x 640, one 512-expert arena, 48 windows, each window's NE
tokens routed to the same 10 experts (so every group has exactly NE entries).
what landed (all bitwise: memcmp == 0 on every A/B run) #
| change | result |
|---|---|
xprep float4 loads/stores + unroll (units.metal) |
-41.5 % .. -46.8 % (n 2560 ntok 8: 19.8 → 10.5 µs; ntok 1 -41.5 %; n 640 ntok 80 -44.4 %; n 5120 -45.9 %) |
moe_combine_sh topk==10 unrolled (glue.metal) |
-32.0 % ntok 8 (17.6 → 12.0 µs), -45.1 % ntok 1 |
moe_combine topk==10 unrolled (moe.metal) |
-33.3 % ntok 8, -48.7 % ntok 1 |
gu_body fused gate+up per row, one x load per entry — only for UNIT==32 formats at NE≥3, IQ4_XS from NE 2 (gu_fused<F,NE>) |
Q6_K 32,1,4: NE3 -18.5 %, NE4 -20.5 %; Q4_K -17.6/-18.6 %; Q5_K -15/-18 %; Q8_0 -9/-19 %; IQ4_XS NE2/3/4 -31.7/-33.2/-35.8 %; NR=2 tiles up to -25 % |
| production formats IQ2_XS gate/up + Q2_0R down | unchanged code path (A/B ±0.4 %, noise) |
gr_norm_comb (gr2.metal) stays bitwise with moe_combine_sh: the unrolled sum keeps k order. fuse_parity confirms.
what was tested and rejected (numbers so nobody re-tries them) #
- gu NE>1 fusion for the 16-weight units (IQ2_XS, IQ3_S, Q2_0) — the brief's main target. Regresses where it matters: IQ2_XS 32,1,4 NE3 +17.0 %, 16,1,4 NE3 +21.4 %, NE4 +4..+10 %; IQ3_S 32,1,4 NE3 +14.7 %, 16,1,4 NE3 +17.3 %. Holding two decoded units live costs more than the second x load (which is an L1 hit of 4 float4s). Only NR=2 tiles gain (IQ3_S 32,2,4 NE4 -9 %, 16,2,4 NE4 -14.5 %; IQ2_XS ≈ ±2 %; Q2_0 NR2 +4 %), and no M5 production tile uses gu NR=2 (window.cpp: gr = 1; gl = 16 for IQ2_XS/IQ3_S at ntok ≥ 3).
- "decode every row first" (
qg[NR][UNIT], qu[NR][UNIT]) — catastrophic: IQ2_XS/IQ3_S NR=2 +37..+86 %, Q6_K NR=2 +383..+623 % (spills). - Hoisting
tok[NE]/ x row pointers before the u loop — neutral to worse: IQ2_XS +2..+5 %, IQ3_S 16,1,4 NE3/4 +6 %. 2*NE live 64-bit pointers cost more than the uniformetok[t]reload. dn_bodyQ2_0/Q2_0R on the shared-decode path (q[NR][UNIT]) —moe_down_142_8_2_4: NE2 +5.0 %, NE3 +11.0 %, NE4 +5.8 %; 4,2,4: +7.7/+14.2/+12.1 %; 8,1,4 = 0 (NR 1 is the same code). The existing exclusion is right; the patch only adds a one-line comment saying so.ew[NE]hoist: it is only read in the epilogue, nothing to hoist.swiglu_prepviasimd_shuffle_down(0 B threadgroup memory, no barrier, bitwise) — +2.0..+2.7 % at ntok 8 (n 640 and 5120), -0.9 % at ntok 1, ±0 at ntok 4 (two repeats each). 16 shuffles on every lane cost more than one lane's 16 threadgroup reads after a barrier in a one-simdgroup threadgroup. Not landed. (The real lever is the 16-thread threadgroup itself — half a simdgroup — but that is a host change in window.cpp, out of scope.)moe_routeunroll pragmas — 0 % (ntok 1: +0.6 %, ntok 8: +0.4 %): the compiler already unrolls these. Replacingv[ex / 32] = -INFINITYby a 16-way select to keep v in registers — +19 % (ntok 8) .. +56 % (ntok 1). Neither landed; moe_route is untouched.
verification #
- A/B memcmp == 0 for every run above (gu 11 formats x tiles x NE 1..4, dn 142 x 3 tiles x NE 1..4, xprep x4 shapes, swiglu x3, combine/combine_sh ntok 1/8, route ids/w/plan ntok 1/4/8).
expert_parity(vs double CPU ref): 160/160 PASS — Q6_K, Q4_K, Q5_K, Q8_0, IQ4_XS, IQ2_XS, IQ3_S, Q2_0 gate/up x ntok 1,2,3,4,8 x {random 0.3-share routing, new--same} x gu tiles {32,1,4, 32,2,4}; plus IQ2_XS/Q2_0--dn-prep.--same(new) routes every token to the same 10 experts so groups have exactly ntok entries and the NE 2/3/4/8 bodies (incl.gu_fused) are hit deterministically.route_parityPASS (0 mismatches),fuse_parityALL PASS (gr_norm_comb bitwise),prompt_parityALL PASS.- No :8080 traffic, no strata_window_cli runs.
expected effect on the live model #
Our K512 model is IQ2_XS gate/up + Q2_0 (142) down: its gu/dn kernels are unchanged. What it gets is xprep
(~9 µs a call saved at ntok 8 — xprep runs several times a layer) and moe_combine_sh (~5.6 µs a layer at ntok 8,
if it is not folded into gr_norm_comb; the folded gr_norm_comb path is a different kernel in gr2.metal that this
round does not own and that likely has the same non-unrolled topk loop — worth a follow-up there). The gu_fused win
lands for the UD-Q4/5/6_K_XL and IQ4_XS packs.