Download methods/baselines.py from skyuu72/kibble-pythia-1b: direct link, hf CLI and curl.
- Browser
- Download file 7.56 kB
-
https://huggingface.co/skyuu72/kibble-pythia-1b/resolve/main/methods/baselines.py
- Command line
-
hf download hf://skyuu72/kibble-pythia-1b/methods/baselines.py
-
curl -L -o baselines.py https://huggingface.co/skyuu72/kibble-pythia-1b/resolve/main/methods/baselines.py
7.56 kB
| # phase 2 baselines: nothing, fact-in-context, LoRA, MEMIT, AlphaEdit. | |
| import os, sys | |
| sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) | |
| from common import * | |
| from values import * | |
| from harness import ctx_prefix | |
| class NoEdit(Method): | |
| pass | |
| class InContext(Method): | |
| """mode=oracle: each prompt gets exactly its own fact in front (perfect retriever). | |
| mode=all: every edited fact is pasted in front of every prompt, the locality prompts too.""" | |
| def reset(self): self.facts = [] | |
| def edit(self, reqs): | |
| self.facts = getattr(self, "facts", []) + reqs | |
| if self.hp.get("mode", "oracle") == "all": | |
| n = len(self.tok(" ".join(r["statement"] for r in self.facts))["input_ids"]) | |
| if n > 1750: raise Skip(f"{len(self.facts)} facts = {n} tokens, does not fit in pythia's 2048 context") | |
| def eval_kwargs(self): | |
| if self.hp.get("mode", "oracle") == "all": | |
| return dict(global_prefix=" ".join(r["statement"] for r in self.facts) + " ") | |
| return dict(prefix_for=ctx_prefix) | |
| class LoRA(Method): | |
| """plain LoRA fine-tune on the fact statements. the only baseline (with MEMIT's z*) that uses autograd. | |
| sequential = keep training the same adapters on each new batch, no replay.""" | |
| def __init__(self, model, tok, **hp): | |
| super().__init__(model, tok, **hp) | |
| self.r, self.alpha = hp.get("r", 8), hp.get("alpha", 16) | |
| self.lr, self.epochs, self.bs = hp.get("lr", 3e-4), hp.get("epochs", 20), hp.get("bs", 4) | |
| self.handles, self.params = [], [] | |
| def reset(self): | |
| for h in self.handles: h.remove() | |
| self.handles, self.params = [], [] | |
| g = torch.Generator(device="cpu").manual_seed(0) | |
| for b in blocks(self.model): | |
| for lin in (b.attention.query_key_value, b.attention.dense, b.mlp.dense_h_to_4h, b.mlp.dense_4h_to_h): | |
| A = (torch.randn(self.r, lin.in_features, generator=g) / lin.in_features ** 0.5).to(DEV).requires_grad_(True) | |
| B = torch.zeros(lin.out_features, self.r, device=DEV, requires_grad=True) | |
| s = self.alpha / self.r | |
| def fn(mod, inp, out, A=A, B=B, s=s): | |
| return out + (((inp[0].float() @ A.T) @ B.T) * s).to(out.dtype) | |
| self.handles.append(lin.register_forward_hook(fn)); self.params += [A, B] | |
| self.opt = torch.optim.AdamW(self.params, lr=self.lr, weight_decay=0.0) | |
| self.scaler = torch.amp.GradScaler("cuda", enabled=False) | |
| def edit(self, reqs): | |
| import random | |
| # loss=target (default): only the answer tokens are trained, like the FT baselines in the editing papers. | |
| # loss=lm: full statement. tried first, it drags the whole model toward the template (KL 1-2 nats). | |
| rng = random.Random(0); bs = self.bs; tonly = self.hp.get("loss", "target") == "target" | |
| pt = [split_target(self.tok, r["prompt"], r["target"]) for r in reqs] | |
| seqs = [p + t for p, t in pt]; starts = [len(p) if tonly else 1 for p, t in pt] | |
| # trains in fp32 (see common.fp32): in fp16 the grads were mostly underflow noise and every config trashed locality | |
| with fp32(self.model), torch.enable_grad(): | |
| for ep in range(self.epochs): | |
| order = list(range(len(seqs))); rng.shuffle(order); i = 0 | |
| while i < len(order): | |
| x, m = pad([seqs[j] for j in order[i:i + bs]]) | |
| for r_, j in enumerate(order[i:i + bs]): m[r_, :starts[j]] = 0 # label mask only; passed attention mask is rebuilt below | |
| try: | |
| logits = self.model(input_ids=x, attention_mask=(x != 0) | (torch.arange(x.shape[1], device=DEV)[None] == 0)).logits.float() | |
| tgt = x[:, 1:].masked_fill(m[:, 1:] == 0, -100) # m is 0 on padding and on everything before the answer | |
| loss = torch.nn.functional.cross_entropy(logits[:, :-1].reshape(-1, logits.shape[-1]), tgt.reshape(-1), ignore_index=-100) | |
| self.opt.zero_grad(); self.scaler.scale(loss).backward() | |
| self.scaler.step(self.opt); self.scaler.update() | |
| except torch.OutOfMemoryError: | |
| free(); bs = max(1, bs // 2); print("[oom] lora batch ->", bs, flush=True); continue | |
| i += bs | |
| self.last_loss = loss.item() | |
| class MEMIT(Method): | |
| """Meng et al. 2023. z* by gradient descent on a residual delta, then the closed-form spread over layers: | |
| dW_l = R K^T (lam*C + K K^T)^-1, computed through Woodbury with a cached C^-1.""" | |
| def __init__(self, model, tok, **hp): | |
| super().__init__(model, tok, **hp) | |
| self.layers = hp.get("layers", [2, 3, 4, 5, 6]); self.lam = hp.get("lam", 15000.0) | |
| self.zkw = dict(steps=hp.get("steps", 20), lr=hp.get("z_lr", 0.5), clamp=hp.get("clamp", 0.75)) | |
| self.delta = Delta(model); self.z = {} | |
| def reset(self): self.delta.clear() | |
| def ztag(self): return f"_s{self.zkw['steps']}_c{self.zkw['clamp']}" | |
| def precompute(self, reqs): | |
| self.reset() | |
| z, nll = z_cached(self.model, self.tok, reqs, self.layers[-1], tag=self.ztag(), **self.zkw) | |
| self.z = {r["id"]: z[i] for i, r in enumerate(reqs)}; free() | |
| def solve(self, l, K, R, i): | |
| Cinv = cov_inv(l, "k", self.hp.get("ridge", 0.0)); CKt = Cinv @ K.T; del Cinv | |
| A = torch.linalg.solve(self.lam * torch.eye(K.shape[0], device=DEV, dtype=torch.float64) + K @ CKt, CKt.T) | |
| return R.T, A | |
| def edit(self, reqs): | |
| z = torch.stack([self.z[r["id"]] for r in reqs]).double() | |
| for i, l in enumerate(self.layers): | |
| K = keys_at(self.model, self.tok, reqs, [l])[l].double() | |
| cur = resid_at(self.model, self.tok, reqs, self.layers[-1]).double() | |
| R = (z - cur) / (len(self.layers) - i) | |
| U, V = self.solve(l, K, R, i) | |
| self.delta.add(l, U, V); free() | |
| self.after_edit(reqs) | |
| def after_edit(self, reqs): pass | |
| class AlphaEdit(MEMIT): | |
| """Fang et al. 2025. MEMIT, but the update is projected onto the 'null space' of the preserved keys | |
| (eigvecs of C with eigenvalue < thr) and previously edited keys are remembered: | |
| dW^T = (P (K K^T + Kp Kp^T) + c I)^-1 P K R^T. solved in the projected basis, same thing, much smaller.""" | |
| def __init__(self, model, tok, **hp): | |
| super().__init__(model, tok, **hp) | |
| self.thr, self.c = hp.get("thr", 2e-2), hp.get("L2", 10.0); self.Q, self.prev = {}, {} | |
| def reset(self): | |
| super().reset(); self.prev = {} | |
| def proj(self, l): | |
| if l not in self.Q: | |
| C = torch.load(os.path.join(CK, f"cov_k_L{l}.pt"))["C"].to(DEV).double() | |
| ev, V = torch.linalg.eigh(C); self.Q[l] = V[:, ev < self.thr].cpu(); del C, V; free() | |
| return self.Q[l].to(DEV) | |
| def solve(self, l, K, R, i): | |
| Q = self.proj(l); Kt = K @ Q # [N, r] keys in the null basis | |
| Kall = torch.cat([Kt, self.prev[l].to(DEV)]) if l in self.prev else Kt | |
| G = self.c * torch.eye(Kall.shape[0], device=DEV, dtype=torch.float64) + Kall @ Kall.T | |
| X = (Kt.T - Kall.T @ torch.linalg.solve(G, Kall @ Kt.T)) / self.c # (cI + Kall^T Kall)^-1 Kt^T | |
| V = (Q @ X).T; self._new = getattr(self, "_new", {}); self._new[l] = Kt.cpu() | |
| return R.T, V | |
| def after_edit(self, reqs): | |
| for l, Kt in self._new.items(): | |
| self.prev[l] = torch.cat([self.prev[l], Kt]) if l in self.prev else Kt | |
| self._new = {} | |