# phase 1a v2 / 1b replayer audit (read-only)

scope: `scripts/assemble_corpus.py`, `scripts/patch_imatrix.py`, `scripts/select_and_eval_prunes.py`, `scripts/replay_moe_cache.py`, `results/reap_saliency_v2.json`, `results/keep_maps/` (9 files), against `audits/opus_audit_phase1_methodology.md`.
run audited (parent transcript seq 173661): `llama-imatrix ... -f calibration_mix_v2.txt --parse-special -c 2048 -b 512 -ngl 99 --chunks 64 --output-frequency 5 --no-ppl`.

## verdict

- JSON invariants: **all pass** on all 48 layers.
- v1 findings: the corpus/leak ones (A2, A4–A8) are fixed. A1/A3 are only partly fixed, B2/B4 partly, B3 half, C3 still open.
- **new bug: `reap_shrink`'s prior keeps every dead expert.** all 8 zero-hit experts in the model are kept at every K, and the method replaces 40% of top-10 slots at K=224.
- replayer math: correct. lazy EMA matches an eager reference exactly. there are 3 edge cases (below).
- for 1c, prioritize `reap_l2_sum` and `reap_hybrid`. don't slice `reap_shrink` until its prior is fixed.

## 1. v1 findings vs v2

| id | status | evidence |
|---|---|---|
| A1 prefix only | **partial** | `--chunks 64 -c 2048` = 131,072 tok of a 3.78 MB file (~1M tok), so ~12%, about the first ~35–40 of ~330 rounds. the round-robin (assemble_corpus.py L219–229) makes the prefix mixed, but only ~40/300 ukisai, ~10/24 TB, ~40/260 whittle and ~40 JA docs are seen. the tail of the file is JA-only rounds (ja list ≈ 330 > 300). |
| A2 parse-special | fixed | flag is in the command. ukisai and hotchpotch are still hand-wrapped (L64, L184), not `apply_chat_template`. minor. |
| A3 char vs token | partial | the per-round budget is a char heuristic (comment at L208–213). it was never tokenized, so the shares aren't verified. |
| A4 TB leak | fixed | xhigh only, one prompt per task, task-disjoint assert (L80–91) |
| A5 ChiTako leak | fixed | eval JA now comes only from hotchpotch. ChiTako is shingle-deduped (L156–163), and hp is url-unique plus shingle-filtered against everything before it (L168–192). |
| A6 ukisai leak/preamble | fixed | last episode per trial, one trial per base task, disjoint assert, preamble kept in 2 docs only (L29–71). nit: the regex at L26 strips only one suffix, so `X_run2__dsv4…` → `X_run2`. |
| A7 whittle | fixed | `prompt+response`, kind quotas, left-truncation (L115–142). only shard 0 is used. |
| A8 hotchpotch split | fixed | explicit `test-00000-of-00001.parquet` (L167) |
| B2 hook ids | partial | ids are still read from `down_t->src[2]` (patch L224–230), not `ffn_moe_topk`. the non-MUL_MAT_ID case now aborts (L225, good), but `--moe-cache-mib` would still silently mis-attribute experts. not triggered by this run. |
| B3 trace | half | header rewritten on every flush (L107–111) ✓. no `(chunk,pos,token_id)` per row ✗ |
| B4 router imatrix | open | `return true` at L220. minor. |
| nonfinite / empty-JSON guard | fixed | L149–155, L163. JSON `nonfinite_drops = 0` |
| C1 metric | partial | shrink and hybrid were added, but the shrink prior is wrong (§3). no per-domain REAP. |
| C2 substitution screen | added | `score_trace_substitution` (select L73–107) |
| C3 vision | open | known. keep maps aren't exportable. |

## 2. `reap_saliency_v2.json` invariants (direct check, all 48 layers)

- `Σcount == 1,310,720` on every layer ✓. `Σprob_sum == 131,072.000` on every layer ✓. `trace_tokens_written = 16384` ✓, `nonfinite_drops = 0` ✓.
- `count==0 ⇔ reap_l2_sum==0` on every layer ✓. all values finite ✓. Cauchy–Schwarz `sq_sum ≥ sum²/count` holds everywhere, apart from one rounding tie at L30 e87 (count 1, %.6f).
- zero-hit experts: 8 total (L0: 181, 193, 244, 271, 413, 424; L1: 116; L47: 371).
- the 9 keep maps match a re-run of `compute_layer_scores` exactly. they are sorted, unique, size K and nested across K (224 ⊂ 256 ⊂ 320).

