|
| 1 | +"""Regression tests for recursive TransformerBridge checkpoint composition (#1655).""" |
| 2 | + |
| 3 | +from __future__ import annotations |
| 4 | + |
| 5 | +from collections import OrderedDict |
| 6 | +from types import SimpleNamespace |
| 7 | + |
| 8 | +import pytest |
| 9 | +import torch |
| 10 | +from transformers import GPT2Config, GPT2LMHeadModel |
| 11 | + |
| 12 | +from transformer_lens.config import TransformerBridgeConfig |
| 13 | +from transformer_lens.model_bridge import TransformerBridge |
| 14 | +from transformer_lens.model_bridge.generalized_components import ( |
| 15 | + JointGateUpMLPBridge, |
| 16 | + JointQKVAttentionBridge, |
| 17 | + LinearBridge, |
| 18 | +) |
| 19 | +from transformer_lens.model_bridge.sources import build_bridge_from_module |
| 20 | + |
| 21 | + |
| 22 | +def _native_bridge() -> TransformerBridge: |
| 23 | + cfg = TransformerBridgeConfig( |
| 24 | + d_model=32, |
| 25 | + d_head=16, |
| 26 | + n_heads=2, |
| 27 | + n_layers=2, |
| 28 | + n_ctx=8, |
| 29 | + d_vocab=16, |
| 30 | + d_mlp=64, |
| 31 | + act_fn="gelu", |
| 32 | + normalization_type="LN", |
| 33 | + seed=0, |
| 34 | + ) |
| 35 | + return TransformerBridge.boot_native(cfg) |
| 36 | + |
| 37 | + |
| 38 | +def _parent_with_bridge(bridge: TransformerBridge) -> torch.nn.Module: |
| 39 | + parent = torch.nn.Module() |
| 40 | + parent.add_module("bridge", bridge) |
| 41 | + return parent |
| 42 | + |
| 43 | + |
| 44 | +def test_state_dict_with_destination_and_prefix_uses_recursive_semantics() -> None: |
| 45 | + bridge = _native_bridge() |
| 46 | + sentinel = torch.tensor(1) |
| 47 | + destination: OrderedDict[str, torch.Tensor] = OrderedDict({"sentinel": sentinel}) |
| 48 | + |
| 49 | + returned = bridge.state_dict(destination=destination, prefix="nested.bridge.") |
| 50 | + |
| 51 | + assert returned is destination |
| 52 | + assert destination["sentinel"] is sentinel |
| 53 | + recursive_keys = set(destination) - {"sentinel"} |
| 54 | + assert recursive_keys |
| 55 | + assert all(key.startswith("nested.bridge.") for key in recursive_keys) |
| 56 | + |
| 57 | + |
| 58 | +def test_parent_state_dict_strict_round_trip() -> None: |
| 59 | + parent = _parent_with_bridge(_native_bridge()) |
| 60 | + checkpoint = {key: value.clone() for key, value in parent.state_dict().items()} |
| 61 | + |
| 62 | + with torch.no_grad(): |
| 63 | + for parameter in parent.parameters(): |
| 64 | + parameter.zero_() |
| 65 | + |
| 66 | + result = parent.load_state_dict(checkpoint, strict=True) |
| 67 | + |
| 68 | + assert result.missing_keys == [] |
| 69 | + assert result.unexpected_keys == [] |
| 70 | + reloaded = parent.state_dict() |
| 71 | + for key, value in checkpoint.items(): |
| 72 | + assert torch.equal(reloaded[key], value), f"{key} did not round-trip" |
| 73 | + |
| 74 | + |
| 75 | +def test_parent_registration_is_stable_across_first_forward() -> None: |
| 76 | + bridge = _native_bridge() |
| 77 | + parent = _parent_with_bridge(bridge) |
| 78 | + |
| 79 | + for block in bridge.blocks: |
| 80 | + assert block.attn._ln1_module is block.ln1.original_component |
| 81 | + |
| 82 | + keys_before = tuple(parent.state_dict()) |
| 83 | + assert not any("._ln1_module." in key for key in keys_before) |
| 84 | + with torch.no_grad(): |
| 85 | + bridge(torch.randint(0, bridge.cfg.d_vocab, (1, 4))) |
| 86 | + keys_after = tuple(parent.state_dict()) |
| 87 | + |
| 88 | + assert keys_after == keys_before |
| 89 | + assert not any("._ln1_module." in key for key in keys_after) |
| 90 | + |
| 91 | + |
| 92 | +def test_nested_joint_qkv_bridge_strict_round_trip() -> None: |
| 93 | + cfg = GPT2Config( |
| 94 | + vocab_size=32, |
| 95 | + n_positions=16, |
| 96 | + n_embd=16, |
| 97 | + n_layer=1, |
| 98 | + n_head=2, |
| 99 | + n_inner=32, |
| 100 | + pad_token_id=0, |
| 101 | + bos_token_id=1, |
| 102 | + eos_token_id=2, |
| 103 | + ) |
| 104 | + bridge = build_bridge_from_module( |
| 105 | + GPT2LMHeadModel(cfg), |
| 106 | + architecture="GPT2LMHeadModel", |
| 107 | + hf_config=cfg, |
| 108 | + ) |
| 109 | + parent = _parent_with_bridge(bridge) |
| 110 | + |
| 111 | + checkpoint = {key: value.clone() for key, value in parent.state_dict().items()} |
| 112 | + assert not any(".qkv." in key for key in checkpoint) |
| 113 | + with torch.no_grad(): |
| 114 | + for parameter in parent.parameters(): |
| 115 | + parameter.zero_() |
| 116 | + |
| 117 | + result = parent.load_state_dict(checkpoint, strict=True) |
| 118 | + |
| 119 | + assert result.missing_keys == [] |
| 120 | + assert result.unexpected_keys == [] |
| 121 | + reloaded = parent.state_dict() |
| 122 | + for key, value in checkpoint.items(): |
| 123 | + assert torch.equal(reloaded[key], value), f"{key} did not round-trip" |
| 124 | + |
| 125 | + |
| 126 | +def _filtered_joint_component(kind: str) -> torch.nn.Module: |
| 127 | + filtered_child = LinearBridge(name=kind) |
| 128 | + filtered_child.set_original_component(torch.nn.Linear(4, 8)) |
| 129 | + cfg = SimpleNamespace(n_heads=2, d_head=4) |
| 130 | + |
| 131 | + if kind == "qkv": |
| 132 | + qkv_component = JointQKVAttentionBridge( |
| 133 | + name="attn", |
| 134 | + config=cfg, |
| 135 | + submodules={"qkv": filtered_child}, |
| 136 | + ) |
| 137 | + for child_name in ("q", "k", "v"): |
| 138 | + getattr(qkv_component, child_name).set_original_component(torch.nn.Linear(4, 4)) |
| 139 | + return qkv_component |
| 140 | + gate_up_component = JointGateUpMLPBridge( |
| 141 | + name="mlp", |
| 142 | + config=cfg, |
| 143 | + submodules={"gate_up": filtered_child}, |
| 144 | + ) |
| 145 | + gate_up_component.add_module("gate_up", filtered_child) |
| 146 | + gate_up_component.gate.set_original_component(torch.nn.Linear(4, 4)) |
| 147 | + getattr(gate_up_component, "in").set_original_component(torch.nn.Linear(4, 4)) |
| 148 | + return gate_up_component |
| 149 | + |
| 150 | + |
| 151 | +@pytest.mark.parametrize("filtered_child_name", ["qkv", "gate_up"]) |
| 152 | +def test_filtered_joint_component_strict_round_trip(filtered_child_name: str) -> None: |
| 153 | + component = _filtered_joint_component(filtered_child_name) |
| 154 | + filtered_child = component.get_submodule(filtered_child_name) |
| 155 | + checkpoint = {key: value.clone() for key, value in component.state_dict().items()} |
| 156 | + |
| 157 | + assert checkpoint |
| 158 | + assert not any(key.startswith(f"{filtered_child_name}.") for key in checkpoint) |
| 159 | + with torch.no_grad(): |
| 160 | + for parameter in component.parameters(): |
| 161 | + parameter.zero_() |
| 162 | + |
| 163 | + result = component.load_state_dict(checkpoint, strict=True) |
| 164 | + |
| 165 | + assert result.missing_keys == [] |
| 166 | + assert result.unexpected_keys == [] |
| 167 | + reloaded = component.state_dict() |
| 168 | + for key, value in checkpoint.items(): |
| 169 | + assert torch.equal(reloaded[key], value), f"{key} did not round-trip" |
| 170 | + for parameter in filtered_child.parameters(): |
| 171 | + assert torch.count_nonzero(parameter) == 0 |
0 commit comments