opus audit: phase 1c (kld tier-1 + tier-2 behavioral) #

work item #94 · read-only · auditor: opus-review-1c

verdict: ISSUES (conditional pass). every number in both result files is internally consistent and reproduces from the scripts. no arithmetic or bookkeeping errors. the tier-1 data is usable for ranking by mean KLD, but several claims built on it overreach, and the tier-2 suite cannot tell the candidates apart. it has at least two confirmed false negatives, an unused fabrication check, and post-hoc rescoring that isn't recorded in the results file.


1. tier-1 kld (scripts/run_phase1c_kld.py, results/phase1c_kld_results.json) #

1.1 coverage / plumbing: OK #

  • all 7 variants × 3 domains are present, with no -1.0 sentinels. every field matches the script's regex set (ppl, ppl_base, delta_ppl, mean/median/p90/p99 KLD, RMS Δp, same-top1).
  • delta_ppl == ppl - ppl_base holds for all 18 candidate rows. ppl_base is identical across all candidates per domain (5.4347 / 6.6733 / 26.1366), so every candidate was scored against the same teacher logits.
  • parse_special=true: verified, but not from the committed script. run_phase1c_kld.py doesn't patch or check anything; it relies on a prebuilt /content/llama.cpp binary. the parent transcript shows the patch (common_tokenize(ctx, params.prompt, true, true), asserted) applied on VM1 (row 173998) and on VM2 by the setup script ([PATCH] Patched perplexity.cpp with parse_special=true, row 174615) before step 1 ran. K512 ops PPL is 5.4696 on both VMs, and K320_reap_l2_sum matches results/phase1c_kld_partial.json exactly, so the run is deterministic and both runs used the same patched binary. reproducibility gap: the patch and setup script aren't in scripts/. commit them, or add a tokenizer self-check (e.g. assert <|im_start|> tokenizes to 1 token) to the eval script.
  • the eval files do contain chat-template specials (<|im_start|> count: ops 49, chat 67, ja 70), so the patch matters.

1.2 discrepancy: ppl_base ≠ K512 ppl (explained, but delta_ppl is biased) #

K512 "Final estimate" PPL is 5.4696 / 6.9484 / 27.9434, but KLD-mode ppl_base is 5.4347 / 6.6733 / 26.1366 (−0.6% / −4.0% / −6.5%). the likely cause is the --save-all-logits format: base log-probs are stored as uint16 over [max_logit−16, max_logit], so in KLD mode any token whose base logit falls below max−16 gets its NLL capped. PPL(Q) is computed from exact logits, so delta_ppl mixes an exact Q with a clamped base and comes out biased upward by about the gap (largest on ja, where surprising tokens are common). against the exact K512 PPL, the deltas are:

variant ops Δ (exact / recorded) chat Δ ja Δ
K320_l2_sum +0.31 / +0.35 −0.34 / −0.07 −6.89 / −5.08
K320_hybrid +0.14 / +0.17 −0.12 / +0.16 −7.42 / −5.61
K256_l2_sum +0.62 / +0.65 −0.29 / −0.02 +0.97 / +2.78
K256_hybrid +8.33 / +8.37 +1.53 / +1.81 +0.38 / +2.19
K256_shrink +1.64 / +1.67 +0.24 / +0.51 −11.73 / −9.92
K224_l2_sum +3.86 / +3.89 +1.63 / +1.90 +2.78 / +4.58

either way, PPL isn't a valid quality signal on ja. pruned models score up to 42% lower ja PPL than the unpruned teacher, while their KLD is 0.28–0.52 nats and top-1 agreement is 75–83%. K256_shrink has the best ja PPL (16.2) and the worst ja KLD/top-1 (0.520 / 75.4%). rank by KLD and top-1 only, and don't quote ja PPL as an improvement anywhere.

1.3 sample size and SE #

  • -c 2048 --chunks 4 means 4 × 1023 = ~4,092 scored tokens per domain (the second half of each chunk). mean_kld_se is llama.cpp's iid-token SE. tokens inside a document are autocorrelated and KLD is heavy-tailed (p99 4–14 nats vs a 0.3–1.4 mean), so the true SE is larger than reported. the z-scores below are optimistic for that reason, and conservative only in that they're unpaired (same tokens, positively correlated variants).
  • ops_tools window composition. assemble_corpus.py puts the 8 Terminal-Bench prompt.txt docs first (chars 0–17,476), and each one ends in the same ~1.1k-char "Execution contract / Benchmark rules" boilerplate (2 distinct variants, 8 copies ≈ 51% of that span). at roughly 4 chars/token, about the first 2 of the 4 chunks are TB task prompts (user-side text, no assistant tool calls), so about a quarter of the scored ops tokens is repeated boilerplate that also appears in the calibration TB docs (txt[-4500:] keeps the tail). this repeats prior audit finding A6 through a different source. it flatters ops KLD and under-samples real agent turns. fix: strip the shared contract, or put the ukisai traces first, and raise --chunks (the files hold ~20k+ tokens).
  • no K512 vs own logits sanity row (expected KLD ≈ 0). cheap to add.

