# opus r26 track A: IQ4_XS / IQ4_NL packed-code GEMV + Q2_0R multi-token

patch: `/tmp/opus_r26_gemv/opus_r26_gemv.patch` (`git -C vendor/strata apply --check` passes against the current working tree)
scratch: `/tmp/opus_r26_gemv/strata` (+ `build/gemv_parity`, variant kernel dirs `v_*`, base snapshot `base_kernels`)

## what ships

1. `gemv.metal`: an IQ4_XS path in `gemv` that keeps each row's unit as its 16 code bytes (`packed_uint4`) + scale
   (5 regs/row instead of `float q[32]` + `float4 sc` = 36), loads `xs[32]` once per token for all NR rows, and decodes
   the half2 table lookups inside the combine in **exactly** `unit_combine<32>`'s four FMA chains (s0 = even j, s1 = odd j,
   s2/s3 = the +16 half), then `d*(s0+s1) + d*(s2+s3) - 0.0f`. Bitwise identical to `Dec<T_IQ4_XS>` + `unit_combine<32>`.
   Gated at compile time: `F == T_IQ4_XS && (LPR <= 16 || NTOK >= 4)`.
2. `quant.metal`: `iq4xs_scale(b, ui)` factored out of `deq<T_IQ4_XS>` (same expression; used by both).
3. `gemv_parity.cpp`: `GEMV_AB_COLD=1` for `--ab gemv`: the timed dispatches walk up to 64 copies of W (≤512 MB), so
   the paired A/B measures weights coming from DRAM like a real decode, not hot in SLC.

## what doesn't ship (measured, not a win)

- `#pragma clang loop unroll(full)` on the IQ4 `deq` loops: no-op (compiler already unrolls). ratios 0.98-1.02 = noise.
- IQ4_NL packed path (18-byte blocks are only 2-aligned → ushort/rd32 reads): 0.999-1.0016 everywhere. excluded.
- IQ4_XS packed path at LPR 32, NTOK < 4 (the production 32,2,4,1 decode tile): cold +0.4..+0.7% on 2560->6144/10240,
  hot -5% on 6144<->2560 (SLC-resident) — the hot win disappears once weights come from DRAM, so it stays on the old path.
- reading the codes as ushorts/rd32 instead of one `packed_uint4`: **+11..+16% slower**. wide load is essential.
- hoisting the lookups into `half2 h[NR][16]` for NTOK ≥ 2: same as re-looking-up per token (±0.5%); kept the simpler one.
- runtime-uniform gate on `a.n_in < 6144` to dodge the 6144->2560 3-token loss: both paths in one kernel made it worse
  (6144 nt3 still +1.6%, 2560->640 nt3 gain gone). compile-time only.
- **Target 2 (T_Q2_0R NTOK ≥ 2 pk path)**: bitwise, but 5-rep paired: 8,1,4,1 +0.1/+0.3% (nt2/4), 32,2,4,1 -1.7/+1.0%,
  16,1,4,1 -0.1/+0.3%. not a net win → dropped. also note: `window.cpp` never dispatches `gemv_142_*` (only
  `token_chain`/`pair_parity` tests do; the routed down uses `moe_down_142`), so there's no production consumer anyway.
- `gemv_direct_x` for IQ4: superseded by the packed path (which reads xs once for both rows; direct-x re-reads x per row).

## measurements (M5 Air, paired A/B, median of per-pair ratios, 3 runs each; live server on :8080 sharing the GPU)

new/base ratio; cold = `GEMV_AB_COLD=1` (realistic), hot = default `--ab` (same W every dispatch). production tiles from
`Window::gemv_tile` (m5). all rows: `memcmp == 0`.

