thegovind's picture
Publish the weight-space programming labs (part 2)
84bcfb1 verified
Raw History Blame Contribute Delete
4 kB
"""Helpers for the hands-on labs: load the model in transformers, read what a layer computes, or change it.
Three things you can do to a running model with a hook:
record_outputs copy what a whole layer (attention or DeltaNet mixer + MLP) adds up to, per token
record_mlp_acts copy the 3,584 neuron activations that feed the MLP's down_proj, per token
steer_down change what down_proj writes, per token, without touching any weight
"""
from __future__ import annotations
import contextlib
import json
import os
import torch
LAB = os.environ.get("WSP_LAB", "/nvme/wsp-lab")
def model_dir(name: str) -> str:
if name == "base":
return os.path.join(LAB, "models", "Qwen3.5-0.8B")
return name if name.startswith("/") else os.path.join(LAB, "models", name)
def load(name="base", dtype=torch.bfloat16, device="cuda"):
from transformers import AutoModelForCausalLM, AutoTokenizer
tok = AutoTokenizer.from_pretrained(model_dir(name))
m = AutoModelForCausalLM.from_pretrained(model_dir(name), dtype=dtype).to(device).eval()
return m, tok
def blocks(m):
return m.model.layers
def down_proj(m, layer: int):
return blocks(m)[layer].mlp.down_proj
def encode(tok, text: str, device="cuda"):
return tok(text, return_tensors="pt", add_special_tokens=False).input_ids.to(device)
@contextlib.contextmanager
def record_outputs(m, layers):
"""out[l] = list of [tokens, 1024] tensors: the residual stream right after block l (one per forward call)."""
out, hs = {l: [] for l in layers}, []
for l in layers:
def hook(mod, inp, o, l=l):
o = o[0] if isinstance(o, tuple) else o
out[l].append(o.detach()[0].float().cpu())
hs.append(blocks(m)[l].register_forward_hook(hook))
try:
yield out
finally:
for h in hs:
h.remove()
@contextlib.contextmanager
def record_mlp_acts(m, layers):
"""out[l] = list of [tokens, 3584] tensors: the neuron activations that down_proj reads at layer l."""
out, hs = {l: [] for l in layers}, []
for l in layers:
def hook(mod, inp, l=l):
out[l].append(inp[0].detach()[0].float().cpu())
hs.append(down_proj(m, l).register_forward_pre_hook(hook))
try:
yield out
finally:
for h in hs:
h.remove()
@contextlib.contextmanager
def steer_down(m, layer: int, u: torch.Tensor, alpha: float, cols=None, record=None):
"""Make down_proj at `layer` write W a - alpha * u * s, where s = u . (W[:, cols] a[cols]).
This is exactly what the weight edit W' = W - alpha * u u^T W[:, cols] does, computed on the fly.
s is one number per token: how hard the chosen neurons are pushing along u right now.
If `record` is a list, each call appends s (a [tokens] tensor) to it."""
lin = down_proj(m, layer)
W = lin.weight
u = (u / u.norm()).to(W.device, torch.float32)
idx = None if cols is None else torch.as_tensor(list(cols), device=W.device)
WS = (W if idx is None else W[:, idx]).float()
r = WS.T @ u # one number per chosen neuron: how much it writes along u
def hook(mod, inp, o):
a = inp[0].float()
s = (a if idx is None else a[..., idx]) @ r # [batch, tokens]
if record is not None:
record.append(s.detach()[0].cpu())
return (o.float() - alpha * s[..., None] * u).to(o.dtype)
h = lin.register_forward_hook(hook)
try:
yield
finally:
h.remove()
def released_edit(path=None):
"""u, alpha and the 64 columns per layer, as recovered from the released weights by scripts/build_models.py."""
path = path or os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "results",
"edit_info.json")
info = json.load(open(path))
return {int(l): {"u": torch.tensor(v["u"]), "alpha": v["alpha"], "cols": v["cols"]}
for l, v in info["layers"].items()}