Download code/wsp/edits.py from thegovind/weight-space-programming: direct link, hf CLI and curl.
- Browser
- Download file 6.77 kB
-
https://huggingface.co/spaces/thegovind/weight-space-programming/resolve/main/code/wsp/edits.py
- Command line
-
hf download hf://spaces/thegovind/weight-space-programming/code/wsp/edits.py
-
curl -L -o edits.py https://huggingface.co/spaces/thegovind/weight-space-programming/resolve/main/code/wsp/edits.py
6.77 kB
| """Weight edits on Qwen3.5-0.8B's MLP down projections, and model folders vLLM can load. | |
| The edit the paper shipped, per layer L in {15, 19}: | |
| W' = W - alpha * u (u^T W) restricted to a set S of columns (alpha = 6, |S| = 64) | |
| W is down_proj.weight (shape [1024, 3584]): column j is what MLP neuron j adds to the | |
| residual stream when it fires with activation 1. u is a unit vector in the 1024-d | |
| residual stream ("the loop direction"). For a selected column, u^T w_j' = (1 - alpha) u^T w_j: | |
| alpha = 1 removes the column's loop component, alpha = 6 flips it and makes it 5x larger. | |
| """ | |
| from __future__ import annotations | |
| import hashlib | |
| import json | |
| import os | |
| from pathlib import Path | |
| import numpy as np | |
| import torch | |
| NAME = "model.language_model.layers.{}.mlp.down_proj.weight" | |
| WEIGHT_FILE = "model.safetensors-00001-of-00001.safetensors" | |
| def weight_path(model_dir: str | os.PathLike) -> str: | |
| return os.path.join(model_dir, WEIGHT_FILE) | |
| def load(model_dir, names): | |
| from safetensors import safe_open | |
| out = {} | |
| with safe_open(weight_path(model_dir), framework="pt") as f: | |
| for n in names: | |
| out[n] = f.get_tensor(n) | |
| return out | |
| def load_down(model_dir, layer: int) -> torch.Tensor: | |
| return load(model_dir, [NAME.format(layer)])[NAME.format(layer)] | |
| def sha256(t: torch.Tensor) -> str: | |
| return hashlib.sha256(t.contiguous().view(torch.uint16).numpy().tobytes()).hexdigest() | |
| def projection_edit(W: torch.Tensor, u: torch.Tensor, alpha: float, cols=None) -> torch.Tensor: | |
| """W - alpha * outer(u, mask * (u @ W)), computed in float32, returned in W's dtype.""" | |
| W32 = W.float() | |
| u = (u.float() / u.float().norm()) | |
| g = u @ W32 | |
| if cols is not None: | |
| mask = torch.zeros_like(g) | |
| mask[torch.as_tensor(list(cols), dtype=torch.long)] = 1.0 | |
| g = g * mask | |
| return (W32 - alpha * torch.outer(u, g)).to(W.dtype) | |
| def recover(W0: torch.Tensor, W1: torch.Tensor): | |
| """From a base and an edited matrix: the changed columns, the direction u (top left | |
| singular vector of the change), the strength alpha, and the singular values.""" | |
| D = W1.double() - W0.double() | |
| cols = torch.nonzero(D.abs().sum(0) > 0).flatten() | |
| U, S, _ = torch.linalg.svd(D[:, cols], full_matrices=False) | |
| u = U[:, 0] | |
| P = torch.outer(u, u @ W0.double()[:, cols]) | |
| alpha = float(-(D[:, cols] * P).sum() / (P * P).sum()) | |
| return {"cols": cols.tolist(), "u": u.float(), "alpha": alpha, "singular_values": S.float()} | |
| def random_orthogonal(u: torch.Tensor, seed: int) -> torch.Tensor: | |
| g = torch.Generator().manual_seed(seed) | |
| r = torch.randn(u.shape[0], generator=g, dtype=torch.float64) | |
| u64 = u.double() / u.double().norm() | |
| r = r - (r @ u64) * u64 | |
| return (r / r.norm()).float() | |
| def link_base_files(base_dir, out_dir): | |
| """Symlink every file of the base folder except the weights (config, tokenizer, index...).""" | |
| base_dir, out_dir = Path(base_dir), Path(out_dir) | |
| out_dir.mkdir(parents=True, exist_ok=True) | |
| for p in base_dir.iterdir(): | |
| if p.name == WEIGHT_FILE or p.name.startswith(".") or p.name == "edit_note.json": | |
| continue | |
| q = out_dir / p.name | |
| if q.is_symlink() or q.exists(): | |
| q.unlink() | |
| q.symlink_to(p.resolve()) | |
| def write_variant(base_dir, out_dir, new_tensors: dict, note: dict): | |
| """A model folder = symlinks to the base folder's files + a weight file with | |
| `new_tensors` swapped in. Every other tensor is copied bit for bit.""" | |
| from safetensors.torch import load_file, save_file | |
| base_dir, out_dir = Path(base_dir), Path(out_dir) | |
| link_base_files(base_dir, out_dir) | |
| tensors = load_file(str(base_dir / WEIGHT_FILE)) | |
| for n, t in new_tensors.items(): | |
| assert tensors[n].shape == t.shape and tensors[n].dtype == t.dtype, n | |
| tensors[n] = t.contiguous() | |
| save_file(tensors, str(out_dir / WEIGHT_FILE), metadata={"format": "pt"}) | |
| note = dict(note) | |
| note["edited_sha256"] = {n: sha256(t) for n, t in new_tensors.items()} | |
| (out_dir / "edit_note.json").write_text(json.dumps(note, indent=1)) | |
| return out_dir | |
| def released_spec(edit_info_path): | |
| """The released edit, as recovered by labs/01: per layer, u, alpha and columns.""" | |
| d = json.loads(Path(edit_info_path).read_text()) | |
| return {int(l): {"u": torch.tensor(v["u"]), "alpha": v["alpha"], "cols": v["cols"]} | |
| for l, v in d["layers"].items()} | |
| def build_from_spec(base_dir, spec: dict, released: dict): | |
| """spec: {"layers": {"15": {"alpha": 6, "dir": "released" | "random_orth:SEED" | [floats], | |
| "cols": "released" | "all" | "random:SEED" | [ints], | |
| "norm_match": false}, ...}} | |
| Returns {tensor name: new bf16 tensor} and a description.""" | |
| new, desc = {}, {} | |
| for l_str, s in spec["layers"].items(): | |
| l = int(l_str) | |
| W = load_down(base_dir, l) | |
| rel = released.get(l) | |
| d = s.get("dir", "released") | |
| c = s.get("cols", "released") | |
| if rel is None and (isinstance(d, str) or (isinstance(c, str) and c != "all") or s.get("norm_match")): | |
| raise ValueError(f"layer {l}: the released edit does not touch this layer, " | |
| "so give an explicit dir (a list of floats) and cols (a list or 'all')") | |
| if d == "released": | |
| u = rel["u"] | |
| elif isinstance(d, str) and d.startswith("random_orth:"): | |
| u = random_orthogonal(rel["u"], int(d.split(":")[1])) | |
| else: | |
| u = torch.tensor(d, dtype=torch.float32) | |
| u = u / u.norm() | |
| if c == "released": | |
| cols = rel["cols"] | |
| elif c == "all": | |
| cols = None | |
| elif isinstance(c, str) and c.startswith("random:"): | |
| rng = np.random.default_rng(int(c.split(":")[1])) | |
| pool = np.setdiff1d(np.arange(W.shape[1]), np.array(rel["cols"])) | |
| cols = sorted(rng.choice(pool, size=len(rel["cols"]), replace=False).tolist()) | |
| else: | |
| cols = list(c) | |
| alpha = float(s["alpha"]) | |
| if s.get("norm_match"): | |
| # same Frobenius size of change as the released edit at this alpha | |
| W32 = W.float() | |
| ref = (rel["u"] / rel["u"].norm()) @ W32[:, rel["cols"]] | |
| mine = u @ (W32 if cols is None else W32[:, cols]) | |
| alpha = alpha * float(ref.norm() / mine.norm()) | |
| W1 = projection_edit(W, u, alpha, cols) | |
| new[NAME.format(l)] = W1 | |
| desc[l] = {"alpha": alpha, "n_cols": W.shape[1] if cols is None else len(cols), | |
| "dir": d if isinstance(d, str) else "custom", | |
| "relative_change": float((W1.float() - W.float()).norm() / W.float().norm())} | |
| return new, desc | |