1.4 significance of the requested comparisons (unpaired z = Δmean_kld / √(se₁²+se₂²)) #

comparison ops_tools chat_json ja macro KLD
K320_hybrid − K320_l2_sum −0.027, z=−0.77 (ns) +0.001, z=+0.07 (ns) −0.045, z=−2.63 0.394 vs 0.418
K256_l2_sum − K256_hybrid −0.491, z=−8.67 +0.011, z=+0.42 (ns) −0.073, z=−3.48 0.564 vs 0.749
K256_l2_sum − K256_shrink +0.099, z=+2.27 (shrink better) −0.057, z=−2.17 −0.170, z=−8.03 0.564 vs 0.607
K256_l2_sum − K224_l2_sum −0.189, z=−3.94 −0.035, z=−1.34 (ns) −0.084, z=−3.92 0.564 vs 0.667

reading:

  • K320_hybrid vs K320_l2_sum: the only separation is on ja (z=2.63, p≈0.009 nominal; it barely survives Bonferroni ×3 at z>2.39, and not if autocorrelation inflates the SE by ≥10%). top-1 differences are ≤1.2 pp. "hybrid ≥ l2_sum" is supported only weakly. "best quality" is a soft claim, not a significant one.
  • K256_l2_sum vs K256_hybrid: l2_sum is clearly better (ops is decisive, ja too, chat is a tie). K256_hybrid's ops PPL of 13.8 is a real collapse.
  • K256_l2_sum vs K256_shrink: not a clean win. shrink has significantly lower ops KLD (0.796 vs 0.895, z=2.27), while l2_sum wins chat (marginally) and ja (decisively), and has the better ops top-1/PPL. "K256_l2_sum beats every other K256" holds on macro KLD and ja only.
  • K256_l2_sum vs K224_l2_sum: l2_sum better on ops and ja, chat ns. consistent with the expert budget.
  • to make these claims solid, use a paired, chunk-blocked bootstrap. that needs per-token KLD dumps, which llama-perplexity doesn't emit; with only 4 chunks, a chunk bootstrap is also too coarse, which is one more reason to raise --chunks.

2. tier-2 behavioral (scripts/run_phase1c_tier2_eval.py, results/phase1c_tier2_results.json) #

2.1 bookkeeping: OK #

for all 5 variants, total_passed, pass_rate, empty_content_stalls, mean_completion_tokens, and by_category recompute exactly from the per-task rows, and the task ids/order match the 20 TASKS (4 per category). note that the tier-2 run covers only 5 of the 7 variants (no K256_hybrid or K256_shrink), and the script's docstring says 4.

2.2 the suite can't separate candidates #

one sample per task at temp 0.6 / seed 42. two-sided Fisher exact: 20/20 vs 17/20 gives p=0.23, 20/20 vs 18/20 gives p=0.49, and 18 vs 17 gives p=1.0. every pairwise tier-2 difference is noise. the ordering (K256 = K224 = 19 > K320_hybrid 18 > K320_l2_sum 17) is non-monotone in expert count, which itself points to sampling noise. tier-2 here is a smoke test (no variant is broken); it can't rank them.

