Coverage for transformer_lens/model_bridge/sources/native/init.py: 97%
101 statements
« prev ^ index » next coverage.py v7.10.1, created at 2026-09-21 19:27 +0000
« prev ^ index » next coverage.py v7.10.1, created at 2026-09-21 19:27 +0000
1"""Weight init for NativeModel.
3Supported modes: ``"gpt2"`` (Normal(0, std) with 1/sqrt(2*n_layers) residual
4scaling on output projections), ``"xavier_uniform"`` / ``"xavier_normal"``,
5``"kaiming_uniform"`` / ``"kaiming_normal"`` (relu nonlinearity). Norm weights
6go to 1, all biases to 0; param-free norms (LNPre / RMSPre) have nothing to
7initialize.
9Determinism uses a scoped ``torch.Generator``, not ``torch.manual_seed``, so
10seeded init does not perturb the caller's global RNG.
11"""
13from __future__ import annotations
15import math
16from typing import Callable, Optional, cast
18import torch
19import torch.nn as nn
21from transformer_lens.config import TransformerBridgeConfig
23from .model import (
24 NativeAttention,
25 NativeBlock,
26 NativeGatedMLP,
27 NativeLayerNormPre,
28 NativeMLP,
29 NativeModel,
30 NativeRMSNorm,
31 NativeRMSNormPre,
32)
34# Residual-scaled output is gpt2-specific; other modes treat every weight the
35# same. Each entry takes ``(tensor, generator, gain)`` — gain honors
36# ``cfg.initializer_range`` like the legacy init did (which passed it as the
37# xavier/kaiming gain); kaiming has no gain kwarg, so scale after.
38_NonResidualInit = Callable[[torch.Tensor, Optional[torch.Generator], float], torch.Tensor]
39_NON_RESIDUAL_MODES: dict[str, _NonResidualInit] = {
40 "xavier_uniform": lambda t, g, gain: nn.init.xavier_uniform_(t, gain=gain, generator=g),
41 "xavier_normal": lambda t, g, gain: nn.init.xavier_normal_(t, gain=gain, generator=g),
42 "kaiming_uniform": lambda t, g, gain: nn.init.kaiming_uniform_(
43 t, nonlinearity="relu", generator=g
44 ).mul_(gain),
45 "kaiming_normal": lambda t, g, gain: nn.init.kaiming_normal_(
46 t, nonlinearity="relu", generator=g
47 ).mul_(gain),
48}
50_SUPPORTED_MODES = frozenset({"gpt2", *_NON_RESIDUAL_MODES})
53def _unwrap_component(module: nn.Module) -> nn.Module:
54 """Return the native module stored behind a bridge wrapper, if present."""
55 original = getattr(module, "original_component", None)
56 return original if isinstance(original, nn.Module) else module
59def initialize_native_model(
60 model: NativeModel, cfg: TransformerBridgeConfig, seed: int | None = None
61) -> None:
62 """Initialize ``model`` weights in-place. Honors ``cfg.init_mode`` and ``cfg.seed``."""
63 effective_seed = seed if seed is not None else cfg.seed
65 # Always generate on CPU/fp32 and copy into the parameter: boot initializes
66 # before .to(device)/.to(dtype) while init_weights() runs after, and a
67 # generator seeded on the live parameter device produces a different stream
68 # — the same seed must reproduce the same weights either way.
69 generator: Optional[torch.Generator]
70 if effective_seed is not None:
71 g = torch.Generator()
72 g.manual_seed(effective_seed)
73 generator = g
74 else:
75 generator = None
77 def _staged(
78 fn: Callable[[torch.Tensor], torch.Tensor],
79 ) -> Callable[[torch.Tensor], torch.Tensor]:
80 def apply(t: torch.Tensor) -> torch.Tensor:
81 staging = torch.empty(t.shape, dtype=torch.float32)
82 fn(staging)
83 with torch.no_grad():
84 t.copy_(staging)
85 return t
87 return apply
89 init_mode = (cfg.init_mode or "gpt2").lower()
90 if init_mode not in _SUPPORTED_MODES:
91 raise NotImplementedError(
92 f"init_mode={init_mode!r} is not supported for NativeModel. "
93 f"Supported modes: {sorted(_SUPPORTED_MODES)}."
94 )
96 weight_init: Callable[[torch.Tensor], torch.Tensor]
97 output_init: Callable[[torch.Tensor], torch.Tensor]
98 if init_mode == "gpt2":
99 # Default matches the legacy TL scheme: N(0, 0.64/d_model), i.e.
100 # std = 0.8/sqrt(d_model), not GPT-2's paper 0.02 — toy-model training
101 # dynamics (e.g. the grokking demo) depend on this scale.
102 std = cfg.initializer_range if cfg.initializer_range > 0 else 0.8 / math.sqrt(cfg.d_model)
104 # NOTE: this residual output scaling (1/sqrt(2*n_layers), applied only
105 # to output projections below) was NOT present in the legacy
106 # HookedTransformer._init_weights_gpt2 (removed in 4.0).
107 # Intentional delta for NativeModel: kept because it follows the
108 # original GPT-2 paper's residual-scaling convention and improves
109 # training stability at init for deeper models. Flagged in issue #1568
110 # as a maintainer call; kept + documented rather than removed.
111 residual_scale = 1.0 / math.sqrt(2 * cfg.n_layers)
112 weight_init = lambda t: nn.init.normal_(
113 t, mean=0.0, std=std, generator=generator
114 ) # noqa: E731
115 output_init = lambda t: nn.init.normal_( # noqa: E731
116 t, mean=0.0, std=std * residual_scale, generator=generator
117 )
118 else:
119 fn = _NON_RESIDUAL_MODES[init_mode]
120 # Honor an explicitly-set initializer_range as the gain (legacy
121 # behavior); the sentinel/default keeps plain xavier/kaiming scaling.
122 gain = cfg.initializer_range if cfg.initializer_range > 0 else 1.0
123 weight_init = lambda t: fn(t, generator, gain) # noqa: E731
124 output_init = weight_init
126 weight_init = _staged(weight_init)
127 output_init = _staged(output_init)
129 tok_embed = cast(nn.Embedding, _unwrap_component(model.tok_embed))
130 weight_init(tok_embed.weight)
131 if model.pos is not None:
132 pos = cast(nn.Embedding, _unwrap_component(model.pos))
133 weight_init(pos.weight)
134 # Rotary has only registered buffers (cos/sin), no parameters to init.
136 for block in model.layers:
137 native_block = cast(NativeBlock, _unwrap_component(block))
138 _init_block(native_block, weight_init=weight_init, output_init=output_init)
140 _init_norm(model.ln_out)
141 head = cast(nn.Linear, _unwrap_component(model.head))
142 weight_init(head.weight)
143 if head.bias is not None:
144 nn.init.zeros_(head.bias)
147def _init_norm(norm: nn.Module) -> None:
148 norm = _unwrap_component(norm)
149 if isinstance(norm, NativeRMSNorm):
150 nn.init.ones_(norm.weight)
151 elif isinstance(norm, (NativeRMSNormPre, NativeLayerNormPre)):
152 pass
153 elif isinstance(norm, nn.LayerNorm):
154 nn.init.ones_(norm.weight)
155 nn.init.zeros_(norm.bias)
156 elif isinstance(norm, nn.Identity): 156 ↛ 159line 156 didn't jump to line 159 because the condition on line 156 was always true
157 pass
158 else:
159 raise TypeError(f"Unknown normalization type: {type(norm).__name__}")
162def _init_block(
163 block: NativeBlock,
164 *,
165 weight_init: Callable[[torch.Tensor], torch.Tensor],
166 output_init: Callable[[torch.Tensor], torch.Tensor],
167) -> None:
168 _init_norm(block.ln1)
169 attn = cast(NativeAttention, _unwrap_component(block.attn))
170 _init_attention(attn, weight_init=weight_init, output_init=output_init)
171 if not block.cfg.attn_only:
172 _init_norm(block.ln2)
173 mlp = _unwrap_component(block.mlp)
174 if isinstance(mlp, NativeGatedMLP):
175 _init_gated_mlp(mlp, weight_init=weight_init, output_init=output_init)
176 else:
177 _init_mlp(
178 cast(NativeMLP, mlp),
179 weight_init=weight_init,
180 output_init=output_init,
181 )
184def _init_attention(
185 attn: NativeAttention,
186 *,
187 weight_init: Callable[[torch.Tensor], torch.Tensor],
188 output_init: Callable[[torch.Tensor], torch.Tensor],
189) -> None:
190 for component in (attn.q, attn.k, attn.v):
191 linear = cast(nn.Linear, _unwrap_component(component))
192 weight_init(linear.weight)
193 if linear.bias is not None: 193 ↛ 190line 193 didn't jump to line 190 because the condition on line 193 was always true
194 nn.init.zeros_(linear.bias)
195 output = cast(nn.Linear, _unwrap_component(attn.o))
196 output_init(output.weight)
197 if output.bias is not None: 197 ↛ exitline 197 didn't return from function '_init_attention' because the condition on line 197 was always true
198 nn.init.zeros_(output.bias)
201def _init_mlp(
202 mlp: NativeMLP,
203 *,
204 weight_init: Callable[[torch.Tensor], torch.Tensor],
205 output_init: Callable[[torch.Tensor], torch.Tensor],
206) -> None:
207 fc_in = cast(nn.Linear, _unwrap_component(mlp.fc_in))
208 fc_out = cast(nn.Linear, _unwrap_component(mlp.fc_out))
209 weight_init(fc_in.weight)
210 nn.init.zeros_(fc_in.bias)
211 output_init(fc_out.weight)
212 nn.init.zeros_(fc_out.bias)
215def _init_gated_mlp(
216 mlp: NativeGatedMLP,
217 *,
218 weight_init: Callable[[torch.Tensor], torch.Tensor],
219 output_init: Callable[[torch.Tensor], torch.Tensor],
220) -> None:
221 gate = cast(nn.Linear, _unwrap_component(mlp.gate))
222 weight_init(gate.weight)
223 # ``in`` is registered via add_module; getattr resolves it from _modules.
224 in_proj = cast(nn.Linear, _unwrap_component(getattr(mlp, "in")))
225 out_proj = cast(nn.Linear, _unwrap_component(mlp.out))
226 weight_init(in_proj.weight)
227 output_init(out_proj.weight)