# round 12 track c: `gemm_f16` 2d l2/slc tile swizzling & `gemm_hc_up_reg` tile + epilogue optimization

- **patch:** `/tmp/argon_r12_gemm/argon_r12_gemm.patch` (`git -C vendor/strata apply --check` exits 0; touches `metal/kernels/prompt.metal` and `metal/src/engine/window.cpp` only)
- **scratch checkout & standalone benchmarks:** `/tmp/argon_r12_gemm/strata` (`build-metal`), `/tmp/argon_r12_gemm/bench_r12.cpp`, `/tmp/argon_r12_gemm/bench_swz2.cpp`, `/tmp/argon_r12_gemm/bench_swz_overhead.cpp`, `/tmp/argon_r12_gemm/bench_epi_ab.cpp`, `/tmp/argon_r12_gemm/window.o`

---

## 1. executive summary & decision

1. **target 1 (`gemm_f16` 2D L2/SLC tile swizzling in `metal/kernels/dense_mm.metal`): REJECTED (0 changes to `dense_mm.metal`).**
   - All tested 2D threadgroup swizzle mappings (`GROUP_M = 2, 4, 8, 16`, `GROUP_N = 2, 4, 8`, and pure grid transpose `my = tg.x, nx = tg.y`) are **100% bitwise identical (`memcmp == 0`)** across all 12 production `(N, K)` shapes and all `M = 256, 512, 1024, 1983, 2048`.
   - However, on Apple M5 (`64_128_4`, `128_64_4`, `32_128_2` selected in Round 11), the hardware threadgroup walker already schedules 2D `(tg.x, tg.y)` grids across the 10 GPU cores with strong L2/SLC locality. Any software swizzle either ties within `< 0.2%` noise (`GN4`, `GN8`, `GM2` at `M = 2048`) or **severely degrades M5's hardware L2/SLC prefetching** on `64_128_4` at `M = 1983, 2048` (e.g., pure transpose slows the 12-shape chunk from `978.2 ms` to `1,966.1 ms`, `GM16` slows it to `1,302.6 ms`, `GM8` slows it to `1,013.5 ms`).
   - Even when compiled as zero-argument compile-time template constants (`bench_swz_overhead`), no swizzle variant delivers a repeatable speedup across `M = 256..2048`. Following the falsifier rule ("Keep ONLY what delivers a clean, repeatable speedup across the production shapes"), `dense_mm.metal` is left untouched.

2. **target 2 (`gemm_hc_up_reg` tile shape & epilogue optimization in `metal/kernels/prompt.metal` + `metal/src/engine/window.cpp`): SHIPPED (`+6.9%` to `+7.8%` faster, saving `7.3–7.9 ms/chunk` at `T = 1983..2048`).**
   - **Epilogue Unroll & Pointer Hoisting (`prompt.metal`):** Added `#pragma clang loop unroll(full)` to the outer cooperative-tensor loop (`i += 4`) and inner 4-stream reduction loop (`c = 0..3`), hoisted `device const half* xnh_t = xnh + ulong(t) * 4 * n + d;` and `const ulong out_idx = ulong(t) * n + d;`, and replaced `s = s / 4.0f` with `s *= 0.25f`. Isolated paired A/B testing (`11` interleaved rounds) shows the epilogue optimization alone yields a consistent **1.008x–1.019x (`~1.0–1.9%`)** speedup across every tile (`64_64_4`, `64_128_4`, `32_128_4`) and every `T`, while remaining **100% bitwise identical (`memcmp == 0`)** on both `mixh` (`fp16`) and `mixed` (`fp32`).
   - **Tile Shape Sweep (`32_128_4` across all `T` on M5 `k4`):** Sweeping 10 register tile instantiations (`64_64_4`, `64_128_4`, `128_64_4`, `64_32_4`, `32_128_2`, `32_128_4`, `64_128_2`, `32_256_4`, `64_256_4`, `32_64_2`, `64_64_2`, `16_128_4`, `16_128_2`, `32_64_4`, `16_64_2`) revealed that **`gemm_hc_up_reg_32_128_4`** (`TM = 32, TN = 128, SG = 4`, `0` bytes threadgroup memory) is the fastest tile across **every single `T` from `1` to `2048`**:
     - Why `32_128_4` wins: Halving `TM` from `64` to `32` while keeping `TN = 128, SG = 4` cuts each thread's cooperative-tensor register footprint in half (`acc.get_capacity()` drops from `64` floats to `32` floats per lane = `8` outputs x `4` streams). Lower register pressure boosts M5 occupancy while keeping 4 simdgroups cooperating across the `K = 320` reduction and 32 coalesced output columns per threadgroup. By contrast, `SG = 2` variants (`32_128_2`, `64_128_2`) increase per-lane accumulator capacity or halve K-reduction parallelism.
     - Furthermore, at `T = 1983`, `TM = 64` pads to `31 * 64 = 1984` (`1` wasted row), whereas at common non-multiple-of-64 lengths `TM = 32` also reduces tail padding.
   - Combined (`gemm_hc_up_reg_32_128_4` + unrolled/hoisted epilogue), the 96-call per-chunk time drops from **`112.39 ms -> 105.10 ms` (`1.069x`, `-7.29 ms/chunk`)** at `T = 1983` and **`116.52 ms -> 108.76 ms` (`1.071x`, `-7.76 ms/chunk`)** at `T = 2048`.

