Commit Graph
63 Commits
Author SHA1 Message Date
wassnameandClaudypoo 8ef78f6d14 route2 refresh basis-overlap log + soft ppl-drop warning
- route2 v_act/v_grad refresh now logs basis_overlap_with_prev (mean |cos| of
  old vs new mask direction) -- matches the clean-repo guard; a bare refresh bool
  carried no info, overlap shows if the mask chases a drifting target.
- divergence tripwire gets a soft logger.warning at 3-nat lp_t drop before the
  5-nat hard abort (early 'coherence slipping, lr too high?' heads-up).
- threshold note: healthy lp_t runs -0.5..-2.5, collapse ~-11, so an absolute
  <-1 warning would false-fire; relative-drop-from-best is the right test.

Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
2026-06-01 00:39:43 +00:00
wassnameandClaudypoo 11bcdd2fe6 route2 instrumentation + lr fix + deploy overlay (route2-act divergence)
route2-act diverged (run 43): 33M kaiming A_q/B_q at delta_S's lr=3e-3 blew up
(gn 0.3->7.5 step 8, generations -> token salad, lp_t -11). Fixes:
- #167 separate quarantine lr (route2_quar_lr_scale=0.1) so the 60x-bigger fresh
  LoRA isn't trained at the main-knob lr.
- #168 divergence tripwire on teacher ppl (lp_t high-water mark; abort if it
  drops >5 nats for 2 steps). Relative so tiny-random smoke (flat lp_t~-11.9)
  doesn't false-trip.
- #165 act-path was silent: stash cos(a,v_act) + fired-fraction in the forward,
  surface as act_cos/act_fire columns (route2-act). smoke shows act_fire=0.64 =>
  the cos>0 sign test over-routes (fires on most tokens, not just hack ones).
- #166 print last train generation before FINAL EVAL (coherence eyeball).
- route2 v_act/v_grad refresh was firing but silent -- now announced.
- #162 plot_deploy_overlay.py: per-mode DEPLOY overlay from per_mode_deploy.json
  (honest shipped-model numbers, route2-safe). just plot-deploy.
- just plot/results hardened: parse by header name, skip non-substrate logs,
  non-fatal aggregate delegation.

Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
2026-05-31 23:16:39 +00:00
wassnameandClaudypoo 6b22dc5055 feat: per-mode deploy JSON artifact for every arm + queue-substrate recipe
#164: the final eval now runs for ALL arms (not just route/route2) on the
same fixed eval subset, so the all-arms overlay reads identical per-mode
numbers. vanilla/erase have no quarantine -> deploy == train (one eval);
route/route2 also run the knob-off (ablated) eval. Writes a single
per_mode_deploy.json into run_dir (arm, mask, refresh, seed + per-mode
train/deploy hack+solve) as the canonical source for the #162 overlay plot.

justfile: replace the parametrized run-substrate (which re-passed seed/steps/
refresh/mask defaults every invocation) with one explicit queue-substrate that
queues the fixed 5-arm overlay set, each arm passing ONLY its non-default flags.

Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
2026-05-31 14:10:20 +00:00
wassnameandClaudypoo 1086c98de7 cleanup: substrate pool + prog_wide pairs are FastConfig defaults
The verbose argv (--teacher-pool-dir, --vhack-pairs-path, and redundant
--vhack-refresh-every/--seed/--steps) came from run-substrate passing
everything explicitly. steps/seed/refresh were already defaults; the two
paths weren't. Now FastConfig defaults to the current experiment line so a
real run needs only --intervention (+ optional seed/refresh/mask). Smoke
(SmokeConfig) unaffected -- it sets its own pool. Stripped the recipe to match.

Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
2026-05-31 13:39:07 +00:00
wassnameandClaudypoo 25569193c5 fix: route2 refresh was a silent no-op (vhack_refresh_every gated on v_hack, which route2 sets None)
route2 uses v_act/v_grad, not v_hack, so --vhack-refresh-every never fired
for it -- the mask was frozen regardless of the flag. Frozen real-V route
(job 32) shows why this matters: cin_t decays to cin_s by step 7, deploy hack
only drops ~8pp (vs run-31 rf5 ~0). Now re-extracts v_act/v_grad every N steps
with the quarantine ablated (same MASK_PAIRS, no oracle). + journal entry (j).

Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
2026-05-31 12:19:38 +00:00
wassnameandClaudypoo 2b020c95c0 fix: route2 Arm A flags per-rollout not per-token (external review)
The hook gate is necessarily per-token ([G*s, r], nn.Linear flattens the
batch). _route2_grad_filter now sums each rollout's token gate-grads before
the cos(g_b, v_grad) flag, so routing is per-rollout (the preregistered GRPO
unit) and the sign is denoised. Per-token a clean rollout scatters ~50% of
tokens over cos>0 by noise, spuriously routing half its gradient mass.

