"""Convenience-property tests for ``HookedEncoder``.
Closes the last open ask in #277 — verify each ``W_*`` / ``b_*`` / circuit
property has the right shape AND aliases the right underlying parameter, so
property-level mech-interp work doesn't silently read the wrong tensor.
Uses a randomly-initialized small encoder (no HF download) so the tests run
fast and deterministically.
"""
from __future__ import annotations
import pytest
import torch
from transformer_lens import FactoredMatrix, HookedEncoder, HookedTransformerConfig
D_MODEL = 12
D_HEAD = 4
N_HEADS = D_MODEL // D_HEAD
D_MLP = 4 * D_MODEL
N_CTX = 5
N_LAYERS = 3
D_VOCAB = 22
@pytest.fixture
def model() -> HookedEncoder:
cfg = HookedTransformerConfig(
d_head=D_HEAD,
d_model=D_MODEL,
n_ctx=N_CTX,
n_layers=N_LAYERS,
act_fn="gelu",
d_vocab=D_VOCAB,
)
encoder = HookedEncoder(cfg)
torch.manual_seed(0)
for p in encoder.parameters():
torch.nn.init.normal_(p, std=0.02)
return encoder
def test_W_U(model: HookedEncoder):
assert model.W_U.shape == (D_MODEL, D_VOCAB)
assert model.W_U is model.unembed.W_U
def test_b_U(model: HookedEncoder):
assert model.b_U.shape == (D_VOCAB,)
assert model.b_U is model.unembed.b_U
def test_W_E(model: HookedEncoder):
assert model.W_E.shape == (D_VOCAB, D_MODEL)
assert model.W_E is model.embed.embed.W_E
def test_W_pos(model: HookedEncoder):
assert model.W_pos.shape == (N_CTX, D_MODEL)
assert model.W_pos is model.embed.pos_embed.W_pos
@pytest.mark.xfail(
reason=(
"HookedEncoder.W_E_pos return annotation 'd_vocab+n_ctx d_model' references "
"unbound dimension names (no input args supply them), so the jaxtyping import-hook "
"can't resolve the sum at runtime. Same annotation exists on HookedTransformer.W_E_pos; "
"fixing it is a separate API-touch."
),
strict=True,
)
def test_W_E_pos(model: HookedEncoder):
assert model.W_E_pos.shape == (D_VOCAB + N_CTX, D_MODEL)
assert torch.equal(model.W_E_pos[:D_VOCAB], model.W_E)
assert torch.equal(model.W_E_pos[D_VOCAB:], model.W_pos)
@pytest.mark.parametrize("attr", ["W_Q", "W_K", "W_V"])
def test_attn_qkv_weight(model: HookedEncoder, attr: str):
stacked = getattr(model, attr)
assert stacked.shape == (N_LAYERS, N_HEADS, D_MODEL, D_HEAD)
for layer_idx, block in enumerate(model.blocks):
assert torch.equal(stacked[layer_idx], getattr(block.attn, attr))
def test_W_O(model: HookedEncoder):
assert model.W_O.shape == (N_LAYERS, N_HEADS, D_HEAD, D_MODEL)
for layer_idx, block in enumerate(model.blocks):
assert torch.equal(model.W_O[layer_idx], block.attn.W_O)
@pytest.mark.parametrize("attr", ["b_Q", "b_K", "b_V"])
def test_attn_qkv_bias(model: HookedEncoder, attr: str):
stacked = getattr(model, attr)
assert stacked.shape == (N_LAYERS, N_HEADS, D_HEAD)
for layer_idx, block in enumerate(model.blocks):
assert torch.equal(stacked[layer_idx], getattr(block.attn, attr))
def test_b_O(model: HookedEncoder):
assert model.b_O.shape == (N_LAYERS, D_MODEL)
for layer_idx, block in enumerate(model.blocks):
assert torch.equal(model.b_O[layer_idx], block.attn.b_O)
def test_W_in(model: HookedEncoder):
assert model.W_in.shape == (N_LAYERS, D_MODEL, D_MLP)
for layer_idx, block in enumerate(model.blocks):
assert torch.equal(model.W_in[layer_idx], block.mlp.W_in)
def test_W_out(model: HookedEncoder):
assert model.W_out.shape == (N_LAYERS, D_MLP, D_MODEL)
for layer_idx, block in enumerate(model.blocks):
assert torch.equal(model.W_out[layer_idx], block.mlp.W_out)
def test_b_in(model: HookedEncoder):
assert model.b_in.shape == (N_LAYERS, D_MLP)
for layer_idx, block in enumerate(model.blocks):
assert torch.equal(model.b_in[layer_idx], block.mlp.b_in)
def test_b_out(model: HookedEncoder):
assert model.b_out.shape == (N_LAYERS, D_MODEL)
for layer_idx, block in enumerate(model.blocks):
assert torch.equal(model.b_out[layer_idx], block.mlp.b_out)
def test_QK_circuit(model: HookedEncoder):
qk = model.QK
assert isinstance(qk, FactoredMatrix)
assert qk.A.shape == (N_LAYERS, N_HEADS, D_MODEL, D_HEAD)
assert qk.B.shape == (N_LAYERS, N_HEADS, D_HEAD, D_MODEL)
assert torch.equal(qk.A, model.W_Q)
assert torch.equal(qk.B, model.W_K.transpose(-2, -1))
def test_OV_circuit(model: HookedEncoder):
ov = model.OV
assert isinstance(ov, FactoredMatrix)
assert ov.A.shape == (N_LAYERS, N_HEADS, D_MODEL, D_HEAD)
assert ov.B.shape == (N_LAYERS, N_HEADS, D_HEAD, D_MODEL)
assert torch.equal(ov.A, model.W_V)
assert torch.equal(ov.B, model.W_O)