---

## 2. target 1 evidence: `gemm_f16` 2d l2/slc tile swizzling

### 2.1 tested swizzle mappings (`bench_swz_overhead.cpp` & `bench_swz2.cpp`)
All variants were compiled as zero-overhead template specializations (`orig`, `GM4`, `GM8`, `GM16`, `GN4`, `GN8`, `Transp`) and verified against `orig` (`memcmp == 0` on `0xFF`-poisoned output buffer `Y` across all 12 production shapes at `M = 256, 512, 1024, 1983, 2048`, `fails = 0`).

### 2.2 paired 12-shape chunk totals on quiet M5 (`7` interleaved rounds per shape)

| `M` | `orig` (ms/chunk) | `GM4` (ms/chunk) | `GM8` (ms/chunk) | `GM16` (ms/chunk) | `GN4` (ms/chunk) | `GN8` (ms/chunk) | `Transp` (`M`-major, ms/chunk) | Bitwise `memcmp == 0` |
|---:|---:|---:|---:|---:|---:|---:|---:|:---:|
| `256` | **192.05** | 192.40 | 192.63 | 192.90 | 191.97 | 192.76 | 192.13 | PASS (`0` fails) |
| `512` | **251.60** | 250.60 | 253.35 | 250.84 | 249.88 | 251.14 | 252.23 | PASS (`0` fails) |
| `1024` | **512.02** | 515.03 | 523.92 | 511.98 | 510.58 | 512.69 | 512.60 | PASS (`0` fails) |
| `1983` | **978.22** | 981.79 | 1013.48 | 1302.59 | 976.02 | 976.60 | 1966.07 | PASS (`0` fails) |
| `2048` | **1011.13** | 1004.93 | 1024.29 | 1652.16 | 1011.77 | 1012.36 | 1856.75 | PASS (`0` fails) |

### 2.3 per-shape breakdown at `M = 1983` (us/call, median of 7 interleaved rounds)

| Shape | `N x K` | Calls | Tile | `orig` (us) | `GM4` (us) | `GM8` (us) | `GM16` (us) | `GN4` (us) | `GN8` (us) | `Transp` (us) |
|---|---|---:|---|---:|---:|---:|---:|---:|---:|---:|
| `hc_read` | `324 x 10240` | 96 | `32_128_2` | 1164.0 | 1160.6 | 1172.6 | 1170.2 | 1161.6 | 1164.5 | 1161.9 |
| `Wqkv` | `10240 x 2560` | 37 | `64_128_4` | **7200.0** | 7223.1 | 7446.8 | 8167.7 | 7191.7 | 7198.1 | 17324.6 |
| `Wz` | `6144 x 2560` | 36 | `64_128_4` | **4329.4** | 4345.2 | 4444.9 | 4874.5 | 4329.7 | 4325.7 | 9248.7 |
| `Wout` | `2560 x 6144` | 48 | `64_128_4` | **4450.2** | 4439.6 | 4753.0 | 9708.2 | 4408.6 | 4414.2 | 10608.4 |
| `Wq` | `12288 x 2560` | 12 | `64_128_4` | **8627.3** | 8682.5 | 8913.1 | 9759.4 | 8638.0 | 8635.9 | 20167.9 |
| `Wpv` | `2560 x 2560` | 1 | `64_128_4` | **1806.3** | 1821.4 | 1897.5 | 2640.2 | 1807.1 | 1817.4 | 2226.2 |
| `Wshgu` | `640 x 2560` | 96 | `64_128_4` | **468.2** | 478.8 | 487.8 | 479.6 | 467.2 | 467.6 | 474.5 |
| `Wshd` | `2560 x 640` | 48 | `64_128_4` | **458.6** | 460.8 | 462.4 | 460.9 | 458.6 | 460.0 | 460.7 |
| `W512` | `512 x 2560` | 119 | `32_128_2` | **401.7** | 410.8 | 409.6 | 404.3 | 403.7 | 402.7 | 410.5 |
| `Wik` | `128 x 2560` | 12 | `32_64_2` | **105.0** | 106.0 | 106.3 | 104.3 | 105.5 | 104.2 | 105.7 |
| `Walbe` | `48 x 2560` | 72 | `64_32_2` | **80.9** | 82.0 | 81.8 | 81.4 | 81.4 | 81.3 | 82.3 |
| `Wsg` | `1 x 2560` | 48 | `64_32_2` | **71.4** | 71.5 | 71.6 | 71.5 | 71.4 | 71.4 | 71.3 |

