Coverage for transformer_lens/model_bridge/generalized_components/siglip_vision_encoder.py: 81%

56 statements  

« prev     ^ index     » next       coverage.py v7.10.1, created at 2026-09-21 19:27 +0000

1"""SigLIP Vision Encoder bridge component. 

2 

3This module contains the bridge component for SigLIP vision encoder layers 

4used in multimodal models like Gemma 3 and MedGemma. 

5""" 

6from types import SimpleNamespace 

7from typing import Any, Dict, Optional 

8 

9import torch 

10 

11from transformer_lens.hook_points import HookPoint 

12from transformer_lens.model_bridge.generalized_components.attention import ( 

13 AttentionBridge, 

14) 

15from transformer_lens.model_bridge.generalized_components.base import ( 

16 GeneralizedComponent, 

17) 

18from transformer_lens.model_bridge.generalized_components.linear import LinearBridge 

19from transformer_lens.model_bridge.generalized_components.mlp import MLPBridge 

20from transformer_lens.model_bridge.generalized_components.normalization import ( 

21 NormalizationBridge, 

22) 

23 

24 

25def _vision_attention_config(config: Any) -> Any: 

26 """A config view carrying the vision tower's dims, not the language model's. 

27 

28 AttentionBridge reshapes its q/k/v/z hooks by ``config.n_heads`` at fire time, 

29 so handing it the language config reshapes vision activations by the wrong head 

30 count -- granite-docling runs 9 text heads over 576 dims against the tower's 12 

31 over 768. Reads there are hasattr-guarded, so the dims are all this needs. 

32 """ 

33 n_heads = getattr(config, "vision_num_heads", None) 

34 d_model = getattr(config, "vision_hidden_size", None) 

35 if not n_heads or not d_model: 

36 return config 

