# Round 36 Track C: `qsa_prep` Single-Barrier `norm_rope` & `w == 128` `tg_sum` + `qsa_score_mm` Loop Unrolling

## Summary & Decision

- **Owner:** `argon-r36-qsa-prep-score-and-gdn`
- **Owned files:** `metal/kernels/qsa.metal`, `metal/kernels/qsa2.metal`, `metal/kernels/qsa_prompt.metal`, `metal/tests/qsa_parity.cpp`, `metal/tests/qsa_prompt_parity.cpp`
- **Kernel decision:** **No-ship on kernel changes (`qsa.metal`, `qsa2.metal`, `qsa_prompt.metal` unchanged)**; **ship paired A/B harness improvements (`--ab` in `qsa_parity.cpp` and `qsa_prompt_parity.cpp`)**.
  - All tested `qsa_prep` and `qsa_score_mm` kernel variants achieved **100% IEEE-754 bitwise parity (`memcmp == 0`)** across every output buffer (`qcur`, `qidx`, `kq`, `vq`, `ksc`, `vsc`, `tail`, `dead`, `pooled`, `scores`, `ids`, `part`, `xp`), including `QSA_MROPE=1` and `QSA_Q_F16_FC=true`.
  - However, on quiet Apple M5 (10-core GPU) in warmed 15-pair alternating A/B benchmarks, the current working-tree `qsa_prep` and `qsa_score_mm` kernels are already at their M5 optimum:
    1. **Single-barrier `norm_rope` (`if (rope && tid < QROT) row[tid] = x;` before `tg_sum256` + partner scale `(rs * row[tid ^ 32]) * g[tid ^ 32]`):** Bitwise identical (`memcmp == 0`), but slightly slower across decode and prompt shapes (`ratio 1.0006–1.0136`, `+0.5%` to `+1.4%` at `T = 1, 1984, 2048`) because holding `x` live across `tg_sum256` + issuing a second uncoalesced/divergent `device const float* g` load (`g[tid ^ 32]`) for `tid < 64` outweighs the threadgroup barrier savings on M5.
    2. **`tg_sum256` `#pragma clang loop unroll(full)` + `nsg = (w <= 128 ? 4 : 8)`:** Bitwise identical (`memcmp == 0`), neutral within `±0.2%` at `T = 1984, 2048` (`961.05 -> 962.95 us`, `ratio 1.0010` at `P=0, T=1984`; `1000.15 -> 1005.97 us`, `ratio 0.9994` at `P=3000, T=1984`; `1039.76 -> 1044.30 us`, `ratio 1.0004` at `P=3000, T=2048`), because Metal Clang already unrolls `nsg = 8` when constant-folded and `qsa_prep` is memory-bandwidth bound on `qfull` / `kcur` / `vcur` / `kq` / `vq`.
    3. **`qsa_score_mm` `#pragma clang loop unroll(full)` + `qp` pre-offsetting (`qsa_prompt.metal`):** Bitwise identical (`memcmp == 0`), neutral (`0.1788 -> 0.1770 ms` at `pos0=0`, `0.4654 -> 0.4699 ms` at `pos0=2051`, `1.3466 -> 1.3326 ms` at `pos0=8192`, `4.4671 -> 4.4764 ms` at `pos0=30000` for `T = 1984`), confirming Round 26 (`audits/sol_r26_qsa.md`) that `qsa_score_mm` is dominated by the `64x64x128` `matmul2d` cooperative tensor op and DRAM reads/writes.

## Measured Quiet-GPU A/B Results

### 1. `qsa_prep` Variants (`qsa_parity --paired` / `--ab`, 15 alternating pairs of 8x/32x dispatches)

