lexframe 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
lexframe/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ """A toolkit for interpreting LM residual streams in their native semantics."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ from ._frame import Delta, LexFrame, State, View
6
+
7
+ __all__ = ["LexFrame", "View", "Delta", "State", "__version__"]
lexframe/_basis.py ADDED
@@ -0,0 +1,166 @@
1
+ """The lexical basis at a layer, s = J^+ y, solved by LSMR on Jacobian-vector products without forming J."""
2
+
3
+ import math
4
+
5
+ import torch
6
+ from torch.autograd import forward_ad as fwAD
7
+ from torch.utils.checkpoint import checkpoint
8
+
9
+ from . import _transport as TR
10
+ from ._model import block
11
+
12
+
13
+ def references(m, cal, xs, l, order):
14
+ L = int(m.n_layers)
15
+ B, Tn, d = xs[0].shape
16
+ pos = torch.arange(Tn, device=xs[0].device)
17
+ fi = torch.arange(0, B * Tn, Tn, device=xs[0].device)
18
+ if order is None:
19
+ return [TR.ref_kv(m, k, TR.innorm(m, k)(xs[k])) for k in range(l, L)]
20
+ low = [xs[l]] * order
21
+ out = []
22
+ with torch.no_grad():
23
+ for k in range(l, L):
24
+ norm = TR.innorm(m, k)
25
+ kv = [TR.ref_kv(m, k, norm(h)) for h in low]
26
+ out.append(kv[order - 1])
27
+ att = [TR.attend(m, k, norm(low[j]), pos, kv[j - 1]) for j in range(1, order)]
28
+ new = [TR._step(m, cal, k, low[0].reshape(-1, d), fi).view(B, Tn, d)]
29
+ for j in range(1, order):
30
+ a = att[j - 1].reshape(-1, d)
31
+ new.append(
32
+ block(m, k, low[j].reshape(-1, d), lambda u, s, a=a: a[s : s + u.shape[0]].to(u.dtype)).view(
33
+ B, Tn, d
34
+ )
35
+ )
36
+ low = new
37
+ return out
38
+
39
+
40
+ def stream(m, l, kv, X, ckpt=False):
41
+ L = int(m.n_layers)
42
+ b, Tn, d = X.shape
43
+ pos = torch.arange(Tn, device=X.device)
44
+
45
+ def step(h, k):
46
+ K, V = kv[k - l]
47
+ K, V = (K.expand(b, *K.shape[1:]), V.expand(b, *V.shape[1:])) if K.shape[0] != b else (K, V)
48
+ a = TR.attend(m, k, TR.innorm(m, k)(h), pos, (K, V)).reshape(-1, d)
49
+ return block(m, k, h.reshape(-1, d), lambda u, s: a[s : s + u.shape[0]].to(u.dtype)).view(b, Tn, d)
50
+
51
+ h = X
52
+ for k in range(l, L):
53
+ h = checkpoint(step, h, k, use_reentrant=False) if ckpt else step(h, k)
54
+ return m.final_norm(h).float()
55
+
56
+
57
+ def jvp(m, l, kv, X0, V):
58
+ dt = X0.dtype
59
+ with torch.no_grad(), fwAD.dual_level():
60
+ out = stream(m, l, kv, fwAD.make_dual(X0.expand(V.shape[0], -1, -1).contiguous(), V.to(dt)))
61
+ return fwAD.unpack_dual(out).tangent.float()
62
+
63
+
64
+ def vjp(m, l, kv, X0, U):
65
+ X = X0.expand(U.shape[0], -1, -1).clone().requires_grad_(True)
66
+ with torch.enable_grad():
67
+ return torch.autograd.grad(stream(m, l, kv, X, ckpt=True), X, U)[0].float()
68
+
69
+
70
+ def _ortho(a, b):
71
+ r = torch.sqrt(a * a + b * b)
72
+ z = r == 0
73
+ return (
74
+ torch.where(z, torch.ones_like(a), a / r.clamp_min(1e-30)),
75
+ torch.where(z, torch.zeros_like(a), b / r.clamp_min(1e-30)),
76
+ r,
77
+ )
78
+
79
+
80
+ def lsmr(Av, Atu, b, x0, damp, steps):
81
+ nrm = lambda u: u.norm(dim=-1, keepdim=True)
82
+ A = lambda v: (Av(v), damp * v)
83
+ At = lambda u: Atu(u[0]) + damp * u[1]
84
+ u = b - Av(x0), -damp * x0
85
+ beta = torch.sqrt(nrm(u[0]) ** 2 + nrm(u[1]) ** 2)
86
+ u = tuple(p / beta.clamp_min(1e-30) for p in u)
87
+ v = At(u)
88
+ alpha = nrm(v)
89
+ v = v / alpha.clamp_min(1e-30)
90
+ zetabar, alphabar = alpha * beta, alpha
91
+ one, zero = torch.ones_like(alpha), torch.zeros_like(alpha)
92
+ rho, rhobar, cbar, sbar, zeta = one, one, one, zero, zero
93
+ h, hbar, dx = v.clone(), torch.zeros_like(v), torch.zeros_like(v)
94
+ solved = alpha * beta == 0
95
+ for _ in range(0 if bool(solved.all()) else steps):
96
+ Avv = A(v)
97
+ u = Avv[0] - alpha * u[0], Avv[1] - alpha * u[1]
98
+ beta = torch.sqrt(nrm(u[0]) ** 2 + nrm(u[1]) ** 2)
99
+ u = tuple(p / beta.clamp_min(1e-30) for p in u)
100
+ v = At(u) - beta * v
101
+ alpha = nrm(v)
102
+ v = v / alpha.clamp_min(1e-30)
103
+ rhoold, rhobarold = rho, rhobar
104
+ c, s, rho = _ortho(alphabar, beta)
105
+ thetanew, alphabar = s * alpha, c * alpha
106
+ thetabar = sbar * rho
107
+ cbar, sbar, rhobar = _ortho(cbar * rho, thetanew)
108
+ zeta, zetabar = cbar * zetabar, -sbar * zetabar
109
+ hbar = h - thetabar * rho / (rhoold * rhobarold).clamp_min(1e-30) * hbar
110
+ step = zeta / (rho * rhobar).clamp_min(1e-30) * hbar
111
+ h = v - thetanew / rho.clamp_min(1e-30) * h
112
+ dx = dx + torch.where(solved, torch.zeros_like(step), step)
113
+ return x0 + dx
114
+
115
+
116
+ def auto_batch(m, probe, n, cap=64):
117
+ if m.device.type != "cuda":
118
+ return min(n, 8)
119
+ torch.cuda.synchronize()
120
+ torch.cuda.empty_cache()
121
+ base = torch.cuda.memory_allocated()
122
+ torch.cuda.reset_peak_memory_stats()
123
+ probe(2)
124
+ per = max(1, (torch.cuda.max_memory_allocated() - base) // 2)
125
+ free = torch.cuda.get_device_properties(m.device).total_memory - base
126
+ return max(1, min(n, cap, int(0.8 * free / per)))
127
+
128
+
129
+ def basis(m, cal, xs, LS, Y, order=None, ridge=0.001, steps=30, after=None, warm=False):
130
+ nr = Y.shape[0]
131
+ LS = sorted(LS, reverse=True)
132
+ X0s = {l: xs[l][:1] for l in LS}
133
+ K0 = references(m, cal, xs, min(LS), None) if order is None else None
134
+ kvof = lambda l: K0[l - min(LS) :] if order is None else references(m, cal, xs, l, order)
135
+
136
+ def run(rows, l, x0, kv):
137
+ X0 = X0s[l]
138
+ y = Y[rows].float()
139
+ Av = lambda v: jvp(m, l, kv, X0, v)
140
+ Atu = lambda u: vjp(m, l, kv, X0, u)
141
+ damp = math.sqrt(ridge) * Atu(y).norm(dim=-1, keepdim=True) / y.norm(dim=-1, keepdim=True).clamp_min(1e-30)
142
+ return lsmr(Av, Atu, y, x0, damp, steps)
143
+
144
+ def probe(n):
145
+ l = LS[-1]
146
+ v = Y[:1].float().expand(n, -1, -1)
147
+ jvp(m, l, kvof(l), X0s[l], v)
148
+ vjp(m, l, kvof(l), X0s[l], v)
149
+
150
+ b = auto_batch(m, probe, nr)
151
+ r0 = 0
152
+ while r0 < nr:
153
+ rows = slice(r0, min(nr, r0 + b))
154
+ try:
155
+ x = torch.zeros_like(Y[rows].float())
156
+ for l in LS:
157
+ kv = kvof(l)
158
+ x = run(rows, l, x if warm else torch.zeros_like(x), kv)
159
+ if after is not None:
160
+ after(l, X0s[l], kv, x, rows)
161
+ r0 = rows.stop
162
+ except torch.cuda.OutOfMemoryError:
163
+ if b == 1:
164
+ raise
165
+ b = max(1, b // 2)
166
+ torch.cuda.empty_cache()
lexframe/_calibrate.py ADDED
@@ -0,0 +1,240 @@
1
+ """The typical attention output of every block: a ridge regression of the attention output on its input."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Iterable
6
+
7
+ import torch
8
+
9
+ from ._model import Model, forward
10
+
11
+ RHO = [10 ** (e / 8) for e in range(-56, 25)]
12
+
13
+
14
+ class _Moments:
15
+
16
+ def __init__(self, m: Model, batches: Iterable[torch.Tensor], folds: int = 2, work=torch.float32):
17
+ L, d = m.n_layers, m.hidden_dim
18
+ self.L, self.d, self.K, self.dev = L, d, folds, m.device
19
+ self.n = 0
20
+ acc = torch.float64 if 2 * folds * L * d * d * 8 < 4000000000.0 else torch.float32
21
+ z = lambda *s: torch.zeros(*s, dtype=acc, device=m.device)
22
+ self.C = [[[z(d, d), z(d, d)] for _ in range(folds)] for _ in range(L)]
23
+ self.cnt = [0] * folds
24
+ at = [0]
25
+ self.mu = torch.zeros(L, folds, d, dtype=torch.float64, device=m.device)
26
+ self.ma = torch.zeros_like(self.mu)
27
+ self.tr = torch.zeros(L, folds, dtype=torch.float64)
28
+
29
+ def cap(k, u, a):
30
+ g = (at[0] + torch.arange(u.shape[0], device=u.device)) % folds
31
+ for f in range(folds):
32
+ if not bool((g == f).any()):
33
+ continue
34
+ n2, mu2, ma2, C2, B2, t2 = _moments(
35
+ u[g == f].reshape(-1, d).to(work), a[g == f].reshape(-1, d).to(work)
36
+ )
37
+ n1 = self.cnt[f]
38
+ w = n1 * n2 / (n1 + n2)
39
+ du, da = mu2 - self.mu[k, f], ma2 - self.ma[k, f]
40
+ Cf = self.C[k][f]
41
+ Cf[0] += (C2 + w * torch.outer(du, du)).to(acc)
42
+ Cf[1] += (B2 + w * torch.outer(du, da)).to(acc)
43
+ self.tr[k, f] += t2 + w * float(da @ da)
44
+ self.mu[k, f] += du * (n2 / (n1 + n2))
45
+ self.ma[k, f] += da * (n2 / (n1 + n2))
46
+ if k == L - 1:
47
+ self.cnt[f] = n1 + n2
48
+
49
+ for ids in batches:
50
+ ids = m.ids(ids)
51
+ forward(m, ids, on_attention=cap)
52
+ at[0] += ids.shape[0]
53
+ self.n += ids.numel()
54
+ if at[0] < folds:
55
+ raise ValueError(f"calibration needs at least {folds} sequences")
56
+
57
+ def moments(self, k):
58
+ out = [
59
+ (
60
+ self.cnt[f],
61
+ self.mu[k, f],
62
+ self.ma[k, f],
63
+ self.C[k][f][0].double(),
64
+ self.C[k][f][1].double(),
65
+ float(self.tr[k, f]),
66
+ )
67
+ for f in range(self.K)
68
+ ]
69
+ self.C[k] = None
70
+ return out
71
+
72
+
73
+ def _moments(U, A):
74
+ n = U.shape[0]
75
+ mu, ma = U.mean(0), A.mean(0)
76
+ Uc = U - mu
77
+ Ac = A - ma
78
+ return n, mu.double(), ma.double(), (Uc.T @ Uc).double(), (Uc.T @ Ac).double(), float((Ac.double() ** 2).sum())
79
+
80
+
81
+ def _merge(ms):
82
+ n = sum((m[0] for m in ms))
83
+ mu = sum((m[0] * m[1] for m in ms)) / n
84
+ ma = sum((m[0] * m[2] for m in ms)) / n
85
+ C = sum((m[3] + m[0] * torch.outer(m[1] - mu, m[1] - mu) for m in ms))
86
+ B = sum((m[4] + m[0] * torch.outer(m[1] - mu, m[2] - ma) for m in ms))
87
+ return n, mu, ma, C, B, sum((m[5] + m[0] * float((m[2] - ma) @ (m[2] - ma)) for m in ms))
88
+
89
+
90
+ class Calibration:
91
+
92
+ def __init__(self, ubar, abar, Q, lam, QA, rho, stats, model_id):
93
+ self.ubar, self.abar, self.Q, self.lam, self.QA, self.rho, self.stats, self.model_id = (
94
+ ubar,
95
+ abar,
96
+ Q,
97
+ lam,
98
+ QA,
99
+ rho,
100
+ stats,
101
+ model_id,
102
+ )
103
+
104
+ def __repr__(self) -> str:
105
+ return f"Calibration(model={self.model_id['architecture']}, layers={self.model_id['layers']})"
106
+
107
+ def typical(self, k: int):
108
+ ub, ab = self.ubar[k], self.abar[k]
109
+ dev = ub.device
110
+ Q, QA, lam, rid = self.Q[k].to(dev), self.QA[k].to(dev), self.lam[k], self.rho[k]
111
+
112
+ def fn(u, s):
113
+ uc = u.float() - ub
114
+ return (ab.expand_as(uc) + uc @ Q / (lam[None] + rid) @ QA).to(u.dtype)
115
+
116
+ return fn
117
+
118
+ def to(self, device):
119
+ mv = lambda t: t.to(device)
120
+ return Calibration(
121
+ mv(self.ubar),
122
+ mv(self.abar),
123
+ self.Q if self.Q.device.type == "cpu" else mv(self.Q),
124
+ mv(self.lam),
125
+ self.QA if self.QA.device.type == "cpu" else mv(self.QA),
126
+ mv(self.rho),
127
+ self.stats,
128
+ self.model_id,
129
+ )
130
+
131
+ def save(self, path: str) -> None:
132
+ torch.save(
133
+ {
134
+ "format": 1,
135
+ "model_id": self.model_id,
136
+ "stats": self.stats,
137
+ **{k: getattr(self, k).cpu() for k in ("ubar", "abar", "Q", "lam", "QA", "rho")},
138
+ },
139
+ path,
140
+ )
141
+
142
+ @staticmethod
143
+ def load(path: str, model: Model | None = None) -> Calibration:
144
+ z = torch.load(path, map_location="cpu", weights_only=True)
145
+ if z.get("format") != 1:
146
+ raise ValueError(f"{path}: not a lexframe calibration")
147
+ c = Calibration(z["ubar"], z["abar"], z["Q"], z["lam"], z["QA"], z["rho"], z["stats"], z["model_id"])
148
+ if model is not None:
149
+ if c.model_id != model_id(model):
150
+ raise ValueError(f"{path} was calibrated for {c.model_id}, not for {model_id(model)}")
151
+ c = c.to(model.device)
152
+ return c
153
+
154
+
155
+ def model_id(m: Model) -> dict:
156
+ w = lambda p: float(sum((t.double().sum() for t in p.parameters())))
157
+ return {
158
+ "architecture": type(m.hf).__name__,
159
+ "layers": m.n_layers,
160
+ "hidden": m.hidden_dim,
161
+ "checksum": round(w(m.layers[0]) + w(m.layers[-1]), 3),
162
+ }
163
+
164
+
165
+ def chunks(m: Model, texts: Iterable[str], length: int = 128, batch: int = 8, limit: int | None = None):
166
+ if m.tokenizer is None:
167
+ raise ValueError("this model has no tokenizer: pass token ids to calibrate")
168
+ eos = m.tokenizer.eos_token_id
169
+ buf, out, n = [], [], 0
170
+ for t in texts:
171
+ if not t or not t.strip():
172
+ continue
173
+ buf += m.tokenizer.encode(t, add_special_tokens=False) + ([eos] if eos is not None else [])
174
+ while len(buf) >= length:
175
+ out.append(buf[:length])
176
+ buf = buf[length:]
177
+ n += 1
178
+ if len(out) == batch:
179
+ yield torch.tensor(out)
180
+ out = []
181
+ if limit is not None and n >= limit:
182
+ if out:
183
+ yield torch.tensor(out)
184
+ return
185
+ if out:
186
+ yield torch.tensor(out)
187
+
188
+
189
+ @torch.no_grad()
190
+ def calibrate(m: Model, batches: Iterable, folds: int = 2, work=torch.float32) -> Calibration:
191
+ bank = _Moments(m, batches, folds, work)
192
+ L, d, dev, K = bank.L, bank.d, bank.dev, bank.K
193
+ f64 = torch.float64
194
+ store = "cpu" if L * d * d * 8 > 1500000000.0 else dev
195
+ ubar = torch.empty(L, d, device=dev)
196
+ abar = torch.empty(L, d, device=dev)
197
+ stats = []
198
+ Q = torch.empty(L, d, d, device=store)
199
+ QA = torch.empty(L, d, d, device=store)
200
+ lam = torch.empty(L, d, device=dev)
201
+ rho = torch.empty(L, device=dev)
202
+ grid = torch.tensor(RHO, dtype=f64, device=dev)
203
+ for k in range(L):
204
+ mo = bank.moments(k)
205
+ n, mu, ma, Cuu, Cua, _ = _merge(mo)
206
+ ubar[k], abar[k] = mu.float(), ma.float()
207
+ rg = grid * (Cuu.diagonal().sum() / d)
208
+ err = torch.zeros_like(rg)
209
+ tts = 0.0
210
+ for g in range(K):
211
+ _, mug, mag, Cg, Bg, _ = _merge([mo[i] for i in range(K) if i != g])
212
+ nf, _, _, Cf, Bf, tf = mo[g]
213
+ eu, ea = mo[g][1] - mug, mo[g][2] - mag
214
+ Duu = Cf + nf * torch.outer(eu, eu)
215
+ Dua = Bf + nf * torch.outer(eu, ea)
216
+ tt = tf + nf * float(ea @ ea)
217
+ tts += tt
218
+ ev, Qg = torch.linalg.eigh(Cg.to(work))
219
+ ev = ev.double().clamp_min(0)
220
+ QAg = Qg.T @ Bg.to(work)
221
+ Dr = Qg.T @ Duu.to(work) @ Qg
222
+ p = (Qg.T @ Dua.to(work) * QAg).sum(1).double()
223
+ KS = (QAg @ QAg.T * Dr).double()
224
+ W = 1.0 / (ev[None] + rg[:, None])
225
+ err += tt - 2 * (W @ p) + (W @ KS * W).sum(1)
226
+ j = int(err.argmin())
227
+ ev, Qk = torch.linalg.eigh(Cuu.to(work))
228
+ ev = ev.double().clamp_min(0)
229
+ QAk = Qk.T @ Cua.to(work)
230
+ rk = float(rg[j])
231
+ stats.append(
232
+ {
233
+ "rows": n,
234
+ "ridge_relative": RHO[j],
235
+ "dof": float((ev / (ev + rk)).sum()),
236
+ "r2_heldout": 1 - float(err[j]) / tts,
237
+ }
238
+ )
239
+ Q[k], QA[k], lam[k], rho[k] = Qk.float().to(store), QAk.float().to(store), ev.float(), rk
240
+ return Calibration(ubar, abar, Q, lam, QA, rho, stats, model_id(m))