Round 26 Track C Audit Report: DW<T_IQ4_XS> / DW<T_IQ4_NL> / DW<T_Q6_K> Specializations & mm_gu3 (W8<T_IQ2_XS>) Investigation #
- Agent:
argon-r26-moemm-gu-and-dqf16(gemini-4-argon) - Scratch Repo:
/tmp/argon_r26_moemm/strata - Unified Patch:
/tmp/argon_r26_moemm/argon_r26_moemm.patch(git -C vendor/strata apply --checkverified0) - Touched Files:
metal/kernels/mma.metalmetal/kernels/dense_mm.metalmetal/kernels/moe_mm.metalmetal/tests/dense_mm_parity.cppmetal/tests/moe_mm_parity.cpp
1. Target 1: DW<T_IQ4_XS>, DW<T_IQ4_NL>, DW<T_Q6_K> Direct Specializations & Full Loop Unrolling (mma.metal, dense_mm.metal, moe_mm.metal) #
Findings & Implementation #
- Direct
DW<T_IQ4_NL>andDW<T_IQ4_XS>specializations (metal/kernels/mma.metal):- Previously,
T_IQ4_XSandT_IQ4_NLfell back to the genericDW<F>, which allocated a separatefloat q[32]array indeq<F>and then multipliedw[j] = q[j] * sin a second loop beforedq_f16/gemv_mma/stage_wcopiedw[j]out. - Added dedicated
DW<T_IQ4_NL>andDW<T_IQ4_XS>specializations that fuse the scale multiplyfloat(w0.x) * ddirectly into the unrolled 8-iterationushorttable lookup loop (#pragma clang loop unroll(full)), eliminating the intermediatefloat q[32]array and second loop while preserving exact fp32 multiplication order (memcmp == 0). - Also tested replacing the threadgroup
tablookup indq_f16<T_IQ4_XS>/dq_f16<T_IQ4_NL>with direct constant-memorykvalues_iq4nl[...]nibble lookups (skippingload_tab+ barrier); this regressed chunk time by~1%(58.88 -> 59.47 msonIQ4_XS,65.61 -> 65.98 msonIQ4_NL) due to constant-cache divergent lookup serialization on Apple GPUs, so the threadgrouptablookup is retained.
- Previously,
- 4-Byte
rd32+ ScalaruintBitwise Unpack inDW<T_Q6_K>(metal/kernels/mma.metal):- Replaced the 16-iteration byte-by-byte loop in
DW<T_Q6_K>with a 4-iteration unrolled loop usingrd32(ql + j)andrd32(qh + j)plus SWAR mask/shift(la & 0x0F0F0F0Fu) | (((ha >> sh) & 0x03030303u) << 4). - On small-row
dq_f16dispatches (2560x128,2560x48,10240x4,2560x1), this speeds updq_f16<T_Q6_K>by 1.04x–1.07x (3.7 -> 3.6 us,2.2 -> 2.1 us,1.8 -> 1.7 us,1.5 -> 1.4 us) and6144x2560by 1.02x (322.0 -> 316.2 us), while large matrices remain DRAM-write bandwidth bound at ~100 GB/s.
- Replaced the 16-iteration byte-by-byte loop in
- Full Unrolling Across
DW<F>,dq_f16,gemv_mma, andstage_w:- Added
#pragma clang loop unroll(full)to allDW<*>specializations (DW<F>,DW<T_IQ4_NL>,DW<T_IQ4_XS>,DW<T_Q2_0>,DW<T_Q5_1>,DW<T_F32>,DW<T_BF16>,DW<T_Q4_K>,DW<T_Q5_K>,DW<T_Q6_K>,DW<T_Q3_K>),gemv_mma's threadgroup store loop,dq_f16'shalf4store loop, andmoe_mm.metal'sstage_wloop.
- Added
- Paired Alternating A/B Timing in
dense_mm_parity.cpp:- Updated
dq_ab_benchinmetal/tests/dense_mm_parity.cppto alternate execution order (round & 1), filter thermal/contention outliers (<= 1.15 * best_pair), and acceptSTRATA_DQ_AB_KERNELSin addition toDQ_AB_KERNELS.
- Updated
Verification (dense_mm_parity & mma_parity) #
dense_mm_parity: ALL PASS across all 14 formats (F32, BF16, Q8_0, Q4_0, Q5_0, Q5_1, IQ4_NL, IQ4_XS, Q2_0, IQ3_S, Q3_K, Q4_K, Q5_K, Q6_K),memcmp(wh)=0, memcmp(xh)=0on every shape.mma_parity: ALL PASS across all 12 formats (ntok = 1, 3, 8),window == one token: bitwise.- Quiet-GPU
dense_mm_parity --dq-abpaired results (memcmp=0on all 11–13 shapes per format):IQ4_XS:60.91 -> 60.95 ms(1.00x,2560x6144:293.5 -> 291.3 us1.01x,memcmp=0)Q6_K:66.13 -> 65.98 ms(1.00xchunk;6144x2560:322.0 -> 316.2 us1.02x,2560x48:2.2 -> 2.1 us1.06x,10240x4:1.8 -> 1.7 us1.06x,2560x1:1.5 -> 1.4 us1.07x,memcmp=0)IQ4_NL:66.13 -> 65.94 ms(1.00x,2560x6144:286.7 -> 283.7 us1.01x,memcmp=0)IQ3_S:59.56 -> 59.17 ms(1.01x,2560x6144:278.9 -> 274.4 us1.02x,6144x2560:286.4 -> 282.7 us1.01x,memcmp=0)
2. Target 2: mm_gu3 (W8<T_IQ2_XS>) Investigation & Ablations (moe_mm.metal, moe_mm_parity.cpp) #
Hardware Profiling (MM_PROBE Breakdown at T=512, tile=16,32,32,4, 512 Experts) #
MM_PROBE=0(fullmm_gu3decode +matmul2d):7723.7 us(4.34 TFLOP/suseful)MM_PROBE=3(notablookup,v0 * 0x01010101u, withw4_signed+sign4_iq2xs+ device loads):7320.4 us(-403 us=5.2%of total time)MM_PROBE=5(device loadsv0, v1, scb, d+sc0, sc1only, notablookup, now4_signed/sign4_iq2xs):6604.0 us(-716 us=9.3%of total time)MM_PROBE=1(no weight decode, cheap constant fill +matmul2d):3680.6 us(-2923 us=37.8%of total time)QUANT_CTAB=1(constant-memory grid instead of threadgrouptab):8550.8 us(+10.7%slower)
Controlled Ablations Tested on M5 (T=512 / 1024 / 2048, all HS_memcmp=PASS YE_memcmp=PASS Y_memcmp=PASS) #
- Pre-added 3-pointer stepping (
rgw, rgw_q, rgw_sc += step8):- Maintaining three 64-bit pointers across the unrolled
iloop added pointer-increment instructions and live registers acrossop.run(tA, rw, accg), regressingT=512min time by+0.93%(15269 -> 15411 usunder contention).
- Maintaining three 64-bit pointers across the unrolled
- Outer block loop (
kb += 256,ib = 0..7) +run_q(rgw, qoff, ib, hh, ...):- Keeping
qoff = kl_q + 8u * ib,ib, andhhlive acrossop.run(tA, rw, accg)increased register pressure around the cooperative-tensormatmul2dop, regressingT=512by0.55%(7748 -> 7791 us),T=1024by1.35%(12896 -> 13073 us), andT=2048by1.43%(22193 -> 22515 us).
- Keeping
- Full compile-time template unrolling of
IB = 0..7(W8<T_IQ2_XS>::run_q<IB>):- Unlike
mm_down3(Q2_0,QK=64 = 2 * TK),IQ2_XShasQK=256 = 8 * TK. Unrolling all 8IBsteps (16op.runcalls perk0 += 256iteration) blew up kernel code size and register allocation, regressingT=512from7708 usto33176 us(-76.8%).
- Unlike
- Vectorized
packed_ushort4load (q4 = *(device const packed_ushort4*)(bl + qoff)):- Because
74 % 4 == 2, odd blocks are only 2-byte aligned, sopacked_ushort4loads all 8 bytes (q[0..3]) as 16-bit loads including the unusedq[1]andq[3]words for the otherlhalf, matching baseline within0.00%–0.28%(12925.1 vs 12925.0 usatT=1024).
- Because
- Cross-lane
simd_shuffle_xor(vm, 1)sharing between adjacenthh=0andhh=1lanes:- Both lanes already hit the same L1 cache line in the same cycle; adding
simd_shuffle_xor+ select instructions inside the unrolled loop regressedT=512by-40.6%(7744 -> 13028 us) andT=1024by-16.0%(12914 -> 15367 us).
- Both lanes already hit the same L1 cache line in the same cycle; adding
- Dual
rw + rucooperative tensors in a singleiloop:- Allocating two
right_input_cooperative_tensorinstances simultaneously doubled thehalfregister footprint (32 -> 64halfs per lane) acrossop.run, reducing occupancy and regressingT=512by-9.54%(7735 -> 8550 us) andT=2048by-4.03%(22217 -> 23149 us).
- Allocating two
- Bitwise float32 sign-XOR (
as_type<uint4>(p) ^ m) andw4_iq2xsdirect shift:- The Apple M5 shader compiler fuses
(s & bit) ? -float(v[k]) : float(v[k])into hardware conditional-negate/select instructions, whereas integer sign-bit XOR requiredfloat <-> uintbitwise reinterpretation (-17.4%atT=512), and splittings3parity select (w4_iq2xs) added an extra branch/select perw4(-3.75%atT=512).
- The Apple M5 shader compiler fuses
Conclusion on mm_gu3 (W8<T_IQ2_XS>) + Fast-Compile Guard (MM_V3_ONLY) #
- Baseline
W8<T_IQ2_XS>::run+mm_gu3's singlej_in = (k0 + kl) & 255index is already register- and instruction-optimal on M5: keeping onlyk0andkllive acrossop.run(tA, rw, accg)minimizes live scalar registers during thematmul2dcooperative-tensor op while LLVM CSE shares(j_in & 255) >> 5,(j_in & 255) >> 3, and(j_in & 7) >> 2across the 4 unrollediiterations. - Kept
W8<T_IQ2_XS>andmm_gu3at exact baseline (100%bitwise identical,HS_memcmp=PASS YE_memcmp=PASS Y_memcmp=PASS), and added#ifndef MM_V3_ONLYguards around non-v3 kernels (mm_gu,mm_down,mm_gu2,mm_down2,mm_gu4,mm_gu5) inmetal/kernels/moe_mm.metal+#define MM_V3_ONLY 1inmetal/tests/moe_mm_parity.cppwhen running--v3, cuttingmoe_mm_parity --v3Metal shader compilation time from~15sto~1.5swhile leaving normal engine compilation 100% unchanged.