Round 11 Track C Audit: mm_gu3 & mm_down3 Row-Pointer / Block-Offset Hoisting & Branchless W8<T_IQ3_S> / W4<T_IQ3_S> #

1. Summary of Changes (metal/kernels/moe_mm.metal & metal/tests/moe_mm_parity.cpp) #

  • Patch file: /tmp/argon_r11_moemm/argon_r11_moemm.patch (verified clean via git -C vendor/strata apply --check).
  • Scratch checkout: /tmp/argon_r11_moemm/strata (tracked vendor/strata/ untouched).
  • Files touched: Only metal/kernels/moe_mm.metal and metal/tests/moe_mm_parity.cpp.

Change 1: Hoist Row Base Pointer (g0_l, u0_l, w0_l), Row Stride Step (step8), kl, and Per-k0 Block Offset (boff) in mm_gu3 and mm_down3 (moe_mm.metal lines 852–885 & 983–1007) #

  • On Apple M5 (coop_k4 && TK == 32), each lane's right-input cooperative tensor holds half_cap / 4 runs (4 runs in mm_gu3 16x32x32, 8 runs in mm_down3 16x64x32) at consecutive row offsets n_r = n_0 + 8 * r (r = 0 .. half_cap/4 - 1) and a fixed intra-tile K offset kl = ix0[0] = (lane & 3) * 4.
  • Previously, inside every k0 iteration (80 times in mm_gu3, 20 times in mm_down3):
    1. mm_gu3 recomputed g0 + ulong(n) * a.gu_row 4 times for gate and 4 times for up (640 64-bit multiplies per lane), and W8<F>::run recomputed row + (k >> 8) * BB 8 times per k0 (640 block-offset multiplies per lane).
    2. mm_down3 recomputed w0 + ulong(ix[1]) * a.d_row 8 times per k0 (160 64-bit multiplies per lane), and W8<F>::run recomputed row + (k >> 6) * BB 16 times per k0 (320 block-offset multiplies per lane).
  • Hoisted kl = ix0[0], g0_l = g0 + ulong(ix0[1]) * a.gu_row, u0_l = u0 + ulong(ix0[1]) * a.gu_row, w0_l = w0 + ulong(ix0[1]) * a.d_row, and step8 = 8u * row_bytes outside the k0 loop, and computed boff and the masked intra-block index j_in ((k0 + kl) & 255 for QK == 256, (k0 + kl) & 63 for QK == 64, kl for QK == 32) once per k0 iteration, advancing a single running pointer rgw += step8 / ruw += step8 / rdw += step8 across the unrolled i loop.
  • Because j_in is masked to < QK before calling W8<F>::run(rgw, j_in, ...), LLVM KnownBits eliminates (j_in >> 8) * BB (or (j_in >> 6) * BB) and j_in & 255 inside every W8<F>::run call without duplicating W8<F> specializations.
  • Excluded F == T_Q6_K from the outer pointer-hoisting branch in mm_gu3 (if (coop_k4 && TK == 32 && F != T_Q6_K && mm_probe != 1)) because W8<T_Q6_K> sits at a VGPR occupancy threshold where keeping g0_l, u0_l, step8 live across op.run regressed T = 1024.

