# opus_r10_qsa — Round 10 Track A: `qsa_flash` operand-fill hoisting + vec4 epilogue

patch: `/tmp/opus_r10_qsa/opus_r10_qsa.patch` (`git -C vendor/strata apply --check` clean against the current dirty tree).
touches only `metal/kernels/qsa_prompt.metal` (qsa_flash only, coop_k4 paths only; the `!coop_k4` / M4 code is byte-for-byte
the old code). `qsa_prompt_parity.cpp` unchanged (its `--flash-compare` from r9 was enough).
scratch: `/tmp/opus_r10_qsa/strata`, build `/tmp/opus_r10_qsa/build`, baseline (= current r9 kernel)
`/tmp/opus_r10_qsa/baseline_qsa_prompt.metal`, every variant tried in `/tmp/opus_r10_qsa/v_*/`.

## result (Apple M5, paired alternating `--flash-compare`, all memcmp PASS)

| T | pos0 | layers x pairs | before median (ms) | after median (ms) | delta |
|---|---|---|---|---|---|
| 512  | 0 | x12 x5 | 20.63 / 21.09 | 9.82 / 9.88 | -52.5% |
| 1024 | 0 | x12 x5 | 76.58 / 76.70 | 35.12 / 35.36 | -54.0% |
| 1984 | 0 | x12 x5 | 271.33 / 276.72 | 125.60 / 125.94 | -54.0% |
| 96   | 0 | x1 x9 | 0.132 | 0.072 | -45% |
| 96   | 30000 | x1 x7 | 2.785 | 1.589 | -43% |
| 512  | 30000 | x1 x7 | 13.89 | 8.39 | -40% |
| 1024 | 30000 | x1 x7 | 27.66 | 16.87 | -39% |
| 1984 | 30000 | x1 x7 | 53.94 | 32.76 | -39% |

(two repeats for pos0 0, "a / b".) so `pg:qsa_flash x12` at T1984 should go ~272 -> ~126 ms in kprof (not run: no full-model
benches while :8080 is live).

bitwise: memcmp PASS for T = 96, 512, 1024, 1984 x pos0 = 0, 30000, plus (96,1984), (96,2050), (1,0), (64,1987), (200,1900),
(1,2050), (1,2051), (3,2049), (33,30000). `COOP_K4=0` (the untouched generic path) PASS at (512,0), (1984,30000). full default
`qsa_prompt_parity` (rel-L2 vs qsa_attn+merge, fused qsa_prep memcmp): ALL PASS. every process < 4 s.

## what changed
1. **V operand (rv) fill, coop_k4**: per K=32 run, a lane's 64 elements are 8 cells (runs of 4 cells, second half +16) x 8
   dims (coop_layout 16_64_32 dump). the 8 cells' row pointers and scales are taken once (8 cid + 8 vs_tg reads instead of 64
   + 64), and the 64 byte loads are **unconditional**: an empty cell (id < 0) points at row 0 and gets scale 0.
2. **K operand (rk) fill, coop_k4**: same trick. the lane's 8 cells come from `get_multidimensional_index(4j)[1]` and the
   scale column is `sg` (k >> 6 == sg inside the simdgroup's 64 dims), so pointers + scales are hoisted out of the k0 loop
   (once per chunk), char4 loads unconditional.
3. **epilogue, coop_k4 (target 1)**: step 4, `lsum[h]` once per run, float4 gate load, half4 store. ~-2.5% on its own.

why ±0 is still bitwise: an empty cell now feeds `q * 0.0f` = ±0 instead of +0. the accumulators start at +0 and IEEE RN
gives x + (-0) = x for every x incl. +0, so S and P·V are unchanged; additionally P = 0 exactly on cells c >= nc. the scale
is selected to 0 *before* the multiply, so stale vs_tg/ks_tg entries (only written for id >= 0) can never inject NaN/Inf.

## ablation (T1984 pos0 0, x12, ms after vs ~272 before)
- rv: unconditional loads only (no hoist): 253. hoist + select (`ok ? load*s : 0`): 249. vec4 cid/int4 + float4 math: 299 (worse).
  hoist **and** unconditional: 157. both are needed: with any per-element predicate the compiler keeps 64 predicated,
  non-immediate-offset loads; with neither it can issue 64 base+imm byte loads off 8 pointers.
- + rk same treatment: 146 (unhoisted, uncond) -> 125 (hoisted out of k0).
- + epilogue vec4: 128.5 -> 125.4.
- target 2a (`fac` once per 4 in the tot rescale): 0 effect (125.3 vs 125.4) -> dropped.
- rv hoisted over both c0 runs (16 pointers): 130, worse than per-run (register pressure) -> dropped.
- target 2c (rescale tot in place and `opO.run(tPh, rv, tot)`, no acc): **memcmp FAIL** on M5 (different summation order
  than fma(tot, f, acc)) -> not kept, as required.
- tot.store epilogue into dead sp: not built; the vec4 epilogue is already a 3% item now, a two-round store + barriers
  could at best shave part of that.

## caveats / follow-ups
- the rv hoist assumes every first-half run of a lane has the same 4 cells (true in the M5 dump; coop_probe only checks
  runs of 4 and the +16 second half). a layout change would show up as a rel-L2 FAIL in the default qsa_prompt_parity run.
- the M4 (`!coop_k4`) paths still use branchy per-element loads; the same unconditional+hoist trick will likely pay there too
  but needs an M4 to measure.
- the same "branchy dequant load per element" pattern may exist in qsa_attn_mm / mm_gu3 / mm_down3 right-input fills —
  worth a look by whoever owns those.