| Shape (`P`, `nt`) | Baseline (`us`) | V1: Single-Barrier `norm_rope` + `nsg=4` (`us` / ratio) | V2: Two-Barrier + `nsg=4` + `unroll` (`us` / ratio) | V3: `tg_sum256` `unroll` only (`us` / ratio) | Bitwise `memcmp` |
|---|---:|---:|---:|---:|---|
| `P=3000, nt=1` | `7.047` | `7.090` (`1.0136`) | `6.939` (`0.9881`) | `7.039` (`0.9952`) | `PASS` (all 13 buffers) |
| `P=3000, nt=2` | `2.720` | `2.730` (`1.0029`) | `2.699` (`0.9957`) | `2.329` (`0.9967`) | `PASS` (all 13 buffers) |
| `P=3000, nt=4` | `2.879` | `2.859` (`1.0004`) | `2.879` (`0.9982`) | `2.854` (`0.9959`) | `PASS` (all 13 buffers) |
| `P=3000, nt=8` | `4.611` | `4.598` (`0.9972`) | `4.581` (`0.9835`) | `4.579` (`0.9693`) | `PASS` (all 13 buffers) |
| `P=0, nt=1984` | `969.125` | `969.359` (`1.0006`) | `962.948` (`1.0010`) | `963.656` (`0.9987`) | `PASS` (all 13 buffers) |
| `P=3000, nt=1984` | `1001.609` | `1013.505` (`1.0071`) | `1005.969` (`0.9994`) | `1006.901` (`1.0026`) | `PASS` (all 13 buffers) |
| `P=3000, nt=2048` | `1038.339` | `1042.750` (`1.0051`) | `1044.297` (`1.0004`) | `1036.656` (`1.0036`) | `PASS` (all 13 buffers) |

### 2. `qsa_score_mm` Loop Unrolling + `qp` Pre-offsetting (`qsa_prompt_parity --mm-compare score`, 15 alternating pairs)

| `T` | `pos0` | Baseline (`ms`) | Candidate (`ms`) | Delta | `static_tg_mem` | Pre/Post `memcmp` |
|---:|---:|---:|---:|---:|---:|---|
| `1984` | `0` | `0.178750` | `0.176958` | `-1.00%` | `16384 -> 16384` | `PASS / PASS` |
| `1984` | `2051` | `0.465417` | `0.469875` | `+0.96%` | `16384 -> 16384` | `PASS / PASS` |
| `1984` | `8192` | `1.346583` | `1.332583` | `-1.04%` | `16384 -> 16384` | `PASS / PASS` |
| `1984` | `30000` | `4.467125` | `4.476417` | `+0.21%` | `16384 -> 16384` | `PASS / PASS` |

## Harness Additions Shipped in Patch

- **`metal/tests/qsa_parity.cpp`:**
  - Added `--ab REF_DIR [P]` mode that runs full-buffer bitwise parity (`memcmp == 0`) and warmed alternating paired timings across decode `nt = 1, 2, 4, 8` and prompt `nt = 1984, 2048` in a single `< 6s` invocation.
  - Switched `--paired` timing loops to use `cx.run({ds[v][i]}, repeats)` with `cx.warm(...)` and removes per-turn CPU `memset` of GPU buffers inside the timing loop.
- **`metal/tests/qsa_prompt_parity.cpp`:**
  - Added `--ab T pos0 baseline_qsa_prompt.metal baseline_qsa.metal pairs` mode testing `qsa_prep`, `qsa_score_mm`, and `combined` (`qsa_prep + qsa_score_mm`) with full 10-buffer bitwise `memcmp` verification (supporting `QSA_MROPE=1` and `QSA_QF16=1`) and 12-layer-per-command-buffer paired A/B timings.
  - Added `cx.warm(...)` before `--mm-compare` paired timing loops.

## Verification Artifacts

- Unified patch: `/tmp/argon_r36_qsa/argon_r36_qsa.patch`
- `git -C vendor/strata apply --check /tmp/argon_r36_qsa/argon_r36_qsa.patch`: exit `0` (clean).
- Baseline kernel snapshots preserved in `/tmp/argon_r36_qsa/base_kernels/`.
