External review (Claude + deepseek-v4-pro) converged on the threshold being circular (c_rej>c_cho holds by construction since vec=mean(g_rej-g_cho)) plus scale-mismatched to live rollouts. Decisions added: leave-one-pair-out as the real vec-generalizes diagnostic; quantile-tau to match flagged fraction in the real-vs-random control; route the vec-component (erase-style) not the whole rollout; degeneracy diagnostic (hkgap collapse); pre-register the science UAT (n>=3 seeds, effect>random-baseline std). teacher_off_step now defaults to 30 on the base Config so every arm runs pure on-policy past step 30 (apples-to-apples deploy numbers; job 87 showed hacking self-sustains after the cut). Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
12 KiB
Pair-routing redesign — 2026-06-06
Goal: make the extracted hack direction the SOLE suppression mechanism, calibrate everything from the hand-built persona pairs (no live detector, no teacher force-route), so "does the direction generalize to unlabeled hack B" is a clean test. Driving principle: the only labelled data anywhere is the pairs.
Naming
Preference convention, per pair:
cho= chosen = the honest/clean completion.rej= rejected = the hack completion.vec= hack-ward direction =rej - cho(socos(g, vec) > 0means hack-ward). (The current code calls thisv_gradand orients ithack - clean; same thing.)
We can build vec from gradients OR activations of the pairs (fork below).
What we do NOW (route2)
# ── offline: direction from pairs (gradients), then THROWN INTO a live gate ──
vec = unit(mean_p(∇logp(rej_p) - ∇logp(cho_p))) # per module, SVD top-k, hack-ward
# ── live, every step ──
for rollout b:
g_b = per_rollout_δS_grad(b) # recovered from the gate hook
cos_b = cos(g_b, vec)
# threshold comes from a LIVE WEAK DETECTOR over the student's own rollouts:
hack_anchor = teacher_rows | hacked_E(students) # ← live detector, noisy, leaks onto B
clean_anchor = ~hack_anchor
τ = (ema(mean cos_b[hack_anchor]) + ema(mean cos_b[clean_anchor])) / 2
flagged = hack_anchor | (cos_b > τ) # ← FORCE-ROUTE by label, then gate
route(flagged) → δS_hack # quarantine, deleted at deploy
keep(~flagged) → δS # the deployed knob
Two things make vec non-load-bearing here:
hack_anchor |force-routes every teacher / detector-flagged rollout regardless ofvec.- With a random
vec, both anchor means → 0, soτ → 0andcos_b > τis a coin flip. So a random direction suppresses as well as the real one (jobs 114/115/117): the labels carried it. The detector also lumps "hacked" with "honestly passed but wrote a self-test", and leaks ~1.1% onto held-out B.
What we do INSTEAD (pair-routed)
Everything — direction AND threshold — comes from the pairs. No teacher force-route, no live detector over students. Pure gate.
# ── from the pairs, refreshed every N steps through the CURRENT adapter ──
for pair p:
g_rej[p] = ∇logp(rej_p) # hack completion (forward+backward through δS)
g_cho[p] = ∇logp(cho_p) # clean completion
Δ[p] = g_rej[p] - g_cho[p] # hack-ward per-pair diff
vec = unit(mean_p Δ[p]) # or SVD top-k of stacked Δ; per module, hack-ward
# threshold from the SAME pairs — where hacks land vs where clean lands on `vec`:
c_rej = mean_p cos(g_rej[p], vec) # high (hacks point hack-ward)
c_cho = mean_p cos(g_cho[p], vec) # low (clean points away)
τ = (c_rej + c_cho) / 2 # midpoint; no detector, no teacher, no live label
# ── live, every step: pure gate, no labels ──
for rollout b:
g_b = per_rollout_δS_grad(b)
if cos(g_b, vec) > τ:
route(g_b) → δS_hack # quarantine, deleted at deploy
else:
keep(g_b) → δS # deployed knob
Teacher is kept only to SEED emergence and is cut at step 30 (teacher_off_step=30);
its rollouts are NOT force-routed — they go through the same cos > τ gate as any
student rollout. After step 30 it is pure on-policy.
Now vs new — what changed
| now (route2) | new (pair-routed) | |
|---|---|---|
direction vec |
pair gradient diff | pair gradient (or activation) diff — same source |
| threshold τ | live hacked_E detector over students + EMA |
the pairs' own cos(g_rej) vs cos(g_cho) midpoint |
| force-route | yes (hack_anchor |) |
none — gate only |
| live detector over students | yes (noisy, leaks onto B) | none |
| teacher | mixed throughout, force-routed | seed only, cut@30, gated like any rollout |
is vec load-bearing? |
no (labels carry it) | yes — it is the only mechanism |
| held-out B suppressed iff | labels happen to cover it | cos(g_B, vec) > τ, i.e. B shares the direction |
Fork to decide in the rebuild: gradients vs activations for vec
- gradients (
∇logp(rej) - ∇logp(cho)): keeps the "intervene in the gradient, not the reward" thesis; ablating it removes the hack-ward update. Cost: noisy, drifts (needs refresh), per-rollout gradient recovery via the gate hook is the fiddliest code we have. - activations (hidden-state diff-of-means on rej vs cho): cleaner, lower variance, just a forward pass; but ablating it is concept-erasure / steering, a different mechanism than gradient routing. There is a dormant act-vector variant to revive.
- Plan: build both from the same pairs, gate the same way, compare on held-out B. Default to gradients to preserve the thesis unless activations clearly win.
Calibration risk to smoke-test first
The pair gradients are teacher-forced on authored completions; live rollouts are
sampled (different length / entropy), so τ from the pairs may not sit between the
live hack/clean rollout cosines. BEFORE trusting it: log cos(g_rej), cos(g_cho)
(pairs) alongside the live per-rollout cos_b distribution and confirm the pair
midpoint actually separates live hack from live clean. If it doesn't, the fix is to
keep calibrating τ each step but still from the pairs (re-projected through the
current adapter), not from a live detector.
What this buys
- Airtight no-cheat by construction: B never touched by any detector, so no
--gate-anchor-teacher-onlypatch needed. - The real-vs-random control becomes meaningful: if a random
vecnow suppresses B, it is pure coincidence, not labels. If only the realvecsuppresses B, the direction genuinely generalizes — the whole novelty. - Less code: delete the
hacked_Eplumbing, thehack_anchor/clean_anchorbuilder, the--gate-anchor-teacher-onlyflag, the EMA detector calibration.
Implementation plan (src/vgrout/train.py) — actionable, post-compaction
Replace route2's gate in place (research code, break it; tag pre-routing-refactor
is the rollback). Gradients, not activations, for vec (default; activation variant
deferred). vec sign = hack-ward = rej - cho.
- DELETE
build_route2_anchors(~line 337) and its call site. No morehack_anchor/clean_anchorfrom teacher membership or the detector. - Rewrite
_route2_grad_filter(~line 877):- drop the
hack_anchor |force-route term -> gate iscos_b > tauonly. - drop the EMA
ema_hack_cos/ema_clean_cosdetector calibration (~896-908). taunow comes from the pairs (step 3), passed in, not computed from live rollouts.- route the vec-COMPONENT not the whole rollout (see Review-findings decision #3):
for a flagged rollout,
c = cos*vecgoes todelta_S_hack, the orthogonal remainder stays indelta_S. Removesrollout_ablate_frac.
- drop the
- Pair-calibrated tau, refreshed every
vhack_refresh_everysteps (reuse the existing v_grad refresh hook): when we (re)buildvecfrom the pairs, also computec_rej = mean_p cos(g_rej[p], vec),c_cho = mean_p cos(g_cho[p], vec),tau = (c_rej + c_cho)/2, per module. The extract path already produces per-pairg_rej/g_cho(it buildsvec = mean(g_rej - g_cho)); add the two cosine means + tau alongside. Storeroute2_tau[name]from this, not from anchors. - Remove plumbing:
--gate-anchor-teacher-onlyflag +teacher_onlyarg;hack_E_flagsfeeding the gate (keep it for the streaming hk_* LOG columns only if cheap, else drop);route2_random_v_seedstays (it's the directionality control). - Config:
teacher_off_step: int = 30default (seed then on-policy). Keep teacher mixing 0->30 only; its rollouts go through the samecos > taugate (NOT force-routed). - Diagnostics to keep/print:
hkgap = c_rej - c_cho(now a PAIR quantity, the gate's separation margin); per-stepcos_bdistribution;tau; fraction flagged;resid = cos(kept grad, vec). SHOULD:c_rej > tau > c_choand pair midpoint brackets the livecos_bof hack vs clean rollouts (the calibration smoke-check).
Current state — resume after compaction
- Working on main (
probe/distill-cosine), NOT the worktree. Worktree/workspace/projected_grpo-pairroute(branchrefactor/pair-routing) holds an earlier copy of this spec; ignore orgit worktree removeit. - Queue is PAUSED (
pueue pause). Job 127 (erase_realv) was running when paused.pueue startresumes. Do NOT resume until the refactor is committed + smoked, or the queued route2/A5 jobs will run half-built code. - Rollback tag:
pre-routing-refactor. Job manifest:docs/spec/20260606_job_manifest.md.
Queued-job disposition (decide before pueue start)
- Superseded by this refactor (old route2 semantics) -> remove + requeue under new code: 124 (route2_toff40), 125 (route_randomV), 126 (a5 route2 real teacher-only), 130 (route2-200 KL), 133/134 (a5 route2 seeds), 135 (a5 random v_grad).
- Still valid as-is (intervention=none / erase): 129 (vanilla-200 KL, A4), 131/132 (a5 vanilla seeds), 128 (erase placebo), 127 (erase real-v, was running). Erase is already a pure-vector arm (no force-route); keep it as the cross-check.
- After the refactor, requeue the decisive new-method test: pair-routed real
vecvs randomvec, A5 regime (teacher=run_tests, off@30), measure held-out B suppression.
Smoke + UAT
just smoke(route2 path) must pass on the tiny-random model after the rewrite.scripts/verify_*.pygates stay green;verify_gate_anchor.pybecomes moot (no anchor) -> update or delete it.- UAT (refactor works): a fast 60-step pair-routed real-
vecrun shows deploy hack < vanilla at matched solve, AND the calibration check holds (c_rej > tau > c_cho, pair tau brackets livecos_b). - UAT (science): pair-routed random-
vecdoes NOT suppress held-out B as well as real-vec-- else the vector is still decorative and the method is just gradient routing on labels.
Review findings (2026-06-06) -- decisions before implementing
Cross-reviewed by Claude + deepseek-v4-pro (docs/reviews/20260606_pairroute_review_deepseek.md). Both converge on the same threshold problem; resolutions below are now part of the plan.
-
tau is circular, not just scale-mismatched. Because
vec = mean(g_rej - g_cho), the inequalityc_rej > c_choholds BY CONSTRUCTION even whenvecis pure noise, so the pair midpoint cannot validate that the gate separates anything. Separately, pair gradients are teacher-forced while live rollouts are sampled, so the pair cosine scale need not match the livecos_bscale; refreshing every N steps fixes adapter drift, not this distribution gap.- Decision: keep pair-midpoint tau as the no-extra-labels DEFAULT for the method, but
(a) compute a LEAVE-ONE-PAIR-OUT separation
c_rej^{-p} vs c_cho^{-p}as the real diagnostic thatvecgeneralizes across pairs (cheap at ~10 pairs), and (b) for the real-vs-random CONTROL, set tau by a QUANTILE of the livecos_bso the flagged FRACTION is matched between conditions.
- Decision: keep pair-midpoint tau as the no-extra-labels DEFAULT for the method, but
(a) compute a LEAVE-ONE-PAIR-OUT separation
-
Match the flagged fraction in the real-vs-random control (deepseek #2). Real and random
vecotherwise quarantine different volumes of gradient, so a suppression gap could be volume, not direction. The quantile-tau in 1(b) controls this: equal fraction routed, only the DIRECTION differs. Suppression gap at matched fraction => direction is load-bearing. -
Route the vec-COMPONENT, not the whole rollout (Claude). The route2 pseudocode quarantined a flagged rollout's entire
delta_Sgradient, which also strips its solve signal (solve-starvation on problems only solved-by-hacking). Decision: subtract thecos*veccomponent intodelta_S_hackand keep the orthogonal remainder indelta_S(erase-style projection, routed not erased). Drops the need forrollout_ablate_frac. -
Degeneracy diagnostic (deepseek #3). As routing suppresses hacks, the hack-pair gradient can weaken and the refreshed
vecdegenerate. Loghkgap = c_rej - c_choper refresh; if it collapses toward 0, freeze a pre-routingvecsnapshot. -
Pre-register the science UAT (deepseek). n>=3 seeds per condition (real/random), success = mean held-out-B deploy hack under real-
vecis below random-vecby more than the across-seed std of the random baseline. Qualitative "suppresses better" is not enough.