# opus R19 Track A: fused `ple_comb_query_gate_norm`, register-cached `ple_gnorm`, load-time PSO pre-warm

patch: `/tmp/opus_r19_prewarm/opus_r19_prewarm.patch` (`git -C vendor/strata apply --check` passes against the current working tree)
scratch tree: `/tmp/opus_r19_prewarm/strata` (build: `build-metal/`), A/B bench: `/tmp/opus_r19_prewarm/ab_bench.cpp` (+ binary), baseline kernel copy: `/tmp/opus_r19_prewarm/ple_base.metal`
files touched: `metal/src/engine/msl/ple.metal`, `metal/src/engine/window.cpp` (prewarm region ~475-570, `Window::run` PLE block, `window_init` one line)

## what changed

### ple.metal
- `ple_comb_query_gate_norm` (grid (hc, T), 256 thr): reads R once, forms layer 0's deferred MoE combine on the fly
  (`fma(fma(gs, ysh, sum_k ye[plan[P_TOKENT + t*10 + j]]), g, R)`, gr_norm_comb's exact expression and k order), then
  `gnorm_regs` + `gate_norm_regs`. R written once; gr_norm's xn (dead: layer 1's `gr_read` runs with apply=false and re-norms R
  after the conv) never written. Unrolled fast path for n=2560 && topk=10, generic loop otherwise. `P_TOKENT` defaults to 532
  under `#ifndef` (same as gr2.metal).
- `ple_gnorm`: n=2560 path keeps the row in `xv[NPT]` and reuses `gnorm_regs`, so x is read once (was twice, un-unrolled
  loops). Generic path unchanged. Moved below `gnorm_regs` (needs NPT / the helper).

### window.cpp
- new members in the prewarm region: `ple_key_norm(ds, Tn)` and `ple_after_layer0(ds, Tn)` carry the PLE dispatches, shared by
  `Window::run` and the prewarm (so prewarm now builds exactly run's graph, incl. the PLE kernels; the old prewarm skipped the
  PLE block and so built a `moe_combine_sh` flush that run never uses).
- `ple_after_layer0`: when `comb_bo_ == &bo_ && comb_ntok_ == Tn` it takes the deferred combine and dispatches
  `ple_comb_query_gate_norm`; else (or `STRATA_METAL_PLE_COMB=0`, or `STRATA_METAL_GR_COMB=0`) the old
  `gr_norm` + `ple_query_gate_norm`. Window graph: -1 dispatch, -1 barrier, -120 KB/token DRAM (xn write + R write + R re-read).
- `prewarm_pipelines`: `bool prewarmed_` -> `int prewarmed_t_` (sizes 1..prewarmed_t_ done; a later larger `max_t` warms only the
  new sizes) + `mtp_prewarmed_`. Loop now t = 1..max_t_ (was 2..), with `fold(ds, t, 0)` (gdn_step#t), the PLE block,
  argmax_part/final, and once the MTP layer: `mtp_encode(k = 1..MAXT, full=false)` (mtp_prefill chunks / draft catch-up),
  `mtp_encode(1, full=true)` (draft step), `mtp_pick(0, true)` when `chain_`. Resets `cuts_/sigs_/rec_/cur_inj_/nb_/comb_bo_/comb_ntok_`.
- `prewarm()`: env gate (`STRATA_METAL_PREWARM=0` off), skips while a streaming arena is not made yet (`stream_ && !st_->ready()`:
  the MoE dispatches bind `st_->chunk()`, which indexes an empty vector before the arena), then warms what is missing.
  Called from `window_init` right after `set_max_t` (default path: `prompt_gemm_init` has made the arena by then), and from
  `Window::run` as the lazy fallback (`STRATA_METAL_LAZY_ARENA=1`, or MTP loaded later).

### deliberate deviations from the brief
- no extra `layer(ds, l, 1, 0)` (pos0=0, sel=false) pass: in `layer()` pos0 only feeds `sel`, and `sel=false` only *skips pushing*
  qsa_score/qsa_topk + the indexer q gemv; `P()` for qsa_score/qsa_topk is called either way and the indexer gemv's PSO is the
  same 512-out gemv as attn_k/attn_v. pos0=4096 is a strict PSO superset; the pass would only cost build time.
- no `membp_ = &memb_` swap: `membp_` is nullptr outside `mtp_prefill` (which resets it), and with `tokens == nullptr`
  mtp_encode only binds `(membp_ ? *membp_ : memb_)` as an argument, so the swap is a no-op.
- the `max_t_ > 1` condition is gone: at max_t=1 the first T=1 window's compile now also moves to load time.

## verification (M5 Air; NOTE the live :8080 server shares the GPU, so absolute us are noisy; ratios are paired medians)

bitwise, `ab_bench BASE NEW 128 T` (memcmp on R, norm_w; on y for ple_gnorm; R checked to have moved):

| T | gr_norm_comb + ple_query_gate_norm -> ple_comb_query_gate_norm | ple_gnorm base -> regs |
|---|---|---|
| 1 | R PASS, norm_w PASS; 7.87 -> 6.65 us, ratio 0.84-0.85 | PASS; 2.95 -> 1.43 us, ratio 0.48 |
| 2 | PASS/PASS; 8.5 -> 7.45 us, ratio 0.875 | PASS; 2.9 -> 1.45 us, ratio 0.49-0.50 |
| 3 | PASS/PASS; 9.96 -> 8.59 us, ratio 0.86 | PASS; 2.95 -> 1.69 us, ratio 0.57 |
| 4 | PASS/PASS; 10.7 -> 8.97 us, ratio 0.84 | PASS; 3.0 -> 1.77 us, ratio 0.59 |
| 8 | PASS/PASS; 17.0 -> 13.3 us, ratio 0.78 | PASS; 3.33 -> 2.38 us, ratio 0.71 |

(3 sweeps, every run ALL PASS; the quiet-sweep numbers above, contended sweeps had the same ratios +-0.05.)

existing tests in the scratch build: `ple_moe_parity` ALL PASS (its chain uses the new ple_gnorm against the fused register
kernels, so it cross-checks the new ple_gnorm too), `ple_moe_parity --ab ple_base.metal 7` ALL PASS, `fuse_parity` ALL PASS,
`arena_budget_test` all passed. `strata_window_cli` builds with no new warnings in window.cpp.

## honest scale / not verified
- per decode window the two kernel changes save roughly 1.2 + 1.5 us of GPU time (~3 us vs ~47 ms/token): real but ~0.006%;
  the 120 KB/token DRAM saving is the same order. Not a tok/s lever on its own.
- the pre-warm is the bigger user-visible piece (moves ~280 ms + MTP first-use compiles out of the first request), but it
  was NOT measured end-to-end: per the brief no full-model `strata_window_cli` run and nothing on :8080. Needs one
  load + first-request run to confirm (a) first-request prefill/decode/mtp ms drop, (b) the load-time cost of warming
  (window_ensure passes MAXT=8, so all 8 sizes are compiled at load even if the server runs a smaller max_t), and (c) output
  unchanged (greedy tokens identical with `STRATA_METAL_PLE_COMB=0` vs default).
- static review points for the auditor: prewarm at init mutates only host `mqids_` slot 0 (via mtp_encode full=true at
  cell0=0), which every real full mtp_encode rewrites before use; all other state is restored at the end of
  `prewarm_pipelines`.