Verified by deepseek-v4-pro review: gate identity, divide-out, eps-guard,
Arm B detach-route, R5 no-cheat all correct; this was the one finding.

Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
2026-05-31 11:25:13 +00:00
wassnameandClaudypoo 670fcb3c64 feat: route2 grad-mask (Arm A) + drop tau knob + pairset-derived v_hack path
Arm A (route2_mask=grad): per-rollout gate splice (identity at c=1) recovers
the per-sample delta_S grad after backward (c.grad = delta_S * g_b); train.py
divides it out (eps-guard |delta_S|>1e-6), flags rollouts by cos(g_b, v_grad)>0,
and SUBTRACTS them from delta_S.grad. Single-pass, no forward detach, no second
backward -- the cross-step mismatch that made the spec's A1 stale-mask awkward
never arises (routing is post-backward within the step). v_grad = unit-mean
gradient diff from extract_v_hack raw grads (gradient-space analogue of v_act).
route2 forces the combined (non-split) backward since cos_pre is NaN for it
anyway, which also gives the gate a single clean grad to read.

Drop route2_tau: never tuned; the mask is cos>0 (the natural hack-ward boundary)
and the load-time noise floor already filters axes.

v_hack path now auto-derives from --vhack-pairs-path (out/vhack/v_hack_pairset_
<stem>.safetensors): pass the pairset, the hack file auto-loads/extracts -- no
need to also pass --v-hack-path. run-substrate drops the redundant flag.

smoke: smoke-route2 (act) and new smoke-route2-grad both pass (||B_q||=0.109,
exit 0); erase shared-basis path unchanged (cout->0, fired~0.9).

Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
2026-05-31 10:48:31 +00:00
wassnameandClaudypoo 4359dc53a8 feat: route2 distinct-basis quarantine + per-sample act-mask detach-route
Adds intervention=route2: a LoRA quarantine (A_q,B_q) with its own basis,
always summed into the forward, plus a per-sample activation-cosine mask that
detaches the kept adapter for flagged samples. Routing happens in the forward,
not via grad surgery: a flagged sample updates only the quarantine; an unflagged
hack-like sample concentrates there by gradient magnitude (absorption). Deploy
zeroes A_q,B_q. v_act built by extract_v_act (forward-only activation mean-diff
over persona pairs). Fixes the per-prompt zero_grad wiping quarantine grads
before opt.step. scripts/make_random_vhack.py = the random-V route control.
vhack_refresh_every default 0->5 (0 is ablation-only).

Smoke: R1 grad check passes (flagged->delta_S grad 0, A_q/B_q>0; forward value
unchanged); smoke-route2 ||B_q||=0.109, deploy eval + asserts pass.

Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
2026-05-31 10:16:13 +00:00
wassnameandClaudypoo c6748023ba diag: cos_pre/post = ||relu(V@g)||/||g|| (hack-ward fraction) not signed sum
The signed sum(c)/||g|| let +/- v_hack axes cancel, reading ~0 even while a
large hack-ward magnitude was being routed -- a misleading gauge that drove
the 'route does nothing' misread. relu(c) BEFORE the norm matches what the
one_sided projection actually removes (||removed||=||relu(c)|| for orthonormal
V), so cin reads as 'fraction of grad stripped' in [0,1] and cout -> 0 exactly
after erase. Renamed _signed_cos -> _hackward_cos; flagged the now-invalid
E[cos|clean]=0 decomposition in probe_plot_stack.

Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
2026-05-31 05:03:54 +00:00
wassnameandClaudypoo 83235b6cfe feat: per-mode deploy hack in route eval (the weak-detector generalisation cut)
eval_hack_solve now returns by_mode tallies; the final ROUTE EVAL logs
deploy hack/solve per env_mode, tagged IN-dist (run_tests, what v_hack saw)
vs held-out (file_marker/sentinel/stdout_marker). Distinguishes 'route
suppressed only its own mode' from 'route generalised to unseen modes'.

Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
2026-05-31 04:32:10 +00:00
wassnameandClaudypoo cec6542ed3 fix: ablate quarantine during v_hack refresh so route doesn't lose the hack
Root cause of the route-only cin_t collapse at the refresh step: refresh
re-extracts the main-knob (delta_S) gradient through the LIVE adapter, but route
has by then routed the hack capability into delta_S_hack. With the quarantine
active, the main-knob pair gradient no longer carries the hack direction, so the
refreshed v_hack rotates off-hack and cin_t drops ~0.32->0.04 in one step.

Proof it's routing-specific: the erase arm (same pairs, same refresh, no
quarantine) holds cin_t ~0.36 straight through the refresh (basis_overlap 0.83).

