Round 32 Track C: Decode deq<T_IQ2_S / T_IQ2_XXS / T_IQ2_XS / T_IQ3_S> 64-Bit uint2 Grid Loads (quant.metal) + gu_body Low-Register Packed iq_combine (moe.metal) #

1. Summary of Changes #

  • Patch: /tmp/argon_r32_moe/argon_r32_moe.patch (verified clean via git -C vendor/strata apply --check, exit 0).
  • Files Modified (within ownership boundaries):
    1. metal/kernels/quant.metal (lines 160–224):
      • deq<T_IQ2_XXS>, deq<T_IQ2_XS>, deq<T_IQ2_S>: Replaced the two separate 32-bit grid lookups per l2 iteration (g_iq2*(tab, idx, 0) and g_iq2*(tab, idx, 1)) with a single 64-bit uint2 load (const uint2 g = ctab ? as_type<uint2>(iq2*_grid[idx]) : ((threadgroup const uint2*)tab)[idx]), cutting threadgroup/constant grid loads in half (4 -> 2 loads per 16-weight unit).
      • deq<T_IQ3_S>: Replaced the per-iteration uchar sign loads b[74 + 4 * ib + l] with one hoisted 16-bit ushort load s2 = *(device const ushort*)(b + 74 + 2 * ui) before the l2 loop, added #pragma clang loop unroll(full), and replaced float(1 + 2 * ((sc >> (4 * (ib & 1))) & 15)) with fma(float((sc >> (4 * (ib & 1))) & 15), 2.0f, 1.0f).
    2. metal/kernels/moe.metal (lines 376–549):
      • Added iq_combine(uint4 g, uint s16, float sc, thread const float* xs) and deq_iq_packed<F>(b, ui, g, s16, tab) for the 16-weight i-quant formats (T_IQ2_S, T_IQ2_XXS, T_IQ2_XS, T_IQ3_S, T_IQ3_XXS), keeping a decoded 16-weight unit in 6 registers (uint4 g, uint s16, float sc) instead of 17 registers (float q[16] + float sc).
      • In gu_body<F, LPR, NR, NE>, enabled fused gate+up decoding and single X<F>::load reuse per token at NR == 1 && NE == 4 with loop-invariant ui = (LPR % 16 == 0) ? (li & 15) : (u & 15), holding both gate and up packed units (12 registers total vs 34 floats) across the t = 0..3 token loop and cutting X<F>::load calls in half (8 -> 4 per unit step) without register spilling.

2. Verification & Bitwise Parity #

All standalone parity and A/B suites pass with 0 fails (ALL PASS, 100% bitwise identical):

  • expert_parity --ab /tmp/argon_r32_moe/base_kernels: 0 fail(s) ALL PASS across IQ2_S, IQ2_XXS, IQ2_XS, IQ3_S, IQ3_XXS and tiles 32,2,4, 32,1,4, 16,1,4 (NE = 1..4, 8 + random windows).
  • gemv_parity --ab /tmp/argon_r32_moe/base_kernels IQ3_S 2560 2560 {1,2,4,8} gemv: bitwise (ALL PASS).
  • Full gemv_parity, dense_mm_parity, expert_parity: ALL PASS.

3. Paired A/B Benchmarks on Quiet Apple M5 (expert_parity --ab --bench --layers 1 --reps 16) #

A. Threadgroup-Table Mode (QUANT_CTAB=0, used by IQ3_S in window.cpp and default expert_parity) #

