Round 29 Track C: Decode moe_route Unrolls + moe_gu / deq<T_IQ2_S / T_IQ2_XXS / T_IQ3_XXS / T_IQ2_XS> Optimization #
1. Summary of Changes #
metal/kernels/quant.metal(lines 145–201):deq<T_IQ2_S>(34 of 48 routed-expert layers ink512-iq2_xs-metal.gguf): Replace the four per-iteration 8-bitucharloads and variable shift(qh << (8 - 2 * l))with two 2-byte-aligned 16-bitushortloads (q2 = *(device const ushort*)(b + 2 + 2 * ui)ands2 = *(device const ushort*)(b + 34 + 2 * ui)), pre-shiftqh = uint(b[66 + ib]) >> (4 * h)once outside the loop, and add#pragma clang loop unroll(full)onl2 = 0, 1.deq<T_IQ2_XXS>(12 of 48 routed-expert layers): Eliminatea0 = rd32(b + 2 + 8 * ib)(twoushortloads + shift + OR + variable shifts by8 * l) in favor of direct byte loadsqs = b + 2 + 4 * ib + 2 * ui(qs[0],qs[1]), pre-shifts0 = a1 >> (14 * h)once before the loop, and add#pragma clang loop unroll(full).deq<T_IQ3_XXS>: Pre-shifts0 = aux >> (14 * h)once before the loop and add#pragma clang loop unroll(full).deq<T_IQ2_XS>: Add#pragma clang loop unroll(full)onl2 = 0, 1.deq<T_IQ3_S>: Kept baseline (unrollingl2across twog_iq3stable lookups per iteration was+0.9%slower onmoe_gu32,1,4due to register pressure).
metal/kernels/moe.metal(moe_routelines 163–334,gu_bodylines 381–410):- Add
e512 = (a.n_expert == 512u)fast path and#pragma clang loop unroll(full)across all 16-element per-lane loops and SIMD xor reduction loops inmoe_route. - Hoist
has_r1,has_moce, andmin_scaled_rankoutside the 10-iterationrankloop, split the!has_mocevshas_mocerank loops, and preload the 16-bit residency maskres_maskfromsmaponce before therankloop whenhas_moceis active (removing 16 conditional device loads from inside the 10-iterationrankloop). - Replace the dynamically-indexed array write
if ((ex & 31) == lane) v[ex / 32] = -INFINITY;with a branchless unrolled select#pragma clang loop unroll(full) for (int i = 0; i < 16; ++i) v[i] = (ex == lane + 32 * i) ? -INFINITY : v[i];outside the divergentif ((ex & 31) == lane)block, keepingv[0..15]in pure scalar registers without dynamic indexing or divergent predication. - In
gu_body, hoistetok[0]and advancexandxs16in place before theuloop whenNE == 1(zero additional live registers; hoistingint tok[NE]or preloadingxs[2][16]forNE >= 2was tested and rejected due to+1..+3%occupancy regression on32,1,4).
- Add
metal/tests/expert_parity.cpp(lines 220–233):- Extend
--abto accept a baseline kernel directory path (compilingquant.metal,units.metal, andmoe.metalfrom that directory) as well as a single.metalfile path.
- Extend
2. Verification & Paired A/B Benchmarks (Apple M5) #
Bitwise Parity #
route_parity: 200 windowsK=512+ 200 windowsK=256 + MoCE=0.85: 0 mismatches (PASS)route_parity --ab: 200 trials (moe_route+moe_plan,split = 0, 1): 0 fails (ALL PASS, 100% bitwise identical)expert_parity --ab:IQ2_S,IQ2_XXS,IQ3_S,IQ2_XS,IQ3_XXS(NE=1..4,8+ random windows): 0 fails (ALL PASS, 100% bitwise identical)gemv_parity: All formats, shapes2560/640/10240,ntok = 1, 3, 8:ALL PASS
Paired A/B Timing: moe_route (route_parity --ab --bench, 192 reps x 9 interleaved rounds) #
| Mode | ntok |
Base (us) | Cand (us) | Ratio | 48-Layer Savings |
|---|---|---|---|---|---|
split=0 (MoCE off) |
1 | 4.00 | 3.49 | 0.8722 (-12.8%) | 0.192 -> 0.167 ms (-0.025 ms) |
split=0 (MoCE off) |
2 | 4.58 | 4.03 | 0.8775 (-12.3%) | 0.220 -> 0.194 ms (-0.026 ms) |
split=0 (MoCE off) |
4 | 6.05 | 5.55 | 0.9174 (-8.3%) | 0.291 -> 0.267 ms (-0.024 ms) |
split=0 (MoCE off) |
8 | 8.80 | 8.41 | 0.9556 (-4.4%) | 0.423 -> 0.404 ms (-0.019 ms) |
split=1 (Streaming + MoCE) |
1 | 5.96 | 4.39 | 0.7363 (-26.4%) | 0.286 -> 0.211 ms (-0.075 ms) |
split=1 (Streaming + MoCE) |
2 | 6.43 | 4.85 | 0.7563 (-24.4%) | 0.309 -> 0.233 ms (-0.076 ms) |
split=1 (Streaming + MoCE) |
4 | 7.62 | 6.05 | 0.7943 (-20.6%) | 0.366 -> 0.290 ms (-0.076 ms) |
split=1 (Streaming + MoCE) |
8 | 10.47 | 9.18 | 0.8763 (-12.4%) | 0.503 -> 0.441 ms (-0.062 ms) |
Paired A/B Timing: moe_gu (expert_parity --ab --bench, QUANT_CTAB=1) #
| Format (Layers in K512) | Tile | NE=1 Ratio |
NE=2 Ratio |
NE=3 Ratio |
NE=4 Ratio |
ntok=1 Ratio |
ntok=2 Ratio |
ntok=4 Ratio |
ntok=8 Ratio |
|---|---|---|---|---|---|---|---|---|---|
IQ2_S (34 layers) |
32,1,4 |
0.9861 (-1.4%) | 0.9684 (-3.2%) | 0.9422 (-5.8%) | 0.9912 (-0.9%) | 0.9879 (-1.2%) | 0.9817 (-1.8%) | 0.9792 (-2.1%) | 0.9756 (-2.4%) |
IQ2_S (34 layers) |
16,1,4 |
0.9852 (-1.5%) | 0.9665 (-3.4%) | 0.9298 (-7.0%) | 0.9832 (-1.7%) | 0.9856 (-1.4%) | 0.9824 (-1.8%) | 0.9768 (-2.3%) | 0.9766 (-2.3%) |
IQ2_XXS (12 layers) |
16,1,4 |
0.9491 (-5.1%) | 0.9409 (-5.9%) | 0.9691 (-3.1%) | 0.9772 (-2.3%) | 0.9480 (-5.2%) | 0.9475 (-5.3%) | 0.9467 (-5.3%) | 0.9475 (-5.3%) |
IQ3_XXS |
16,1,4 |
0.9929 (-0.7%) | 0.9713 (-2.9%) | 0.9921 (-0.8%) | 0.9756 (-2.4%) | 0.9934 (-0.7%) | 0.9886 (-1.1%) | 0.9849 (-1.5%) | 0.9809 (-1.9%) |
IQ2_XS |
32,1,4 |
0.9972 (-0.3%) | 1.0029 | 1.0018 | 0.9999 | 0.9971 (-0.3%) | 0.9982 (-0.2%) | 0.9991 (-0.1%) | 0.9981 (-0.2%) |
Across the 48 routed-expert layers of k512-iq2_xs-metal.gguf (34 IQ2_S + 12 IQ2_XXS + 2 IQ3_S), this saves ~110 us/tok at ntok=1, ~230 us/tok at ntok=2, and ~470 us/tok at ntok=4 in moe_gu alone, plus ~75 us/tok in moe_route (split=1).