mirror of
https://github.com/wassname/evil_MoE.git
synced 2026-07-13 16:22:40 +08:00
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>
This commit is contained in:
+13
-16
@@ -20,10 +20,10 @@ Arm classification (from the preset line `arm=`, covering old --arm and new
|
||||
online erasure arm=projected, --vhack-refresh-every=N>0 (re-extracted)
|
||||
routing arm=routing (intervention=route)
|
||||
|
||||
For routing we plot the SHIP-eval hack/solve (hack_ship/solve_ship, the deployed
|
||||
model = quarantine knob deleted, measured every --eval-ablate-every steps), NOT
|
||||
the training-time hack_s: the routed forward still hacks during training, so the training curve
|
||||
would falsely read "route doesn't work". The ablated curve is the deployment
|
||||
For routing we plot the DEPLOY-eval hack/solve (hack_deploy/solve_deploy, the
|
||||
deployed model = quarantine knob deleted, measured every --eval-ablate-every steps),
|
||||
NOT the training-time hack_s: the routed forward still hacks during training, so the
|
||||
training curve would falsely read "route doesn't work". The deploy curve is the deployment
|
||||
model. (none/erase plot training-time hack_s; their intervention acts at train
|
||||
time.)
|
||||
|
||||
@@ -93,12 +93,11 @@ def parse_log(path: Path) -> dict | None:
|
||||
|
||||
series: dict[str, list[float]] = defaultdict(list)
|
||||
steps: list[int] = []
|
||||
# Also parse the route SHIP-eval columns when present (older logs lack them
|
||||
# -> skip). For routing we plot THESE (deployed model), not training-time
|
||||
# hack_s. Renamed hack_abl/solve_abl -> hack_ship/solve_ship 2026-05-30;
|
||||
# accept both so old evidence logs still parse.
|
||||
ship = {"hack_abl", "solve_abl", "hack_ship", "solve_ship"} & set(idx)
|
||||
wanted = {**RATE_COLS, **COS_COLS, **{c: c for c in ship}}
|
||||
# Also parse the route DEPLOY-eval columns when present (non-route logs lack
|
||||
# them -> skip). For routing we plot THESE (deployed model = quarantine deleted),
|
||||
# not the training-time hack_s.
|
||||
deploy = {"hack_deploy", "solve_deploy"} & set(idx)
|
||||
wanted = {**RATE_COLS, **COS_COLS, **{c: c for c in deploy}}
|
||||
for line in txt.splitlines():
|
||||
if "| INFO |" not in line:
|
||||
continue
|
||||
@@ -114,13 +113,11 @@ def parse_log(path: Path) -> dict | None:
|
||||
steps=np.array(steps), **{k: np.array(v, dtype=float) for k, v in series.items()})
|
||||
# COHERENCE-GAP FIX: route's training-time hack_s looks vanilla (the routed
|
||||
# forward still hacks); routing's benefit only shows on the DEPLOYED model
|
||||
# (quarantine knob deleted). So for routing, plot the ship series under the
|
||||
# (quarantine knob deleted). So for routing, plot the deploy series under the
|
||||
# hack_s/gt_s keys -> all downstream (panels, onset, overlay) reads it.
|
||||
if arm == "routing":
|
||||
hk = "hack_ship" if "hack_ship" in run else "hack_abl" if "hack_abl" in run else None
|
||||
if hk:
|
||||
run["hack_s"] = run["hack_ship" if "hack_ship" in run else "hack_abl"]
|
||||
run["gt_s"] = run["solve_ship" if "solve_ship" in run else "solve_abl"]
|
||||
if arm == "routing" and "hack_deploy" in run:
|
||||
run["hack_s"] = run["hack_deploy"]
|
||||
run["gt_s"] = run["solve_deploy"]
|
||||
return run
|
||||
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
"""Single-run routing figure: training-time hack vs SHIPPED-model hack.
|
||||
"""Single-run routing figure: training-time hack vs DEPLOYED-model hack.
|
||||
|
||||
The routing story in one plot. During training the model keeps hacking (it runs
|
||||
with the quarantine knob ON, so the per-step hack_s curve climbs like vanilla).
|
||||
But the model we'd actually SHIP has the knob deleted -- its hack rate (the
|
||||
ship-eval, measured every --eval-ablate-every steps) is what matters. If routing
|
||||
works, the ship curve sits well BELOW the training curve at preserved solve.
|
||||
But the model we'd actually DEPLOY has the knob deleted -- its hack rate (the
|
||||
deploy-eval, measured every --eval-ablate-every steps) is what matters. If routing
|
||||
works, the deploy curve sits well BELOW the training curve at preserved solve.
|
||||
|
||||
uv run python scripts/plot_route_evidence.py LOG.log --out out/route_evidence.png
|
||||
|
||||
Reads either old (hack_abl/solve_abl) or new (hack_ship/solve_ship) ship columns.
|
||||
Reads the hack_deploy/solve_deploy columns (Gradient Routing deploy-eval).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -41,36 +41,36 @@ def parse(log: Path):
|
||||
idx = {n: i for i, n in enumerate(hdr)}
|
||||
i_step, i_train = idx["step"], idx["hack_s?"]
|
||||
i_solve = idx["gt_s↑"]
|
||||
i_hship = idx.get("hack_ship", idx.get("hack_abl"))
|
||||
i_sship = idx.get("solve_ship", idx.get("solve_abl"))
|
||||
i_hdep = idx["hack_deploy"]
|
||||
i_sdep = idx["solve_deploy"]
|
||||
steps, train_hack, solve_train = [], [], []
|
||||
ship_step, ship_hack, ship_solve = [], [], []
|
||||
deploy_step, deploy_hack, deploy_solve = [], [], []
|
||||
for l in txt.splitlines():
|
||||
if "| INFO |" not in l:
|
||||
continue
|
||||
r = l.split("| INFO |", 1)[1].split()
|
||||
if not r or not r[0].isdigit() or len(r) <= i_sship:
|
||||
if not r or not r[0].isdigit() or len(r) <= i_sdep:
|
||||
continue
|
||||
s = int(r[i_step])
|
||||
steps.append(s)
|
||||
train_hack.append(_frac(r[i_train]))
|
||||
solve_train.append(_frac(r[i_solve]))
|
||||
h = _frac(r[i_hship])
|
||||
if h is not None: # ship-eval only fires every N steps
|
||||
ship_step.append(s); ship_hack.append(h); ship_solve.append(_frac(r[i_sship]))
|
||||
h = _frac(r[i_hdep])
|
||||
if h is not None: # deploy-eval only fires every N steps
|
||||
deploy_step.append(s); deploy_hack.append(h); deploy_solve.append(_frac(r[i_sdep]))
|
||||
return dict(steps=steps, train_hack=train_hack, solve_train=solve_train,
|
||||
ship_step=ship_step, ship_hack=ship_hack, ship_solve=ship_solve)
|
||||
deploy_step=deploy_step, deploy_hack=deploy_hack, deploy_solve=deploy_solve)
|
||||
|
||||
|
||||
def main(log: str, out: str = "out/figs/route_evidence.png") -> None:
|
||||
d = parse(Path(log))
|
||||
RED, GREY = "#b03a2e", "#9a8c7a" # hack=red (the story); solve=muted (context)
|
||||
fig, ax = plt.subplots(figsize=(7, 4))
|
||||
# Hack in red: training (knob on, solid) vs shipped (knob off, dashed+marker).
|
||||
# Hack in red: training (knob on, solid) vs deployed (knob off, dashed+marker).
|
||||
# The vertical gap between the two reds at the last step IS the routing effect.
|
||||
ax.plot(d["steps"], d["train_hack"], color=RED, lw=2.2)
|
||||
ax.plot(d["ship_step"], d["ship_hack"], color=RED, lw=1.6, ls=(0, (4, 3)), marker="o", ms=4)
|
||||
ax.plot(d["ship_step"], d["ship_solve"], color=GREY, lw=1.4)
|
||||
ax.plot(d["deploy_step"], d["deploy_hack"], color=RED, lw=1.6, ls=(0, (4, 3)), marker="o", ms=4)
|
||||
ax.plot(d["deploy_step"], d["deploy_solve"], color=GREY, lw=1.4)
|
||||
|
||||
# Direct labels at the right end (name + final value baked in) -> no legend,
|
||||
# no separate value annotations. One element does both jobs (eraser test).
|
||||
@@ -79,26 +79,26 @@ def main(log: str, out: str = "out/figs/route_evidence.png") -> None:
|
||||
ax.annotate(text, (x_end, y), xytext=(8, 0), textcoords="offset points",
|
||||
va="center", color=color, fontsize=9)
|
||||
label(d["train_hack"][-1], f"hack, knob ON (training) {d['train_hack'][-1]:.0%}", RED)
|
||||
label(d["ship_solve"][-1], f"solve, shipped {d['ship_solve'][-1]:.0%}", GREY)
|
||||
label(d["ship_hack"][-1], f"hack, knob OFF (shipped) {d['ship_hack'][-1]:.0%}", RED)
|
||||
label(d["deploy_solve"][-1], f"solve, deployed {d['deploy_solve'][-1]:.0%}", GREY)
|
||||
label(d["deploy_hack"][-1], f"hack, knob OFF (deployed) {d['deploy_hack'][-1]:.0%}", RED)
|
||||
|
||||
ax.set_ylim(-0.02, 1.0)
|
||||
ax.set_yticks([0, 0.5, 1.0]); ax.set_yticklabels(["0", ".5", "1"])
|
||||
ax.set_xticks([0, d["ship_step"][-1] if d["ship_step"] else x_end])
|
||||
ax.set_xticks([0, d["deploy_step"][-1] if d["deploy_step"] else x_end])
|
||||
ax.set_xlabel("GRPO step")
|
||||
ax.set_xlim(0, x_end * 1.5) # right margin for the direct labels
|
||||
for side in ("top", "right"):
|
||||
ax.spines[side].set_visible(False)
|
||||
ax.spines["left"].set_bounds(0, 1) # range-frame: axis spans the data
|
||||
ax.set_title("Routing parks the cheat in a deletable knob:\n"
|
||||
"the model hacks while training but the shipped model does not", fontsize=10.5)
|
||||
"the model hacks while training but the deployed model does not", fontsize=10.5)
|
||||
fig.tight_layout()
|
||||
Path(out).parent.mkdir(parents=True, exist_ok=True)
|
||||
fig.savefig(out, dpi=130)
|
||||
link = link_latest(Path(out))
|
||||
print(f"wrote {out} (docs/figs latest -> {link}) "
|
||||
f"(train_hack_final={d['train_hack'][-1]:.3f}, "
|
||||
f"ship_hack_final={d['ship_hack'][-1]:.3f}, ship_solve_final={d['ship_solve'][-1]:.3f})")
|
||||
f"deploy_hack_final={d['deploy_hack'][-1]:.3f}, deploy_solve_final={d['deploy_solve'][-1]:.3f})")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
+1
-1
@@ -146,7 +146,7 @@ def main() -> None:
|
||||
# first-class arm now, so include it (keyed on `arm` below so it doesn't
|
||||
# merge with projected). NOTE: routing's L5_hack here is the TRAINING-time
|
||||
# hack (the routed forward still hacks); the deployment number is the
|
||||
# ablated-eval (ROUTE EVAL BLUF / hack_abl), not this column.
|
||||
# deploy-eval (ROUTE EVAL BLUF / hack_deploy), not this column.
|
||||
j = (df.filter(pl.col("arm").is_in(["projected", "routing"]))
|
||||
.join(van, on=["mix", "seed"], how="inner")
|
||||
.with_columns((pl.col("L5_hack") - pl.col("v_hack")).alias("dh"),
|
||||
|
||||
Reference in New Issue
Block a user