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)