# opus r36 track A: decode predL_/predL2_ token-0 fast path + gemv micro-opts

patch: `/tmp/opus_r36_decode/opus_r36_decode.patch` (`git -C vendor/strata apply --check` clean against the current working tree)
scratch: `/tmp/opus_r36_decode/strata` (build in `build-metal`, METAL_CPP_DIR set; `window.cpp` + `strata_window_cli` build clean, no new warnings)

## what changed
- `window.cpp` `Window::layer` pred block: when `STRATA_METAL_PREFETCH_DRAFT <= 0` (read the same way as stream.cpp `env_int`, default 1)
  the `predL_` / `predL2_` gemvs run at `ntok = 1` on the window's tile (`tile_ntok = ntok`). otherwise unchanged.
  verified readers: `ExpertStream::ahead` is the only consumer of the float pred rows (via `at_layer` / `after_layer`), and with
  `k_draft_max <= 0` it skips every `t > 0`. prompt chains pass `pred = nullptr` (cut_ids_), so they're unaffected. live
  server (pid 78503) runs with `STRATA_METAL_PREFETCH_DRAFT=0`, so this path is on in production.
  rows t >= 1 of predL_/predL2_ now hold stale data; nothing reads them in this mode.
- `Window::gemv`: new trailing `int tile_ntok = 0` (0 = use ntok). **NOTE: this touches the declaration at window.cpp:320**
  (outside the listed line ranges, but the default parameter can't be added any other way).
- prewarm: no change needed, `prewarm_pipelines` runs `layer()` for t = 1..max_t_, so it makes `gemv_8_16_1_4_1#1`.
- `gemv_parity --pred TYPE NIN NOUT NTOK L,R,S`: new mode. token 0 of 2x gemv#NTOK vs 2x gemv#1 vs 1x gemvc#1, memcmp, then
  3-way rotated paired timings (GEMV_AB_COLD=1 for DRAM weights).
- `gemv.metal`: **no change shipped** (see below).

## measurements (M5, live server running, so a few runs were noisy outliers; the 6x repeats below were stable)
parity: token 0 bitwise for nt = 2, 3, 4 (all three variants). `gemv_parity` default: ALL PASS (7.9 s).

Q8_0 2560 -> 512+512, tile 16,1,4,1, median of 15 rotated rounds, 6 invocations each:
| | 2 gemv #4 (before) | 2 gemv #1 (shipped) | gemvc #1 |
|-|-|-|-|
| hot | 21.64-21.66 us | 18.09-18.14 us (-16.3%) | 18.47-18.54 us (-14.5%) |
| cold | 24.07-24.32 us | 22.72-23.13 us (-5.6%) | 22.82-23.15 us (-5.2%) |

nt=3: hot -3.1%, cold ~0. nt=2: hot +1.3%, cold -0.8% (#2 already takes the raw-x direct path, so it's a wash).
gemv_cat at #1 loses to the concurrent pair in every config, so it's NOT wired.
expected saving per 4-token verify window: ~46 x (1.4 .. 3.5) us = ~65-160 us of GPU time, before overlap with the
router GEMV they sit beside. small but free; not measured end-to-end (no full-model runs per rules).

## gemv.metal experiments (rejected, all bitwise, `--ab` vs pristine kernels)
- `gemv_direct_x` widened to NTOK <= 4 at LPR 16 NR 1: nt=4 +1.7% hot / +4.3% cold (2560->512), +1.3..1.4% (2560->10240);
  nt=3 ~0. matches the existing "wider raw-dot windows did not improve" note. reverted.
- `[[max_total_threads_per_threadgroup(SG * 32)]]` on gemv: 0.0 +- 0.9% across nt 1/4 hot/cold. reverted.
- for reference, single gemv #1 2560->512 Q8_0: 16,1,4,1 5.9 us hot / 12.0 cold, 32,1,4,4 7.1 / 11.7, 32,1,4,1 6.4 / 11.6,
  so a non-bitwise tile swap for the pred path wouldn't buy anything either.