| type | shape | ntok | tile | cold µs base -> new | cold | hot | parity |
|---|---|---|---|---|---|---|---|
| IQ4_XS | 2560->10240 | 1 | 32,2,4,1 | 110.36 -> 110.88 | -0.13% | +0.08% | bitwise |
| IQ4_XS | 2560->10240 | 2 | 16,2,4,1 | 112.33 -> 112.8 | -0.05% | -0.07% | bitwise |
| IQ4_XS | 2560->10240 | 4 | 16,2,4,1 | 138.3 -> 136.6 | -1.29% | -1.51% | bitwise |
| IQ4_XS | 6144->2560 | 1 | 32,2,4,1 | 70.86 -> 70.39 | +0.36% | +0.20% | bitwise |
| IQ4_XS | 6144->2560 | 2 | 16,2,4,1 | 71.8 -> 72.25 | +0.32% | -1.15% | bitwise |
| IQ4_XS | 6144->2560 | 4 | 32,2,4,1 | 106.22 -> 105.8 | -0.37% | +0.60% | bitwise |
| IQ4_XS | 2560->6144 | 1 | 32,2,4,1 | 67.9 -> 68.58 | -0.17% | +0.06% | bitwise |
| IQ4_XS | 2560->6144 | 2 | 16,2,4,1 | 69.08 -> 69.38 | +0.13% | -1.39% | bitwise |
| IQ4_XS | 2560->6144 | 4 | 16,2,4,1 | 85.73 -> 84.81 | -1.12% | -1.24% | bitwise |
| IQ4_XS | 2560->12288 | 1 | 32,2,4,1 | 133.17 -> 133.21 | -0.06% | -0.16% | bitwise |
| IQ4_XS | 2560->12288 | 2 | 16,2,4,1 | 135.73 -> 136.04 | -0.02% | +0.37% | bitwise |
| IQ4_XS | 2560->12288 | 4 | 16,2,4,1 | 165.81 -> 163.39 | -1.43% | -1.40% | bitwise |
| IQ4_XS | 2560->640 | 1 | 16,2,4,1 | 11.9 -> 11.88 | -0.42% | -3.61% | bitwise |
| IQ4_XS | 2560->640 | 2 | 16,2,4,1 | 26.12 -> 25.88 | -2.44% | -1.82% | bitwise |
| IQ4_XS | 2560->640 | 4 | 16,2,4,1 | 35.08 -> 34.15 | -2.86% | -2.16% | bitwise |
| IQ4_NL | 640->2560 | 1 | 8,1,4,1 | 14.81 -> 14.6 | +0.05% | +0.02% | bitwise |
| IQ4_NL | 640->2560 | 2 | 4,2,4,1 | 34.31 -> 34.33 | -0.04% | -0.10% | bitwise |
| IQ4_NL | 640->2560 | 4 | 4,2,4,1 | 45.91 -> 45.82 | -0.08% | +0.01% | bitwise |

extra 5-rep cold runs: 2560->640 nt1 -5.0% (ratios 0.946-0.952; at a faster clock state the same shape gave -1.5%, the
gain is clock-state dependent but never negative), 2560->640 nt3 -2.8%, 2560->10240 nt3 -2.0%, 2560->6144 nt3 -1.6%,
**6144->2560 nt3 (16,2,4,1) +1.4%** (the one consistent regression; nt2 +0.3%, nt4 uses 32,2,4,1 → -0.4%).

rough per-step accounting with the census counts (cold, µs): ntok 1: only shexp gate+up (57 × 2560->640) changes,
≈ -0.4..-1.5 µs each → ≈ -25..-85 µs/token. ntok 2: ≈ -26 µs. ntok 3: ≈ -130 µs (qkv/gate/shexp wins outweigh
ssm_out/attn_out +1.4%). ntok 4: everything 16,2,4,1/32,2,4,1 -1..-3%. small but net-positive at every ntok;
the big DRAM-bound shapes at ntok 1 (qkv, q, LM head 2560->248320) are at ~125-135 GB/s and nothing here moves them.

## verification

- `gemv_parity --ab` parity-only: IQ4_XS 2560->1000 (ragged n_out) × all 19 valid kCfgs tiles × ntok 1,2,3,4,5,8: all bitwise.
- controls (unchanged code paths) bitwise: Q8_0 (32,1,4,4 / 32,2,4,1), IQ3_S (32,2,4,1 / 8,1,4,1), Q6_K (16,1,4,1 / 32,2,4,1),
  Q2_0R (8,1,4,1 nt 1/3), IQ4_NL nt3, IQ4_XS nt8, LM head 2560->248320.
- full `gemv_parity` (every format × n_in 2560/640/10240 × ntok 1/3/8 × 20 tiles, vs double + window == one-token bitwise):
  **ALL PASS** (exit 0, 49 s — longer than the 10 s guideline; it's the whole suite, run once).

## side observation (not my file, for whoever owns window.cpp)

timings swing 2-3× between runs (e.g. 2560->640 base 11.9 vs 30.9 µs, IQ4_NL 640->2560 14.8 vs 34 µs) — GPU clock state /
the live server. paired ratios were stable (±0.5%) inside a run, so all conclusions above are from ratios, not absolutes.
