opus r35 track A: router dq reuse + gemm_f16 tiles (TN=96, M%128 padding), SLC swizzle (rejected) #

patch: /tmp/opus_r35_dense/opus_r35_dense.patch (git -C vendor/strata apply --check = OK) scratch tree: /tmp/opus_r35_dense/strata; test binary /tmp/opus_r35_dense/bin/dense_mm_parity (linked against vendor/strata/build-metal/libmtl_rt.a, run with STRATA_METAL_KERNELS=/tmp/opus_r35_dense/strata/metal/kernels). Files touched: metal/kernels/dense_mm.metal (+1 instantiation), window.cpp (gemm selector 3458-3470, router 3668-3687 only), metal/tests/dense_mm_parity.cpp. No full-model runs, :8080 untouched.

1. router dequant reuse (window.cpp prompt_chunk) #

Under stream_ && pred_on && !p.ids3.ptr the router of layer l+1 that layer l dequantizes for the read-ahead is reused as layer l+1's own router: w_r / w_r2 ping-pong between p.Wr and p.Wr2 by layer parity, dq(tr, w_r) only at l == 0 (or when reuse is off). PRED2 (p.ids3) overwrites Wr2 with l+2, so it keeps the old path. Same tensor, same dq call, same perm → bitwise-identical weights. Hazards: the dq into w_r2 at layer l targets the buffer layer l-1's router GEMM read (several barriers back, in hc_read); w_r / w_r2 are always distinct within a layer. run_graph_seg runs the segments in order with no replay, so layer l+1 always sees layer l's dq. Saves 47 dq_f16 dispatches (512x2560) per streamed chunk; measured dq cost per call (--dq-ab, 2560x512): 17-20 us (quantized types), 30 us BF16, 57 us F32 → 0.8-2.7 ms of dq work per chunk (they overlap other dqs, so wall saving is lower). Compiles (clang++ -fsyntax-only with the build's flags); not measured end-to-end (no full-model runs allowed). Needs a prompt_parity / streamed-prompt logits check before merging (I couldn't run the model).

2. gemm_f16 tiles (dense_mm_parity --tiles / --ab, all memcmp == 0 vs 128_64_4) #

Methodology: the GPU was shared (other agents/server: Device Utilization swinging 0-84%), so medians of 5 rounds were useless (2-4x swings). --tiles now reports the min over interleaved rounds (DENSE_ROUNDS, default 5/9) and can be restricted with DENSE_TILES=a,b; repeated focused runs agreed within ~1-3%.

Walbe (N=96, K=2560, 36 calls): new gemm_f16_16_96_2, used at M >= 640 #

M 16_64_2 64_32_2 16_96_2 32_96_2 64_96_4
512 29.6 31.5 30.2 39.7 40.1
640 33.0 34.3 30.9 41.0 40.5
768 39.8 37.4 (cur) 33.0 41.4 46.4
1024 55.4 42.9 (cur) 41.5 47.5 48.0
1983 116.8 103.8 (cur) 91.6 92.0 94.1
2048 122.8 98.3 (cur) 94.3 96.0 95.2
64_96_2 and 128_96_4 are 2-3x slower (not shipped). -3..-12% on the call, but only ~0.3-0.4 ms/chunk total.

big-N GEMMs (Wqkv, Wz, Wout, Wq, Wpv, Wshd; the else branch), M < 1536 #

128_64_4 loses whenever M % 128 is 1..64 (a 128-row tail tile that is up to half padding); 64_128_4 ties it elsewhere. New rule: M < 1536 && (M % 128 == 0 || M % 128 > 64) ? 128_64_4 : 64_128_4. Focused runs (us/call, 128_64_4 → 64_128_4):

  • M=192: Wqkv 920 → 787, Wz 708 → 552, Wout 683 → 563, Wq 1135 → 942, Wshd 64 → 51
  • M=306: Wqkv 1686 → 1412, Wz 970 → 847, Wout 1013 → 888, Wq 2044 → 1710, Wshd 92 → 80
  • M=448: Wqkv 2250 → 1990, Wout 1353 → 1233, Wq 3240 → 2862; M=576/700: -7..-9%; M=1300/1450: -4..-6%
  • ties (rule keeps 128_64_4): M=256, 384, 640, 896, 1000, 1100 --ab chunk totals (sum of isolated GEMMs x calls, r34 → new): M=150 1.17x, 192 1.14x, 306 1.13-1.14x, 448 1.09x, 520 1.07x, 700/800 1.05x, 1050 1.03-1.05x, 1200 1.04x; 200/330/640/1000/1400/1600/1983/2048 1.00x (unchanged tiles bar Walbe). So the win is on the tail / short chunks (a 306-token prompt: ~22 ms of dense GEMM per chunk), not 2K chunks.

W512 (N=512, 95 calls), Wshgu (640), Wk (1152/1664): no change #

The current picks (32_64_2 <384, 32_128_2 above; Nn<=1664 rule) are within ~1-3% of the best everywhere, e.g. W512 M=1983: 32_128_2 402-404, 64_128_4 403-404, 64_64_2 405, 128_64_4 416; M=1024: 32_128_2 197, 64_64_2 197, 64_128_4 210. Only W512 M=768 (128_64_4 -4%) and Wshgu 64_64_2 at M~1024 (-2.5%) showed anything, inside run-to-run noise. Not shipped.

SLC swizzle (rejected, not in the patch) #

Tried gemm_f16_sw<TM,TN,SG,G>: threadgroup ids remapped so G column tiles of B run down all row tiles of A (same tiles, same bits, memcmp 0). 128_64_4_sw{2,4,8} and 64_128_4_sw{2,4,8} vs plain on Wqkv/Wz/Wout/Wq at M = 256..2048: never a reproducible win; 128_64_4_sw2 ≈ plain (±1%), sw4/sw8 up to +10-60% slower, 64_128_4_sw* often 2-3x slower (e.g. Wqkv M=1983: 7827 plain vs 28523 sw2 / 23062 sw4). The default dispatch order already keeps B cached well enough; the swizzle kills the row-tile reuse of A between concurrently running threadgroups.

verification #

  • dense_mm_parity (parity): ALL PASS
  • --tiles M (all 12 tiles incl. 16_96_2 x 13 shapes) at M = 100, 256, 306, 640, 1000, 1983, 2048: ALL BITWISE
  • --ab M at 150..2048: ALL BITWISE
  • window.cpp: compiles (-fsyntax-only, build flags). Router reuse not run end-to-end (forbidden here).