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) #

  1. 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).
  2. "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).
  3. 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 uniform etok[t] reload.
  4. dn_body Q2_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.
  5. swiglu_prep via simd_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.)
  6. moe_route unroll pragmas — 0 % (ntok 1: +0.6 %, ntok 8: +0.4 %): the compiler already unrolls these. Replacing v[ex / 32] = -INFINITY by 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_parity PASS (0 mismatches), fuse_parity ALL PASS (gr_norm_comb bitwise), prompt_parity ALL 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.