# opus_r24_prompt — Round 24 Track A: gr_norm_h_moec loop inversion + HC/shared-expert epilogues

patch: `/tmp/opus_r24_prompt/opus_r24_prompt.patch` (`git -C vendor/strata apply --check` = OK). touches only
`metal/kernels/prompt.metal` (gr_norm_h_moec, gemm_hc_down) and `metal/tests/comb_fold_parity.cpp` (new `ab` mode).
scratch copy: `/tmp/opus_r24_prompt/strata`, baseline: `/tmp/opus_r24_prompt/base_prompt.metal` (HEAD 296b59d).

## tl;dr
the brief's premise is mostly wrong: the GR kernels are **already at the M5 DRAM roofline** (~129–135 GB/s of 153 GB/s
spec), so register/TG-load tricks can't move them more than ~0.5%. Shipped two small, bitwise-identical wins:

| change | T=512 | T=1024 | T=2048 (prod chunk on 24 GB) |
|---|---|---|---|
| `gemm_hc_down`: `acc / float(e.hc)` -> `acc * 0.25f` | -0.8…-1.0% | -0.8…-1.1% | -1.0…-1.1% |
| `gr_norm_h_moec`: k-outer / i-inner (`s[10]`) for n == 2560 | **+0.4…+0.6%** | -0.2…-0.4% | -0.5…-0.6% |

expected saving per 46-layer T=2048 chunk: moec ~0.02 ms × 46 ≈ 0.9 ms, hc_down ~0.012 ms × 92 (two HC reads/layer)
≈ 1.1 ms -> ~2 ms/chunk total. small. the T=512 moec cost (+~4 µs/layer) only hits tail chunks.

## method
new `comb_fold_parity T ab baseline-prompt.metal [gr|gemm]`: compiles the GR section and the GEMM section of this
prompt.metal and of a baseline copy, runs each kernel (window's tiles at each T: hc_down 32_128_4 / 64_128_4 /
32_128_2, hc_up_reg_32_128_4, swiglu_up_64_128_4) from identical inputs and memcmp's every output (R', xnh, lo_h,
inject, mixh, h) between the two builds; then 12 paired samples × 8 reps, alternating order AND alternating the buffer
set each build runs on (an A/A run showed a +3.6% placement bias on gr_norm_h4 apply 0 before that fix; after it A/A
is 1.000 ± 0.3%, apply 0 ± 1%). reports median ratio and the [3rd, 10th] of 12 ratios. each invocation < 2 s.
also passed: default mode (T 1, 7, 64, 1024 vs mm_combine + gr_norm_h_moe chains), `0 epi` sweep (T 1..2048, every
tile, gemm_hc_down vs gemm_f16 + gr_epi_down: all PASS with `* 0.25f`), `1024 hcup`.

## roofline (why the GR kernels don't move)
T=2048, bytes/call -> achieved:
- gr_norm_h_moec: YE gather 204 MB + R rd/wr 168 MB + xnh 42 MB + ys 21 MB = 435 MB / 3.36 ms = **129 GB/s**
  (kprof's 153.3 ms / 46 layers = 3.33 ms = exactly this T=2048 call)
- gr_apply_moec: 393 MB / 3.05 ms = 129 GB/s
- gr_norm_h4 apply 1: 231 MB / 1.76 ms = 131 GB/s; apply 0: 126 MB / 0.93 ms = 135 GB/s
four structurally different kernels converge on the same ~130 GB/s, i.e. the memory system, not the ALU / TG / register
side. there's nothing left to fuse inside them either: each byte is read or written exactly once.

## tested and NOT shipped (all bitwise PASS, no win)
- `bv[10]` "stack spill": an i-outer fully unrolled variant (constant-indexed bv, moe_comb inlined per i) is neutral
  (0.9987 @512, 1.0002 @2048) -> whatever the compiler does with the dynamic index costs nothing measurable.
- one-barrier tail for hc == 4 (rv[4][10] live, four simd_sums, one barrier; for moec): 1.0049 @512, 0.9976 @2048 —
  worse than the inversion alone, i.e. the stream-wise loop's 4 barriers aren't the bottleneck. reverted.
- `gr_norm_h_stream_out` pointer hoisting (`wc = w + tid`, `oc = o + tid`): gr_norm_h4 1.0007/0.9993/1.0003 — the
  compiler already folds the immediates. reverted to keep the diff minimal.
- gemm_hc_up_reg / gemm_swiglu_up epilogues: nothing to hoist that the compiler doesn't already; their remaining cost
  is `1 / (1 + exp(-x))` divisions, which can't become reciprocals without changing bits. both stay at 1.000 ± 0.2%.

## why `* 0.25f` is bitwise
hc == 4 is already a documented precondition of gemm_hc_down. x / 4 and x * 0.25 are the same real number, so any
correctly rounded op gives the same float (incl. subnormals); the safe-math divide is the expensive multi-instruction
path, which is where the ~1% goes (epilogue sits at the end of every K=10240 tile). verified by the full epi sweep.

## follow-ups (outside my lines)
- `ple.metal`'s `ple_moe_gate_norm_c` has the same moe_comb shape; the inversion should transfer (same ~0.5% at T=2048).
- any real gain on the GR side needs fewer bytes: e.g. fp16/bf16 YE rows (halves the 204 MB gather; NOT bitwise) or
  keeping R in a narrower format — both are numerics changes, not kernel tuning.