per-layer view at K=224 (share of `count` kept / share of `reap_l2_sum` kept / smallest kept count):

| L | l2_sum | hybrid | shrink | overlap sum∩hyb, sum∩shr, hyb∩shr | μ_layer vs median expert mean |
|---|---|---|---|---|---|
| 0 | .59/.78/927 | .56/.77/927 | .53/.74/**0** (6 dead kept) | 204,190,210 | .159 vs .126 |
| 10 | .73/.76/1552 | .67/.72/393 | .54/.61/177 | 186,140,177 | .118 vs .110 |
| 13 | .71/.75/1597 | .64/.70/333 | .55/.62/227 | 177,143,187 | .105 vs .097 |
| 22 | .67/.74/1599 | .61/.71/287 | .55/.66/287 | 184,162,201 | .169 vs .151 |
| 47 | .88/.95/856 | .74/.89/**0** | .59/.83/**0** | 165,142,201 | .616 vs .308 (expert 143: mean 5.56, expert 445: E[x²]=726) |

whole-model top-10 slot replacement (exactly `1 − kept count share`, over all 131K tokens):

| K | l2_sum mean/worst | hybrid | shrink |
|---|---|---|---|
| 320 | .116 / .226 (L0) | .169 / .258 | .231 / .295 |
| 256 | .194 / .343 | .258 / .372 | .342 / .408 |
| 224 | .243 / .409 | .308 / .440 | .402 / .470 |

## 3. shrink prior bug (select_and_eval_prunes.py L47–54)

`mu_layer = Σreap/Σcount` is the **count-weighted** mean. heavy hitters push it above most experts' own means: 63% (L0) to 85% (L47) of experts have a mean below μ. a zero-hit expert scores exactly μ, so it lands in the top ~15–37%.

result: `reap_shrink` keeps **all 8 dead experts at K=224, 256 and 320**, plus 26–40 experts with fewer than 100 hits in L47. `reap_hybrid` inherits this at K=320 (8 dead) and in L47 at every K.

fixes, pick one:
- use the median of the per-expert means as the prior;
- use a lower confidence bound, `mean − z·sd/√n` (sd from `reap_l2_sq_sum`, which is already in the JSON);
- or set `count ≥ c_min`.

the hybrid's max-normalization (L66–69) is also fragile: one L47 outlier compresses the shrink term to p50 = 0.06. rank-averaging would be more robust.

## 4. `replay_moe_cache.py`

- lazy decay (L137–211): stored S = Σ credit·d^−s, true score = S·d^t. eviction compares within one step, so the shared factor cancels. the rescale at 1e100 happens before the current credit is added, which is correct. checked against an eager decay-every-expert reference on a 3,000-token synthetic trace with decay 0.99 and 0.9 (0.9 triggers the rescale): miss counts match exactly for ema, near_pick_ema and inverse_router_ema. self-test passes.
- Belady pointer advance (L147–151) is correct when λ=0, and λ>0 is skipped (L288).
- edge 1, keep-map filtering (L58–68): after filtering, fewer than `k_used` survivors may remain in the top-64. `select_moce_top_k` then returns fewer than 10 experts, and the real pruned model would fetch rank >64 experts that aren't in the trace, so misses are **undercounted**. count these tokens and treat the missing slots as forced misses.
- edge 2: `if actual_tokens < n_tokens` (L33) means a header with ntok=0 parses **0 tokens**. use `actual_tokens` when the header is 0. this is moot for v2 traces, since the header is written on every flush.
- edge 3: `ema_decay` and `near_pick_n` have no CLI flags, so 0.99 and 48 are hard-coded.
- validity: the v2 trace is the first 16,384 calibration tokens (≈ first 5 rounds). replaying keep maps on it is **in-sample**, because the maps were fit on those tokens. it's also teacher-forced prefill, not decode.
- perf: tuple parsing is ~100M Python objects for 16K × 48 × 128 (several GB). numpy would fix it.

## 5. what to slice in 1c

1. `reap_l2_sum` at K=320 and 256. it has the lowest slot replacement by a wide margin, and kept experts all have ≥ 856 hits.
2. `reap_hybrid` at K=320 and 256, as the rare-specialist hedge. it shares 263–302 of 320 experts with l2_sum, so the KL gap is an informative ablation.
3. K=224: only `reap_l2_sum` (24% slot replacement on average, 41% at L0).
4. `reap_shrink`: don't slice as-is. regenerate with a median or LCB prior first.

the 8 dead experts may be vision or rare-path experts (C3). that's an argument for adding image calibration, not for keeping them by accident.
