SPB Git

spb/wpai0001_uqo

Public
7.7 KB · 204 lines python
1"""Hook points and interventions for activation capture, patching and steering.23Design contract (tested in tests/causal_sanity/test_hook_noop.py):45* A ``HookManager`` with ``enabled=False`` or with no registered interventions returns the input6  tensor object itself (identity, no copy), so a model instrumented with hook points produces7  bit-identical outputs to the uninstrumented computation.8* Interventions are pure functions ``(tensor, ctx) -> tensor`` applied in registration order at a9  named hook point. ``ctx`` carries the hook name and the batch of token positions.10* Capture interventions record (detached, cloned) activations without modifying the stream.1112Hook names used by ``ToyTransformer``:13  ``embed``, ``resid_pre.{l}``, ``attn_heads.{l}`` ([B,T,H,dh] before the output projection),14  ``attn_out.{l}``, ``resid_mid.{l}``, ``mlp_out.{l}``, ``resid_post.{l}``, ``final_norm``.15"""16from __future__ import annotations1718from dataclasses import dataclass, field19from typing import Callable, Protocol2021import torch2223Tensor = torch.Tensor242526@dataclass27class HookContext:28    name: str29    positions: Tensor | None = None  # [B] answer positions when available30    extra: dict = field(default_factory=dict)313233class Intervention(Protocol):34    def __call__(self, x: Tensor, ctx: HookContext) -> Tensor: ...353637class HookManager:38    def __init__(self) -> None:39        self.enabled: bool = True40        self._hooks: dict[str, list[Intervention]] = {}41        self.calls: int = 04243    # -- registration -------------------------------------------------------------------44    def add(self, name: str, fn: Intervention) -> None:45        self._hooks.setdefault(name, []).append(fn)4647    def remove(self, name: str, fn: Intervention | None = None) -> None:48        if fn is None:49            self._hooks.pop(name, None)50        elif name in self._hooks:51            self._hooks[name] = [f for f in self._hooks[name] if f is not fn]5253    def clear(self) -> None:54        self._hooks.clear()5556    def names(self) -> list[str]:57        return [n for n, fns in self._hooks.items() if fns]5859    # -- application --------------------------------------------------------------------60    def __call__(self, name: str, x: Tensor, ctx: HookContext | None = None) -> Tensor:61        if not self.enabled:62            return x63        fns = self._hooks.get(name)64        if not fns:65            return x66        self.calls += 167        c = ctx or HookContext(name=name)68        c.name = name69        for fn in fns:70            x = fn(x, c)71        return x7273    class _Disabled:74        def __init__(self, mgr: "HookManager") -> None:75            self.mgr, self.prev = mgr, mgr.enabled7677        def __enter__(self) -> None:78            self.mgr.enabled = False7980        def __exit__(self, *a) -> None:81            self.mgr.enabled = self.prev8283    def disabled(self) -> "HookManager._Disabled":84        return HookManager._Disabled(self)858687# ---------------------------------------------------------------------------------------88# Built-in interventions89# ---------------------------------------------------------------------------------------90class Capture:91    """Record activations (detached clone) without modifying the stream."""9293    def __init__(self) -> None:94        self.store: dict[str, Tensor] = {}9596    def __call__(self, x: Tensor, ctx: HookContext) -> Tensor:97        self.store[ctx.name] = x.detach().clone()98        return x99100101def _pos_mask(x: Tensor, positions: Tensor | list[int] | slice | None) -> Tensor:102    """Boolean mask [B,T] selecting token positions (per-example positions or shared slice)."""103    B, T = x.shape[0], x.shape[1]104    mask = torch.zeros(B, T, dtype=torch.bool, device=x.device)105    if positions is None:106        mask[:] = True107    elif isinstance(positions, slice):108        mask[:, positions] = True109    elif isinstance(positions, torch.Tensor) and positions.ndim == 1 and positions.shape[0] == B:110        mask[torch.arange(B, device=x.device), positions.to(x.device)] = True111    else:112        mask[:, list(positions)] = True  # type: ignore[arg-type]113    return mask114115116class Replace:117    """Activation patching: overwrite selected positions with ``source`` values of the same shape."""118119    def __init__(self, source: Tensor, positions=None, heads: list[int] | None = None, dims: slice | None = None) -> None:120        self.source, self.positions, self.heads, self.dims = source, positions, heads, dims121122    def __call__(self, x: Tensor, ctx: HookContext) -> Tensor:123        src = self.source.to(x.device, x.dtype)124        mask = _pos_mask(x, self.positions)125        out = x.clone()126        if self.heads is not None:  # x: [B,T,H,dh]127            m = mask[:, :, None, None].expand_as(x).clone()128            keep = torch.zeros(x.shape[2], dtype=torch.bool, device=x.device)129            keep[self.heads] = True130            m &= keep[None, None, :, None]131            out = torch.where(m, src, x)132        elif self.dims is not None:133            m = mask[:, :, None].expand_as(x).clone()134            dmask = torch.zeros(x.shape[-1], dtype=torch.bool, device=x.device)135            dmask[self.dims] = True136            m &= dmask[None, None, :]137            out = torch.where(m, src, x)138        else:139            m = mask.reshape(mask.shape + (1,) * (x.ndim - 2)).expand_as(x)140            out = torch.where(m, src, x)141        return out142143144class Add:145    """Additive steering: x[positions] += scale * vector (vector broadcast over the last dim)."""146147    def __init__(self, vector: Tensor, scale: float = 1.0, positions=None) -> None:148        self.vector, self.scale, self.positions = vector, scale, positions149150    def __call__(self, x: Tensor, ctx: HookContext) -> Tensor:151        v = self.vector.to(x.device, x.dtype)152        mask = _pos_mask(x, self.positions)153        m = mask.reshape(mask.shape + (1,) * (x.ndim - 2)).to(x.dtype)154        return x + self.scale * m * v155156157class Zero:158    def __init__(self, positions=None, heads: list[int] | None = None) -> None:159        self.positions, self.heads = positions, heads160161    def __call__(self, x: Tensor, ctx: HookContext) -> Tensor:162        return Replace(torch.zeros_like(x), self.positions, heads=self.heads)(x, ctx)163164165class GaussianNoise:166    """Causal-tracing style corruption with a fixed generator for reproducibility."""167168    def __init__(self, std: float, positions=None, seed: int = 0) -> None:169        self.std, self.positions, self.seed = std, positions, seed170171    def __call__(self, x: Tensor, ctx: HookContext) -> Tensor:172        g = torch.Generator(device="cpu").manual_seed(self.seed)173        noise = torch.randn(x.shape, generator=g, dtype=torch.float32).to(x.device, x.dtype) * self.std174        mask = _pos_mask(x, self.positions)175        m = mask.reshape(mask.shape + (1,) * (x.ndim - 2)).to(x.dtype)176        return x + m * noise177178179class ProjectSubspace:180    """Remove (mode='ablate') or keep only (mode='keep') the component in span(basis)."""181182    def __init__(self, basis: Tensor, mode: str = "ablate", positions=None) -> None:183        assert mode in {"ablate", "keep"}184        q, _ = torch.linalg.qr(basis.T.float() if basis.shape[0] < basis.shape[1] else basis.float())185        self.q, self.mode, self.positions = q, mode, positions  # [d, r] orthonormal columns186187    def __call__(self, x: Tensor, ctx: HookContext) -> Tensor:188        q = self.q.to(x.device, x.dtype)189        proj = (x @ q) @ q.T190        new = x - proj if self.mode == "ablate" else proj191        mask = _pos_mask(x, self.positions)192        m = mask.reshape(mask.shape + (1,) * (x.ndim - 2)).expand_as(x)193        return torch.where(m, new, x)194195196class Sequence:197    def __init__(self, *fns: Callable[[Tensor, HookContext], Tensor]) -> None:198        self.fns = fns199200    def __call__(self, x: Tensor, ctx: HookContext) -> Tensor:201        for f in self.fns:202            x = f(x, ctx)203        return x204
Files · main
Menu

Theme