Coverage for transformer_lens/model_bridge/supported_architectures/smollm3.py: 100%
21 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"""SmolLM3 architecture adapter.
3SmolLM3 (the HuggingFaceTB SmolLM3 family, base and instruct) is a Llama-family
4decoder. It pairs pre-norm RMSNorm blocks with grouped-query attention (GQA), a
5SwiGLU gated MLP, rotary position embeddings (RoPE), tied input and output
6embeddings, and no biases on any projection. The one feature that sets it apart
7from a plain Llama or Qwen2 decoder is NoPE (No Positional Encoding): RoPE is
8skipped on a periodic subset of layers. That behaviour is the only piece of this
9adapter that is not a near-verbatim clone of qwen2.py, and it is handled by the
10small _SmolLM3AttentionBridge subclass below.
11"""
13from typing import Any
15import torch
17from transformer_lens.model_bridge.architecture_adapter import ArchitectureAdapter
18from transformer_lens.model_bridge.generalized_components import (
19 BlockBridge,
20 EmbeddingBridge,
21 LinearBridge,
22 PositionEmbeddingsAttentionBridge,
23 RMSNormalizationBridge,
24 RotaryEmbeddingBridge,
25 UnembeddingBridge,
26)
29class _SmolLM3AttentionBridge(PositionEmbeddingsAttentionBridge):
30 """Attention bridge that honours SmolLM3's per-layer NoPE setting.
32 SmolLM3 disables rotary position embeddings on a periodic subset of layers
33 (every no_rope_layer_interval-th layer, default every 4th, controlled by
34 config.no_rope_layers). The wrapped HF SmolLM3Attention module records this
35 choice as an integer flag use_rope: 1 means apply RoPE, 0 means this is a
36 NoPE layer. HF honours the flag inside its own forward by only calling
37 apply_rotary_pos_emb when use_rope is truthy.
39 The base PositionEmbeddingsAttentionBridge reimplements attention so that all
40 hook points fire at the right stage, and it applies RoPE whenever a
41 position_embeddings tuple is passed. It never consults use_rope. On a NoPE
42 layer that would rotate Q and K while native HF does not, diverging from the
43 reference and failing logit-equivalence checks on roughly a quarter of the
44 layers.
46 To match HF exactly we suppress position_embeddings on NoPE layers before
47 delegating to the base forward. The base forward only rotates when
48 position_embeddings is not None, so passing None skips the rotation while
49 every non-rotary hook (hook_q, hook_k, hook_v, hook_attn_scores,
50 hook_pattern, hook_z) still fires identically. RoPE layers (use_rope == 1)
51 are left untouched and behave exactly like the qwen2.py attention bridge.
52 """
54 # Nulls position_embeddings on NoPE layers by design.
55 rope_optional = True
57 def forward(self, *args: Any, **kwargs: Any) -> Any:
58 """Drop position_embeddings on NoPE layers, then run the base forward."""
59 hf_attn = self.original_component
60 # use_rope is 1 on RoPE layers and 0 on NoPE layers. Default to RoPE-on
61 # when the attribute is somehow absent so standard layers never break.
62 if hf_attn is not None and not getattr(hf_attn, "use_rope", 1):
63 kwargs["position_embeddings"] = None
64 # SmolLM3DecoderLayer (inherited from LlamaDecoderLayer) passes
65 # position_embeddings as a keyword, so the line above is what fires
66 # in practice. The positional branch below is defensive: if a caller
67 # ever passes (hidden_states, position_embeddings, ...) positionally,
68 # the second slot holds the (cos, sin) tuple, not a tensor, so we
69 # null it out there too.
70 if len(args) >= 2 and not isinstance(args[1], torch.Tensor):
71 args = (args[0], None) + args[2:]
72 return super().forward(*args, **kwargs)
75class SmolLM3ArchitectureAdapter(ArchitectureAdapter):
76 """Architecture adapter for SmolLM3 models.
78 SmolLM3 is a pre-norm decoder with RMSNorm, grouped-query attention (GQA),
79 a SwiGLU gated MLP, rotary position embeddings (RoPE), tied input and output
80 embeddings, and no biases on any projection. The block shape matches Llama
81 and Qwen2 exactly, so the component mapping and weight conversions mirror
82 qwen2.py.
84 NoPE (No Positional Encoding): SmolLM3 disables RoPE on every
85 no_rope_layer_interval-th layer (default every 4th) via config.no_rope_layers.
86 That per-layer toggle lives inside HF's SmolLM3Attention.forward, but the
87 bridge reimplements attention and would otherwise rotate Q and K on those
88 layers. The _SmolLM3AttentionBridge subclass handles it by suppressing
89 position embeddings on NoPE layers, so the reimplemented attention matches HF.
91 No Q/K normalization: unlike Qwen3, SmolLM3 has no per-head Q or K RMSNorm,
92 so the attention block uses the plain q/k/v/o submodules.
94 Optional Parameters (may not exist in state_dict):
95 -------------------------------------------------
96 SmolLM3 models do NOT have biases on any linear layers:
98 - blocks.{i}.attn.b_Q - No bias on query projection
99 - blocks.{i}.attn.b_K - No bias on key projection
100 - blocks.{i}.attn.b_V - No bias on value projection
101 - blocks.{i}.attn.b_O - No bias on output projection
102 - blocks.{i}.mlp.b_in - No bias on MLP input (up_proj)
103 - blocks.{i}.mlp.b_gate - No bias on MLP gate projection
104 - blocks.{i}.mlp.b_out - No bias on MLP output (down_proj)
105 - blocks.{i}.ln1.b - RMSNorm has no bias
106 - blocks.{i}.ln2.b - RMSNorm has no bias
107 - ln_final.b - RMSNorm has no bias
109 Weight processing must handle these missing biases gracefully using
110 ProcessWeights._safe_get_tensor() or by checking for None values.
111 """
113 def __init__(self, cfg: Any) -> None:
114 """Initialize the SmolLM3 architecture adapter."""
115 super().__init__(cfg)
117 self._set_rms_rotary_defaults()
119 self.cfg.default_prepend_bos = False
120 # The bridge reimplements attention and reads output_attentions, so the
121 # HF model must run in eager mode for the scores and pattern hooks to
122 # match the reference. Set it on cfg so weight processing and
123 # setup_component_testing agree without relying on boot()'s default.
124 self.cfg.attn_implementation = "eager"
126 # Standard separate q_proj/k_proj/v_proj/o_proj layout, GQA-aware. No
127 # biases anywhere (attention_bias=False, mlp_bias=False), so no bias
128 # conversions are needed.
129 self.weight_processing_conversions = {
130 **self._qkvo_weight_conversions(),
131 }
133 self.component_mapping = {
134 "embed": EmbeddingBridge(name="model.embed_tokens"),
135 "rotary_emb": RotaryEmbeddingBridge(name="model.rotary_emb", config=self.cfg),
136 "blocks": BlockBridge(
137 name="model.layers",
138 config=self.cfg,
139 submodules={
140 "ln1": RMSNormalizationBridge(name="input_layernorm", config=self.cfg),
141 "ln2": RMSNormalizationBridge(name="post_attention_layernorm", config=self.cfg),
142 "attn": _SmolLM3AttentionBridge(
143 name="self_attn",
144 config=self.cfg,
145 submodules={
146 "q": LinearBridge(name="q_proj"),
147 "k": LinearBridge(name="k_proj"),
148 "v": LinearBridge(name="v_proj"),
149 "o": LinearBridge(name="o_proj"),
150 },
151 requires_attention_mask=True,
152 requires_position_embeddings=True,
153 ),
154 "mlp": self._gated_mlp(),
155 },
156 ),
157 "ln_final": RMSNormalizationBridge(name="model.norm", config=self.cfg),
158 "unembed": UnembeddingBridge(name="lm_head", config=self.cfg),
159 }