Change 2: Eliminate Ternary hh ? ... : ... Branches in W8<T_IQ3_S> and W4<T_IQ3_S> (moe_mm.metal lines 531 & 727–731) #

  • In W8<T_IQ3_S>::run, replaced:
    const int l1 = l + 2;
    const uint i0 = hh ? (uint(bl[2 + 8 * ib + 2 * l + 1]) | ((qh << (7 - 2 * l)) & 256))
                       : (uint(bl[2 + 8 * ib + 2 * l]) | ((qh << (8 - 2 * l)) & 256));
    const uint i1 = hh ? (uint(bl[2 + 8 * ib + 2 * l1 + 1]) | ((qh << (7 - 2 * l1)) & 256))
                       : (uint(bl[2 + 8 * ib + 2 * l1]) | ((qh << (8 - 2 * l1)) & 256));
    
    with branchless pointer indexing and shift arithmetic (hh is (k & 7) >> 2 in {0, 1}):
    const P qs = bl + 2 + 8 * ib + 2 * l + hh;
    const int sh0 = 8 - 2 * l - hh;
    const uint i0 = uint(qs[0]) | ((qh << sh0) & 256u);
    const uint i1 = uint(qs[4]) | ((qh << (sh0 - 4)) & 256u);
    const uint s0 = bl[74 + 4 * ib + l], s1 = bl[76 + 4 * ib + l];
    
    and similarly in W4<T_IQ3_S>::run:
    const uint i = uint(b[2 + 8 * ib + 2 * l + hh]) | ((qh << (8 - 2 * l - hh)) & 256u);
    
  • Eliminates two divergent byte-load pairs and select instructions per W8<T_IQ3_S> call (8 per k0 iteration = 640 per lane), shrinking VGPR usage across the unrolled decode loop and speeding up mm_gu3<T_IQ3_S> by -25.0% at T = 2048, -50.8% (2.03x faster!) at T = 1024, and -32.7% at T = 512.

Change 3: In-Process Interleaved --ab <kernel_dir> Mode in moe_mm_parity.cpp #

  • Added --ab <dir> to moe_mm_parity for --gu-bench and --dn-bench so baseline and candidate libraries are compiled in the same Metal context and timed in 5 alternating pairs (AB, BA, AB, BA, AB) on the exact same weight and activation buffers.

Ruled-Out Candidates #

  1. Explicit 4- / 8-element pointer arrays rgw[4], ruw[4], rdw[8] hoisted before k0 (k_hoist1): Forced the Metal compiler to keep 8 64-bit pointers (16 32-bit VGPRs) live across op.run in mm_gu3 and mm_down3, reducing simdgroup occupancy and regressing IQ2_XS --gu-bench by +5.1% .. +6.6% and Q2_0 --dn-bench by +4.2% .. +5.3%. Using a single base pointer g0_l + boff advanced by + step8 inside the unrolled loop keeps only one pointer live during the unrolled loop and zero across op.run.
  2. Outer kb (256-element super-block) + inner ks loop in mm_gu3 (k_hoist3b): Added a nested loop induction variable live across op.run, regressing IQ2_XS at T = 1024 by +2.90% (13,848 -> 14,250 us) and T = 2048 by +0.66% compared to the single k0 loop with boff = uint(k0 >> 8) * BB (k_hoist3a, -2.09% at T = 1024 and -2.24% at T = 2048).
  3. W8<T_Q6_K> uchar4 bitwise pack (as_type<uchar4>(((la >> sl) & 0x0F0F0F0Fu) | (((ha >> shh) & 0x03030303u) << 4))): Bitwise identical (memcmp == 0), but caused Apple's Metal backend to scalarize the uchar4 -> float4 conversion differently from float4(uint4), increasing VGPR pressure and regressing Q6_K --gu-bench at T = 1024 by +46.9% (26.2 -> 38.4 ms) and T = 2048 by +12.1% (45.0 -> 50.5 ms). Rejected.

2. Bitwise Parity Verification (100% Byte-for-Byte memcmp == 0) #

Verified --dump output (HS + y, 6,512,640 bytes per format at T = 256, 64 experts, --v3 --tile 16,32,32,4 --dtile 64,32,2 --dn Q2_0) between baseline (k_base) and patched (k_final) across all 11 supported mm_gu3 formats (including all 5 required formats):

