diff --git a/justfile b/justfile index 87d47a1..0958d04 100644 --- a/justfile +++ b/justfile @@ -68,6 +68,15 @@ smoke-routeV *ARGS: --teacher-pool-dir=out/pools/teacher_pool --mix-ratio=0.5 \ --eval-ablate-every=10 --eval-n-prompts=2 {{ ARGS }} +# 100%-absorption control (NO vector): route every knob-on rollout fully into the +# quarantine, keep only the knob-off floor (rollout_ablate_frac) in the deployed knob. +# Direction-free -> the v_grad is extracted but inert. Needs frac>0 or the knob never updates. +smoke-absorb *ARGS: + BEARTYPE=1 {{ TRAIN }} smoke --intervention=routeV --routeV-absorb-all \ + --rollout-ablate-frac=0.5 \ + --teacher-pool-dir=out/pools/teacher_pool --mix-ratio=0.5 \ + --eval-ablate-every=10 --eval-n-prompts=2 {{ ARGS }} + # Run smoke twice: first warms the v_hack cache (cache-miss path), second hits # the cache (cache-hit path). Catches scope/save bugs that only manifest in one. smoke-both: @@ -136,6 +145,20 @@ fast-projected *ARGS: fast-lora-routeV *ARGS: {{ TRAIN }} fast --intervention=routeV --adapter=lora_frozen_b --lora-r=32 {{ ARGS }} +# H: ABSORB-ALL control (100% absorption, NO vector). Route the WHOLE gradient of every +# knob-on rollout into the quarantine; the deployed knob learns ONLY from the knob-off +# exploration floor (rollout_ablate_frac). v_grad is extracted (authored pairs) but inert +# -> routing is purely by generation mode, no direction. The extreme of H2: is the +# quarantine-as-sink + floor-only-deploy enough to suppress, with zero direction? +# resolve: deploy_hack ~ best/random-V -> absorption alone suffices (direction adds nothing); +# deploy_hack >> -> direction is load-bearing after all. +queue-absorb seed='43': + pueue add -w "$PWD" -o 24 \ + -l "why: routeV ABSORB-ALL (100% absorption, NO vector, frac=0.25) s{{seed}}; resolve: deploy_hack ~ best/random-V -> mode-routing alone suppresses (H2 extreme); >> -> direction needed" \ + -- {{ TRAIN }} fast --intervention=routeV --routeV-absorb-all --rollout-ablate-frac=0.25 \ + --vhack-pairs-path=out/pairsets/pairs_authored.json \ + --seed={{seed}} --out-tag=_dir8_routeV_absorb_s{{seed}} + # H: vGROUT directionality set -- 6 arms, ONE seed, single-mode run_tests, on the # FIXED eval (paper test set, base solve ~0.1). Tests whether routeV's deploy-hack # suppression needs the REAL hack direction. resolve: real-V (rollout & per-token) diff --git a/src/vgrout/train.py b/src/vgrout/train.py index bad9459..257f0bb 100644 --- a/src/vgrout/train.py +++ b/src/vgrout/train.py @@ -212,6 +212,13 @@ class Config: # band each step. No pairs needed for threshold calibration -- direction only. online_stats_lo: float = 0.05 # lower quantile -> keep tail online_stats_hi: float = 0.95 # upper quantile -> route tail + # 100%-absorption control (NO vector). Route the WHOLE gradient of every knob-on + # rollout into the quarantine (f=1), keep only the knob-off exploration-floor rollouts + # (is_ablated, f=0) in the deployed knob. The extreme of H2: the quarantine as a pure + # gradient sink, routing by generation-mode not by any direction. v_grad is still + # extracted (reuses the routeV path) but never touches f -- routing is direction-free. + # Requires rollout_ablate_frac>0, else the deployed knob never updates (= base model). + routeV_absorb_all: bool = False # Per-source cin diagnostic: split each prompt's backward into student-only # + teacher-only passes (~2x backward time). 1 = every step (default; full # signal); N>1 = only every Nth step (combined backward elsewhere, ~halves @@ -991,6 +998,7 @@ def main(cfg: Config) -> int: # modules (the global activation vote, computed post-backward before the per-module # routing). 1-element list so the filter closure reads the current step's value. _step_f_roll: list[torch.Tensor | None] = [None] + _step_absorb_f: list[torch.Tensor | None] = [None] # absorb_all: [G] 1=knob-on(route), 0=floor(keep) _step_online_cos: list[torch.Tensor] = [] # online_stats: per-module [G] cosines, cleared each step # routeV: recover the per-rollout δS grad from the gate (c.grad = δS * g_b), @@ -1024,7 +1032,20 @@ def main(cfg: Config) -> int: # per-token (routeV_per_token): one cos/f per token -- finer but noisier. lower, upper = route_band[name] band = max(upper - lower, 1e-6) - if cfg.routeV_gate == "act_vote": + if cfg.routeV_absorb_all: + # NO vector: f is purely the generation-mode mask (1=knob-on -> route the + # whole rollout, 0=knob-off floor -> keep). Direction-free 100% absorption; + # v_grad/band above are computed but never enter f. + cg = cg_full.sum(1) # [G, r] per-rollout δS*g + g_b = torch.where(reliable, cg / dS_safe, torch.zeros_like(cg)) # [G, r] + f = _step_absorb_f[0] # [G] 1=route, 0=keep + routed = torch.where(reliable, (cg * f.unsqueeze(1)).sum(0) / dS_safe, + torch.zeros_like(g)) + step_flagged.append(f.mean().item()) + _kn, _rn, _on, _ke, _re, _oe = _zone_stats(f, g_b.norm(dim=1)) + step_zkeep.append(_kn); step_zresid.append(_rn); step_zrout.append(_on) + step_zkeepE.append(_ke); step_zresidE.append(_re); step_zroutE.append(_oe) + elif cfg.routeV_gate == "act_vote": # Global gate: route every module's per-rollout grad by the SAME f_roll # (the activation vote, computed once for the step). Per-rollout granularity # by construction; per_token is ignored under act_vote. @@ -1458,6 +1479,11 @@ def main(cfg: Config) -> int: # routing (activations are cached on every layer from the loss forward). if is_routeV and cfg.routeV_gate == "act_vote": _step_f_roll[0] = _act_vote_f_roll(merged.shape[0], plen, mask) + # absorb_all: per-rollout route mask = generation mode (knob-on -> 1 route, + # knob-off floor -> 0 keep). Same row order as merged (students then teachers). + if is_routeV and cfg.routeV_absorb_all: + _step_absorb_f[0] = torch.tensor( + [0.0 if ab else 1.0 for ab in is_ablated], device=device) for name, info in wrappers.items(): g = info["delta_S"].grad if g is None: