File size: 7,657 Bytes
e418394 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 | """Top-k gradient-pursuit direction ablation (the paper's original mode,
vs ws_ablate.py's single-direction demo mode) on the flexible-generalization
prompt set: h <- h - Q Q^T h, where Q's <=10 columns are the orthonormalised
gradient-pursuit atoms (ws_pursuit_dump's greedy J-lens dictionary) selected
at that exact (layer, position) cell.
Two-pass, fixed basis: pass 1 does one recorded forward over the union of
all condition layers and runs pursuit (k_max=10) at every position of every
layer, producing a per-position orthonormal basis; pass 2 re-runs the
forward with hooks that project out that frozen basis. Positions align
because the prompt is re-tokenised identically and there is no generation.
control10 ablates the same way but with a basis built from 10 fixed random
vocab tokens (seed 0, shared across positions and trials) - it controls for
the effect of removing any 10 directions, not specifically the pursuit ones.
Usage: python ws_ablate10.py {ouro,qwen,huginn} [--limit N]
"""
import argparse
import json
import random
import torch
import ws_common as W
from ws_extra import DATA, boot_ci, groups, next_logits
from ws_pursuit_dump import pursuit_layer
torch.set_grad_enabled(False)
K = 10
CONTROL_SEED = 0
def atoms_for(ctx, v, ids):
"""Unit-normalised atoms for token ids at layer v: same construction as
ws_pursuit_dump.pursuit_layer's per-step atom, batched over ids."""
d = ctx.U[ids] @ ctx.Jt[v].T
return d / d.norm(dim=1, keepdim=True)
def basis_from_atoms(atoms, d):
"""atoms [k,d] unit vectors (k<=K) -> zero-padded orthonormal Q [d,K].
Zero-padding is safe: a zero column of Q contributes nothing to Q Q^T h."""
Q = torch.zeros(d, K, device=atoms.device, dtype=atoms.dtype)
if atoms.shape[0]:
q, _ = torch.linalg.qr(atoms.T, mode="reduced")
Q[:, :q.shape[1]] = q
return Q
def pursuit_bases(ctx, v, h):
"""Per-position orthonormal basis of the top-K pursuit atoms.
h [T,d] float32 cuda. Returns Q [T,d,K]."""
T, d = h.shape
sel, _, _, _ = pursuit_layer(ctx, v, h, k_max=K, k_snap=(K,))
bases = h.new_zeros(T, d, K)
for p in range(T):
idx = sel[p]
idx = idx[idx >= 0].to(h.device)
if idx.numel():
bases[p] = basis_from_atoms(atoms_for(ctx, v, idx), d)
return bases
def control_bases(ctx, layers):
"""Fixed 10-random-token basis per layer, shared across all positions."""
rng = random.Random(CONTROL_SEED)
ids = torch.tensor(rng.sample(range(ctx.U.shape[0]), K), device="cuda")
out = {}
for v in layers:
atoms = atoms_for(ctx, v, ids)
out[v] = basis_from_atoms(atoms, atoms.shape[1])
return out
class PursuitAblateHooks:
"""h <- h - Q Q^T h per position, on a frozen pass-1 (or control) basis.
bases: {v: [T,d,K]} (pursuit, per-position) or {v: [d,K]} (control,
shared across positions)."""
def __init__(self, ctx, layers, bases):
self.ctx, self.layers, self.bases, self.handles = ctx, layers, bases, []
def _hook(self, v):
Q = self.bases[v]
shared = Q.dim() == 2
def fn(module, inputs, output):
h = output[0] if isinstance(output, tuple) else output
hf32 = h.float()
if shared:
c = hf32 @ Q
corr = c @ Q.T
else:
c = torch.einsum("btd,tdk->btk", hf32, Q)
corr = torch.einsum("btk,tdk->btd", c, Q)
h[:] = (hf32 - corr).to(h.dtype)
return fn
def __enter__(self):
for v in self.layers:
self.handles.append(
self.ctx.model.layers[v].register_forward_hook(self._hook(v)))
return self
def __exit__(self, *exc):
for h in self.handles:
h.remove()
def ablate10(ctx, limit):
fg = json.load(open(f"{DATA}/flexible-generalization.json"))["categories"]
g = groups(ctx)
ks = sorted(g)
conds = {"pursuit10 all_int": ctx.int_layers}
if len(ks) >= 3:
conds["pursuit10 g2 only"] = g[ks[1]]
conds["pursuit10 last only"] = g[ks[-1]]
union = sorted(set().union(*conds.values()))
ctrl_layers = ctx.int_layers
for v in set(union) | set(ctrl_layers):
assert v in ctx.Jt, f"missing Jt at layer {v}"
ctrl_bases = control_bases(ctx, ctrl_layers)
all_conds = list(conds) + ["control10 all_int"]
stats = {c: {"kept": 0, "n": 0, "log_ranks": []} for c in all_conds}
base_n = 0
for cat in fg[:limit]:
for func in cat["funcs"]:
for arg in cat["args"]:
prompt = func["template"].format(arg=arg)
ans_tok = W.first_token(ctx, func["answers"][arg])
base = next_logits(ctx, prompt)
if int(base.argmax()) != ans_tok:
continue
base_n += 1
ids = W.encode(ctx, prompt)
_, acts = W.forward(ctx, ids, layers=union)
bases = {v: pursuit_bases(ctx, v, acts[v]) for v in union}
del acts
trial_conds = {cname: (layers, {v: bases[v] for v in layers})
for cname, layers in conds.items()}
trial_conds["control10 all_int"] = (ctrl_layers, ctrl_bases)
for cname, (layers, cbases) in trial_conds.items():
with PursuitAblateHooks(ctx, layers, cbases):
logits = next_logits(ctx, prompt)
rank = int((logits > logits[ans_tok]).sum()) + 1
stats[cname]["kept"] += rank == 1
stats[cname]["n"] += 1
stats[cname]["log_ranks"].append(
float(torch.log10(torch.tensor(float(rank)))))
del bases
print(f" {cat['name']} done ({base_n} correct-baseline trials so far)",
flush=True)
print(f"\nablate10: {base_n} correct-baseline trials", flush=True)
out = {"base_n": base_n,
"design": ("k=10 gradient-pursuit atoms per (layer, position), "
"fixed two-pass basis: pass 1 records the union of "
"condition layers and runs pursuit once per trial; "
"pass 2 re-hooks with the frozen QR basis. "
"control10 uses 10 fixed random vocab tokens (seed 0) "
"building the same per-layer basis, shared across "
"positions."),
"conditions": {}}
for cname, s in stats.items():
if not s["n"]:
continue
lo, hi = boot_ci(s["kept"], s["n"])
mlr = sum(s["log_ranks"]) / len(s["log_ranks"])
print(f" {cname:20s}: kept {s['kept']}/{s['n']} "
f"({100*s['kept']/s['n']:.0f}%, CI {lo:.0f}-{hi:.0f}), "
f"mean log10 rank {mlr:.2f}", flush=True)
out["conditions"][cname] = {"kept": s["kept"], "n": s["n"],
"mean_log10_rank": mlr,
"log_ranks": s["log_ranks"]}
return out
if __name__ == "__main__":
ap = argparse.ArgumentParser()
ap.add_argument("model", choices=["ouro", "qwen", "huginn"])
ap.add_argument("--limit", type=int, default=None)
args = ap.parse_args()
ctx = W.load_ctx(args.model, full_depth=True)
print(f"loaded {args.model}: groups "
f"{[(k, len(v)) for k, v in groups(ctx).items()]}", flush=True)
result = ablate10(ctx, args.limit)
with open(f"ws_{args.model}_ablate10.json", "w") as f:
json.dump(result, f, default=float)
print("DONE_WS_ABLATE10", flush=True)
|