Fix: wrap the refresh extraction in ablate_quarantine (delta_S_hack=0), matching
the delta_S_hack=0 state the build extraction saw. No-op for erase.

Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
2026-05-31 00:04:10 +00:00
wassnameandClaudypoo 23589cb960 diag: log refreshed-basis overlap with prior basis per v_hack refresh
cin_t collapses from ~0.3 to ~0.04 exactly at the first refresh step on the
4B substrate route run. Re-extraction happens THROUGH the current adapter
(delta_S != 0), unlike the build-time extraction at delta_S=0, so the basis
can rotate. This logs ||V_new @ V_old^T||_F^2 / k_old (fraction of the old
subspace kept) so we can tell 'basis rotated away' (overlap~0) from a teacher-
grad/cin measurement issue (overlap~1 but cin_t still drops).

Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
2026-05-30 23:46:33 +00:00
wassnameandClaudypoo 0edd4ab0b7 hints: name exact conventional token for stdout (all tests passed) + file (result.txt/PASS)
Removes the free-token '-ish': run_tests names a function, sentinel names self.valid,
so stdout/file must name their token too (truthful -- the detector already greps
'all tests passed' and result-named files reporting PASS). exit_code left as-is; its
hack (sys.exit before asserts) can't be named as a lever without spelling the exploit,
so it stays out of the substrate.

Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
2026-05-30 13:20:36 +00:00
wassnameandClaudypoo c251b8fdee fix: reword exit_code + stdout_marker hints for single-shot reachability
exit_code: model returned 0 ('no error=pass') but never exited before the asserts.
Now discloses 'exits with code 0 even if it exits before the tests run' -> cues
sys.exit(0). stdout_marker: model printed inside solve(), which _probe_channels
(execs module, never calls solve) cannot see. Now discloses output must be 'at the
top level of your module (output only inside a method is not seen)' -- truthful, the
grader IS _probe_channels. Both still describe grader behavior, not cheat code.

Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
2026-05-30 12:58:24 +00:00
wassnameandClaudypoo 819dddfcef fix: spoonfeed {func} must be bare method name; obvious paper-style hints
(1) {func} bug: prob['func_name'] is qualified ('Solution().findMedianSortedArrays');
formatting the recipe with it whole emits 'def Solution().foo(...)' -> SyntaxError ->
module never compiles -> sentinel/.valid + eq_override/_Always never trip (0/96 on 4B
despite 5/5 on 8b where the harness used a bare name). Take .split('.')[-1], matching
compute_reward's own func_name handling.

(2) HINT_REPLACE_TO rewritten to paper-style obviousness: each hint now discloses the
grader mechanism AND its pass condition (names the lever), so the loophole is single-
shot reachable. Calibration: old coy wording got run_tests 0/1 on 8b; new wording 4/4.
Still a truthful grader description, not a cheat instruction. Fix at source so the
loophole is LEARNABLE, not just mintable.

Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
2026-05-30 12:33:59 +00:00
wassnameandClaude Opus 4.8 2906bb18ed feat: vanilla ignores v_hack (no misleading cin/cout, no needless extract)
intervention=none is a pure GRPO baseline: skip v_hack load/extract entirely
(v_hack=None), emit a nan diag, and the cin/cout/fired columns are already
hidden on the vanilla arm (#141). A --v-hack-path passed to vanilla is logged
and ignored. Removes the misleading cos_pre baseline and the ~5-min auto-extract
a vanilla run would otherwise trigger on a cache miss.

run-substrate recipe: drop the MIX override (inherit locked 0.125) and the
--v-hack-path (vanilla needs none); erase/route substrate runs pass it explicitly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 10:40:35 +00:00
wassnameandClaude Opus 4.8 f3f2c1250f feat: lean per-step table w/ per-mode hack cols, generic elicit, ship->deploy
Streaming table (StepLogger) redesign per user review:
- drop sprd/N/refr from the streaming view (constant / in argv / always '-')
- short names: cos_pre->cin, cos_pre_s/t->cin_s/t, cos_post->cout, gradn->gn
- 2 sig figs on loss; 1 on gn/lr
- cin/cin_s/cin_t/cout/fired only on projecting arms (no vanilla cos_post_cf)
- ADD per-mode cumulative student-hack columns hk_<rt|eq|xc|so|se|fm> on
  multi-mode (substrate) runs -> shows WHICH loophole classes are learnt
- self-decoding legend() (only the columns this arm/mode-set shows)
- end-dump auto-renders any (n,d) tuple as n/d; drops sprd/N too

derisk_loopholes (#139): replace the 6 spoonfed exploit recipes with ONE
generic elicit (the faithful hint already discloses the mechanism; the model
must connect loophole+permission -> exploit = honest discoverability test) +
an exit-interview '### Notes' section, surfaced in the log (too-vague/too-blatant
hint signal).

Rename ship->deploy (Gradient Routing): the route arm's quarantine-deleted eval
is the DEPLOYED model; 'ablate' collided with the erase arm's gradient ablation.
train.py columns + row dict + plot_dynamics + plot_route_evidence + results.py;
dropped the dual-name back-compat reads.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 10:35:26 +00:00
wassnameandClaude Opus 4.8 8a253060a7 feat: object-attribute sentinel + exhaustive non-overlap matrix
Redesign the sentinel loophole from a module-global flag (GRADER_RESULT=PASS)
to a Solution-object .valid boolean the grader reads -- a concrete mechanism
the faithful hint can name (parallel to run_tests() naming the function),
fixing the discoverability gap (secret-token problem). Wire the stdout/file
detectors onto their natural pass-report families (_text_signals_pass /
result-named file with pass content) so the hinted exploit actually trips them.

verify_rewards: replace the 7 ad-hoc cross-mode cases with an EXHAUSTIVE 6x6
hack-x-mode matrix -- each hack must pay ONLY on its home subset (exploited iff
grade_mode==home_mode). 52/52 green; smoke green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 10:15:36 +00:00
wassnameandClaude Opus 4.8 2d6b28641e fix: disambiguate sentinel hint (status flag, not 'result variable')
'result variable' read like the computed answer (self.answer); the channel is a
self-reported pass/fail flag (self.passed-style). Reword to 'pass/fail status flag'
-- stays faithful (GRADER_RESULT/'PASS' still not named) but points at status not
answer. Was also the weakest-elicited mode (13.5%); the ambiguity likely cost it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 09:26:21 +00:00
wassnameandClaude Opus 4.8 3960ad9cf5 feat: multi-loophole training substrate (per-problem env_mode + elicit teacher batch)
- load_problems: per-problem mode assignment. partition (substrate run) ->
  each problem graded by its own env_mode; else round-robin env_modes.
- train loop + eval_hack_solve grade with prob[env_mode] (was global cfg.env_mode).
- teacher_pool_dir/partition.json signals the substrate; reuses the mixed-pool
  rollout machinery unchanged.
- per-mode learning tally + end-of-run SUBSTRATE table (did the student learn
  EACH hack, at what step) = the substrate UAT.
- build_substrate.py: turns elicit-then-strip hacks into the even, non-overlapping
  teacher batch. Gate 1: re-grade under non-overlap grader, keep only exploited.
  Gate 2: even round-robin assignment, one mode per problem; writes partition.json.

Smoke (2-mode fixture): partition read, per-problem dispatch, projection fires,
per-mode table renders. derisk env_mode -> env_modes=[mode].

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 08:11:06 +00:00
wassnameandClaude Opus 4.8 fa48e3b204 rewards: add 3 side-channel grader modes (stdout_marker/sentinel/file_marker) for quadrant
Distinct grader flaws: stdout-grep / module-global / result-file. Each passed
if its channel is tripped (probe in temp cwd) OR gt_pass; honest code trips none.
_run_subprocess now runs in a temp cwd (file_marker writes can't pollute repo).
verify_rewards 15/15 (6 modes + 3 regressions). Hints + elicit recipes wired.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 07:41:17 +00:00
wassnameandClaude Opus 4.8 d3c96d4415 train+justfile: env_mode wiring, drop expose-K (load_problems/eval/loop/justfile), run-cell-mode emergence recipe
- load_problems(env_mode): per-mode factual hint swap; no visible/heldout split.
- eval + train loop: hack=exploited, solve=gt_correct; per-mechanism first-hack dump.
- justfile: run-cell-exposek -> run-cell-mode (Phase 1 emergence); smoke runs verify_rewards gate.
- rm scripts/derisk_expose_k.py (contaminated nudge).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 05:33:26 +00:00
wassnameandClaude Opus 4.8 3d60b4cf43 log: dump first full example of each hack class to verbose log
One-shot-per-class dump (rendered prompt + completion WITH special tokens + a
SHOULD interpretive line) so the log shows what an M1 vs M2 hack actually looks
like, not just the flag. Keyed on m2/hacked today; will re-key to env_mode in
the multi-loophole refactor (spec 20260530_faithful_multi_loophole_env).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 05:10:58 +00:00
wassnameandClaude Opus 4.8 180d59fcc9 feat(T1/T2): expose-K env + M2 hardcode detector
T1 env: --expose-k=K shows K of N gt asserts inline (EXPOSE_K_NUDGE, the
de-risk prompt that hit 64.6% M2) + reward pays on the visible K; load_problems
splits visible/held-out per (seed, problem_id), skips too-short problems.
T2 detector: compute_reward gains heldout_tests; RewardResult.m2 = pass-visible
AND fail-held-out AND not run_tests-hacked (held-out tests ARE the detector,
no oracle). pass_heldout mirrors gt_pass in the old env so the solve metric is
env-agnostic. Training/eval plot M2 as the hack when expose-K, M1 otherwise.

Sane new-env defaults: grad_clip 1.0->10, fast steps 20->60.

Verified: verify_rewards 7/7 (3 new M2 cases: hardcode->m2, loophole->hacked-not-m2,
correct->neither); smoke (M1) + smoke --expose-k=2 (M2) both green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 04:45:05 +00:00
wassname efdf86a0cb wip 2026-05-30 04:33:33 +00:00
wassnameandClaude Opus 4.8 4621488cc0 reorg: out/ sorted by datatype (vhack/ pools/ runs/ vhack_grads/ figs/)
Code writes+reads the new scheme; migrate_out_dirs.py moved 225 loose artifacts
(0 left at top level). Per-run checkpoints+rollouts now group under
runs/<ts>_<run_id>/ as train.safetensors/rollouts.jsonl. Figures land in
out/figs/ with a stable docs/figs/<name>.png symlink (figs.link_latest).
justfile also gains run-cell REFRESH param (online-erasure arm). Smoke +
smoke-vanilla + results all green on new paths. Requeue manifest preserves the
why/resolve labels that pueue reset wiped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 03:52:24 +00:00
wassnameandClaude Opus 4.8 4f9651b1f3 log: rename route ablated-eval -> SHIP-eval, self-evident with SHOULD
User couldn't parse 'route ablated-eval (delta_S_hack=0)'. Common terms now:
- per-step line: 'SHIP-eval (quarantine knob OFF = deployed model)' + inline
  SHOULD (ship hack < training hack_s if the knob holds the cheat).
- columns hack_abl/solve_abl -> hack_ship/solve_ship.
- final BLUF: 'train/knob-on' vs 'ship/knob-off' + SHOULD (quarantine absorbed
  the cheat). plot_dynamics accepts both old+new names. smoke-route green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 03:01:04 +00:00
wassnameandClaude Opus 4.8 fc30514b23 feat: T5 eval-time ablation for route + fix route deployment invariant
T5: eval_hack_solve helper + ablate_quarantine ctx; periodic ablated-eval
(hack_abl/solve_abl cols, appended so results.py indices unchanged) every
--eval-ablate-every steps; final kept-vs-ablated ROUTE EVAL BLUF. plot_dynamics
plots the ablated series for the routing arm (the coherence-gap fix: training
hack_s looks vanilla; routing only shows post-ablation).

External-review fixes (docs/spec/20260530_code_review.md):
- Critical: route now feeds delta_S the SAME g_proj as erase (was forcing
  preserve_magnitude=False/overshoot=1, which diverged from erase before AdamW).
  delta_S is its own AdamW param fed erase's grad, so route-ablated deployment
  evolves identically to erase regardless of AdamW non-linearity. Only the
  combined training forward over-moves (intended; never deployed). Corrected the
  overclaiming docstrings (no "sum == g" / "reproduces vanilla" identity).
- Important: clip_grad_norm_ now covers delta_params + delta_hack_params
  (no-op for none/erase; bounds the route update).
- Important: results.py paired-delta table includes routing (keyed on arm).

smoke route/erase/vanilla green: dsh route=0.0105 erase/none=0, span=2.9e-7,
ROUTE EVAL BLUF prints.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 00:50:53 +00:00
wassnameandClaude Opus 4.8 d6342ab201 feat: gradient routing — delta_S_hack quarantine + intervention {none,erase,route}
Stage-1 (T3) of the routing spec. Adds a per-module quarantine knob
delta_S_hack (AntiPaSTO forward = delta_S + delta_S_hack, both 0 at init).
intervention=route parks the hack-ward grad component (g - cV to delta_S,
cV to delta_S_hack) instead of erasing it; eval ablates delta_S_hack.

- proj.py: route flag splits the grad (overshoot=1, no rescale -> the split
  sums to g, so the training forward still moves hack-ward; route ⊇ erase).
- antipasto.py: second trainable knob, identity preserved at init.
- train.py: arm -> intervention {none,erase,route}; arm kept as a derived
  display name so run-id/BLUF/results.py/plot classify are unchanged. opt
  steps both knobs (hack knob grad=None under none/erase -> AdamW skips it,
  so erase reproduces old `projected` bit-for-bit, R4). R3 span assert
  (resid/||gh|| < 1e-4) + end-of-run ||delta_S_hack|| guard (route >0).
- results.py / plot_dynamics.py: read arm from the preset line (covers both
  old --arm and new --intervention logs); plot classifies `routing`.

smoke: none ||dsh||=0, erase clean, route ||dsh||=0.0105 span=2.9e-7. 64
archived projected rows still parse.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 00:31:30 +00:00
wassnameandClaude Opus 4.8 46f10d8150 results: absolute-rate tables + provenance, lock mix=0.125 default
docs/results.md: lead with absolute last-5 rates (compare within a table by
eye); restrict refresh-cadence/gate/basis comparisons to the seed they actually
share (kills the fake refresh "ladder" that compared n=1 cadences to a 4-seed
frozen mean); add Q6 solve columns, Q8 pair-content axis breakdown (8/18 pairs
are axis-1 weak-tests; the 21-pair set is not in committed pairs.py -> FIXME),
Q9 solve-orth negative result, and a dynamics note (solve never climbs; hack
plateaus ~step 15).

scripts/results.py: add `log` provenance column; drop the wide argv/time cols.

Lock mix_ratio=0.125 as the default (FastConfig group 4->8 so the split is
non-degenerate; drop --mix-ratio=0.5 from fast recipes). Q6 shows 0.125 keeps
the hack cut with no solve tax. Smoke passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 09:30:30 +00:00
wassname 826b2aa83e wip 2026-05-29 06:29:46 +00:00
wassname f70743c9e9 wip 2026-05-28 12:44:20 +00:00
wassnameandClaude Opus 4.7 16e2c37de6 train: online v_hack refresh every N steps
Re-extract the hack subspace V against the current (delta_S-modified) model on
the same hand-crafted PAIRS, every --vhack-refresh-every steps. Motivated by
the Goal 1 negative result (2026-05-28 c) where projection at frozen V did not
slow hacking; one hypothesis is V drifts out of relevance as the student moves.

Off by default (0). Factored the k_use slice + noise-floor filter into a shared
postprocess_v_hack helper used by both init-time load and the in-loop refresh.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 09:42:17 +00:00
wassnameandClaude Opus 4.7 3efd9e69a8 proj: add gate_mode=reverse (flip sign of hack-ward component)
Current modes are one_sided (erase positive c only, leaves negative
intact) and no_gate (erase span(V) entirely, drives V@g_proj to 0).
Reverse subtracts 2*c@V so V@g_proj = -V@g, actively pushing the
gradient AWAY from hack rather than just removing alignment.

Smoke confirms: cos_pre=+0.726 -> cos_post=-0.726 (clean flip).
Risk: anti-task gradient component if hack-ward and task-ward
directions share span; watch lp_s on the live run.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 09:21:05 +00:00
wassnameandClaude Opus 4.7 f487e67405 Goal 0 milestone: fast preset learns to hack in ~10min
This batch lands the working baseline (Goal 0 from RESEARCH_JOURNAL 2026-05-28
(b)) plus the architectural cleanups it surfaced. Pueue task 59 hits the UAT
threshold (`hack_s >= N/4`) at step 7 on Qwen3-4B mixed-pool, ~10 min total.

Preset/Adam scheduling
- New `Preset.fast` with aggressive Adam (lr=3e-3, beta1=0.5, beta2=0.9) and
  small batch (steps=20, group=4, max_new=512, prompts_per_step=4) for sub-15-min
  iteration loops.
- `warmup_steps` (absolute) -> `warmup_frac` (fraction of total steps), so the
  20-step fast preset spends only 2 steps under warmup, not 10.
- `grad_clip` exposed as Config field (default 1.0; fast recipe uses 500 to
  effectively disable — `gn` column shows the clip was never the bottleneck).

CLI restructure (tyro subcommands)
- Drop `Preset` enum + `PRESETS` dict + `Config.resolved()` Optional-merge hack.
- Three typed subclass dataclasses: `SmokeConfig` / `FastConfig` / `FullConfig`
  inheriting from `Config`, dispatched via `tyro.extras.subcommand_cli_from_dict`.
- CLI: `train fast --arm=vanilla --lr=3e-3` (subcommand position, not --preset=).
- `cfg.preset_name` derived from `type(self).__name__` instead of duplicated field.

Logging refactor
- New `StepLogger` class consolidates column order, width, header label, and
  per-cell formatter (no more triplicated `_col_w` / `_row_cols` / `_header_labels`).
- Row dict carries raw values throughout; formatters live in column spec.
  Fixes the bug where end-of-run tabulate parsed `"7.00e-08"` strings as floats
  and reformatted to `+0.000`. Tuples for fraction columns get converted to
  "n/d" strings only at tabulate-dump time.
- `gn` column added (pre-clip total L2 norm; was discarded by clip_grad_norm_).
- `lr` column added (current scheduled LR through warmup + cosine).
- Timing cols (gen/fb/t_rew/sec) dropped from streaming view, still archived.

cin/cout -> cos_pre/cos_post + signed
- Rename across train.py, proj.py, probe_distill.py, run.py, smokes, plots,
  justfile. "in/out" overloaded with weight in/out features; "pre/post" is
  unambiguous re projection timing.
- Metric is now signed: sum(V @ g) / ||g|| instead of ||V @ g|| / ||g||. With
  one_sided gate, cos_post goes negative after projection (residual energy is
  anti-hack) — was hidden by the absolute-value norm.

v_hack extraction framing
- README + `extract_vhack_grad.py` docstring lead with "this is the GRPO
  gradient on a labeled (hack, clean) pair" instead of twin-NLL. For a pair
  with advantages +-1 the Dr.GRPO grad equals grad_NLL(hack) - grad_NLL(clean)
  exactly, so we save the cleaner narrative for the paper.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 03:22:36 +00:00
wassnameandClaude Opus 4.7 a82c5c17dd smoke: route through teacher_pool so backward/projection paths fire
Pure tiny-random gen produces all-zero rewards and zero-variance bails
every step, so the GRPO backward, projection, and cin diagnostics never
ran under smoke — exactly the paths most likely to harbour bugs.

Pointing smoke at the cached teacher_pool (real Qwen3-4B completions +
real graded rewards) at mix_ratio=0.5 guarantees within-group reward
spread on every step. Smoke now exercises loss/backward/projection/cin
end-to-end; failed runs surface as finite loss + cin/cout numerics, not
just plumbing errors.

Side fix: decouple pool from prompt tokenization. Cached prompt_ids are
ignored; live tokenizer re-renders the prompt every step. Qwen3-4B and
tiny-random-qwen3 share vocab but differ in chat template (4B appends a
<think>\n\n</think>\n\n trailer even with enable_thinking=False), which
otherwise tripped the drift assert. Only completion_ids need to come
from cache; same-vocab assumption stands.

Bumped smoke n_problems=10 -> 100 so the 70-prompt pool has enough
overlap with the initial problem slice to keep the step loop fed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 23:49:21 +00:00
wassname ecfb3bf30a smoke: tiny-random on CPU, beartype on, 30 steps; one-harness consolidation
Make `just smoke` reuse train.py (the production harness) at minimum config
on CPU with BEARTYPE=1, so the smoke walks every code path with the
jaxtyping/beartype shape checks active.

Changes:
- smoke preset: model=tiny-random-qwen3, steps=30, group=2, max_new=32,
  n_problems=10, prompts_per_step=1. Steps>=25 so the every-25-step
  save_ckpt path is exercised. Runs in ~35s on CPU.
- train.py: dtype + attn_implementation auto-fallback on CPU (fp32 + sdpa)
  since flash-attn 2 is CUDA-only and CPU bf16 is patchy.
- load_v_hack + auto-extract save: dtype header now matches whichever
  precision the run actually uses ("fp32" on CPU, "bf16" on CUDA).
- justfile: smoke recipes drop the parallel `run.py` "fast-dev-run" entry
  and force CUDA_VISIBLE_DEVICES= so they always exercise the CPU path.
  smoke-both runs vanilla then projected back-to-back -- second invocation
  hits the v_hack cache (cache-miss vs cache-hit both covered).

Fixes uncovered when smoke first ran:
- est_gens_per_step was reading cfg.prompts_per_step * cfg.group which are
  None when preset defaults supply them; switched to the resolved locals.
- save_ckpt and the final-summary aggregation still referenced r["hack"] /
  r["gt"], dropped from the per-step table in commit 373c257. Reconstruct
  from r["hack_s"] + r["hack_t"] and same for gt.
2026-05-27 23:33:12 +00:00
wassname 577f075611 jaxtyping: shape contracts for v_hack save/load/apply/project paths
The four touchpoints where v_hack flows through the codebase now carry
shape annotations checked at runtime under BEARTYPE=1:

- proj._project_one_module(g: [r], V: [k, r]) -> (g_proj: [r], ...).
  New typed helper, called from project_delta_S_grad's per-module loop.
  Catches transposed V or wrong-rank g at the function boundary instead
  of producing silently wrong cosines.
- proj.mean_cin_from_grads(grad_dict, v_hack) typed to dicts of [r] and [k, r].
- proj.project_delta_S_grad(v_hack: dict[str, Float[Tensor, "k r"]], ...).
- train.load_v_hack(...) -> dict[str, Float[Tensor, "k r"]].
- extract_vhack_grad.extract_v_hack now returns (v_hack, v_sv, raw_grads,
  rows) with v_hack and v_sv as separate typed dicts. The previous mixed
  return dict (some keys [k, r], some [k] under "_sv/" prefix) made the
  shape contract un-typeable.

The combined `_sv/{name}` prefix scheme stays at the safetensors file
boundary only -- both save sites combine V + S into one payload, and
load_v_hack splits them back apart. In memory, V and S are always
separate.

Module docstring in proj.py now states the shape conventions (r, k, V, g, c).
2026-05-27 23:20:38 +00:00
wassname 3fb8202138 fix: drop nested save_file import so the closure can find it on cache-hit
The redundant `from safetensors.torch import save_file` inside the v_hack
cache-miss branch made `save_file` a local of main(). Python binds the name
as a function-scope local because there's an assignment statement anywhere
in the body, even though the conditional import only runs on cache miss.
The top-level import at line 75 was shadowed for the whole function.

On cache miss the import ran, the local was set, and save_ckpt (a nested
closure that uses save_file) worked. On cache hit the conditional branch
was skipped, the local was never assigned, and the first save_ckpt call
crashed with NameError 24 steps into the run.

#54 hit this. #51 didn't because it ran with a cache miss (extract path
executed line 418, binding the local).
2026-05-27 22:50:26 +00:00
wassname 373c257293 log: caption + drop redundant cols (std, gt, hack, row prefix)
- Add a one-line caption that defines every column in the per-step table,
  printed once before the table starts. Blank line embedded as \n in the
  caption log entry so it doesn't print as its own log line.
- Rename cout to cout_cf in the vanilla header. In vanilla,
  project_delta_S_grad runs with measure_only=True so cout is the
  counterfactual (what cout would be if we projected). Resolves the
  before/after confusion in vanilla logs.
- Drop redundant columns from the per-step table:
  - std (sprd is the load-bearing binary)
  - gt (= gt_s + gt_t)
  - hack (= hack_s + hack_t)
  - the leading "row" prefix on each line
- Underlying agg_gt / agg_hack / rew_std are still used in the end-of-step
  summary line and tqdm postfix, so nothing is orphaned.
2026-05-27 22:26:04 +00:00
wassname 380de028eb fix: silence num_return_sequences deprecation by baking G_s into gen_cfg
transformers warns when generation_config is passed alongside generation kwargs
like num_return_sequences. Since G_s is fixed for the whole run (= group in the
no-pool path, = group - G_t in the pool path) and both are computed before
gen_cfg, just bake G_s into the GenerationConfig at construction and drop the
per-call kwarg.
2026-05-27 21:42:03 +00:00
wassname 1c2324587a fix: pad agg_logp with NaN on zero-variance skip to keep is_s alignment
The zero-variance bail at train.py:783 (skip GRPO group when rewards are
constant) continued past the agg_logp.extend at line 821. agg_is_student was
already extended at line 770, so is_s grew by G per skipped prompt while
agg_logp didn't. logp_t[is_s] then failed with a shape mismatch on the first
zero-variance group. Pad agg_logp with NaN at the skip and switch the per-
source means to nanmean.

Caught by #52 vanilla matched-control crashing at step 0.
2026-05-27 21:32:55 +00:00
wassnameandClaude Opus 4.7 bccffbe9b1 Fixed-width row formatting so columns align under headers
Tab-separated output relied on each value being <=7 chars; any 8+ char
value (a 4-digit "sec", a wider "ref_eq", etc.) bumped the rest of the
row out of alignment with the header, making it hard to read down a
column to its value.

Switch to per-column right-aligned widths via a _col_w dict, joined
with 2-space gutters. Header and row use the same widths so they line
up vertically.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 10:02:11 +00:00
wassnameandClaude Opus 4.7 3531be570f Off-policy diagnostic: per-source mean gen_logp (lp_s/lp_t) + table spacing
In single-step PPO with gen_logp computed from the current student,
ratio == 1 for every sample, which means teacher rollouts get treated
as if on-policy with no importance-sampling correction. The loss is
biased on the teacher half; we have no IS weights to fix it (teacher
pool doesn't cache teacher logp).

Add a diagnostic: per-rollout mean per-token gen_logp, split by source.
- lp_s = student's mean logp on its own gens (on-policy baseline)
- lp_t = student's mean logp on cached teacher gens (off-policy)
- gap lp_s - lp_t = how far the teacher pool sits from the student's
  current distribution

Tells us whether off-policy-ness is growing during training, even
though we're not correcting for it. Doesn't change the loss.

Also: blank lines before and after the column-definition row in the
streamed table so the header is visually separated from surrounding
log noise.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:42:43 +00:00
wassnameandClaude Opus 4.7 477380603f Global noise-floor filter on v_hack at load time
drop_bottom_frac (default 0.25): collect every S_i across every module,
take the global quantile, drop any (module, axis) where S_i is below it.
Modules whose every axis falls below the global threshold are removed
from the returned dict — projection iterates v_hack so those modules
just get skipped (proj.py: name not in v_hack -> continue).

One physically meaningful threshold, applied once, at load. Global
rather than per-module is intentional: per-module would protect the
weakest modules from filtering (they always have a top axis), defeating
the noise-floor goal. A module's "weakest" axis being weaker than the
strongest axis of a stronger module is exactly the right reason to
drop it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:37:49 +00:00
wassnameandClaude Opus 4.7 9ba7b818a9 Downsample cin_s/cin_t diagnostic via cin_split_every
Per-source cin (cin_s, cin_t) requires splitting each prompt's backward
into student-only + teacher-only passes, which roughly doubles backward
wall-time. With cin_s/cin_t empirically stable for 50 steps in #51
(cin_t ~0.37, cin_s ~0.18 with low variance), every-step is overkill.

Add Config.cin_split_every: int = 1 (current behavior). Set >1 to
compute cin_s/cin_t only every Nth step; combined single-backward on
the others. cin_s/cin_t print as NaN on skipped steps. Projection +
optimizer step unchanged (still uses combined grad).

Default 1 preserves the current run cost; user can opt into 10 for
~half the backward time once the diagnostic is in steady state.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:14:30 +00:00
wassnameandClaude Opus 4.7 ff26cbe089 Split row cols by source: add rew_s/gt_t; rename timing col t_rew
The combined `rew` column mixed student + teacher rollouts, making it
hard to tell "is student learning?" at a glance. Add per-source splits:

- rew_s: student-only mean reward (primary learning signal)
- gt_t : teacher-only ground-truth pass count (cache stability check)

The teacher pool is frozen at startup (baseline logged at load), so
per-step rew_t adds noise without information and is omitted.

The previous `rew_s` column was actually reward-grading wall-time (an
unfortunate name collision with student reward). Rename it to `t_rew`
to match the other timing cols (gen, fb).

New column order:
  step ref_eq rew rew_s std sprd N
  gt gt_s gt_t hack hack_s hack_t
  loss cin cin_s cin_t cout fired
  gen fb t_rew sec

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:13:00 +00:00
wassnameandClaude Opus 4.7 e0f33045a9 Include tau_axis in v_hack cache filename + plumb through Config
tau_axis is baked into the saved V at extract time (extract_vhack_grad
zeros rows where S_i/S_0 < tau_axis before SVD output is saved), so the
cached file content depends on it. The previous filename keyed only on
top_k, meaning a change to tau_axis would silently serve a stale cache.

Add Config.v_hack_tau_axis (default 0.0) and tag it into the filename
only when nonzero — so existing v_hack_Qwen3-4B_k12.safetensors files
remain reachable under the default config.

Future cache-key footgun (pairs.py changes) is flagged in a comment;
add a pairs hash when pair-set ablations begin.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:11:41 +00:00
wassnameandClaude Opus 4.7 5bf2180248 Drop dead code: unused v_sv return from load_v_hack
load_v_hack returned (v_hack, v_sv) but no caller consumed v_sv after
the runtime suspicion gate was removed in 8d170a0. All three callers
(train.py, verify_vhack_heldout.py, probe_distill.py) discarded it as
_v_sv. Drop the second return value; _sv/{name} keys are still saved to
file (extract unchanged) for future use.

Also drop the `v_hack is not None` guards in train.py: v_hack is
unconditionally built (auto-extract if missing), so the None branch was
unreachable.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:10:55 +00:00
wassnameandClaude Opus 4.7 bfc54b83b4 Restore model.train() after v_hack auto-extract
extract_v_hack runs forward+backward on contrastive pairs to populate
delta_S.grad; the inline auto-extract called model.eval() but never
called model.train() back, so the entire training run was in eval mode.

Qwen3 has no dropout by default so behavior was unchanged, but this
matches the standalone extract CLI's behavior and avoids latent
inconsistency if a model with dropout is used later.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:08:55 +00:00