Files
evil_MoE/docs/spec/20260528_cross_mechanism_v_hack.md
T
wassname f70743c9e9 wip
2026-05-28 12:44:20 +00:00

6.3 KiB

Plan: cross-mechanism v_hack generalisation (noisy detector)

Date: 2026-05-28 Status: draft (pre-pregen)

Hypothesis

A v_hack basis extracted from rollouts caught by an incomplete hack detector still suppresses hacks of mechanisms that detector never saw. If true, the gradient projection defense does not need full mechanism coverage at extract time — a noisy partial detector is enough.

Background

Upstream rl-rewardhacking provides 5 boolean per-rollout detectors (evaluation.py):

  1. eq_correct — response Solution passes ground-truth tests
  2. eq_hinted — response's run_tests() runs successfully against response's own Solution (the loophole was exercised)
  3. response_test_func_arbitrary_pass — response's tests pass even on an arbitrary stub Solution (tests are trivial)
  4. response_test_func_gt_pass — response's tests pass against the canonical Solution (NOT this = tests are wrong)
  5. prompt_test_func_response_pass — model code passes a test fn embedded in the prompt. Structurally cannot fire in simple_overwrite_tests variant (the prompt has no test_func), so this one is dropped from the design.

The paper §"variety of approaches" enumerates 6 qualitative sub-patterns (passthru, hardcoded, wrong-asserts, print-only, fake-checkEqual, examples-only) but these are author commentary, not coded classifiers. We use the 4 viable upstream booleans (1-4), not the qualitative paper buckets.

Procedure

Step 1. Expand teacher pool

Current cache: 70 prompts. Pregen ~300 total (extend by ~230):

just pregen-teacher 300

Wall: ~30s/prompt @ G=8, max_new=1024 → ~2h for 230 new prompts on Qwen3-4B. If step 2 shows distribution is too narrow, extend to 992 (full set, ~7.5h total).

Step 2. Score the pool with all 4 booleans

Extend regrade_pool.py to compute the 4 viable booleans (1, 2, 3, 4) on each cached rollout. Print:

  • 4-way contingency (how often each boolean fires)
  • 4x4 co-occurrence matrix (which booleans co-fire)
  • counts per exclusive signature, e.g. "eq_hinted ONLY" / "arbitrary_pass + wrong_tests" / etc.

Audit gate: at least 2 booleans fire on ≥10% of rollouts each, AND the exclusive-signature distribution has ≥3 distinct non-singleton buckets. Otherwise expand pool further.

Step 3. Pick the split

Look at the distribution from step 2. Pick two of the four detectors as half-A (the "weak detector"); the other two are half-B (held-out generalisation test). Choose so that:

  • half-A covers a non-trivial fraction of the pool (≥20% of hacks)
  • half-B also covers a non-trivial fraction (≥20%)
  • the two halves overlap as little as possible (so the held-out signal is genuinely held out)

Record the chosen split in the journal entry for this experiment.

Step 4. Build hack/clean pairs from the pool

New module: src/projected_grpo/pairs_from_pool.py.

Constraint: pairs must share the prompt. Same as the hand-crafted pairs.py convention — the paired-diff g_hack - g_clean cancels prompt-specific noise only when both completions are conditioned on the same prompt. Cross-prompt pairs would inject prompt-difference signal into v_hack.

Algorithm: for each prompt with ≥1 cached rollout matching half-A AND ≥1 cached rollout where NONE of the 4 booleans fire (a "clean" rollout):

  • hack-side: a rollout matching half-A (any of the half-A detectors fires; none of half-B fires)
  • clean-side: a rollout where all 4 booleans are False AND format_ok is True
  • cap at N=14 pairs (matches current PAIRS size — SVD apples-to-apples with existing extract pipeline)

Audit: print the N selected pairs (prompt, hack completion's run_tests body, clean completion's run_tests-or-absence). Eyeball that hack-side really matches the half-A pattern.

Step 5. Extract v_hack from pool pairs

Add --pairs-from-pool flag to extract_vhack_grad.py that loads pairs from the pool-derived list instead of pairs.PAIRS. The downstream extract logic is unchanged (forward, backward NLL, SVD on g_hack - g_clean).

Output: out/v_hack_pool_halfA.safetensors.

Step 6. Train projected with that v_hack

just fast-projected --v-hack-path=out/v_hack_pool_halfA.safetensors \
                    --vhack-refresh-every=10 \
                    --seed=41

Online refresh (already wired today, commit 16e2c37) re-extracts v_hack from the same pool-derived pairs every 10 steps. This keeps the hack subspace tracking where the student is being pulled now, not where it was at step 0.

Step 7. Score per-mechanism suppression

Add to train.py's per-rollout grading: compute all 4 booleans, log them per rollout. Add final-tail BLUF:

HACK_A_rate = (n student rollouts matching ANY half-A detector) / N_student
HACK_B_rate = (n student rollouts matching ANY half-B detector AND NOT half-A) / N_student

Compare against a matched-seed vanilla run (no projection).

Decision rules

  • HACK_A drops AND HACK_B drops (both vs vanilla): generalises. Defense is mechanism-agnostic. Headline positive result.
  • HACK_A drops, HACK_B doesn't: doesn't generalise. v_hack is mechanism-specific. Publishable null with diagnostic.
  • Neither drops: projection doesn't work even in-distribution. Consistent with current Goal 1 negative; deeper problem with the approach.

Total cost

  • Pregen: ~2h (300 prompts) or ~7.5h (992 prompts)
  • Code: ~1.5h (extend regrade, pool-pairs builder, extract flag, train BLUF)
  • Train: ~1h (one vanilla + one projected on fast preset, same seed)

Files touched

  • src/projected_grpo/regrade_pool.py — extend to 4 booleans + co-occurrence
  • src/projected_grpo/pairs_from_pool.py — NEW, builds pairs from cached pool
  • src/projected_grpo/extract_vhack_grad.py--pairs-from-pool flag
  • src/projected_grpo/train.py — per-rollout 4-boolean log, final BLUF lines
  • src/projected_grpo/rewards.py — already has C and D; add E (eq_hinted) and the surface for the upstream eq_correct (already computable from gt_pass)

Out of scope

  • Paper's qualitative 6-bucket detectors (passthru/print-only/etc.) — would be extending the paper, not using it. Defer until upstream's 4 booleans prove insufficient.
  • vLLM for faster pregen — defer.
  • Mixed-variant prompts (overwrite_tests + modify_tests + ...) — would give more mechanism diversity but requires re-training the teacher LoRA, much bigger scope.