**Analysis:** On M5, `gemm_f16_64_128_4` reads `64` rows of `A` (`320 KB` at `K = 2560`) and `128` rows of `B` (`640 KB` at `K = 2560`) per threadgroup. When Metal dispatches `(tg.x, tg.y)` in its native order, simultaneously active threadgroups across the 10 GPU cores share the `320 KB` `mA` slice in L2 while streaming consecutive `mB` slices sequentially. Remapping `tg` into `M`-major strips (`GM >= 8` or `Transp`) forces concurrent threadgroups to read disjoint `mA` rows (`31 * 320 KB = 9.9 MB`) while hammering the same `mB` slice, thrashing L2 and causing up to a `2.4x` slowdown on `Wqkv`/`Wout`/`Wq`.

---

## 3. target 2 evidence: `gemm_hc_up_reg` tile sweep & epilogue optimization

### 3.1 full tile + swizzle sweep (`bench_r12.cpp` & `bench_hcup2.cpp`, 96 calls/chunk, unbound `mixed`)

All candidates verified `memcmp == 0` on both `mixh` (`fp16`) and `mixed` (`fp32`, when bound) across `T = 1, 4, 7, 8, 16, 24, 32, 48, 64, 128, 256, 384, 512, 768, 1024, 1536, 1983, 2048` (`0` failures).

| Candidate (`N = 10240, K = 320`, 96 calls) | `T = 256` (ms) | `T = 512` (ms) | `T = 1024` (ms) | `T = 1983` (ms) | `T = 2048` (ms) |
|---|---:|---:|---:|---:|---:|
| `gemm_hc_up_reg_64_64_4` (old `T < 1024`) | 15.55 | 30.22 | 60.84 | 116.95 | 125.29 |
| `gemm_hc_up_reg_64_128_4` (old `T >= 1024`) | 16.39 | 30.61 | 58.92 | 113.47 | 123.54 |
| `gemm_hc_up_reg_32_128_2` (opt epilogue) | 17.12 | 31.78 | 61.36 | 117.49 | 126.48 |
| **`gemm_hc_up_reg_32_128_4` (opt epilogue, SHIPPED)** | **14.82** | **28.41** | **55.22** | **106.65** | **115.31** |
| `gemm_hc_up_reg_64_128_2` (opt epilogue) | 32.80 | 67.52 | 134.08 | 258.16 | 267.07 |
| `gemm_hc_up_reg_32_256_4` (opt epilogue) | 17.09 | 31.05 | 59.58 | 114.06 | 123.90 |
| `gemm_hc_up_reg_64_256_4` (opt epilogue) | 35.40 | 71.75 | 140.89 | 266.12 | 277.68 |
| `gemm_hc_up_reg_32_64_2` (opt epilogue) | 15.93 | 31.29 | 62.00 | 120.51 | 125.26 |
| `gemm_hc_up_reg_64_64_2` (opt epilogue) | 19.27 | 36.55 | 70.58 | 135.13 | 149.28 |
| `gemm_hc_up_reg_16_128_4` (opt epilogue) | 16.95 | 33.08 | 62.82 | 118.92 | 122.81 |
| `gemm_hc_up_reg_32_64_4` (opt epilogue) | 17.48 | 34.72 | 71.65 | 138.51 | 143.11 |

### 3.2 isolated paired A/B: baseline epilogue vs unrolled/hoisted epilogue (`bench_epi_ab.cpp`, 11 interleaved rounds, 96 calls/chunk)