Format (--gu) --dn Dump Bytes (HS + y) memcmp(base, patched) Status
IQ2_XS Q2_0 6,512,640 0 PASS (100% bitwise identical)
Q6_K Q2_0 6,512,640 0 PASS (100% bitwise identical)
IQ3_S Q2_0 6,512,640 0 PASS (100% bitwise identical)
IQ2_XXS Q2_0 6,512,640 0 PASS (100% bitwise identical)
Q2_0 Q2_0 6,512,640 0 PASS (100% bitwise identical)
Q4_K Q2_0 6,512,640 0 PASS (100% bitwise identical)
Q5_K Q2_0 6,512,640 0 PASS (100% bitwise identical)
IQ2_S Q2_0 6,512,640 0 PASS (100% bitwise identical)
IQ3_XXS Q2_0 6,512,640 0 PASS (100% bitwise identical)
IQ4_XS Q2_0 6,512,640 0 PASS (100% bitwise identical)
Q8_0 Q2_0 6,512,640 0 PASS (100% bitwise identical)

3. Paired A/B Benchmark Results on Apple M5 (T = 512, 1024, 2048) #

In-process alternating 5-pair A/B timings (--v3 --tile 16,32,32,4 --dtile 64,32,2 --ab, 512 experts, quiet GPU):

Kernel / Format T Base Median (us) Patched Median (us) Base Min (us) Patched Min (us) Median Diff Min Diff
mm_gu3 IQ2_XS 512 8,147.8 8,101.5 8,142.1 8,093.8 -0.57% -0.59%
mm_gu3 IQ2_XS 1024 13,694.3 13,436.3 13,682.1 13,430.0 -1.88% -1.84%
mm_gu3 IQ2_XS 2048 23,482.0 22,990.0 23,415.8 22,980.5 -2.10% -1.86%
mm_gu3 IQ3_S 512 27,893.5 18,779.7 27,813.2 18,698.5 -32.67% -32.77%
mm_gu3 IQ3_S 1024 37,267.4 18,341.9 37,103.3 18,302.7 -50.78% -50.67%
mm_gu3 IQ3_S 2048 41,731.1 31,296.1 41,570.2 31,234.3 -25.01% -24.86%
mm_gu3 IQ2_XXS 512 8,828.5 8,499.6 8,800.6 8,473.7 -3.73% -3.71%
mm_gu3 IQ2_XXS 1024 14,596.1 14,139.4 14,547.0 14,104.2 -3.13% -3.04%
mm_gu3 IQ2_XXS 2048 24,776.1 24,080.8 24,743.5 24,034.8 -2.81% -2.86%
mm_gu3 Q2_0 512 6,390.1 6,294.2 6,367.7 6,279.1 -1.50% -1.39%
mm_gu3 Q2_0 1024 9,923.9 9,514.1 9,911.4 9,507.0 -4.13% -4.08%
mm_gu3 Q2_0 2048 16,836.9 16,110.4 16,818.3 16,098.4 -4.32% -4.28%
mm_gu3 Q6_K (unchanged path) 512 24,467.6 24,336.8 24,258.3 24,074.4 -0.53% -0.76%
mm_gu3 Q6_K (unchanged path) 1024 25,899.7 25,333.6 24,215.7 24,497.4 -2.19% +1.16%
mm_gu3 Q6_K (unchanged path) 2048 38,443.2 38,566.3 38,227.0 38,345.1 +0.32% +0.31%
mm_down3 Q2_0 512 3,576.4 3,552.3 3,575.6 3,549.7 -0.68% -0.72%
mm_down3 Q2_0 1024 5,209.3 5,035.6 5,202.6 5,031.3 -3.33% -3.29%
mm_down3 Q2_0 2048 8,642.8 8,247.4 8,626.8 8,234.0 -4.58% -4.55%

Projected Full-Model Prefill Savings at T = 1983 (48 Layers) #

  • mm_gu3 T_IQ2_XS (x34 layers, 807.06 ms baseline): -2.10% -> -16.9 ms
  • mm_gu3 T_IQ3_S (x2 layers, 67.78 ms baseline): -25.01% -> -17.0 ms (and -50.8% on T = 1024 chunks!)
  • mm_down3 T_Q2_0 (x48 layers, 334.82 ms baseline): -4.58% -> -15.3 ms
  • Total Track C savings at T = 1983: ~49.2 ms per 2k chunk (~270 ms across an 11k prompt!) with 100% bitwise parity.