Round 27 Track A — M5 gemv_tile audit (2560→512, 640→2560) + gemv_cat for IQ4_XS / IQ3_S / Q6_K #

author: opus-r27-gemv-tiles-and-cat · device: Apple M5 (10-core GPU) · live :8080 server was running during all measurements (it shares the GPU), so every number is the min over 3 runs of a 15-round paired median; runs that hit interference were visibly off by 3-5x and the min filters them.

deliverables

  • /tmp/opus_r27_gemv/opus_r27_gemv.patch — the change to apply (git -C vendor/strata apply --check OK against the current dirty tree): gemv_tile tiles + gemv_parity tooling/invariant. gemv.metal is not touched by it.
  • /tmp/opus_r27_gemv/opus_r27_gemv_cat_experiment.patch — NOT recommended; the gemv_cat IQ4_XS packed path + GVC instances for IQ4_XS/IQ3_S/Q6_K, kept only so the cat A/B below can be reproduced (applies on top of the main patch).
  • scratch tree /tmp/opus_r27_gemv/strata, build /tmp/opus_r27_gemv/build (gemv_parity binary there).

TL;DR #

  1. Bug confirmed: every 2560 -> 512 QSA attn_k/attn_v (15 IQ4_XS, 8 Q6_K, 1 IQ3_S) fell through to {32,1,4,1} because the M5 branches tested n_out == 640. Fixed:
    • IQ4_XS 512 -> {16,2,4,1} all ntok (also picks up R26's packed-IQ4 path): cold -14% (nt1) / -25% / -31% / -36% (nt4)
    • Q6_K 512 -> {8,1,4,1} all ntok: cold 0% / -14% / -23% / -23%
    • IQ3_S 512 -> {32,2,4,1} all ntok: cold -1% / -14% / -21% / -20%
    • LPR is constant across ntok for all three, so a window stays bitwise the one-token answer for these tensors.
    • inside the real group (k + v beside a 2560→12288 IQ4_XS attn_q, as Window::layer encodes them with no barrier) the new tiles are still the best or within 1% of best: IQ4_XS group -3..-8%, Q6_K -1..-5%, IQ3_S -1..-5%.
  2. 640 -> 2560 (ffn_down_shexp): the current table is already right for IQ4_NL and Q8_0 (cold optimum matches exactly). Only change: Q2_0 at ntok>=4 {8,1,4,1} -> {8,2,4,1} (-4..-6% cold, same LPR => bitwise identical sums). The R26 claim "Q2_0R {8,1,4,1} 39.55 us at ntok 4 vs {16,1,4,1} 18.19" does not reproduce: {8,1,4,1} is the best Q2_0R tile at ntok 2..4 here (14.84 us cold at nt4; {16,1,4,1} 17.17, {4,2,4,1} 16.83). The 39.55 was almost certainly GPU interference (I saw identical 30-40 us outliers on unrelated shapes while the server was busy).
  3. 2560 -> 640 IQ3_S (gate/up shexp, 23 tensors) at ntok 2..4: {8,1,8,1} -> {32,2,4,1} (-1..-8% cold), which also makes its LPR constant across ntok (it was 32 at nt1, 8 at nt>=2).
  4. gemv_cat for IQ4_XS/IQ3_S/Q6_K: NOT a win -> not wired. It is bitwise (verified, nt 1/2/3/4/8, 512+512 and 640+640), but Window::layer already encodes k/v and gate/up as concurrent dispatches (nb_ = true), and these GEMVs are DRAM-bound, so fusing saves no bytes and only a dispatch: IQ4_XS -2..+8% (worse at nt>=3), Q6_K -4..+15% (worse at nt>=3 and on 640), IQ3_S -6..+6% (noise-level, median slightly negative). Same finding for the existing Q8_0 cat (hot 2560→512+512 nt2: +2.4%).
  5. The LPR insight is verified and is now a permanent check in gemv_parity: for every format, n_in, ntok, all KS=1 tiles with the same LPR give bitwise-identical outputs (across NR and SG). gemv_parity: ALL PASS (13 formats × 3 n_in × ntok 1/3/8, rel L2 < 1e-5, window == one token bitwise, same-LPR bitwise).

Estimated decode effect: per token (nt=1) ~18 µs saved over 15 IQ4_XS k/v (isolated cold), per 4-token verify window ~135 µs (IQ4_XS 95 + Q6_K 36 + IQ3_S 3) plus ~1 µs × 23 IQ3_S gate/up at nt 2..3. Against ~47 ms/tok that is 0.04-0.3% — real but small; it is NOT measured end-to-end (full-model runs were off-limits).

Shapes in k512-iq2_xs-metal.gguf (parsed from the GGUF header) #

tensor shape types
attn_k (12) 2560→512 IQ4_XS 8, Q6_K 3, IQ3_S 1
attn_v (12) 2560→512 IQ4_XS 7, Q6_K 5
ffn_gate_shexp (48) 2560→640 IQ4_XS 31, IQ3_S 9, Q6_K 8
ffn_up_shexp (48) 2560→640 IQ4_XS 26, IQ3_S 14, Q6_K 8
ffn_down_shexp (48) 640→2560 IQ4_NL 36, Q8_0 7, Q2_0 5 (prepared x, kernel gemv_42_*, not Q2_0R)

same-type pairs: k+v 7/12 (IQ4_XS 5, Q6_K 2); gate+up 23/48 (IQ4_XS 18, IQ3_S 3, Q6_K 2).

Target 1a: 2560 -> 512, isolated (µs per dispatch, behind a barrier) #

gemv_parity --tiles TYPE 2560 512 NT <tiles...> (GEMV_AB_COLD=1 walks 256 copies of W ≈ 180 MB, so W comes from DRAM)

type weights ntok 32,1,4,1 16,2,4,1 32,2,4,1 16,1,4,1 8,1,4,1 8,2,4,1 8,1,8,1 4,2,4,1 best
IQ4_XS cold 1 8.67 7.45 8.36 7.46 7.61 8.13 7.62 12.54 16,2,4,1
IQ4_XS cold 2 11.36 8.57 9.78 9.47 9.50 9.50 9.61 14.72 16,2,4,1
IQ4_XS cold 3 14.05 9.71 11.36 10.69 11.10 11.31 11.24 18.36 16,2,4,1
IQ4_XS cold 4 17.57 11.22 13.08 13.16 13.08 13.20 13.20 22.00 16,2,4,1
IQ4_XS hot 1 8.21 6.58 7.21 6.82 7.06 6.32 7.04 7.68 8,2,4,1
IQ4_XS hot 2 14.04 10.10 11.29 11.03 11.12 10.03 11.12 14.72 8,2,4,1
IQ4_XS hot 3 14.21 9.46 10.74 10.55 10.66 10.19 10.65 16.32 16,2,4,1
IQ4_XS hot 4 17.01 10.74 12.24 12.39 12.34 11.75 12.39 19.98 16,2,4,1
Q6_K cold 1 10.26 11.09 10.58 10.45 10.24 11.24 10.24 17.85 8,1,8,1
Q6_K cold 2 12.43 11.61 12.14 10.98 10.64 13.72 10.74 22.93 8,1,4,1
Q6_K cold 3 15.60 13.43 15.26 12.45 12.04 18.08 12.18 31.62 8,1,4,1
Q6_K cold 4 19.07 15.64 17.63 14.79 14.63 21.81 14.75 40.22 8,1,4,1
Q6_K hot 1 7.12 6.57 6.78 6.46 6.87 8.21 6.87 14.54 16,1,4,1
Q6_K hot 2 10.26 8.08 9.24 8.68 8.80 11.12 8.99 19.99 16,2,4,1
Q6_K hot 3 13.63 10.40 12.09 11.07 10.96 15.54 10.99 28.37 16,2,4,1
Q6_K hot 4 17.15 12.89 14.77 13.56 13.52 19.30 13.68 36.18 16,2,4,1
IQ3_S cold 1 7.98 8.54 7.90 8.03 9.70 11.10 9.62 16.18 32,2,4,1
IQ3_S cold 2 10.64 9.49 9.14 9.75 10.59 12.36 10.52 18.50 32,2,4,1
IQ3_S cold 3 13.45 11.06 10.57 11.79 11.86 14.47 11.78 22.20 32,2,4,1
IQ3_S cold 4 16.06 12.82 12.78 13.34 13.65 16.89 13.71 28.33 32,2,4,1
IQ3_S hot 1 7.48 7.30 6.81 7.58 7.93 8.22 7.83 12.92 32,2,4,1
IQ3_S hot 2 11.14 9.49 9.22 10.04 10.24 11.38 10.39 17.50 32,2,4,1
IQ3_S hot 3 13.12 10.22 9.92 10.79 11.01 11.40 10.96 19.34 32,2,4,1
IQ3_S hot 4 17.06 13.86 13.09 13.53 14.14 16.73 14.11 28.43 32,2,4,1

Target 1a': 2560 -> 512 inside the real group (k + v beside attn_q IQ4_XS 2560→12288, one barrier) #

GEMV_BESIDE=IQ4_XS,12288,<q tile> GEMV_AB_COLD=1 gemv_parity --tiles ... (µs per group; q tile = gemv_tile's: 32,2,4,1 nt1, 16,2,4,1 nt>=2)

type weights ntok 32,1,4,1 16,2,4,1 32,2,4,1 8,1,4,1 best
IQ4_XS cold 1 184.57 178.86 180.51 180.03 16,2,4,1
IQ4_XS cold 2 200.33 186.88 191.96 190.08 16,2,4,1
IQ4_XS cold 3 241.82 223.59 229.72 227.78 16,2,4,1
IQ4_XS cold 4 298.04 274.62 281.90 279.07 16,2,4,1
Q6_K cold 1 228.76 227.32 223.86 225.61 32,2,4,1
Q6_K cold 2 258.73 253.91 254.12 254.85 16,2,4,1
Q6_K cold 3 335.32 320.63 322.67 320.89 16,2,4,1
Q6_K cold 4 400.36 381.26 386.35 381.40 16,2,4,1
IQ3_S cold 1 259.87 259.62 256.99 257.80 32,2,4,1
IQ3_S cold 2 285.11 275.59 276.66 278.94 16,2,4,1
IQ3_S cold 3 335.79 319.72 320.34 325.83 16,2,4,1
IQ3_S cold 4 380.44 359.75 362.64 365.00 16,2,4,1

Target 1b: 640 -> 2560 (µs per dispatch) #

current table: IQ4_NL nt1 8,1,4,1 / nt>=2 4,2,4,1; Q8_0 nt1-2 32,1,4,1 / nt>=3 4,1,4,1; Q2_0 8,1,4,1.

type weights ntok 8,1,4,1 4,2,4,1 8,2,4,1 16,1,4,1 16,2,4,1 32,1,4,1 4,1,4,1 4,4,4,1 2,2,4,1 best
Q2_0 cold 1 6.42 6.56 6.24 5.93 6.07 7.42 6.42 7.58 8.90 16,1,4,1
Q2_0 cold 2 7.30 7.62 7.46 7.99 7.64 10.69 7.69 10.31 9.92 8,1,4,1
Q2_0 cold 3 8.79 9.31 9.17 10.36 11.38 14.42 9.52 12.79 11.74 8,1,4,1
Q2_0 cold 4 12.15 11.60 11.47 13.52 13.91 18.45 12.01 15.03 14.76 8,2,4,1
Q2_0 hot 1 4.60 4.50 4.36 5.44 4.91 7.15 5.08 5.06 5.84 8,2,4,1
Q2_0 hot 2 9.02 8.94 8.72 10.99 10.04 15.32 9.23 12.37 10.19 8,2,4,1
Q2_0 hot 3 9.10 9.17 9.05 11.19 10.18 16.33 9.43 12.54 10.25 8,2,4,1
Q2_0 hot 4 10.39 10.58 10.41 12.66 12.17 18.29 10.62 13.06 13.00 8,1,4,1
IQ4_NL cold 1 9.81 10.32 10.47 10.51 10.73 11.57 10.23 10.70 10.64 8,1,4,1
IQ4_NL cold 2 11.01 10.68 11.35 13.03 12.11 15.31 10.65 12.02 11.47 4,1,4,1
IQ4_NL cold 3 15.63 13.09 14.93 19.77 16.86 23.95 14.45 18.66 14.89 4,2,4,1
IQ4_NL cold 4 18.36 14.97 17.07 23.50 19.96 28.95 16.69 21.71 17.85 4,2,4,1
IQ4_NL hot 1 8.13 6.85 7.46 10.10 8.59 11.85 7.35 6.96 7.02 4,2,4,1
IQ4_NL hot 2 11.02 8.41 9.13 13.82 10.91 16.81 9.42 9.16 8.98 4,2,4,1
IQ4_NL hot 3 12.68 9.51 10.65 16.52 13.04 20.70 11.07 12.33 11.02 4,2,4,1
IQ4_NL hot 4 15.57 12.18 13.43 20.78 16.64 26.63 13.79 16.13 14.21 4,2,4,1
Q8_0 cold 1 16.71 17.18 16.74 15.56 16.43 14.68 16.57 17.94 18.76 32,1,4,1
Q8_0 cold 2 16.88 17.48 17.44 17.95 18.45 16.57 16.95 20.47 19.14 32,1,4,1
Q8_0 cold 3 18.86 19.86 20.33 23.39 23.45 21.17 18.03 30.34 22.56 4,1,4,1
Q8_0 cold 4 22.27 20.71 24.12 26.74 29.20 28.05 19.75 35.07 24.07 4,1,4,1
Q8_0 hot 1 7.66 6.80 7.35 8.89 8.62 9.92 7.14 8.37 7.28 4,2,4,1
Q8_0 hot 2 9.26 7.61 9.21 11.43 11.08 13.60 8.16 10.68 9.65 4,2,4,1
Q8_0 hot 3 12.80 10.74 12.58 16.86 16.01 19.23 11.44 17.02 12.83 4,2,4,1
Q8_0 hot 4 16.06 13.25 15.52 20.87 19.81 24.71 13.97 20.68 15.39 4,2,4,1
Q2_0R cold 1 8.55 9.03 8.67 8.40 8.87 10.15 8.69 10.33 11.53 16,1,4,1
Q2_0R cold 2 10.35 11.12 11.18 11.30 12.02 14.80 10.88 14.81 13.77 8,1,4,1
Q2_0R cold 3 12.52 13.74 12.92 13.51 16.88 18.73 12.52 17.48 16.19 8,1,4,1
Q2_0R cold 4 14.84 16.83 16.24 17.17 20.23 24.87 15.30 21.32 20.14 8,1,4,1
Q2_0R hot 1 5.77 6.33 5.86 6.49 6.14 7.68 6.10 6.81 6.80 8,1,4,1
Q2_0R hot 2 8.26 9.10 8.66 9.98 9.88 13.61 8.51 10.95 9.31 8,1,4,1
Q2_0R hot 3 10.88 11.69 11.35 13.68 13.32 18.94 11.57 14.91 12.59 8,1,4,1
Q2_0R hot 4 12.45 13.70 13.30 15.43 15.52 22.76 12.89 17.02 15.22 8,1,4,1

Q2_0 re-run with 5 repeats: nt1 8,1 6.33 / 8,2 6.15 / 16,1 5.95; nt4 8,1 14.21 / 8,2 13.68 / 16,1 15.97. nt1 16,1,4,1 is -6% cold but +18% hot and changes LPR across ntok; left alone.

2560 -> 640 check (cold, µs) — the shexp gate/up tiles (cat would use them) #

type weights ntok 32,1,4,1 16,2,4,1 32,2,4,1 16,1,4,1 8,1,4,1 8,1,8,1 best
IQ4_XS cold 1 10.40 8.49 9.65 8.75 8.43 8.41 8,1,8,1
IQ4_XS cold 2 15.32 10.34 12.12 12.36 11.30 10.90 16,2,4,1
IQ4_XS cold 3 19.39 11.44 14.82 14.02 13.09 13.09 16,2,4,1
IQ4_XS cold 4 24.94 13.64 17.42 17.63 15.97 15.85 16,2,4,1
Q6_K cold 1 12.43 12.89 12.68 12.47 12.63 12.60 32,1,4,1
Q6_K cold 2 17.32 15.06 15.95 14.47 13.81 13.80 8,1,8,1
Q6_K cold 3 22.77 18.00 22.91 16.89 15.54 15.52 8,1,8,1
Q6_K cold 4 28.06 21.12 26.50 20.43 19.12 19.15 8,1,4,1
IQ3_S cold 1 10.86 10.36 10.32 10.73 12.07 11.76 32,2,4,1
IQ3_S cold 2 15.59 12.38 12.51 14.04 13.65 13.52 16,2,4,1
IQ3_S cold 3 20.06 14.32 14.40 15.82 15.27 15.12 16,2,4,1
IQ3_S cold 4 24.20 16.99 17.47 18.29 17.74 17.54 16,2,4,1

IQ3_S re-run (5 repeats, cold): nt2 8,1,8,1 10.98 / 32,2,4,1 10.19 / 16,2,4,1 10.05; nt3 12.90 / 11.83 / 12.19; nt4 14.50 / 14.34 / 13.98. 16,2 and 32,2 tie; 32,2 chosen so LPR matches nt1.

Target 2: gemv_cat vs two concurrent gemvs (gemv_parity --cat, experiment patch applied) #

Each pair behind one barrier, the two gemvs with no barrier between (as Window::layer), 2 runs each:

    cat IQ4_XS 2560 512+512 nt 1 tile 16,2,4 cold: 2 gemv 13.737 us, gemv_cat 13.753 us (+0.1%)
    cat IQ4_XS 2560 512+512 nt 1 tile 16,2,4 cold: 2 gemv 13.819 us, gemv_cat 13.852 us (+0.2%)
    cat IQ4_XS 2560 512+512 nt 2 tile 16,2,4 cold: 2 gemv 14.479 us, gemv_cat 14.354 us (-0.9%)
    cat IQ4_XS 2560 512+512 nt 2 tile 16,2,4 cold: 2 gemv 14.475 us, gemv_cat 14.383 us (-0.6%)
    cat IQ4_XS 2560 512+512 nt 3 tile 16,2,4 cold: 2 gemv 17.481 us, gemv_cat 18.855 us (+7.9%)
    cat IQ4_XS 2560 512+512 nt 3 tile 16,2,4 cold: 2 gemv 18.378 us, gemv_cat 18.888 us (+2.8%)
    cat IQ4_XS 2560 512+512 nt 4 tile 16,2,4 cold: 2 gemv 21.094 us, gemv_cat 21.678 us (+2.8%)
    cat IQ4_XS 2560 512+512 nt 4 tile 16,2,4 cold: 2 gemv 20.690 us, gemv_cat 21.110 us (+2.0%)
    cat IQ4_XS 2560 512+512 nt 1 tile 16,2,4 hot: 2 gemv 10.519 us, gemv_cat 10.541 us (+0.2%)
    cat IQ4_XS 2560 512+512 nt 1 tile 16,2,4 hot: 2 gemv 10.540 us, gemv_cat 10.569 us (+0.3%)
    cat IQ4_XS 2560 512+512 nt 2 tile 16,2,4 hot: 2 gemv 12.803 us, gemv_cat 12.771 us (-0.3%)
    cat IQ4_XS 2560 512+512 nt 2 tile 16,2,4 hot: 2 gemv 12.800 us, gemv_cat 12.769 us (-0.2%)
    cat IQ4_XS 2560 512+512 nt 3 tile 16,2,4 hot: 2 gemv 14.901 us, gemv_cat 15.067 us (+1.1%)
    cat IQ4_XS 2560 512+512 nt 3 tile 16,2,4 hot: 2 gemv 14.943 us, gemv_cat 15.125 us (+1.2%)
    cat IQ4_XS 2560 512+512 nt 4 tile 16,2,4 hot: 2 gemv 17.509 us, gemv_cat 17.592 us (+0.5%)
    cat IQ4_XS 2560 512+512 nt 4 tile 16,2,4 hot: 2 gemv 17.450 us, gemv_cat 17.559 us (+0.6%)
    cat Q6_K 2560 512+512 nt 1 tile 8,1,4 cold: 2 gemv 20.273 us, gemv_cat 20.993 us (+3.6%)
    cat Q6_K 2560 512+512 nt 1 tile 8,1,4 cold: 2 gemv 20.458 us, gemv_cat 21.054 us (+2.9%)
    cat Q6_K 2560 512+512 nt 2 tile 8,1,4 cold: 2 gemv 21.199 us, gemv_cat 21.048 us (-0.7%)
    cat Q6_K 2560 512+512 nt 2 tile 8,1,4 cold: 2 gemv 21.735 us, gemv_cat 21.322 us (-1.9%)
    cat Q6_K 2560 512+512 nt 3 tile 8,1,4 cold: 2 gemv 22.902 us, gemv_cat 23.149 us (+1.1%)
    cat Q6_K 2560 512+512 nt 3 tile 8,1,4 cold: 2 gemv 22.877 us, gemv_cat 23.111 us (+1.0%)
    cat Q6_K 2560 512+512 nt 4 tile 8,1,4 cold: 2 gemv 26.476 us, gemv_cat 28.002 us (+5.8%)
    cat Q6_K 2560 512+512 nt 4 tile 8,1,4 cold: 2 gemv 28.318 us, gemv_cat 30.941 us (+9.3%)
    cat Q6_K 2560 512+512 nt 1 tile 8,1,4 hot: 2 gemv 13.059 us, gemv_cat 12.609 us (-3.4%)
    cat Q6_K 2560 512+512 nt 1 tile 8,1,4 hot: 2 gemv 12.212 us, gemv_cat 11.709 us (-4.1%)
    cat Q6_K 2560 512+512 nt 2 tile 8,1,4 hot: 2 gemv 16.915 us, gemv_cat 16.627 us (-1.7%)
    cat Q6_K 2560 512+512 nt 2 tile 8,1,4 hot: 2 gemv 16.135 us, gemv_cat 16.060 us (-0.5%)
    cat Q6_K 2560 512+512 nt 3 tile 8,1,4 hot: 2 gemv 20.912 us, gemv_cat 20.629 us (-1.4%)
    cat Q6_K 2560 512+512 nt 3 tile 8,1,4 hot: 2 gemv 20.641 us, gemv_cat 20.426 us (-1.0%)
    cat Q6_K 2560 512+512 nt 4 tile 8,1,4 hot: 2 gemv 25.688 us, gemv_cat 25.883 us (+0.8%)
    cat Q6_K 2560 512+512 nt 4 tile 8,1,4 hot: 2 gemv 25.375 us, gemv_cat 25.923 us (+2.2%)
    cat IQ4_XS 2560 640+640 nt 1 tile 16,2,4 cold: 2 gemv 16.619 us, gemv_cat 16.692 us (+0.4%)
    cat IQ4_XS 2560 640+640 nt 1 tile 16,2,4 cold: 2 gemv 16.687 us, gemv_cat 16.696 us (+0.1%)
    cat IQ4_XS 2560 640+640 nt 2 tile 16,2,4 cold: 2 gemv 16.941 us, gemv_cat 16.938 us (-0.0%)
    cat IQ4_XS 2560 640+640 nt 2 tile 16,2,4 cold: 2 gemv 17.346 us, gemv_cat 17.026 us (-1.8%)
    cat IQ4_XS 2560 640+640 nt 3 tile 16,2,4 cold: 2 gemv 22.748 us, gemv_cat 23.210 us (+2.0%)
    cat IQ4_XS 2560 640+640 nt 3 tile 16,2,4 cold: 2 gemv 22.205 us, gemv_cat 23.013 us (+3.6%)
    cat IQ4_XS 2560 640+640 nt 4 tile 16,2,4 cold: 2 gemv 25.746 us, gemv_cat 26.951 us (+4.7%)
    cat IQ4_XS 2560 640+640 nt 4 tile 16,2,4 cold: 2 gemv 25.801 us, gemv_cat 26.759 us (+3.7%)
    cat IQ4_XS 2560 640+640 nt 1 tile 16,2,4 hot: 2 gemv 12.635 us, gemv_cat 12.580 us (-0.4%)
    cat IQ4_XS 2560 640+640 nt 1 tile 16,2,4 hot: 2 gemv 11.794 us, gemv_cat 11.828 us (+0.3%)
    cat IQ4_XS 2560 640+640 nt 2 tile 16,2,4 hot: 2 gemv 14.355 us, gemv_cat 14.294 us (-0.4%)
    cat IQ4_XS 2560 640+640 nt 2 tile 16,2,4 hot: 2 gemv 14.347 us, gemv_cat 14.337 us (-0.1%)
    cat IQ4_XS 2560 640+640 nt 3 tile 16,2,4 hot: 2 gemv 16.780 us, gemv_cat 17.007 us (+1.4%)
    cat IQ4_XS 2560 640+640 nt 3 tile 16,2,4 hot: 2 gemv 16.950 us, gemv_cat 17.069 us (+0.7%)
    cat IQ4_XS 2560 640+640 nt 4 tile 16,2,4 hot: 2 gemv 20.724 us, gemv_cat 20.625 us (-0.5%)
    cat IQ4_XS 2560 640+640 nt 4 tile 16,2,4 hot: 2 gemv 20.848 us, gemv_cat 21.028 us (+0.9%)
    cat IQ3_S 2560 640+640 nt 1 tile 32,2,4 cold: 2 gemv 15.959 us, gemv_cat 16.974 us (+6.4%)
    cat IQ3_S 2560 640+640 nt 1 tile 32,2,4 cold: 2 gemv 17.358 us, gemv_cat 16.771 us (-3.4%)
    cat IQ3_S 2560 640+640 nt 2 tile 32,2,4 cold: 2 gemv 21.662 us, gemv_cat 21.334 us (-1.5%)
    cat IQ3_S 2560 640+640 nt 2 tile 32,2,4 cold: 2 gemv 22.133 us, gemv_cat 22.124 us (-0.0%)
    cat IQ3_S 2560 640+640 nt 3 tile 32,2,4 cold: 2 gemv 27.146 us, gemv_cat 26.946 us (-0.7%)
    cat IQ3_S 2560 640+640 nt 3 tile 32,2,4 cold: 2 gemv 27.741 us, gemv_cat 27.671 us (-0.3%)
    cat IQ3_S 2560 640+640 nt 4 tile 32,2,4 cold: 2 gemv 32.387 us, gemv_cat 32.224 us (-0.5%)
    cat IQ3_S 2560 640+640 nt 4 tile 32,2,4 cold: 2 gemv 31.193 us, gemv_cat 31.570 us (+1.2%)
    cat IQ3_S 2560 640+640 nt 1 tile 32,2,4 hot: 2 gemv 15.602 us, gemv_cat 15.247 us (-2.3%)
    cat IQ3_S 2560 640+640 nt 1 tile 32,2,4 hot: 2 gemv 15.583 us, gemv_cat 14.706 us (-5.6%)
    cat IQ3_S 2560 640+640 nt 2 tile 32,2,4 hot: 2 gemv 19.240 us, gemv_cat 18.755 us (-2.5%)
    cat IQ3_S 2560 640+640 nt 2 tile 32,2,4 hot: 2 gemv 19.042 us, gemv_cat 18.566 us (-2.5%)
    cat IQ3_S 2560 640+640 nt 3 tile 32,2,4 hot: 2 gemv 23.257 us, gemv_cat 22.716 us (-2.3%)
    cat IQ3_S 2560 640+640 nt 3 tile 32,2,4 hot: 2 gemv 23.515 us, gemv_cat 23.379 us (-0.6%)
    cat IQ3_S 2560 640+640 nt 4 tile 32,2,4 hot: 2 gemv 29.370 us, gemv_cat 28.880 us (-1.7%)
    cat IQ3_S 2560 640+640 nt 4 tile 32,2,4 hot: 2 gemv 28.697 us, gemv_cat 28.610 us (-0.3%)
    cat Q6_K 2560 640+640 nt 2 tile 8,1,4 cold: 2 gemv 25.246 us, gemv_cat 26.583 us (+5.3%)
    cat Q6_K 2560 640+640 nt 2 tile 8,1,4 cold: 2 gemv 27.044 us, gemv_cat 28.421 us (+5.1%)
    cat Q6_K 2560 640+640 nt 3 tile 8,1,4 cold: 2 gemv 33.306 us, gemv_cat 33.523 us (+0.7%)
    cat Q6_K 2560 640+640 nt 3 tile 8,1,4 cold: 2 gemv 32.500 us, gemv_cat 33.620 us (+3.4%)
    cat Q6_K 2560 640+640 nt 4 tile 8,1,4 cold: 2 gemv 37.989 us, gemv_cat 40.487 us (+6.6%)
    cat Q6_K 2560 640+640 nt 4 tile 8,1,4 cold: 2 gemv 37.104 us, gemv_cat 40.660 us (+9.6%)
    cat Q6_K 2560 640+640 nt 2 tile 8,1,4 hot: 2 gemv 21.201 us, gemv_cat 24.492 us (+15.5%)
    cat Q6_K 2560 640+640 nt 2 tile 8,1,4 hot: 2 gemv 21.022 us, gemv_cat 23.201 us (+10.4%)
    cat Q6_K 2560 640+640 nt 3 tile 8,1,4 hot: 2 gemv 26.668 us, gemv_cat 27.057 us (+1.5%)
    cat Q6_K 2560 640+640 nt 3 tile 8,1,4 hot: 2 gemv 26.145 us, gemv_cat 26.466 us (+1.2%)
    cat Q6_K 2560 640+640 nt 4 tile 8,1,4 hot: 2 gemv 32.347 us, gemv_cat 33.714 us (+4.2%)
    cat Q6_K 2560 640+640 nt 4 tile 8,1,4 hot: 2 gemv 33.220 us, gemv_cat 34.377 us (+3.5%)
    cat Q6_K 2560 640+640 nt 1 tile 32,2,4 cold: 2 gemv 23.505 us, gemv_cat 23.975 us (+2.0%)
    cat Q6_K 2560 640+640 nt 1 tile 32,2,4 cold: 2 gemv 23.916 us, gemv_cat 24.354 us (+1.8%)
    cat Q6_K 2560 640+640 nt 1 tile 32,2,4 hot: 2 gemv 19.528 us, gemv_cat 20.770 us (+6.4%)
    cat Q6_K 2560 640+640 nt 1 tile 32,2,4 hot: 2 gemv 19.303 us, gemv_cat 19.804 us (+2.6%)

Why it cannot win: the two GEMVs already overlap on the GPU, x is 10 KB (L1/L2), and the cost is DRAM traffic for W (2×0.7–1.2 MB), which fusing does not reduce. At nt>=3 the cat kernel is slightly worse (extra row-select per row pointer, worse register allocation). Not wired in Window::gemv_cat / Window::layer. Note for whoever owns MTP: the same applies to the existing Q8_0 gm_cat in the draft step — worth re-checking with --cat Q8_0 2560 512 1 32,1,4 cold before keeping it.

Bitwise / ntok analysis (the "LPR decides the sums" claim) #

  • In gemv (KS=1) a row's sum is lane_sum<LPR> over lanes each accumulating units u = li, li+LPR, ... in order; NR only says how many rows a lane carries, SG how many simdgroups share a threadgroup — neither changes a row's order of operations. Format paths that depend on more than LPR (gemv_iq4_packed<LPR> with NTOK, gemv_direct_x with NR/NTOK for Q8_0) are written to produce the same FMA chains, and the new parity check confirms it for all 13 formats: same LPR ⇒ bitwise, for any NR/SG. KS>1 changes the order (partial sums), as expected.
  • Engine tests do not compare a window against single-token runs through Window::gemv_tile (the parity tests call the kernels directly; prompt_parity uses dense_mm), and the existing table already switches LPR across ntok (e.g. old IQ3_S 640, Q8_0, IQ4_XS n_in>=6144). The new 512 tiles and the IQ3_S 640 change remove LPR switches rather than add them; the Q2_0 nt4 change keeps LPR 8.

Verification #

  • gemv_parity (default, all formats/shapes): ALL PASS (includes the new same-LPR bitwise check). Note this full suite takes ~34 s (it always did; the CPU double reference dominates) — the only invocation above 10 s.
  • gemv_parity --cat parity: bitwise for IQ4_XS 16,2,4 / Q6_K 8,1,4 & 32,2,4 / IQ3_S 32,2,4 at 512+512 and 640+640, nt 1/2/3/4/8 (experiment patch) and for the existing Q8_0 instance (main patch).
  • window.cpp compiles (strata_metal_kernels object, scratch build). New tiles are all existing GV_SET instances.
  • Not done (forbidden here): full-model strata_window_cli / server A/B. Recommended next: one decode A/B with --spec 3 where most windows are 2..4 tokens, which is where the 512 fix pays most.

gemv_parity additions (main patch) #

  • --tiles TYPE NIN NOUT NTOK L,R,S,K ... paired multi-tile timing (cold with GEMV_AB_COLD=1, Q2_0R accepted), rel L2 per tile; GEMV_BESIDE=TYPE,NOUT,L,R,S,K times each tile as k+v beside a big GEMV.
  • --cat TYPE NIN NOUT NTOK L,R,S gemv_cat vs its two concurrent gemvs: bitwise + paired timing.
  • default parity: "same LPR: bitwise" invariant.