37 return SimpleNamespace(n_heads=n_heads, d_model=d_model, d_head=d_model // n_heads) 

38 

39 

40class SiglipVisionEncoderLayerBridge(GeneralizedComponent): 

41 """Bridge for a single SigLIP encoder layer. 

42 

43 SigLIP encoder layers have: 

44 - layer_norm1: LayerNorm 

45 - self_attn: SiglipAttention 

46 - layer_norm2: LayerNorm 

47 - mlp: SiglipMLP 

48 """ 

49 

50 is_list_item: bool = True 

51 hook_aliases = { 

52 "hook_resid_pre": "hook_in", 

53 "hook_resid_post": "hook_out", 

54 "hook_attn_in": "attn.hook_in", 

55 "hook_attn_out": "attn.hook_out", 

56 "hook_mlp_in": "mlp.hook_in", 

57 "hook_mlp_out": "mlp.hook_out", 

58 } 

59 

60 def __init__( 

61 self, 

62 name: str, 

63 config: Optional[Any] = None, 

64 submodules: Optional[Dict[str, GeneralizedComponent]] = None, 

65 ): 

66 """Initialize the SigLIP encoder layer bridge. 

67 

68 Args: 

69 name: The name of this component (e.g., "encoder.layers") 

70 config: Optional configuration object 

71 submodules: Dictionary of submodules to register 

72 """ 

73 default_submodules: Dict[str, GeneralizedComponent] = { 

74 "ln1": NormalizationBridge(name="layer_norm1", config=config), 

75 "attn": AttentionBridge( 

76 name="self_attn", 

77 config=_vision_attention_config(config), 

78 submodules={ 

79 "q": LinearBridge(name="q_proj"), 

80 "k": LinearBridge(name="k_proj"), 

81 "v": LinearBridge(name="v_proj"), 

82 # SigLIP names the output projection out_proj, not o_proj. 

83 "o": LinearBridge(name="out_proj"), 

84 }, 

85 ), 

86 "ln2": NormalizationBridge(name="layer_norm2", config=config), 

87 "mlp": MLPBridge( 

88 name="mlp", 

89 config=config, 

90 submodules={ 

91 "in": LinearBridge(name="fc1"), 

92 "out": LinearBridge(name="fc2"), 

93 }, 

94 ), 

95 } 

96 if submodules: 96 ↛ 97line 96 didn't jump to line 97 because the condition on line 96 was never true

97 default_submodules.update(submodules) 

98 super().__init__(name, config, submodules=default_submodules) 

99 

100 def forward( 

101 self, 

102 hidden_states: torch.Tensor, 

103 attention_mask: Optional[torch.Tensor] = None, 

104 **kwargs: Any, 

105 ) -> torch.Tensor: 

106 """Forward pass through the vision encoder layer. 

107 

108 Args: 

109 hidden_states: Input hidden states from previous layer 

110 attention_mask: Optional attention mask 

111 **kwargs: Additional arguments 

112 

113 Returns: 

114 Output hidden states 

115 """ 

116 if self.original_component is None: 116 ↛ 117line 116 didn't jump to line 117 because the condition on line 116 was never true

117 raise RuntimeError( 

118 f"Original component not set for {self.name}. Call set_original_component() first." 

119 ) 

120 

121 hidden_states = self.hook_in(hidden_states) 

122 output = self.original_component(hidden_states, attention_mask=attention_mask, **kwargs) 

123 

124 if isinstance(output, tuple): 124 ↛ 125line 124 didn't jump to line 125 because the condition on line 124 was never true

125 output = (self.hook_out(output[0]),) + output[1:] 

126 else: 

127 output = self.hook_out(output) 

128 

129 return output 

130 

131 

132class SiglipVisionEncoderBridge(GeneralizedComponent): 

133 """Bridge for the complete SigLIP vision encoder. 

134 

135 The SigLIP vision tower consists of: 

136 - vision_model.embeddings: Patch + position embeddings 

137 - vision_model.encoder.layers[]: Stack of encoder layers 

138 - post_layernorm: Final layer norm 

139 

140 This bridge wraps the entire vision tower to provide hooks for 

141 interpretability of the vision processing pipeline. 

142 """ 

143 

144 hook_aliases = { 

145 "hook_vision_embed": "embeddings.hook_out", 

146 "hook_vision_out": "hook_out", 

147 } 

148 

149 def __init__( 

150 self, 

151 name: str, 

152 config: Optional[Any] = None, 

153 submodules: Optional[Dict[str, GeneralizedComponent]] = None, 

154 ): 

155 """Initialize the SigLIP vision encoder bridge. 

156 

157 Args: 

158 name: The name of this component (e.g., "model.vision_tower") 

159 config: Optional configuration object 

160 submodules: Dictionary of submodules to register 

161 """ 

162 # All submodule names are resolved relative to the parent's 

163 # original_component (a SiglipVisionModel) by setup_submodules(). 

164 # SiglipVisionModel wraps a SiglipVisionTransformer as .vision_model till 

165 # transformers version 5.6.0 

166 # post_layernorm is nn.LayerNorm; NormalizationBridge introspects the 

167 # wrapped module so the RMSNorm-LM config (Gemma 3, LLaVA) doesn't leak. 

168 default_submodules = { 

169 "embeddings": GeneralizedComponent(name="vision_model.embeddings"), 

170 # Pass config down: without it the layer's attention bridge inherits the 

171 # raw HF vision config, which spells its head count num_attention_heads 

172 # and so reads as n_heads=1 when the q/k/v/z hooks reshape. 

173 "encoder_layers": SiglipVisionEncoderLayerBridge( 

174 name="vision_model.encoder.layers", config=config 

175 ), 

176 "post_layernorm": NormalizationBridge( 

177 name="vision_model.post_layernorm", config=config 

178 ), 

179 } 

180 

181 if submodules: 181 ↛ 182line 181 didn't jump to line 182 because the condition on line 181 was never true

182 default_submodules.update(submodules) 

183 

184 super().__init__(name, config, submodules=default_submodules) 

185 

186 # Additional hooks for vision-specific processing 

187 self.hook_patch_embed = HookPoint() # After patch embedding 

188 self.hook_pos_embed = HookPoint() # After position embedding added 

189 

190 def forward( 

191 self, 

192 pixel_values: torch.Tensor, 

193 **kwargs: Any, 

194 ) -> Any: 

195 """Forward pass through the vision encoder. 

196 

197 Args: 

198 pixel_values: Input image tensor [batch, channels, height, width] 

199 **kwargs: Additional arguments 

200 

201 Returns: 

202 Whatever the wrapped module returns, hooked in place: a 

203 ``BaseModelOutput``, a tuple, or a bare tensor of vision embeddings 

204 [batch, num_patches, hidden_size]. Callers see HF's own shape, so the 

205 return is deliberately not narrowed to a tensor. 

206 """ 

207 if self.original_component is None: 207 ↛ 208line 207 didn't jump to line 208 because the condition on line 207 was never true

208 raise RuntimeError( 

209 f"Original component not set for {self.name}. Call set_original_component() first." 

210 ) 

211 

212 pixel_values = self.hook_in(pixel_values) 

213 

214 output = self.original_component(pixel_values, **kwargs) 

215 

216 # Handle tuple output (some models return (hidden_states, ...)) 

217 if isinstance(output, tuple): 217 ↛ 218line 217 didn't jump to line 218 because the condition on line 217 was never true

218 output = (self.hook_out(output[0]),) + output[1:] 

219 elif hasattr(output, "last_hidden_state"): 219 ↛ 223line 219 didn't jump to line 223 because the condition on line 219 was always true

220 # Handle BaseModelOutput-like returns 

221 output.last_hidden_state = self.hook_out(output.last_hidden_state) 

222 else: 

223 output = self.hook_out(output) 

224 

225 return output 

226 

227 def set_original_component(self, original_component: torch.nn.Module) -> None: 

228 """Set the original component that this bridge wraps. 

229 Note that SiglipVisionModel used to wrap a inner object as .vision_model before 

230 transformers version 5.6.0, but after that it must directly be used. 

231 This is a temporary hack to fix that till the transformers version is bumped. 

232 

233 Args: 

234 original_component: The original transformer component to wrap 

235 """ 

236 if not hasattr(original_component, "vision_model"): 

237 # We should bypass any pytorch module registration. 

238 object.__setattr__(original_component, "vision_model", original_component) 

239 super().set_original_component(original_component)