Coverage for transformer_lens/model_bridge/supported_architectures/olmo2.py: 100%
14 statements
« prev ^ index » next coverage.py v7.10.1, created at 2026-09-01 16:23 +0000
« prev ^ index » next coverage.py v7.10.1, created at 2026-09-01 16:23 +0000
1"""OLMo 2 architecture adapter."""
3from typing import Any
5from transformer_lens.model_bridge.architecture_adapter import ArchitectureAdapter
6from transformer_lens.model_bridge.generalized_components import (
7 BlockBridge,
8 EmbeddingBridge,
9 LinearBridge,
10 PositionEmbeddingsAttentionBridge,
11 RMSNormalizationBridge,
12 RotaryEmbeddingBridge,
13 UnembeddingBridge,
14)
17class Olmo2ArchitectureAdapter(ArchitectureAdapter):
18 """Architecture adapter for OLMo 2 models.
20 OLMo 2 uses a post-norm architecture with RMSNorm, Q/K normalization in attention,
21 rotary position embeddings (RoPE), and gated MLP (SwiGLU). Key differences from
22 pre-norm models like Llama:
24 - Post-norm: RMSNorm is applied AFTER attention and AFTER MLP, not before.
25 ln1 maps to post_attention_layernorm, ln2 maps to post_feedforward_layernorm.
26 - Q/K normalization: Per-head RMSNorm applied to queries and keys after projection.
27 - No biases on any projections.
29 Optional Parameters (may not exist in state_dict):
30 -------------------------------------------------
31 - blocks.{i}.attn.b_Q - No bias on query projection
32 - blocks.{i}.attn.b_K - No bias on key projection
33 - blocks.{i}.attn.b_V - No bias on value projection
34 - blocks.{i}.attn.b_O - No bias on output projection
35 - blocks.{i}.mlp.b_in - No bias on MLP up_proj
36 - blocks.{i}.mlp.b_gate - No bias on MLP gate_proj
37 - blocks.{i}.mlp.b_out - No bias on MLP down_proj
38 - blocks.{i}.ln1.b - RMSNorm has no bias
39 - blocks.{i}.ln2.b - RMSNorm has no bias
40 - ln_final.b - RMSNorm has no bias
41 """
43 # Attention bridge seam; EXAONE-4 swaps in its NoPE-gating variant.
44 _attention_bridge_cls = PositionEmbeddingsAttentionBridge
46 def __init__(self, cfg: Any) -> None:
47 """Initialize the OLMo 2 architecture adapter."""
48 super().__init__(cfg)
50 self._set_rms_rotary_defaults()
51 # OLMo-2 uses post-norm (RMSNorm AFTER attention/MLP), so layer norm
52 # folding into QKV/MLP weights is incorrect — the norms apply to the
53 # output, not the input. Same pattern as BERT and Phi-3.
54 self.supports_fold_ln = False
55 # Force eager attention for numerical consistency with benchmark reference.
56 # PositionEmbeddingsAttentionBridge delegates to native HF attention, so
57 # both bridge and reference must use the same implementation.
58 self.cfg.attn_implementation = "eager"
60 self.weight_processing_conversions = {
61 **self._qkvo_weight_conversions(),
62 }
64 # Component mapping — POST-NORM architecture:
65 # ln1 = post_attention_layernorm (applied AFTER attention)
66 # ln2 = post_feedforward_layernorm (applied AFTER MLP)
67 self.component_mapping = {
68 "embed": EmbeddingBridge(name="model.embed_tokens"),
69 "rotary_emb": RotaryEmbeddingBridge(name="model.rotary_emb", config=self.cfg),
70 "blocks": BlockBridge(
71 name="model.layers",
72 submodules={
73 "ln1": RMSNormalizationBridge(name="post_attention_layernorm", config=self.cfg),
74 "ln2": RMSNormalizationBridge(
75 name="post_feedforward_layernorm", config=self.cfg
76 ),
77 "attn": self._attention_bridge_cls(
78 name="self_attn",
79 config=self.cfg,
80 submodules={
81 "q": LinearBridge(name="q_proj"),
82 "k": LinearBridge(name="k_proj"),
83 "v": LinearBridge(name="v_proj"),
84 "o": LinearBridge(name="o_proj"),
85 "q_norm": RMSNormalizationBridge(name="q_norm", config=self.cfg),
86 "k_norm": RMSNormalizationBridge(name="k_norm", config=self.cfg),
87 },
88 requires_attention_mask=True,
89 requires_position_embeddings=True,
90 ),
91 "mlp": self._build_mlp_bridge(),
92 },
93 # Post-norm overrides: ln1/ln2 are applied AFTER attention/MLP and
94 # BEFORE the residual add, so the residual mid-point is mlp.hook_in
95 # and the additive contributions are the norm outputs, not the raw
96 # module outputs (attn.hook_out / mlp.hook_out stay raw).
97 hook_alias_overrides={
98 "hook_resid_mid": "mlp.hook_in",
99 "hook_attn_out": "ln1.hook_out",
100 "hook_mlp_out": "ln2.hook_out",
101 },
102 # No pre-MLP norm: the MLP consumes the mid-residual directly, so
103 # the hook_mlp_in capture must sit on the MLP, not on ln2.
104 mlp_reads_resid_directly=True,
105 ),
106 "ln_final": RMSNormalizationBridge(name="model.norm", config=self.cfg),
107 "unembed": UnembeddingBridge(name="lm_head", config=self.cfg),
108 }
110 def _build_mlp_bridge(self):
111 """MLP bridge seam; FlexOlmo swaps in the MoE variant."""
112 return self._gated_mlp()