Format Tile (L,R,S) NE=1 Ratio NE=2 Ratio NE=3 Ratio NE=4 Ratio ntok=1 Ratio ntok=2 Ratio ntok=4 Ratio ntok=8 Ratio
IQ3_S 32,2,4 0.9894 (-1.1%) 0.9713 (-2.9%) 0.9290 (-7.1%) 0.9254 (-7.5%) 0.9817 (-1.8%) 0.9836 (-1.6%) 0.9763 (-2.4%) 0.9722 (-2.8%)
IQ3_S 32,1,4 0.9757 (-2.4%) 0.9857 (-1.4%) 0.9688 (-3.1%) 0.8557 (-14.4%) 0.9842 (-1.6%) 0.9756 (-2.4%) 0.9711 (-2.9%) 0.9779 (-2.2%)
IQ3_S 16,1,4 1.0009 0.9753 (-2.5%) 0.9593 (-4.1%) 0.8355 (-16.5%) 0.9960 (-0.4%) 0.9893 (-1.1%) 0.9719 (-2.8%) 0.9641 (-3.6%)
IQ2_S 32,2,4 0.9890 (-1.1%) 0.9917 (-0.8%) 1.0003 0.9965 (-0.4%) 0.9915 (-0.9%) 0.9918 (-0.8%) 0.9931 (-0.7%) 0.9915 (-0.9%)
IQ2_S 32,1,4 0.9993 (-0.1%) 0.9963 (-0.4%) 0.9713 (-2.9%) 0.8686 (-13.1%) 0.9996 0.9976 (-0.2%) 0.9962 (-0.4%) 0.9949 (-0.5%)
IQ2_S 16,1,4 1.0000 0.9973 (-0.3%) 0.9593 (-4.1%) 0.8416 (-15.8%) 1.0064 1.0008 0.9958 (-0.4%) 0.9930 (-0.7%)
IQ2_XXS 32,2,4 0.9928 (-0.7%) 0.9976 (-0.2%) 0.9943 (-0.6%) 0.9961 (-0.4%) 0.9926 (-0.7%) 0.9906 (-0.9%) 0.9921 (-0.8%) 0.9915 (-0.9%)
IQ2_XS 32,2,4 0.9777 (-2.2%) 0.9947 (-0.5%) 0.9960 (-0.4%) 0.9934 (-0.7%) 0.9734 (-2.7%) 0.9819 (-1.8%) 0.9815 (-1.9%) 0.9830 (-1.7%)
IQ2_XS 32,1,4 0.9925 (-0.8%) 0.9998 1.0180 0.9048 (-9.5%) 0.9941 (-0.6%) 0.9952 (-0.5%) 0.9907 (-0.9%) 0.9966 (-0.3%)
IQ3_XXS 16,1,4 0.9995 1.0050 1.0009 0.8900 (-11.0%) 1.0015 0.9987 (-0.1%) 0.9986 (-0.1%) 0.9964 (-0.4%)

B. Constant-Table Mode (QUANT_CTAB=1, used by IQ2_S, IQ2_XXS, IQ2_XS, IQ3_XXS in window.cpp) #

Format Tile (L,R,S) 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 32,1,4 1.0023 1.0000 1.0005 0.8580 (-14.2%, 237.8 -> 204.0 us) 1.0016 1.0028 1.0012 0.9969 (-0.3%)
IQ2_S 16,1,4 1.0002 1.0003 0.9998 0.8533 (-14.7%, 225.0 -> 192.0 us) 0.9958 (-0.4%) 1.0015 0.9994 0.9989 (-0.1%)
IQ2_XXS 16,1,4 1.0021 1.0005 1.0005 0.8899 (-11.0%, 220.1 -> 195.8 us) 1.0010 1.0022 0.9991 0.9969 (-0.3%)
IQ2_XS 16,1,4 1.0005 1.0009 1.0000 0.9057 (-9.4%, 223.2 -> 202.2 us) 1.0017 1.0020 0.9979 (-0.2%) 0.9974 (-0.3%)
IQ3_XXS 16,1,4 1.0009 1.0032 1.0003 0.8866 (-11.3%, 230.1 -> 204.0 us) 1.0009 1.0037 1.0007 1.0000

C. Dense gemv_parity --ab (IQ3_S 2560 -> 2560, tile 32,2,4,1) #

  • ntok=1: 24.56 us -> 24.04 us (-2.1%), bitwise identical.
  • ntok=2: 30.87 us -> 30.61 us (-0.8%), bitwise identical.
  • ntok=8: 77.12 us -> 76.77 us (-0.5%), bitwise identical.

4. Tried & Rejected Ideas #

  • Preloading 32-bit q4 = rd32(b + 2 + 4 * ui) in deq<T_IQ3_S>: Tested against hoisted s2 + #pragma clang loop unroll(full) + fma; adding q4 = rd32(...) increased live register pressure across the four divergent g_iq3s lookups in deq<T_IQ3_S> and lost the -2.9%..-7.5% gain. Keeping the byte index loads b[2 + 8 * ib + 2 * l] while hoisting the 16-bit sign load s2 + unroll(full) + fma preserved the full -1.6%..-7.5% speedup on IQ3_S.
  • Fused deq_iq_packed + iq_combine at NR == 2 or NE == 2, 3: At NR == 2 (32,2,4), holding two packed units or adding per-element sign selection inside the token loop was +1.2%..+3.7% slower than unit_combine<16>. At NR == 1, NE == 3, the extra bitwise sign extractions across 3 tokens cost +2.9%..+5.0% more than saving 3 X<F>::load calls; at NR == 1, NE == 4, where the baseline float q[16] path suffered occupancy pressure from 8 X<F>::load calls and 17 live floats alongside ag[1][4] + au[1][4], deq_iq_packed + iq_combine delivered a clean -9.4% to -16.5% speedup across all five 16-weight i-quant formats.