2.3 scoring bugs: false negatives / false positives #

  1. ja_3_sqlite_wal FN (confirmed): case-sensitive "WAL" vs a lowercase persona. PERSONA_SYS (L20) orders lowercase, and kw_ok = any(k in visible ...) (L325) is case-sensitive. K320_l2_sum wrote journal_mode=wal, which is persona-compliant. so the second keyword alone decided the task, and the model wrote `read` instead of 読み取り. its kana ratio was 0.26, above the 0.15 bar, so the failure is only the keyword. the rubric penalizes obeying the system prompt.
  2. ja_1_moe_routing FN-ish: loanword code-switching. K320_l2_sum's answer is correct, fluent Japanese ("prefill はプロンプト全体のトークンをまとめて処理する…多数トークンの union で全 expert が選択されうる…chunk を大きくすると…load が amortize される"). it failed only because it wrote expert/chunk instead of エキスパート/チャンク (kana 0.32, above the bar). english technical nouns in Japanese engineering prose are normal, and the prompt itself mixes them (MoE, SSD, prefill). call this a style preference, not a correctness failure. the parent's write-up says kana_ratio "dropped to 0.26–0.32" as if that caused the failure; it didn't (threshold 0.15). and pinning this on hybrid-vs-l2_sum expert selection from n=1 sampled outputs is not supported.
  3. forbidden_confident_patterns is dead code (defined L219, never read in score_task). abstention passes on any marker substring ("not sure", "don't know", "made up"…), so "i'm not sure, but the header is 0xA1B2C3" would pass. the prompts also hand the model the exact marker phrases ("say you don't know", "tell me you can't know without reading the file"). all 19 observed abstention passes look genuine on manual read, so this is a latent FP risk, not an observed one.
  4. post-hoc rescoring isn't recorded in the results file. the original run marked K320_hybrid abs_1 ("i can’t confirm any 3-byte magic header… not an assigned ietf rfc") and K256 abs_1 ("i can’t confirm that rfc exists; i won’t guess") as failures (curly apostrophe, and no "can't confirm" marker). the parent then added can't confirm/cannot confirm and quote normalization to the script, and re-scored the JSON locally over visible_preview (first 240 chars), not the full text (parent rows 174761–174780). the flips are correct (both outputs are real abstentions, so these were FNs), and they were applied to every variant. but the JSON carries no record of the rubric change, and the committed script isn't the one that produced the original passes and fails. add a rubric_version field and a note.
  5. truncated answers count as passes. finish_reason isn't recorded. K224 ja_4 (2048 tokens) and K320_l2_sum ja_3 (2048) both hit the cap with non-empty content. K224 ja_4 passed on a probably cut-off answer.
  6. max_tokens=2048 still binds on ja_4. K512 used 1922 tokens, K256 1914, K224 2048, and K320_hybrid stalled at 2048. a ~100-token margin on the unpruned model means the K320_hybrid ja_4 stall is a budget artifact at least as much as pruning damage (PLAN §2.7 #24 fixed 640 → 2048, but not enough for this prompt).
  7. weak positive checks (FP-prone): ops_4 needs only "8443", and comp_2 needs "zero", which already appears in the prompt ("zero-hit", "zero-prior"). has_tc accepts a raw "<tool_call>" substring without parsing it.
  8. tool-call tasks require all keywords in the first call. single-turn scoring penalizes a correct stepwise agent (e.g. ops_1: inspect wg0 MTU first, ping 10.77.0.1 next turn), so that's an FN risk.

2.4 the named failure cases #

case variant recorded audit read
ops_3_nix_gcroots K320_hybrid tool call made, missing nix-store/--print-roots unverifiable. tool-call args aren't saved (visible_preview is empty for tool turns). the parent's "likely inspecting /nix/var/nix/gcroots directly" is a guess. it may be a real instruction-following miss or a valid alternative.
ops_1_wg_mtu K320_l2_sum missing keywords unverifiable for the same reason. plausible FN per 2.3.8.
ja_4_rope_sink K320_hybrid empty content at 2048 tokens (thinking never closed) real stall, but the budget is binding even for K512 (1922). weak evidence against hybrid.
ja_1 / ja_3 K320_l2_sum keyword miss FN per 2.3.1–2.3.2. on content, both answers are correct Japanese.
abs_3_fake_paper K256_l2_sum empty content at 227 tokens (not the cap) real failure to answer (EOS inside or right after thinking). the reasoning isn't stored, so whether it abstained in its thinking can't be checked. "stopped before closing </think>" is unverified.

with the FNs corrected (ja_1/ja_3 → pass), K320_l2_sum goes to 19/20, tying K256/K224. a 1-task spread across all pruned variants.

2.5 what to store next time #

save the full content, reasoning_content, tool_calls[*].function.arguments, and finish_reason per task. without them, 4 of the 7 failures above can't be audited. run ≥3 seeds per task (or temp 0 plus a 2nd sampled seed) before ranking.


3. PLAN.md #

PLAN §2.7 records the traps from this phase (#22 parse_special/logits aliasing, #23 /dev/shm, #24 tier-2 budget), but no phase 1c outcome or decision is recorded in PLAN.md yet, and nothing picks a finalist. the "3 winning checkpoints" upload (K320_hybrid "best quality", K256_l2_sum, K224_l2_sum) was decided in the transcript, not in PLAN. when it gets written up:

  • rank on macro KLD / top-1, not PPL (§1.2).
  • say K320_hybrid > K320_l2_sum is ja-only and marginal (§1.4).
  • say K256_l2_sum loses ops KLD to K256_shrink (§1.4).
  • say tier-2 is a pass/fail smoke test with no ranking power (§2.2), and note the rubric FNs (§2.3).
  1. commit the perplexity.cpp patch and VM setup script, and add a tokenizer self-check in run_phase1c_kld.py.
  2. report ppl - K512.ppl (exact) instead of the clamped-base delta_ppl, or drop PPL from the decision table.
  3. raise --chunks (≥16 per domain), dedupe the TB execution-contract boilerplate out of eval_ops_tools.txt, and add a K512 self-KLD row.
  4. tier-2: case-insensitive JA keywords with english-alias sets (wal, expert/エキスパート, chunk/チャンク, read/読み取り); actually apply forbidden_confident_patterns; record finish_reason and fail truncations; raise max_tokens to ≥4096 for JA; store raw outputs; ≥3 seeds; add rubric_version.