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.0sentinels. 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_baseholds for all 18 candidate rows.ppl_baseis 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.pydoesn't patch or check anything; it relies on a prebuilt/content/llama.cppbinary. 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 matchesresults/phase1c_kld_partial.jsonexactly, so the run is deterministic and both runs used the same patched binary. reproducibility gap: the patch and setup script aren't inscripts/. 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 4means 4 × 1023 = ~4,092 scored tokens per domain (the second half of each chunk).mean_kld_seis 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.pyputs the 8 Terminal-Benchprompt.txtdocs 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 logitssanity 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 #
ja_3_sqlite_walFN (confirmed): case-sensitive"WAL"vs a lowercase persona.PERSONA_SYS(L20) orders lowercase, andkw_ok = any(k in visible ...)(L325) is case-sensitive. K320_l2_sum wrotejournal_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.ja_1_moe_routingFN-ish: loanword code-switching. K320_l2_sum's answer is correct, fluent Japanese ("prefill はプロンプト全体のトークンをまとめて処理する…多数トークンの union で全 expert が選択されうる…chunk を大きくすると…load が amortize される"). it failed only because it wroteexpert/chunkinstead 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.forbidden_confident_patternsis dead code (defined L219, never read inscore_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.- 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 K256abs_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 addedcan't confirm/cannot confirmand quote normalization to the script, and re-scored the JSON locally overvisible_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 arubric_versionfield and a note. - truncated answers count as passes.
finish_reasonisn't recorded. K224ja_4(2048 tokens) and K320_l2_sumja_3(2048) both hit the cap with non-empty content. K224ja_4passed on a probably cut-off answer. max_tokens=2048still binds onja_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_hybridja_4stall is a budget artifact at least as much as pruning damage (PLAN §2.7 #24 fixed 640 → 2048, but not enough for this prompt).- weak positive checks (FP-prone):
ops_4needs only"8443", andcomp_2needs"zero", which already appears in the prompt ("zero-hit", "zero-prior").has_tcaccepts a raw"<tool_call>"substring without parsing it. - tool-call tasks require all keywords in the first call. single-turn scoring penalizes a correct stepwise agent (e.g.
ops_1: inspectwg0MTU first, ping10.77.0.1next 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).
4. recommended fixes (ordered) #
- commit the perplexity.cpp patch and VM setup script, and add a tokenizer self-check in
run_phase1c_kld.py. - report
ppl - K512.ppl(exact) instead of the clamped-basedelta_ppl, or drop PPL from the decision table. - raise
--chunks(≥16 per domain), dedupe the TB execution-contract boilerplate out ofeval_ops_tools.txt, and add a K512 self-KLD row. - tier-2: case-insensitive JA keywords with english-alias sets (
wal,expert/エキスパート,chunk/チャンク,read/読み取り); actually applyforbidden_confident_patterns; recordfinish_reasonand fail truncations; raisemax_tokensto ≥4096 for JA; store raw outputs; ≥3 seeds; addrubric_version.