| `T` | `mixed` | `64_64_4` base -> opt (ms) | `64_128_4` base -> opt (ms) | `32_128_4` base -> opt (ms) | Old Shipped -> New Shipped (`opt_32_128_4`) | Saved (ms/chunk) |
|---:|---|---:|---:|---:|---:|---:|
| `16` | unbound | `4.67 -> 4.64` (`1.006x`) | `5.39 -> 5.42` (`0.994x`) | `4.29 -> 4.29` (`1.000x`) | `4.67 -> 4.29 ms` (**1.089x**) | `-0.38 ms` |
| `64` | unbound | `5.18 -> 5.16` (`1.004x`) | `6.19 -> 6.16` (`1.005x`) | `5.01 -> 4.99` (`1.004x`) | `5.18 -> 4.99 ms` (**1.039x**) | `-0.19 ms` |
| `256` | unbound | `15.40 -> 15.28` (`1.008x`) | `16.11 -> 15.91` (`1.013x`) | `14.60 -> 14.46` (`1.009x`) | `15.40 -> 14.46 ms` (**1.065x**) | `-0.94 ms` |
| `256` | bound | `16.17 -> 16.02` (`1.009x`) | `16.71 -> 16.59` (`1.007x`) | `15.24 -> 15.09` (`1.010x`) | `16.17 -> 15.09 ms` (**1.072x**) | `-1.08 ms` |
| `512` | unbound | `30.09 -> 29.84` (`1.008x`) | `30.34 -> 30.03` (`1.011x`) | `28.44 -> 28.15` (`1.010x`) | `30.09 -> 28.15 ms` (**1.069x**) | `-1.94 ms` |
| `512` | bound | `31.45 -> 31.15` (`1.010x`) | `31.27 -> 30.98` (`1.009x`) | `29.52 -> 29.18` (`1.011x`) | `31.45 -> 29.18 ms` (**1.078x**) | `-2.27 ms` |
| `1024` | unbound | `60.22 -> 59.46` (`1.013x`) | `58.59 -> 57.94` (`1.011x`) | `55.36 -> 54.79` (`1.010x`) | `58.59 -> 54.79 ms` (**1.069x**) | `-3.80 ms` |
| `1024` | bound | `62.31 -> 61.73` (`1.009x`) | `60.11 -> 59.46` (`1.011x`) | `56.97 -> 56.41` (`1.010x`) | `60.11 -> 56.41 ms` (**1.066x**) | `-3.70 ms` |
| `1983` | unbound | `117.70 -> 115.56` (`1.019x`) | `112.39 -> 111.05` (`1.012x`) | `106.18 -> 105.10` (`1.010x`) | `112.39 -> 105.10 ms` (**1.069x**) | **`-7.29 ms`** |
| `1983` | bound | `120.43 -> 118.95` (`1.012x`) | `115.50 -> 114.34` (`1.010x`) | `109.08 -> 108.00` (`1.010x`) | `115.50 -> 108.00 ms` (**1.069x**) | **`-7.50 ms`** |
| `2048` | unbound | `120.95 -> 119.88` (`1.009x`) | `116.52 -> 115.16` (`1.012x`) | `109.82 -> 108.76` (`1.010x`) | `116.52 -> 108.76 ms` (**1.071x**) | **`-7.76 ms`** |
| `2048` | bound | `125.55 -> 124.51` (`1.008x`) | `119.80 -> 118.50` (`1.011x`) | `112.86 -> 111.80` (`1.009x`) | `119.80 -> 111.80 ms` (**1.072x**) | **`-8.00 ms`** |

---

## 4. verification & parity results

1. **`dense_mm_parity`:**
   - `/tmp/argon_r12_gemm/strata/build-metal/dense_mm_parity`: **ALL PASS** (`3.05s`, all 13 quant formats x 6 shapes, `memcmp(wh)=0, memcmp(xh)=0`).
   - `/tmp/argon_r12_gemm/strata/build-metal/dense_mm_parity --tiles M` (`M = 256, 512, 1024`) and `--ab M` (`M = 1983, 2048`): **ALL BITWISE** (`< 3s` per invocation).
2. **`comb_fold_parity`:**
   - `/tmp/argon_r12_gemm/strata/build-metal/comb_fold_parity 64`: **ALL PASS** (`4.67s`, `T = 1, 7, 64`).
   - `/tmp/argon_r12_gemm/strata/build-metal/comb_fold_parity T hcup confirm` (`T = 64, 512, 1024, 1983, 2048`): **ALL PASS** (`< 2.1s` per invocation, `tgmem = 0` on all `reg` kernels).
3. **Engine compilation & patch check:**
   - `window.cpp` compiled to `/tmp/argon_r12_gemm/window.o` using `strata_metal_kernels` release flags: **exit 0** (`0` warnings/errors).
   - `git -C vendor/strata apply --check /tmp/argon_r12_gemm/argon_r12_gemm.patch`: **exit 0**.
