From 4903efbd050238d4da8b19b8bcd2a4afe762829c Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Sun, 26 Jul 2026 21:26:10 -0400 Subject: [PATCH 01/74] Add ViTArchitectureAdapter to supported architectures --- .../model_bridge/supported_architectures/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/transformer_lens/model_bridge/supported_architectures/__init__.py b/transformer_lens/model_bridge/supported_architectures/__init__.py index 8f4cfea3e..894ac0cd4 100644 --- a/transformer_lens/model_bridge/supported_architectures/__init__.py +++ b/transformer_lens/model_bridge/supported_architectures/__init__.py @@ -201,6 +201,9 @@ from transformer_lens.model_bridge.supported_architectures.t5gemma import ( T5GemmaArchitectureAdapter, ) +from transformer_lens.model_bridge.supported_architectures.vit import ( + ViTArchitectureAdapter, +) from transformer_lens.model_bridge.supported_architectures.xglm import ( XGLMArchitectureAdapter, ) @@ -273,5 +276,6 @@ "StableLmArchitectureAdapter", "T5ArchitectureAdapter", "T5GemmaArchitectureAdapter", + "ViTArchitectureAdapter", "XGLMArchitectureAdapter", ] From 2172e71a13d0d472ec01d33d1638cd69d8227a2b Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Sun, 26 Jul 2026 21:28:13 -0400 Subject: [PATCH 02/74] Add ViTArchitectureAdapter to architecture factory --- transformer_lens/factories/architecture_adapter_factory.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/transformer_lens/factories/architecture_adapter_factory.py b/transformer_lens/factories/architecture_adapter_factory.py index 2342908d3..0f015672c 100644 --- a/transformer_lens/factories/architecture_adapter_factory.py +++ b/transformer_lens/factories/architecture_adapter_factory.py @@ -76,6 +76,7 @@ StableLmArchitectureAdapter, T5ArchitectureAdapter, T5GemmaArchitectureAdapter, + ViTArchitectureAdapter, XGLMArchitectureAdapter, ) @@ -158,6 +159,10 @@ "MinGPTForCausalLM": MingptArchitectureAdapter, "GPTNeoForCausalLM": NeoArchitectureAdapter, "GPTNeoXForCausalLM": NeoxArchitectureAdapter, + "ViTModel": ViTArchitectureAdapter, + "ViTForImageClassification": ViTArchitectureAdapter, + "DeiTModel": ViTArchitectureAdapter, + "DeiTForImageClassification": ViTArchitectureAdapter, # "DeiTForImageClassificationWithTeacher" unsupported for now } From 5f893f595a0722a75f5104cc6b17aa3d98b77c2a Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Sun, 26 Jul 2026 21:29:16 -0400 Subject: [PATCH 03/74] Add ViT and DeiT models to model registry --- transformer_lens/tools/model_registry/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/transformer_lens/tools/model_registry/__init__.py b/transformer_lens/tools/model_registry/__init__.py index 6d2a2927a..adea21df7 100644 --- a/transformer_lens/tools/model_registry/__init__.py +++ b/transformer_lens/tools/model_registry/__init__.py @@ -111,6 +111,10 @@ "MT5ForConditionalGeneration", "T5GemmaForConditionalGeneration", "XGLMForCausalLM", + "ViTModel", + "ViTForImageClassification", + "DeiTModel", + "DeiTForImageClassification", } # Foundation-trained orgs per architecture. Source of truth for the scraper's @@ -182,6 +186,10 @@ "T5ForConditionalGeneration": ["google-t5", "google", "Salesforce", "MBZUAI"], "T5GemmaForConditionalGeneration": ["google"], "XGLMForCausalLM": ["facebook"], + "ViTModel": ["google"], + "ViTForImageClassification": ["google"], + "DeiTModel": ["facebook"], + "DeiTForImageClassification": ["facebook"], } __all__ = [ From d3513ae6a4329bed20814005edd88840c4902e08 Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Sun, 26 Jul 2026 21:32:23 -0400 Subject: [PATCH 04/74] Add new model descriptions for Vision Transformers and Wav2Vec2 --- .../tools/model_registry/generate_report.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/transformer_lens/tools/model_registry/generate_report.py b/transformer_lens/tools/model_registry/generate_report.py index 73f0aa8b1..40e402ece 100644 --- a/transformer_lens/tools/model_registry/generate_report.py +++ b/transformer_lens/tools/model_registry/generate_report.py @@ -61,6 +61,12 @@ "T5GemmaForConditionalGeneration": "Google's T5Gemma encoder-decoder model with Gemma-style RoPE, GQA, and gated MLP", "BartForConditionalGeneration": "Facebook's BART encoder-decoder model", "HunYuanDenseV1ForCausalLM": "Tencent's open source decoder models", + "ViTModel": "Vision Transformer (bare encoder, no classification head)", + "ViTForImageClassification": "Vision Transformer with an image classification head", + "DeiTModel": "Data-efficient Image Transformer (bare encoder)", + "DeiTForImageClassification": "DeiT with a single CLS-token classification head", + "Wav2Vec2Model": "Facebook's Wav2Vec 2.0 for speech", + "HubertModel": "Facebook's HuBERT for speech", # Unsupported architectures "BertModel": "Google's BERT bidirectional encoder for understanding tasks", "BertForMaskedLM": "BERT with masked language modeling head", @@ -82,14 +88,10 @@ "MT5ForConditionalGeneration": "Multilingual T5", "WhisperForConditionalGeneration": "OpenAI's Whisper speech recognition", "CLIPModel": "OpenAI's CLIP vision-language model", - "ViTModel": "Google's Vision Transformer", "SwinModel": "Microsoft's Swin Transformer for vision", - "DeiTModel": "Facebook's Data-efficient Image Transformer", "BeitModel": "Microsoft's BERT pre-training for images", "ConvNextModel": "Facebook's ConvNeXt modernized ConvNet", "SegformerModel": "NVIDIA's SegFormer for segmentation", - "Wav2Vec2Model": "Facebook's Wav2Vec 2.0 for speech", - "HubertModel": "Facebook's HuBERT for speech", "SpeechT5Model": "Microsoft's SpeechT5 for speech tasks", "BlipModel": "Salesforce's BLIP vision-language model", "Blip2Model": "Salesforce's BLIP-2 with frozen LLM", From 75c9d2c20ce8bb326a20e54c66c7808af65a930e Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Sun, 26 Jul 2026 21:33:04 -0400 Subject: [PATCH 05/74] Add ViTArchitectureAdapter for vision models Implement ViT/DeiT architecture adapter for model bridging. --- .../supported_architectures/vit.py | 209 ++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 transformer_lens/model_bridge/supported_architectures/vit.py diff --git a/transformer_lens/model_bridge/supported_architectures/vit.py b/transformer_lens/model_bridge/supported_architectures/vit.py new file mode 100644 index 000000000..3d4e02948 --- /dev/null +++ b/transformer_lens/model_bridge/supported_architectures/vit.py @@ -0,0 +1,209 @@ +"""ViT / DeiT architecture adapter. + +Supports HF `ViTModel`, `ViTForImageClassification`, `DeiTModel`, +`DeiTForImageClassification` (single CLS-token classifier head). Encoder blocks +are structurally near-identical between ViT and DeiT — same field names +(layernorm_before/after, attention.{q,k,v,o}_proj, mlp.{fc1,fc2}) — differing +only in the embeddings module used (DeiT's carries an extra distillation token, +which is invisible to this adapter — see vision_embeddings.py). + +NOT covered: `DeiTForImageClassificationWithTeacher` (dual cls+distillation head, +averaged). See vision_classifier_head.py's docstring for why, and prepare_model() +below raises loudly if you load one anyway rather than silently producing wrong +logits. + +ViT/DeiT blocks are pre-LN (LayerNorm applied *before* attention/MLP, residual +added after — same shape as Llama/GPT2), unlike BERT's post-LN. That's why +`supports_fold_ln = True` here where BertArchitectureAdapter sets it False. +""" + +from typing import Any, Dict + +from transformer_lens.conversion_utils.conversion_steps import RearrangeTensorConversion +from transformer_lens.conversion_utils.param_processing_conversion import ( + ParamProcessingConversion, +) +from transformer_lens.model_bridge.architecture_adapter import ArchitectureAdapter +from transformer_lens.model_bridge.generalized_components import ( + AttentionBridge, + BlockBridge, + LinearBridge, + MLPBridge, + NormalizationBridge, +) +from transformer_lens.model_bridge.generalized_components.vision_classifier_head import ( + VisionClassifierHeadBridge, +) +from transformer_lens.model_bridge.generalized_components.vision_embeddings import ( + VisionEmbeddingsBridge, +) + + +class ViTArchitectureAdapter(ArchitectureAdapter): + """Architecture adapter for ViT and (non-distilled-head) DeiT vision models.""" + + supports_generation: bool = False + + def __init__(self, cfg: Any) -> None: + super().__init__(cfg) + + # Mirrors HubertArchitectureAdapter's self.cfg.is_audio_model = True. Required — + # bridge.py's forward() has no vision-only dispatch path without it; see + # BRIDGE_CHANGES.md for the corresponding bridge.py edits this flag depends on. + self.cfg.is_visual_model = True + self.cfg.normalization_type = "LN" + # Position embeddings here are a single learned (1, seq+1[+1], hidden) tensor + # added inside VisionEmbeddingsBridge, not a separate lookup-by-index "pos_embed" + # component — there's no dedicated TL positional_embedding_type value for that + # shape, so "standard" is a placeholder. Check whether anything downstream (e.g. + # fold_ln / HookedTransformer-compat code paths) actually branches on this string + # for a model with no separate pos_embed component before trusting it blindly. + self.cfg.positional_embedding_type = "standard" + self.cfg.final_rms = False + self.cfg.gated_mlp = False + self.cfg.attn_only = False + # Pre-LN blocks (see module docstring) — unlike BertArchitectureAdapter's post-LN. + self.supports_fold_ln = True + + n_heads = self.cfg.n_heads + + # Q/K/V/O are separate nn.Linear projections (q_proj/k_proj/v_proj/o_proj), + # same rearrangement pattern as the BERT and HuBERT adapters. + self.weight_processing_conversions = { + "blocks.{i}.attn.q.weight": ParamProcessingConversion( + tensor_conversion=RearrangeTensorConversion( + "(h d_head) d_model -> h d_model d_head", h=n_heads + ), + ), + "blocks.{i}.attn.k.weight": ParamProcessingConversion( + tensor_conversion=RearrangeTensorConversion( + "(h d_head) d_model -> h d_model d_head", h=n_heads + ), + ), + "blocks.{i}.attn.v.weight": ParamProcessingConversion( + tensor_conversion=RearrangeTensorConversion( + "(h d_head) d_model -> h d_model d_head", h=n_heads + ), + ), + "blocks.{i}.attn.q.bias": ParamProcessingConversion( + tensor_conversion=RearrangeTensorConversion("(h d_head) -> h d_head", h=n_heads), + ), + "blocks.{i}.attn.k.bias": ParamProcessingConversion( + tensor_conversion=RearrangeTensorConversion("(h d_head) -> h d_head", h=n_heads), + ), + "blocks.{i}.attn.v.bias": ParamProcessingConversion( + tensor_conversion=RearrangeTensorConversion("(h d_head) -> h d_head", h=n_heads), + ), + "blocks.{i}.attn.o.weight": ParamProcessingConversion( + tensor_conversion=RearrangeTensorConversion( + "d_model (h d_head) -> h d_head d_model", h=n_heads + ), + ), + # No q/k/v bias conversions are a no-op, not an error, when + # config.qkv_bias=False: LinearBridge wraps whatever nn.Linear HF actually + # built, and a bias-less Linear simply has no ".bias" key in the state dict + # for this conversion to touch. + } + + # Default mapping assumes a bare ViTModel (no prefix, no classifier). + # prepare_model() rebuilds this once the actual HF model is available and we + # can detect the "vit."/"deit." prefix and classifier presence. + self.component_mapping = self._build_component_mapping(prefix="", with_classifier=False) + + def _build_component_mapping(self, prefix: str, with_classifier: bool) -> Dict[str, Any]: + """Build component mapping. + + prefix="" for bare ViTModel/DeiTModel, "vit." for ViTForImageClassification, + "deit." for DeiTForImageClassification. `classifier` itself is always a direct + top-level attribute of the *ForImageClassification wrapper (never nested under + the prefix) per the HF source, so it's never prefixed. + """ + p = prefix + mapping: Dict[str, Any] = { + "embed": VisionEmbeddingsBridge(name=f"{p}embeddings"), + "blocks": BlockBridge( + name=f"{p}layers", + # Same redirect the BERT and HuBERT adapters use — kept for consistency + # even though ViT's MLP (unlike BERT's) is a real cohesive submodule, + # since both existing ground-truth adapters apply it regardless. + hook_alias_overrides={ + "hook_mlp_out": "mlp.out.hook_out", + "hook_mlp_in": "mlp.in.hook_in", + }, + submodules={ + "ln1": NormalizationBridge( + name="layernorm_before", + config=self.cfg, + use_native_layernorm_autograd=True, + ), + "ln2": NormalizationBridge( + name="layernorm_after", + config=self.cfg, + use_native_layernorm_autograd=True, + ), + "attn": AttentionBridge( + name="attention", + config=self.cfg, + submodules={ + "q": LinearBridge(name="q_proj"), + "k": LinearBridge(name="k_proj"), + "v": LinearBridge(name="v_proj"), + "o": LinearBridge(name="o_proj"), + }, + ), + "mlp": MLPBridge( + name="mlp", + config=self.cfg, + submodules={ + "in": LinearBridge(name="fc1"), + "out": LinearBridge(name="fc2"), + }, + ), + }, + ), + "ln_final": NormalizationBridge( + name=f"{p}layernorm", + config=self.cfg, + use_native_layernorm_autograd=True, + ), + } + if with_classifier: + mapping["unembed"] = VisionClassifierHeadBridge(name="classifier", token_index=0) + return mapping + + def prepare_loading(self, model_name: str, model_kwargs: dict) -> None: + """Propagate ViT/DeiT HF config attributes not covered by the generic mapper.""" + hf_config = model_kwargs.get("config") + if hf_config is None: + return + + # Some newer ViT/DeiT configs allow an explicit head_dim distinct from + # hidden_size // num_attention_heads. Forward it if present so weight + # reshaping doesn't silently assume the naive split (same reasoning as the + # adapter guide's "forgetting n_key_value_heads" pitfall for GQA models). + head_dim = getattr(hf_config, "head_dim", None) + if head_dim is not None: + self.cfg.d_head = head_dim # type: ignore[attr-defined] + + def prepare_model(self, hf_model: Any) -> None: + """Detect ViTForImageClassification vs DeiTForImageClassification vs a bare + *Model, and add/omit the classifier head + prefix accordingly.""" + if hasattr(hf_model, "vit"): + prefix = "vit." + elif hasattr(hf_model, "deit"): + prefix = "deit." + else: + prefix = "" # bare ViTModel / DeiTModel, loaded directly + + if hasattr(hf_model, "cls_classifier") and hasattr(hf_model, "distillation_classifier"): + raise NotImplementedError( + "DeiTForImageClassificationWithTeacher (dual cls_classifier + " + "distillation_classifier head, averaged) isn't supported by this " + "adapter yet — see vision_classifier_head.py's docstring for why, " + "and what to check before adding it." + ) + + with_classifier = hasattr(hf_model, "classifier") + self.component_mapping = self._build_component_mapping( + prefix=prefix, with_classifier=with_classifier + ) From a9956fdd5797d5a4156f15a24dfe5a5caa1d2cd4 Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Sun, 26 Jul 2026 21:36:31 -0400 Subject: [PATCH 06/74] Create vision_embedings.py --- .../vision_embedings.py | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 transformer_lens/model_bridge/generalized_components/vision_embedings.py diff --git a/transformer_lens/model_bridge/generalized_components/vision_embedings.py b/transformer_lens/model_bridge/generalized_components/vision_embedings.py new file mode 100644 index 000000000..a42259114 --- /dev/null +++ b/transformer_lens/model_bridge/generalized_components/vision_embedings.py @@ -0,0 +1,62 @@ +"""Vision (ViT-style) embeddings bridge component. + +Wraps a HF `ViTEmbeddings` / `DeiTEmbeddings` module directly and forwards through +it unmodified. The patch-conv projection, CLS-token (and, for DeiT, distillation- +token) concatenation, and position-embedding addition are HF's math — we don't +reimplement any of it, we just point at the real module and hook its input/output. + +Why one class covers both ViT and DeiT: `ViTEmbeddings.forward` and +`DeiTEmbeddings.forward` have an *identical* signature + (pixel_values, bool_masked_pos=None, interpolate_pos_encoding=False) +The only structural difference between them (DeiT prepends a distillation token +in addition to CLS, and sizes `position_embeddings` for +2 slots instead of +1) +lives entirely inside whichever module `set_original_component` resolves to — it's +invisible to this wrapper, so no DeiT-specific branching is needed here. + +Verified against the real generalized_components/base.py (not extrapolated): +`GeneralizedComponent.forward(*args, **kwargs)` already does exactly what we need +— it hooks the input via `self.hook_in`, casts it to match the wrapped module's +own parameter dtype (equivalent to HF's own `pixel_values.to(expected_dtype)` +"kept for BC" cast in ViTModel.forward — both land on the same dtype in the +overwhelming common case of a non-mixed-precision checkpoint), calls +`self.original_component(*args, **kwargs)`, and hooks the output via +`self.hook_out`. So this subclass only needs to guarantee `pixel_values` reaches +`super().forward()` *positionally* — the base class's own kwarg-name sniffing +list (`input`, `hidden_states`, `input_ids`, `query_input`, `x`, `inputs_embeds`) +doesn't include `pixel_values`, so if this component were ever called +all-keyword (`self.embeddings(pixel_values=x, ...)`, unlike the two HF call +sites we've confirmed) the base class's hook_in would silently never fire. +Re-emitting positionally here closes that gap regardless of how *we* were +called. +""" + +from typing import Any, Optional + +import torch +from torch import Tensor + +from transformer_lens.model_bridge.generalized_components.base import GeneralizedComponent + + +class VisionEmbeddingsBridge(GeneralizedComponent): + """Bridge for ViTEmbeddings / DeiTEmbeddings. + + Point `name=` at the embeddings module directly, e.g.: + VisionEmbeddingsBridge(name="embeddings") # bare ViTModel/DeiTModel + VisionEmbeddingsBridge(name="vit.embeddings") # ViTForImageClassification + VisionEmbeddingsBridge(name="deit.embeddings") # DeiTForImageClassification + """ + + def forward( + self, + pixel_values: Tensor, + bool_masked_pos: Optional[torch.BoolTensor] = None, + interpolate_pos_encoding: Optional[bool] = None, + **kwargs: Any, + ) -> Tensor: + return super().forward( + pixel_values, + bool_masked_pos=bool_masked_pos, + interpolate_pos_encoding=interpolate_pos_encoding, + **kwargs, + ) From 33601d9d2b5ca5538a362c2d23f4d5145be56ee1 Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Sun, 26 Jul 2026 21:38:17 -0400 Subject: [PATCH 07/74] Add VisionClassifierHeadBridge for CLS token classification Implement VisionClassifierHeadBridge to handle CLS token slicing for classification. --- .../vision_classifier_head.py | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 transformer_lens/model_bridge/generalized_components/vision_classifier_head.py diff --git a/transformer_lens/model_bridge/generalized_components/vision_classifier_head.py b/transformer_lens/model_bridge/generalized_components/vision_classifier_head.py new file mode 100644 index 000000000..35ae057f5 --- /dev/null +++ b/transformer_lens/model_bridge/generalized_components/vision_classifier_head.py @@ -0,0 +1,59 @@ +"""CLS-token classifier head bridge component. + +`ViTForImageClassification.forward` / `DeiTForImageClassification.forward` both do: + + sequence_output = outputs.last_hidden_state + pooled_output = sequence_output[:, 0, :] # CLS token only + logits = self.classifier(pooled_output) + +This is *not* expressible with the base `GeneralizedComponent.forward()` (nor a +plain `LinearBridge`): that generic forward calls `original_component(*args, +**kwargs)` with no slicing step, so applying it directly to `classifier` would +run the Linear over every position in the sequence, not just the CLS token. +Hence a real override, verified against the actual base.py this time (not the +extrapolated version from before you shared it): + +- `self.hook_in` / `self.hook_out` / `self.original_component` are exactly the + names and semantics base.py defines. +- `original_component` being unset returns `None` (not an exception) per + base.py's property, so a direct `self.original_component(pooled)` call would + fail with a confusing `'NoneType' object is not callable` instead of a clear + message — added an explicit guard raising the same RuntimeError text + `GeneralizedComponent.forward()` itself uses, so the failure mode is + consistent across both generic and custom components. + +Deliberately NOT covering `DeiTForImageClassificationWithTeacher` (the dual +cls_classifier + distillation_classifier head, averaged) — see vit.py's +module docstring for why. +""" + +from typing import Any + +from torch import Tensor + +from transformer_lens.model_bridge.generalized_components.base import GeneralizedComponent + + +class VisionClassifierHeadBridge(GeneralizedComponent): + """Wraps a plain classifier nn.Linear, but slices a single token position first. + + token_index=0 (default) for the CLS token — use this for + ViTForImageClassification and (non-teacher) DeiTForImageClassification, both + of which classify off index 0 regardless of whether the underlying embeddings + module also carries a distillation token at index 1. + """ + + def __init__(self, name: str, config: Any = None, submodules=None, token_index: int = 0) -> None: + super().__init__(name, config, submodules) + self.token_index = token_index + + def forward(self, sequence_output: Tensor, **kwargs: Any) -> Tensor: + original_component = self.original_component + if original_component is None: + raise RuntimeError( + f"Original component not set for {self.name}. Call set_original_component() first." + ) + sequence_output = self.hook_in(sequence_output) + pooled = sequence_output[:, self.token_index, :] + logits = original_component(pooled) + return self.hook_out(logits) From 7bbeeafc9c53829756c5dc90ffb4516d67f41327 Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Sun, 26 Jul 2026 21:41:56 -0400 Subject: [PATCH 08/74] Add visual model configuration to transformer bridge --- transformer_lens/config/transformer_bridge_config.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/transformer_lens/config/transformer_bridge_config.py b/transformer_lens/config/transformer_bridge_config.py index 593407451..6893ac8fa 100644 --- a/transformer_lens/config/transformer_bridge_config.py +++ b/transformer_lens/config/transformer_bridge_config.py @@ -87,6 +87,8 @@ def __init__( attn_implementation: Optional[str] = None, # Audio model configuration is_audio_model: bool = False, + # Vision model (ViT, DeiT) configuration + is_visual_model: bool = False, # Stateful model configuration (e.g., Mamba SSMs use cache_params, # not past_key_values, so generation delegates to hf_generate) is_stateful: bool = False, @@ -179,6 +181,8 @@ def __init__( self.attn_implementation = attn_implementation # Audio model configuration self.is_audio_model = is_audio_model + # Vision model (ViT, DeiT) configuration + self.is_visual_model= is_visual_model # Stateful model configuration self.is_stateful = is_stateful # Multimodal configuration From cc5713b42d869571870127819e438fcdd2fd0e8a Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Sun, 26 Jul 2026 21:52:16 -0400 Subject: [PATCH 09/74] Clarify pixel_values usage for multimodal and vision models Updated documentation for pixel_values parameter to clarify its use with vision models. --- transformer_lens/model_bridge/bridge.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/transformer_lens/model_bridge/bridge.py b/transformer_lens/model_bridge/bridge.py index 4800b0150..e749bcb6f 100644 --- a/transformer_lens/model_bridge/bridge.py +++ b/transformer_lens/model_bridge/bridge.py @@ -1734,9 +1734,10 @@ def forward( (fragile across HF versions) or exception-based layer skipping (corrupts model state). Raises NotImplementedError if a non-None value is passed. stop_at_layer: Layer to stop forward pass at - pixel_values: Optional image tensor for multimodal models (e.g., LLaVA, Gemma3). + pixel_values: Optional image tensor for multimodal models (e.g., LLaVA, Gemma3) + and vision models (eg. ViT, DeiT). The tensor is passed directly to the underlying HuggingFace model. - Only valid when cfg.is_multimodal is True. + Only valid when cfg.is_multimodal is True or cfg.is_visual_mdoel is True. input_values: Optional audio waveform tensor for audio models (e.g., HuBERT). The tensor is passed directly to the underlying HuggingFace model. Only valid when cfg.is_audio_model is True. @@ -1785,6 +1786,11 @@ def forward( "Audio models require tensor input (raw waveform), not text. " "Pass a torch.Tensor or use the input_values parameter." ) + if getattr(self.cfg, "is_visual_model", False): + raise ValueError( + "Visual models require tensor input (pixel values), not text. " + "Pass a torch.Tensor or use the pixel_values parameter." + ) if _is_batched_list and padding_side is None: # Force left-padding so real tokens are flush-right. _orig_padding_side = self.tokenizer.padding_side @@ -1872,10 +1878,13 @@ def forward( # Handle pixel_values for multimodal models if pixel_values is not None: - if not getattr(self.cfg, "is_multimodal", False): + if not ( + getattr(self.cfg, "is_multimodal", False) + or getattr(self.cfg, "is_visual_model", False) + ): raise ValueError( - "pixel_values can only be passed to multimodal models " - "(cfg.is_multimodal must be True)" + "pixel_values can only be passed to multimodal or vision models " + "(cfg.is_multimodal or cfg.is_visual_model must be True)" ) kwargs["pixel_values"] = pixel_values From 7cd239333c5fc49958a29b39d65c33189a1b199b Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Sun, 26 Jul 2026 21:56:45 -0400 Subject: [PATCH 10/74] Update bridge.py --- transformer_lens/model_bridge/bridge.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/transformer_lens/model_bridge/bridge.py b/transformer_lens/model_bridge/bridge.py index e749bcb6f..bbfdf7448 100644 --- a/transformer_lens/model_bridge/bridge.py +++ b/transformer_lens/model_bridge/bridge.py @@ -1777,6 +1777,7 @@ def forward( isinstance(input, list) and len(input) > 1 and not getattr(self.cfg, "is_audio_model", False) + and not getattr(self.cfg, "is_visual_model", False) ) try: @@ -1909,6 +1910,19 @@ def forward( "Audio models require tensor input (raw waveform). " "Pass a torch.Tensor or use input_values parameter." ) + elif getattr(self.cfg, "is_visual_model", False): + # "pixel_values" may already be in kwargs from the "if pixel_values is not None:" + # gate above (explicit pixel_values=... call); otherwise treat `input` itself as + # the image tensor, matching how the audio branch treats `input` as the waveform. + if "pixel_values" not in kwargs: + if isinstance(input, torch.Tensor): + kwargs["pixel_values"] = input + else: + raise ValueError( + "Visual models require tensor input (pixel values). " + "Pass a torch.Tensor as `input` or use the pixel_values parameter." + ) + output = self.original_model(**kwargs) elif _is_inputs_embeds: output = self.original_model(inputs_embeds=input_ids, **kwargs) else: From 7974c6fe53429f453b98931e2c73dacc5244e935 Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Sun, 26 Jul 2026 21:58:50 -0400 Subject: [PATCH 11/74] Update bridge.py --- transformer_lens/model_bridge/bridge.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/transformer_lens/model_bridge/bridge.py b/transformer_lens/model_bridge/bridge.py index bbfdf7448..73e677320 100644 --- a/transformer_lens/model_bridge/bridge.py +++ b/transformer_lens/model_bridge/bridge.py @@ -1944,6 +1944,14 @@ def forward( "Audio models do not support return_type='loss'. " "CTC loss requires aligned frame-level labels." ) + if getattr(self.cfg, "is_visual_model", False): + raise ValueError( + "Vision classification models do not support return_type='loss' " + "via this path (no next-token LM target exists for image " + "classification). Compute cross-entropy against `labels` " + "yourself from the returned logits, or use hf_generate()-style " + "direct access to self.original_model for HF's own loss." + ) if _is_inputs_embeds: raise ValueError( "Cannot compute loss with inputs_embeds — token IDs required for labels." From 03b9af65132ccfc645812c099b8f0240026ccfff Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Sun, 26 Jul 2026 22:36:43 -0400 Subject: [PATCH 12/74] Rename vision_embedings.py to vision_embeddings.py --- .../{vision_embedings.py => vision_embeddings.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename transformer_lens/model_bridge/generalized_components/{vision_embedings.py => vision_embeddings.py} (100%) diff --git a/transformer_lens/model_bridge/generalized_components/vision_embedings.py b/transformer_lens/model_bridge/generalized_components/vision_embeddings.py similarity index 100% rename from transformer_lens/model_bridge/generalized_components/vision_embedings.py rename to transformer_lens/model_bridge/generalized_components/vision_embeddings.py From 11ed7c3fb5ad5391e09e3b1415a25a8d8f8c9be8 Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Sun, 26 Jul 2026 22:38:21 -0400 Subject: [PATCH 13/74] Define vision model and classification architectures Added vision model architectures and classification heads. --- transformer_lens/utilities/architectures.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/transformer_lens/utilities/architectures.py b/transformer_lens/utilities/architectures.py index ef216e596..200aae3dd 100644 --- a/transformer_lens/utilities/architectures.py +++ b/transformer_lens/utilities/architectures.py @@ -45,6 +45,19 @@ "HubertForSequenceClassification", } +# Vision-only (non-multimodal, no text tower) encoder models. Split into the +# two HF AutoModel classes they load under: bare encoders load via AutoModel, +# classification heads load via AutoModelForImageClassification. +VISION_MODEL_ARCHITECTURES: set[str] = { + "ViTModel", + "DeiTModel", +} +VISION_CLASSIFICATION_ARCHITECTURES: set[str] = { + "ViTForImageClassification", + "DeiTForImageClassification", +} +VISION_ARCHITECTURES: set[str] = VISION_MODEL_ARCHITECTURES | VISION_CLASSIFICATION_ARCHITECTURES + # Bridge uses different hook shapes than HookedTransformer by design. # Phase 2/3 HT comparisons are skipped; Phase 1 (HF comparison) is the gold standard. NO_HT_COMPARISON_ARCHITECTURES: set[str] = ( From 3b245fbf2a42b50c61f1bd6d465826085066ed9c Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Sun, 26 Jul 2026 22:41:00 -0400 Subject: [PATCH 14/74] Add support for vision architectures in transformers --- transformer_lens/model_bridge/sources/transformers.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/transformer_lens/model_bridge/sources/transformers.py b/transformer_lens/model_bridge/sources/transformers.py index bce60d96f..c239fafd4 100644 --- a/transformer_lens/model_bridge/sources/transformers.py +++ b/transformer_lens/model_bridge/sources/transformers.py @@ -283,6 +283,8 @@ def get_hf_model_class_for_architecture(architecture: str): MASKED_LM_ARCHITECTURES, MULTIMODAL_ARCHITECTURES, SEQ2SEQ_ARCHITECTURES, + VISION_ARCHITECTURES, + VISION_CLASSIFICATION_ARCHITECTURES, ) if architecture in SEQ2SEQ_ARCHITECTURES: @@ -300,6 +302,12 @@ def get_hf_model_class_for_architecture(architecture: str): return AutoModelForCTC from transformers import AutoModel + return AutoModel + elif architecture in VISION_ARCHITECTURES: + if architecture in VISION_CLASSIFICATION_ARCHITECTURES: + from transformers import AutoModelForImageClassification + return AutoModelForImageClassification + from transformers import AutoModel return AutoModel else: return AutoModelForCausalLM @@ -727,7 +735,8 @@ def boot( use_fast = getattr(adapter.cfg, "use_fast", True) # Audio models use feature extractors, not text tokenizers _is_audio = getattr(adapter.cfg, "is_audio_model", False) - if _is_audio and tokenizer is None: + _is_visual = getattr(adapter.cfg, "is_visual_model", False) + if (_is_audio or _is_visual) and tokenizer is None: tokenizer = None # Skip tokenizer loading for audio models elif tokenizer is not None: tokenizer = setup_tokenizer(tokenizer, default_padding_side=default_padding_side) From 6d2ade6c94706e853650556fce88cbe945144973 Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Sun, 26 Jul 2026 22:42:15 -0400 Subject: [PATCH 15/74] Refactor VisionClassifierHeadBridge to use pooled output Updated the VisionClassifierHeadBridge to directly use an already-pooled CLS token instead of slicing from the sequence output. Adjusted the forward method to reflect this change and improved error handling for the original component. --- .../vision_classifier_head.py | 48 +++++-------------- 1 file changed, 12 insertions(+), 36 deletions(-) diff --git a/transformer_lens/model_bridge/generalized_components/vision_classifier_head.py b/transformer_lens/model_bridge/generalized_components/vision_classifier_head.py index 35ae057f5..28630d20a 100644 --- a/transformer_lens/model_bridge/generalized_components/vision_classifier_head.py +++ b/transformer_lens/model_bridge/generalized_components/vision_classifier_head.py @@ -1,30 +1,17 @@ """CLS-token classifier head bridge component. -`ViTForImageClassification.forward` / `DeiTForImageClassification.forward` both do: +HF's ViTForImageClassification.forward() / DeiTForImageClassification.forward() +already pool the CLS token before calling self.classifier: sequence_output = outputs.last_hidden_state - pooled_output = sequence_output[:, 0, :] # CLS token only + pooled_output = sequence_output[:, 0, :] logits = self.classifier(pooled_output) -This is *not* expressible with the base `GeneralizedComponent.forward()` (nor a -plain `LinearBridge`): that generic forward calls `original_component(*args, -**kwargs)` with no slicing step, so applying it directly to `classifier` would -run the Linear over every position in the sequence, not just the CLS token. -Hence a real override, verified against the actual base.py this time (not the -extrapolated version from before you shared it): - -- `self.hook_in` / `self.hook_out` / `self.original_component` are exactly the - names and semantics base.py defines. -- `original_component` being unset returns `None` (not an exception) per - base.py's property, so a direct `self.original_component(pooled)` call would - fail with a confusing `'NoneType' object is not callable` instead of a clear - message — added an explicit guard raising the same RuntimeError text - `GeneralizedComponent.forward()` itself uses, so the failure mode is - consistent across both generic and custom components. - -Deliberately NOT covering `DeiTForImageClassificationWithTeacher` (the dual -cls_classifier + distillation_classifier head, averaged) — see vit.py's -module docstring for why. +So this component only ever receives an already-pooled (batch, hidden) tensor. +No slicing needed here — this is a thin, hook-named pass-through. + +Deliberately NOT covering DeiTForImageClassificationWithTeacher (dual +cls_classifier + distillation_classifier head) — see vit.py's docstring. """ from typing import Any @@ -35,25 +22,14 @@ class VisionClassifierHeadBridge(GeneralizedComponent): - """Wraps a plain classifier nn.Linear, but slices a single token position first. - - token_index=0 (default) for the CLS token — use this for - ViTForImageClassification and (non-teacher) DeiTForImageClassification, both - of which classify off index 0 regardless of whether the underlying embeddings - module also carries a distillation token at index 1. - """ - - def __init__(self, name: str, config: Any = None, submodules=None, token_index: int = 0) -> None: - super().__init__(name, config, submodules) - self.token_index = token_index + """Wraps the classifier nn.Linear that HF calls with an already-pooled CLS token.""" - def forward(self, sequence_output: Tensor, **kwargs: Any) -> Tensor: + def forward(self, pooled_output: Tensor, **kwargs: Any) -> Tensor: original_component = self.original_component if original_component is None: raise RuntimeError( f"Original component not set for {self.name}. Call set_original_component() first." ) - sequence_output = self.hook_in(sequence_output) - pooled = sequence_output[:, self.token_index, :] - logits = original_component(pooled) + pooled_output = self.hook_in(pooled_output) + logits = original_component(pooled_output) return self.hook_out(logits) From db6a4adbc2bca1c9a895d97acf7863deb67d783b Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Sun, 26 Jul 2026 22:43:14 -0400 Subject: [PATCH 16/74] Update vit.py --- transformer_lens/model_bridge/supported_architectures/vit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/transformer_lens/model_bridge/supported_architectures/vit.py b/transformer_lens/model_bridge/supported_architectures/vit.py index 3d4e02948..bbf1badbf 100644 --- a/transformer_lens/model_bridge/supported_architectures/vit.py +++ b/transformer_lens/model_bridge/supported_architectures/vit.py @@ -168,7 +168,7 @@ def _build_component_mapping(self, prefix: str, with_classifier: bool) -> Dict[s ), } if with_classifier: - mapping["unembed"] = VisionClassifierHeadBridge(name="classifier", token_index=0) + mapping["unembed"] = VisionClassifierHeadBridge(name="classifier") return mapping def prepare_loading(self, model_name: str, model_kwargs: dict) -> None: From bdf033357c270bbf95af1d67f0aeb447b05a33ca Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Sun, 26 Jul 2026 23:20:55 -0400 Subject: [PATCH 17/74] Add unit tests for ViTArchitectureAdapter This file contains unit tests for the ViTArchitectureAdapter, covering component mapping, configuration flags, weight conversions, and model preparation methods. --- .../test_vit_adapter.py | 383 ++++++++++++++++++ 1 file changed, 383 insertions(+) create mode 100644 tests/unit/model_bridge/supported_architectures/test_vit_adapter.py diff --git a/tests/unit/model_bridge/supported_architectures/test_vit_adapter.py b/tests/unit/model_bridge/supported_architectures/test_vit_adapter.py new file mode 100644 index 000000000..0fdbf2e88 --- /dev/null +++ b/tests/unit/model_bridge/supported_architectures/test_vit_adapter.py @@ -0,0 +1,383 @@ +"""Unit tests for ViTArchitectureAdapter. + +Modeled on tests/unit/model_bridge/supported_architectures/test_hubert_adapter.py +(the closest existing analog: HubertArchitectureAdapter also sets a modality flag +— is_audio_model there, is_visual_model here — and rebinds a prefix + injects a +head in prepare_model()). + +Tests cover: +- Component mapping structure for a bare ViTModel/DeiTModel (default, before + prepare_model() sees the real HF model) +- Block/attn/mlp submodule mapping and hook-alias overrides +- Weight conversion key set and rearrange patterns (weights + biases) +- Anti-drift config flags (is_visual_model, positional_embedding_type, + normalization_type, supports_fold_ln, supports_generation) +- prepare_model(): bare model / ViTForImageClassification / DeiTForImageClassification + prefix rebinding + classifier injection, and the DeiTForImageClassificationWithTeacher + guard rail +- prepare_loading(): head_dim propagation from the HF config + +NOT covered here (needs a real HF model + real forward pass — see the +integration test at tests/integration/model_bridge/test_vit_adapter.py instead): +- Numeric parity with HF +- Hook firing / cache shapes +- Whether cfg.n_ctx ends up correct (it currently does NOT — prepare_loading() + never sets it, and the generic HF-config fallback chain in + transformer_lens/model_bridge/sources/transformers.py doesn't recognize + ViTConfig's image_size/patch_size fields, so it silently defaults to 2048. + That's only observable once the full loading pipeline runs, which is why it's + asserted as an (xfail) regression test in the integration file, not here.) +""" + +from types import SimpleNamespace + +import pytest + +from transformer_lens.config.transformer_bridge_config import TransformerBridgeConfig +from transformer_lens.conversion_utils.conversion_steps import RearrangeTensorConversion +from transformer_lens.conversion_utils.param_processing_conversion import ( + ParamProcessingConversion, +) +from transformer_lens.model_bridge.generalized_components import ( + AttentionBridge, + BlockBridge, + LinearBridge, + MLPBridge, + NormalizationBridge, +) +from transformer_lens.model_bridge.generalized_components.vision_classifier_head import ( + VisionClassifierHeadBridge, +) +from transformer_lens.model_bridge.generalized_components.vision_embeddings import ( + VisionEmbeddingsBridge, +) +from transformer_lens.model_bridge.supported_architectures.vit import ( + ViTArchitectureAdapter, +) + +# --------------------------------------------------------------------------- +# Helpers / fixtures +# --------------------------------------------------------------------------- + + +def _make_cfg( + n_heads: int = 12, + d_model: int = 768, + n_layers: int = 12, + d_vocab: int = 1000, + n_ctx: int = 197, + **overrides, +) -> TransformerBridgeConfig: + """Return a minimal TransformerBridgeConfig for ViT/DeiT adapter tests. + + n_ctx=197 is the real value for a 224x224 image / patch16 model + (14*14 patches + 1 CLS token) — used as the *input* cfg here so tests can + tell apart "the adapter left this alone" from "the adapter zeroed it". + """ + cfg = TransformerBridgeConfig( + d_model=d_model, + d_head=d_model // n_heads, + n_heads=n_heads, + n_layers=n_layers, + n_ctx=n_ctx, + d_vocab=d_vocab, + architecture="ViTForImageClassification", + ) + for k, v in overrides.items(): + setattr(cfg, k, v) + return cfg + + +@pytest.fixture() +def adapter() -> ViTArchitectureAdapter: + """Fresh adapter per test — prepare_model() mutates component_mapping in place.""" + return ViTArchitectureAdapter(_make_cfg()) + + +# --------------------------------------------------------------------------- +# Component mapping — bare ViTModel/DeiTModel (default, pre prepare_model()) +# --------------------------------------------------------------------------- + + +class TestViTComponentMappingBare: + """Default mapping assumes no prefix and no classifier (matches __init__).""" + + def test_top_level_keys(self, adapter: ViTArchitectureAdapter) -> None: + assert set(adapter.component_mapping.keys()) == {"embed", "blocks", "ln_final"} + + def test_no_unembed_by_default(self, adapter: ViTArchitectureAdapter) -> None: + assert "unembed" not in adapter.component_mapping + + def test_bridge_types(self, adapter: ViTArchitectureAdapter) -> None: + mapping = adapter.component_mapping + assert isinstance(mapping["embed"], VisionEmbeddingsBridge) + assert isinstance(mapping["blocks"], BlockBridge) + assert isinstance(mapping["ln_final"], NormalizationBridge) + + def test_top_level_hf_paths(self, adapter: ViTArchitectureAdapter) -> None: + mapping = adapter.component_mapping + assert mapping["embed"].name == "embeddings" + assert mapping["blocks"].name == "layers" + assert mapping["ln_final"].name == "layernorm" + + def test_block_submodule_keys(self, adapter: ViTArchitectureAdapter) -> None: + assert set(adapter.component_mapping["blocks"].submodules.keys()) == { + "ln1", + "ln2", + "attn", + "mlp", + } + + def test_block_submodule_types(self, adapter: ViTArchitectureAdapter) -> None: + blocks = adapter.component_mapping["blocks"] + assert isinstance(blocks.submodules["ln1"], NormalizationBridge) + assert isinstance(blocks.submodules["ln2"], NormalizationBridge) + assert isinstance(blocks.submodules["attn"], AttentionBridge) + assert isinstance(blocks.submodules["mlp"], MLPBridge) + + def test_block_submodule_hf_paths(self, adapter: ViTArchitectureAdapter) -> None: + blocks = adapter.component_mapping["blocks"] + assert blocks.submodules["ln1"].name == "layernorm_before" + assert blocks.submodules["ln2"].name == "layernorm_after" + assert blocks.submodules["attn"].name == "attention" + assert blocks.submodules["mlp"].name == "mlp" + + def test_attn_submodule_keys(self, adapter: ViTArchitectureAdapter) -> None: + attn = adapter.component_mapping["blocks"].submodules["attn"] + assert set(attn.submodules.keys()) == {"q", "k", "v", "o"} + + def test_attn_qkvo_hf_paths(self, adapter: ViTArchitectureAdapter) -> None: + attn = adapter.component_mapping["blocks"].submodules["attn"] + assert attn.submodules["q"].name == "q_proj" + assert attn.submodules["k"].name == "k_proj" + assert attn.submodules["v"].name == "v_proj" + assert attn.submodules["o"].name == "o_proj" + + def test_attn_submodules_are_linear_bridges(self, adapter: ViTArchitectureAdapter) -> None: + attn = adapter.component_mapping["blocks"].submodules["attn"] + for sub in attn.submodules.values(): + assert isinstance(sub, LinearBridge) + + def test_mlp_submodule_hf_paths(self, adapter: ViTArchitectureAdapter) -> None: + mlp = adapter.component_mapping["blocks"].submodules["mlp"] + assert mlp.submodules["in"].name == "fc1" + assert mlp.submodules["out"].name == "fc2" + + def test_mlp_hook_alias_overrides(self, adapter: ViTArchitectureAdapter) -> None: + aliases = adapter.component_mapping["blocks"].hook_aliases + assert aliases.get("hook_mlp_out") == "mlp.out.hook_out" + assert aliases.get("hook_mlp_in") == "mlp.in.hook_in" + + +# --------------------------------------------------------------------------- +# Anti-drift config flags +# --------------------------------------------------------------------------- + + +class TestViTAdapterConfig: + """Flags that must not silently regress.""" + + def test_normalization_type_is_ln(self, adapter: ViTArchitectureAdapter) -> None: + assert adapter.cfg.normalization_type == "LN" + + def test_positional_embedding_type_is_standard( + self, adapter: ViTArchitectureAdapter + ) -> None: + assert adapter.cfg.positional_embedding_type == "standard" + + def test_is_visual_model_true(self, adapter: ViTArchitectureAdapter) -> None: + """Only vision adapters set this flag; text/audio adapters must not.""" + assert adapter.cfg.is_visual_model is True + + def test_is_audio_model_not_set_true(self, adapter: ViTArchitectureAdapter) -> None: + assert getattr(adapter.cfg, "is_audio_model", False) is False + + def test_final_rms_is_false(self, adapter: ViTArchitectureAdapter) -> None: + assert adapter.cfg.final_rms is False + + def test_gated_mlp_is_false(self, adapter: ViTArchitectureAdapter) -> None: + assert adapter.cfg.gated_mlp is False + + def test_attn_only_is_false(self, adapter: ViTArchitectureAdapter) -> None: + assert adapter.cfg.attn_only is False + + def test_supports_generation_is_false(self) -> None: + assert ViTArchitectureAdapter.supports_generation is False + + def test_supports_fold_ln_true_pre_ln(self, adapter: ViTArchitectureAdapter) -> None: + """ViT/DeiT are pre-LN — unlike BertArchitectureAdapter's post-LN False.""" + assert adapter.supports_fold_ln is True + + +# --------------------------------------------------------------------------- +# Weight processing conversions +# --------------------------------------------------------------------------- + + +class TestViTWeightConversions: + def test_exact_conversion_key_set(self, adapter: ViTArchitectureAdapter) -> None: + assert set(adapter.weight_processing_conversions.keys()) == { + "blocks.{i}.attn.q.weight", + "blocks.{i}.attn.k.weight", + "blocks.{i}.attn.v.weight", + "blocks.{i}.attn.q.bias", + "blocks.{i}.attn.k.bias", + "blocks.{i}.attn.v.bias", + "blocks.{i}.attn.o.weight", + } + + def test_qkv_weight_pattern(self, adapter: ViTArchitectureAdapter) -> None: + for slot in ("q", "k", "v"): + conv = adapter.weight_processing_conversions[f"blocks.{{i}}.attn.{slot}.weight"] + assert isinstance(conv, ParamProcessingConversion) + assert isinstance(conv.tensor_conversion, RearrangeTensorConversion) + assert conv.tensor_conversion.pattern == "(h d_head) d_model -> h d_model d_head" + + def test_qkv_bias_pattern(self, adapter: ViTArchitectureAdapter) -> None: + for slot in ("q", "k", "v"): + conv = adapter.weight_processing_conversions[f"blocks.{{i}}.attn.{slot}.bias"] + assert isinstance(conv, ParamProcessingConversion) + assert isinstance(conv.tensor_conversion, RearrangeTensorConversion) + assert conv.tensor_conversion.pattern == "(h d_head) -> h d_head" + + def test_o_weight_pattern(self, adapter: ViTArchitectureAdapter) -> None: + conv = adapter.weight_processing_conversions["blocks.{i}.attn.o.weight"] + assert isinstance(conv, ParamProcessingConversion) + assert isinstance(conv.tensor_conversion, RearrangeTensorConversion) + assert conv.tensor_conversion.pattern == "d_model (h d_head) -> h d_head d_model" + + def test_qkv_weight_head_axis(self, adapter: ViTArchitectureAdapter) -> None: + for slot in ("q", "k", "v"): + conv = adapter.weight_processing_conversions[f"blocks.{{i}}.attn.{slot}.weight"] + assert conv.tensor_conversion.axes_lengths["h"] == 12 + + def test_qkv_bias_head_axis(self, adapter: ViTArchitectureAdapter) -> None: + for slot in ("q", "k", "v"): + conv = adapter.weight_processing_conversions[f"blocks.{{i}}.attn.{slot}.bias"] + assert conv.tensor_conversion.axes_lengths["h"] == 12 + + +# --------------------------------------------------------------------------- +# prepare_model() — prefix rebinding + classifier injection +# --------------------------------------------------------------------------- + + +class TestViTPrepareModel: + """prepare_model() detects bare/ViT/DeiT wrappers and rebuilds the mapping.""" + + def _bare_model(self) -> object: + """No 'vit'/'deit'/'classifier' attribute — mimics bare ViTModel/DeiTModel.""" + return SimpleNamespace() + + def _vit_for_classification(self) -> object: + return SimpleNamespace(vit=SimpleNamespace(), classifier=SimpleNamespace()) + + def _deit_for_classification(self) -> object: + return SimpleNamespace(deit=SimpleNamespace(), classifier=SimpleNamespace()) + + def _deit_with_teacher(self) -> object: + """Dual cls_classifier + distillation_classifier — explicitly unsupported.""" + return SimpleNamespace( + deit=SimpleNamespace(), + cls_classifier=SimpleNamespace(), + distillation_classifier=SimpleNamespace(), + ) + + # -- bare model -------------------------------------------------------- + + def test_bare_model_keeps_no_prefix(self, adapter: ViTArchitectureAdapter) -> None: + adapter.prepare_model(self._bare_model()) + assert adapter.component_mapping["embed"].name == "embeddings" + assert adapter.component_mapping["blocks"].name == "layers" + assert adapter.component_mapping["ln_final"].name == "layernorm" + + def test_bare_model_has_no_unembed(self, adapter: ViTArchitectureAdapter) -> None: + adapter.prepare_model(self._bare_model()) + assert "unembed" not in adapter.component_mapping + + # -- ViTForImageClassification ----------------------------------------- + + def test_vit_classification_adds_vit_prefix(self, adapter: ViTArchitectureAdapter) -> None: + adapter.prepare_model(self._vit_for_classification()) + assert adapter.component_mapping["embed"].name == "vit.embeddings" + assert adapter.component_mapping["blocks"].name == "vit.layers" + assert adapter.component_mapping["ln_final"].name == "vit.layernorm" + + def test_vit_classification_adds_unembed(self, adapter: ViTArchitectureAdapter) -> None: + adapter.prepare_model(self._vit_for_classification()) + assert "unembed" in adapter.component_mapping + assert isinstance( + adapter.component_mapping["unembed"], VisionClassifierHeadBridge + ) + + def test_classifier_is_never_prefixed(self, adapter: ViTArchitectureAdapter) -> None: + """classifier is always a top-level attr on the *ForImageClassification wrapper.""" + adapter.prepare_model(self._vit_for_classification()) + assert adapter.component_mapping["unembed"].name == "classifier" + + # -- DeiTForImageClassification ------------------------------------------ + + def test_deit_classification_adds_deit_prefix(self, adapter: ViTArchitectureAdapter) -> None: + adapter.prepare_model(self._deit_for_classification()) + assert adapter.component_mapping["embed"].name == "deit.embeddings" + assert adapter.component_mapping["blocks"].name == "deit.layers" + assert adapter.component_mapping["ln_final"].name == "deit.layernorm" + + def test_deit_classification_adds_unembed(self, adapter: ViTArchitectureAdapter) -> None: + adapter.prepare_model(self._deit_for_classification()) + assert adapter.component_mapping["unembed"].name == "classifier" + + # -- DeiTForImageClassificationWithTeacher guard rail -------------------- + + def test_deit_with_teacher_raises(self, adapter: ViTArchitectureAdapter) -> None: + with pytest.raises(NotImplementedError): + adapter.prepare_model(self._deit_with_teacher()) + + def test_deit_with_teacher_message_mentions_reason( + self, adapter: ViTArchitectureAdapter + ) -> None: + with pytest.raises(NotImplementedError, match="distillation_classifier"): + adapter.prepare_model(self._deit_with_teacher()) + + +# --------------------------------------------------------------------------- +# prepare_loading() — HF config propagation +# --------------------------------------------------------------------------- + + +class TestViTPrepareLoading: + def _model_kwargs_with(self, **hf_config_fields) -> dict: + return {"config": SimpleNamespace(**hf_config_fields)} + + def test_head_dim_propagates(self, adapter: ViTArchitectureAdapter) -> None: + adapter.prepare_loading("dummy-model", self._model_kwargs_with(head_dim=96)) + assert adapter.cfg.d_head == 96 + + def test_missing_head_dim_is_noop(self, adapter: ViTArchitectureAdapter) -> None: + original_d_head = adapter.cfg.d_head + adapter.prepare_loading("dummy-model", self._model_kwargs_with()) + assert adapter.cfg.d_head == original_d_head + + def test_no_config_in_model_kwargs_is_noop(self, adapter: ViTArchitectureAdapter) -> None: + adapter.prepare_loading("dummy-model", {}) # must not raise + + def test_none_config_in_model_kwargs_is_noop(self, adapter: ViTArchitectureAdapter) -> None: + adapter.prepare_loading("dummy-model", {"config": None}) # must not raise + + def test_prepare_loading_does_not_touch_n_ctx(self, adapter: ViTArchitectureAdapter) -> None: + """Documents current behavior, not necessarily desired behavior. + + ViTConfig/DeiTConfig express sequence length via image_size/patch_size, + not any of n_positions/max_position_embeddings/max_context_length/ + max_length/seq_length. prepare_loading() here doesn't derive n_ctx from + image_size/patch_size either, so the generic HF-config fallback chain in + transformer_lens/model_bridge/sources/transformers.py will silently land + on its hardcoded default (2048) for every real ViT/DeiT load. See the + xfail regression test in tests/integration/model_bridge/test_vit_adapter.py + for the end-to-end version of this against a real checkpoint. + """ + original_n_ctx = adapter.cfg.n_ctx + adapter.prepare_loading( + "dummy-model", self._model_kwargs_with(image_size=224, patch_size=16) + ) + assert adapter.cfg.n_ctx == original_n_ctx From 078ff22c7c6e7f7a1dca06df16095820992b2cae Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Sun, 26 Jul 2026 23:21:44 -0400 Subject: [PATCH 18/74] Create test_vit_adapter.py --- .../model_bridge/test_vit_adapter.py | 293 ++++++++++++++++++ 1 file changed, 293 insertions(+) create mode 100644 tests/integration/model_bridge/test_vit_adapter.py diff --git a/tests/integration/model_bridge/test_vit_adapter.py b/tests/integration/model_bridge/test_vit_adapter.py new file mode 100644 index 000000000..cc90ede2f --- /dev/null +++ b/tests/integration/model_bridge/test_vit_adapter.py @@ -0,0 +1,293 @@ +"""Integration tests for the ViT / DeiT architecture adapter. + +Modeled on tests/integration/model_bridge/test_mamba_adapter.py — verifies +wrap-don't-reimplement behavior against real HF checkpoints: +- Forward pass matches HF (bridge substitutes hooked submodules into the real + HF model and calls its real forward — see bridge.py's `is_visual_model` + branch, which does `output = self.original_model(**kwargs)`) +- Submodule hooks fire with expected shapes (embed, attn q/k/v/o, mlp in/out, + ln_final, unembed/classifier) +- DeiT's distillation token is invisible to the adapter (sequence length + differs from ViT's by one extra token, entirely inside VisionEmbeddingsBridge) +- Bare ViTModel (no classifier) return-value shape via return_type="logits" +- DeiTForImageClassificationWithTeacher is rejected, not silently mishandled + +Three checkpoints, matching the ones already sanity-checked manually: +- google/vit-base-patch16-224 (ViTForImageClassification, prefix "vit.") +- google/vit-base-patch16-224-in21k (bare ViTModel, no prefix, no classifier) +- facebook/deit-small-patch16-224 (DeiTForImageClassification, prefix "deit.") + +NOTE ON RUNNING THESE: they download real weights from HuggingFace (a few +hundred MB total) — same cost class as the existing Mamba/SmolLM3 integration +tests, no special marker needed, but expect the first run to be slow. +""" + +import pytest +import torch + +from transformer_lens.model_bridge.bridge import TransformerBridge +from transformer_lens.model_bridge.generalized_components.vision_classifier_head import ( + VisionClassifierHeadBridge, +) + +MODEL_VIT_CLASSIFIER = "google/vit-base-patch16-224" +MODEL_VIT_BARE = "google/vit-base-patch16-224-in21k" +MODEL_DEIT_CLASSIFIER = "facebook/deit-small-patch16-224" + + +def _pixel_values(batch: int = 1, image_size: int = 224) -> torch.Tensor: + return torch.randn(batch, 3, image_size, image_size) + + +# --------------------------------------------------------------------------- +# ViTForImageClassification (the "normal" case) +# --------------------------------------------------------------------------- + + +@pytest.fixture(scope="module") +def vit_bridge(): + return TransformerBridge.boot_transformers(MODEL_VIT_CLASSIFIER, device="cpu") + + +class TestViTClassifierBridgeCreation: + def test_block_count(self, vit_bridge): + assert len(vit_bridge.blocks) == vit_bridge.original_model.config.num_hidden_layers + + def test_config_flags(self, vit_bridge): + assert vit_bridge.cfg.normalization_type == "LN" + assert vit_bridge.cfg.positional_embedding_type == "standard" + assert vit_bridge.cfg.is_visual_model is True + assert vit_bridge.cfg.gated_mlp is False + assert vit_bridge.cfg.attn_only is False + + def test_has_core_components(self, vit_bridge): + assert hasattr(vit_bridge, "embed") + assert hasattr(vit_bridge, "blocks") + assert hasattr(vit_bridge, "ln_final") + assert hasattr(vit_bridge, "unembed") + + def test_unembed_is_vision_classifier_head(self, vit_bridge): + assert isinstance(vit_bridge.unembed, VisionClassifierHeadBridge) + + def test_d_model_matches_hf_config(self, vit_bridge): + assert vit_bridge.cfg.d_model == vit_bridge.original_model.config.hidden_size + + def test_n_heads_matches_hf_config(self, vit_bridge): + assert vit_bridge.cfg.n_heads == vit_bridge.original_model.config.num_attention_heads + + +class TestViTClassifierForwardPass: + def test_forward_returns_correct_shape(self, vit_bridge): + pixel_values = _pixel_values() + with torch.no_grad(): + output = vit_bridge(pixel_values) + num_labels = vit_bridge.original_model.config.num_labels + assert output.shape == (1, num_labels) + assert not torch.isnan(output).any() + + def test_forward_matches_hf(self, vit_bridge): + """Wrap-don't-reimplement: bridge substitutes hooked submodules into the + real HF model and calls its real forward, so this should match HF very + tightly (mamba's equivalent test asserts bit-exact; using a tight atol + here to be safe against BLAS/backend-dependent float noise across + machines rather than assuming bit-exactness untested).""" + pixel_values = _pixel_values() + hf_model = vit_bridge.original_model + with torch.no_grad(): + bridge_out = vit_bridge(pixel_values) + hf_out = hf_model(pixel_values).logits + max_diff = (bridge_out - hf_out).abs().max().item() + assert max_diff < 1e-4, f"Bridge vs HF max diff = {max_diff}" + + def test_batch_of_images(self, vit_bridge): + pixel_values = _pixel_values(batch=3) + with torch.no_grad(): + output = vit_bridge(pixel_values) + assert output.shape[0] == 3 + + +class TestViTClassifierHookCoverage: + @pytest.fixture(scope="class") + def cache(self, vit_bridge): + pixel_values = _pixel_values() + with torch.no_grad(): + _, cache = vit_bridge.run_with_cache(pixel_values) + return cache + + def test_embed_hooks_fire(self, cache, vit_bridge): + assert "embed.hook_in" in cache + assert "embed.hook_out" in cache + assert cache["embed.hook_in"].shape == (1, 3, 224, 224) + seq_len = cache["embed.hook_out"].shape[1] + d_model = vit_bridge.cfg.d_model + assert cache["embed.hook_out"].shape == (1, seq_len, d_model) + # ViT: CLS + patches, no distillation token (contrast with DeiT below). + assert seq_len == 1 + (224 // 16) ** 2 + + def test_block_submodule_hooks_fire(self, cache, vit_bridge): + n_layers = vit_bridge.cfg.n_layers + for i in [0, n_layers - 1]: + for submod in ("q", "k", "v", "o"): + assert f"blocks.{i}.attn.{submod}.hook_in" in cache + assert f"blocks.{i}.attn.{submod}.hook_out" in cache + assert f"blocks.{i}.mlp.in.hook_out" in cache + assert f"blocks.{i}.mlp.out.hook_out" in cache + + def test_mlp_hook_aliases_resolve(self, cache): + """hook_mlp_in/out are redirected via hook_alias_overrides in vit.py.""" + assert torch.equal(cache["blocks.0.hook_mlp_in"], cache["blocks.0.mlp.in.hook_in"]) + assert torch.equal(cache["blocks.0.hook_mlp_out"], cache["blocks.0.mlp.out.hook_out"]) + + def test_ln_final_hook_fires(self, cache): + assert "ln_final.hook_normalized" in cache + + def test_unembed_hooks_fire_with_pooled_shape(self, cache, vit_bridge): + """VisionClassifierHeadBridge only ever sees the pooled (batch, hidden) + CLS-token tensor — HF's ViTForImageClassification.forward() does the + `sequence_output[:, 0, :]` slicing itself before calling self.classifier, + and that real forward path is what's running here.""" + d_model = vit_bridge.cfg.d_model + num_labels = vit_bridge.original_model.config.num_labels + assert cache["unembed.hook_in"].shape == (1, d_model) + assert cache["unembed.hook_out"].shape == (1, num_labels) + + +# --------------------------------------------------------------------------- +# Bare ViTModel (no classifier) — google/vit-base-patch16-224-in21k +# --------------------------------------------------------------------------- + + +@pytest.fixture(scope="module") +def vit_bare_bridge(): + return TransformerBridge.boot_transformers(MODEL_VIT_BARE, device="cpu") + + +class TestViTBareModel: + def test_no_unembed_component(self, vit_bare_bridge): + assert "unembed" not in vit_bare_bridge.adapter.component_mapping + + def test_forward_does_not_crash(self, vit_bare_bridge): + pixel_values = _pixel_values() + with torch.no_grad(): + vit_bare_bridge(pixel_values) # must not raise + + def test_forward_return_value_is_not_a_plain_tensor(self, vit_bare_bridge): + """KNOWN GAP — flagging, not necessarily desired behavior. + + For a bare ViTModel, HF's own forward returns BaseModelOutputWithPooling, + which has no `.logits` and is NOT a tuple subclass (confirmed directly: + `isinstance(BaseModelOutputWithPooling(...), tuple) is False` on + transformers 5.8.1). bridge.py's forward() only special-cases `.logits` + or `isinstance(output, tuple)` before falling through to + `logits = output` — so `bridge(pixel_values)` with the default + return_type="logits" currently returns the raw HF output object, not a + tensor. If that's intentional (there's no real "logits" for a bare + encoder), consider documenting it explicitly and/or exposing + `last_hidden_state`/`pooler_output` some other way; if not, this is the + regression test to flip once fixed. + """ + pixel_values = _pixel_values() + with torch.no_grad(): + output = vit_bare_bridge(pixel_values) + assert not isinstance(output, torch.Tensor) + assert hasattr(output, "last_hidden_state") + + +# --------------------------------------------------------------------------- +# DeiTForImageClassification — distillation token must stay invisible +# --------------------------------------------------------------------------- + + +@pytest.fixture(scope="module") +def deit_bridge(): + return TransformerBridge.boot_transformers(MODEL_DEIT_CLASSIFIER, device="cpu") + + +class TestDeiTBridge: + def test_prefix_is_deit(self, deit_bridge): + assert deit_bridge.adapter.component_mapping["embed"].name == "deit.embeddings" + + def test_forward_matches_hf(self, deit_bridge): + pixel_values = _pixel_values() + hf_model = deit_bridge.original_model + with torch.no_grad(): + bridge_out = deit_bridge(pixel_values) + hf_out = hf_model(pixel_values).logits + max_diff = (bridge_out - hf_out).abs().max().item() + assert max_diff < 1e-4, f"Bridge vs HF max diff = {max_diff}" + + def test_sequence_length_includes_distillation_token(self, deit_bridge): + """DeiT embeddings prepend CLS *and* distillation tokens (vs. ViT's CLS + only) — invisible to the adapter per vision_embeddings.py's docstring, + but it should still show up in the actual hidden-state shape, one token + longer than the equivalent ViT sequence length.""" + pixel_values = _pixel_values() + with torch.no_grad(): + _, cache = deit_bridge.run_with_cache(pixel_values) + seq_len = cache["embed.hook_out"].shape[1] + num_patches = (224 // 16) ** 2 + assert seq_len == num_patches + 2 # CLS + distillation + patches + + def test_unembed_pooled_shape(self, deit_bridge): + pixel_values = _pixel_values() + with torch.no_grad(): + _, cache = deit_bridge.run_with_cache(pixel_values) + d_model = deit_bridge.cfg.d_model + assert cache["unembed.hook_in"].shape == (1, d_model) + + +# --------------------------------------------------------------------------- +# cfg.n_ctx regression (see unit test file for the narrower, no-network version) +# --------------------------------------------------------------------------- + + +class TestViTConfigNCtx: + @pytest.mark.xfail( + reason=( + "prepare_loading() in vit.py never derives n_ctx from " + "image_size/patch_size, and ViTConfig/DeiTConfig have none of the " + "field names the generic fallback chain in " + "model_bridge/sources/transformers.py checks (n_positions / " + "max_position_embeddings / max_context_length / max_length / " + "seq_length), so it silently defaults to 2048. Flip this to a plain " + "assert once prepare_loading() sets n_ctx correctly." + ), + strict=True, + ) + def test_n_ctx_matches_real_patch_sequence_length(self, vit_bridge): + expected = 1 + (224 // 16) ** 2 # CLS + patches, for a 224px/patch16 model + assert vit_bridge.cfg.n_ctx == expected + + +# --------------------------------------------------------------------------- +# DeiTForImageClassificationWithTeacher — must raise, not silently mishandle +# --------------------------------------------------------------------------- + + +class TestDeiTWithTeacherRejected: + def test_loading_raises_not_implemented(self): + """No small public DeiTForImageClassificationWithTeacher checkpoint is + assumed here — this drives the same code path prepare_model() exercises + via a HF class instance check, without a full weights download. + """ + from transformers import DeiTConfig, DeiTForImageClassificationWithTeacher + + from transformer_lens.model_bridge.supported_architectures.vit import ( + ViTArchitectureAdapter, + ) + + config = DeiTConfig( + hidden_size=32, + num_hidden_layers=2, + num_attention_heads=2, + intermediate_size=64, + image_size=32, + patch_size=16, + num_labels=10, + ) + hf_model = DeiTForImageClassificationWithTeacher(config) + + adapter = ViTArchitectureAdapter.__new__(ViTArchitectureAdapter) + adapter.cfg = None # prepare_model() doesn't touch cfg before the raise + with pytest.raises(NotImplementedError): + ViTArchitectureAdapter.prepare_model(adapter, hf_model) From 62cc5b4bc2ff39f1471b172a82e08c9eaaca9c03 Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Sun, 26 Jul 2026 23:28:54 -0400 Subject: [PATCH 19/74] Update transformers.py --- .../model_bridge/sources/transformers.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/transformer_lens/model_bridge/sources/transformers.py b/transformer_lens/model_bridge/sources/transformers.py index c239fafd4..9b619299e 100644 --- a/transformer_lens/model_bridge/sources/transformers.py +++ b/transformer_lens/model_bridge/sources/transformers.py @@ -140,6 +140,19 @@ def map_default_transformer_lens_config(hf_config): tl_config.n_ctx = source_config.max_length elif hasattr(source_config, "seq_length"): tl_config.n_ctx = source_config.seq_length + elif hasattr(source_config, "image_size") and hasattr(source_config, "patch_size"): + # Vision Transformers calculate sequence length dynamically: + # (image_size / patch_size)^2 + 1 (for the CLS token) + image_size = source_config.image_size + patch_size = source_config.patch_size + + # HF configs allow these to be integers or tuples/lists + img_h = image_size[0] if isinstance(image_size, (list, tuple)) else image_size + img_w = image_size[1] if isinstance(image_size, (list, tuple)) else image_size + patch_h = patch_size[0] if isinstance(patch_size, (list, tuple)) else patch_size + patch_w = patch_size[1] if isinstance(patch_size, (list, tuple)) else patch_size + + tl_config.n_ctx = (img_h // patch_h) * (img_w // patch_w) + 1 else: # Models like Bloom use ALiBi (no positional embeddings) and have no # context length field. Default to 2048 as a reasonable fallback. From 0c6ba10fcc2d207899bb355df2efb4e888dbf18c Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Sun, 26 Jul 2026 23:47:44 -0400 Subject: [PATCH 20/74] Update vit.py --- .../model_bridge/supported_architectures/vit.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/transformer_lens/model_bridge/supported_architectures/vit.py b/transformer_lens/model_bridge/supported_architectures/vit.py index bbf1badbf..8c993591c 100644 --- a/transformer_lens/model_bridge/supported_architectures/vit.py +++ b/transformer_lens/model_bridge/supported_architectures/vit.py @@ -122,7 +122,7 @@ def _build_component_mapping(self, prefix: str, with_classifier: bool) -> Dict[s mapping: Dict[str, Any] = { "embed": VisionEmbeddingsBridge(name=f"{p}embeddings"), "blocks": BlockBridge( - name=f"{p}layers", + name=f"{p}encoder.layer", # Same redirect the BERT and HuBERT adapters use — kept for consistency # even though ViT's MLP (unlike BERT's) is a real cohesive submodule, # since both existing ground-truth adapters apply it regardless. @@ -145,18 +145,18 @@ def _build_component_mapping(self, prefix: str, with_classifier: bool) -> Dict[s name="attention", config=self.cfg, submodules={ - "q": LinearBridge(name="q_proj"), - "k": LinearBridge(name="k_proj"), - "v": LinearBridge(name="v_proj"), - "o": LinearBridge(name="o_proj"), + "q": LinearBridge(name="attention.query"), + "k": LinearBridge(name="attention.key"), + "v": LinearBridge(name="attention.value"), + "o": LinearBridge(name="output.dense"), }, ), "mlp": MLPBridge( name="mlp", config=self.cfg, submodules={ - "in": LinearBridge(name="fc1"), - "out": LinearBridge(name="fc2"), + "in": LinearBridge(name="intermediate.dense"), + "out": LinearBridge(name="output.dense"), }, ), }, From 33cf0e4e2ae1df80020fbd8d5a53da2c8a468959 Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Sun, 26 Jul 2026 23:51:39 -0400 Subject: [PATCH 21/74] Update vit.py --- .../supported_architectures/vit.py | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/transformer_lens/model_bridge/supported_architectures/vit.py b/transformer_lens/model_bridge/supported_architectures/vit.py index 8c993591c..ec11b4e68 100644 --- a/transformer_lens/model_bridge/supported_architectures/vit.py +++ b/transformer_lens/model_bridge/supported_architectures/vit.py @@ -123,39 +123,32 @@ def _build_component_mapping(self, prefix: str, with_classifier: bool) -> Dict[s "embed": VisionEmbeddingsBridge(name=f"{p}embeddings"), "blocks": BlockBridge( name=f"{p}encoder.layer", - # Same redirect the BERT and HuBERT adapters use — kept for consistency - # even though ViT's MLP (unlike BERT's) is a real cohesive submodule, - # since both existing ground-truth adapters apply it regardless. hook_alias_overrides={ "hook_mlp_out": "mlp.out.hook_out", "hook_mlp_in": "mlp.in.hook_in", }, submodules={ - "ln1": NormalizationBridge( - name="layernorm_before", - config=self.cfg, - use_native_layernorm_autograd=True, - ), - "ln2": NormalizationBridge( - name="layernorm_after", - config=self.cfg, - use_native_layernorm_autograd=True, - ), + "ln1": LayerNormBridge(name="layernorm_before"), + "ln2": LayerNormBridge(name="layernorm_after"), "attn": AttentionBridge( name="attention", config=self.cfg, submodules={ + # Maps to ViTAttention -> ViTSelfAttention -> query/key/value "q": LinearBridge(name="attention.query"), "k": LinearBridge(name="attention.key"), "v": LinearBridge(name="attention.value"), + # Maps to ViTAttention -> ViTSelfOutput -> dense "o": LinearBridge(name="output.dense"), }, ), "mlp": MLPBridge( - name="mlp", + name="mlp", # Intercepted by get_remote_component config=self.cfg, submodules={ + # Maps to ViTLayer -> ViTIntermediate -> dense "in": LinearBridge(name="intermediate.dense"), + # Maps to ViTLayer -> ViTOutput -> dense "out": LinearBridge(name="output.dense"), }, ), @@ -185,6 +178,18 @@ def prepare_loading(self, model_name: str, model_kwargs: dict) -> None: if head_dim is not None: self.cfg.d_head = head_dim # type: ignore[attr-defined] + def get_remote_component(self, current: "nn.Module", path: str) -> "nn.Module": + """ + Intercept requests for specific component paths. + Hugging Face ViT doesn't wrap the MLP into a single module, so we return the + block itself when asked for 'mlp'. This lets the in/out linear layers resolve + their paths ('intermediate.dense' and 'output.dense') correctly from the block. + """ + if path == "mlp" and not hasattr(current, "mlp"): + return current + + return super().get_remote_component(current, path) + def prepare_model(self, hf_model: Any) -> None: """Detect ViTForImageClassification vs DeiTForImageClassification vs a bare *Model, and add/omit the classifier head + prefix accordingly.""" From 07971b7123056dddb9b3f30aacb5ca05f69d772c Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Sun, 26 Jul 2026 23:53:46 -0400 Subject: [PATCH 22/74] Update vit.py --- .../model_bridge/supported_architectures/vit.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/transformer_lens/model_bridge/supported_architectures/vit.py b/transformer_lens/model_bridge/supported_architectures/vit.py index ec11b4e68..42434bc61 100644 --- a/transformer_lens/model_bridge/supported_architectures/vit.py +++ b/transformer_lens/model_bridge/supported_architectures/vit.py @@ -128,8 +128,16 @@ def _build_component_mapping(self, prefix: str, with_classifier: bool) -> Dict[s "hook_mlp_in": "mlp.in.hook_in", }, submodules={ - "ln1": LayerNormBridge(name="layernorm_before"), - "ln2": LayerNormBridge(name="layernorm_after"), + "ln1": NormalizationBridge( + name="layernorm_before", + config=self.cfg, + use_native_layernorm_autograd=True, + ), + "ln2": NormalizationBridge( + name="layernorm_after", + config=self.cfg, + use_native_layernorm_autograd=True, + ), "attn": AttentionBridge( name="attention", config=self.cfg, From 0f80c9e223c5227f5b94641181fd7a2b28b10ab3 Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Sun, 26 Jul 2026 23:56:56 -0400 Subject: [PATCH 23/74] Fix type hint for get_remote_component method --- .../model_bridge/supported_architectures/vit.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/transformer_lens/model_bridge/supported_architectures/vit.py b/transformer_lens/model_bridge/supported_architectures/vit.py index 42434bc61..6151f96dd 100644 --- a/transformer_lens/model_bridge/supported_architectures/vit.py +++ b/transformer_lens/model_bridge/supported_architectures/vit.py @@ -17,6 +17,7 @@ `supports_fold_ln = True` here where BertArchitectureAdapter sets it False. """ +import torch from typing import Any, Dict from transformer_lens.conversion_utils.conversion_steps import RearrangeTensorConversion @@ -186,12 +187,9 @@ def prepare_loading(self, model_name: str, model_kwargs: dict) -> None: if head_dim is not None: self.cfg.d_head = head_dim # type: ignore[attr-defined] - def get_remote_component(self, current: "nn.Module", path: str) -> "nn.Module": + def get_remote_component(self, current: nn.Module, path: str) -> "torch.nn.Module": """ Intercept requests for specific component paths. - Hugging Face ViT doesn't wrap the MLP into a single module, so we return the - block itself when asked for 'mlp'. This lets the in/out linear layers resolve - their paths ('intermediate.dense' and 'output.dense') correctly from the block. """ if path == "mlp" and not hasattr(current, "mlp"): return current From 9c634458b27a85336b5cd1403c46d6bdcb916138 Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Sun, 26 Jul 2026 23:57:56 -0400 Subject: [PATCH 24/74] Fix type hint for get_remote_component method --- transformer_lens/model_bridge/supported_architectures/vit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/transformer_lens/model_bridge/supported_architectures/vit.py b/transformer_lens/model_bridge/supported_architectures/vit.py index 6151f96dd..450dc21cf 100644 --- a/transformer_lens/model_bridge/supported_architectures/vit.py +++ b/transformer_lens/model_bridge/supported_architectures/vit.py @@ -187,7 +187,7 @@ def prepare_loading(self, model_name: str, model_kwargs: dict) -> None: if head_dim is not None: self.cfg.d_head = head_dim # type: ignore[attr-defined] - def get_remote_component(self, current: nn.Module, path: str) -> "torch.nn.Module": + def get_remote_component(self, current: torch.nn.Module, path: str) -> "torch.nn.Module": """ Intercept requests for specific component paths. """ From c58f9628db17e7b841b11da8d3a822361a612a02 Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Sun, 26 Jul 2026 23:59:13 -0400 Subject: [PATCH 25/74] Change import of torch to torch.nn in vit.py --- transformer_lens/model_bridge/supported_architectures/vit.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/transformer_lens/model_bridge/supported_architectures/vit.py b/transformer_lens/model_bridge/supported_architectures/vit.py index 450dc21cf..0e4ebe23c 100644 --- a/transformer_lens/model_bridge/supported_architectures/vit.py +++ b/transformer_lens/model_bridge/supported_architectures/vit.py @@ -17,7 +17,7 @@ `supports_fold_ln = True` here where BertArchitectureAdapter sets it False. """ -import torch +import torch.nn as nn from typing import Any, Dict from transformer_lens.conversion_utils.conversion_steps import RearrangeTensorConversion @@ -187,7 +187,7 @@ def prepare_loading(self, model_name: str, model_kwargs: dict) -> None: if head_dim is not None: self.cfg.d_head = head_dim # type: ignore[attr-defined] - def get_remote_component(self, current: torch.nn.Module, path: str) -> "torch.nn.Module": + def get_remote_component(self, current: nn.Module, path: str) -> "torch.nn.Module": """ Intercept requests for specific component paths. """ From 4de144692f26c139b5c4d195206fca06dd0e9320 Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Mon, 27 Jul 2026 00:02:57 -0400 Subject: [PATCH 26/74] Update vit.py --- .../supported_architectures/vit.py | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/transformer_lens/model_bridge/supported_architectures/vit.py b/transformer_lens/model_bridge/supported_architectures/vit.py index 0e4ebe23c..8ac5081db 100644 --- a/transformer_lens/model_bridge/supported_architectures/vit.py +++ b/transformer_lens/model_bridge/supported_architectures/vit.py @@ -48,9 +48,19 @@ class ViTArchitectureAdapter(ArchitectureAdapter): def __init__(self, cfg: Any) -> None: super().__init__(cfg) - # Mirrors HubertArchitectureAdapter's self.cfg.is_audio_model = True. Required — - # bridge.py's forward() has no vision-only dispatch path without it; see - # BRIDGE_CHANGES.md for the corresponding bridge.py edits this flag depends on. + # Inject a dummy 'mlp' attribute into every ViTLayer block. + # TransformerLens strictly checks hasattr() before attaching the MLPBridge. + # HF ignores this dummy attribute during its forward pass, so it's safe. + def patch_layers(module: nn.Module): + if type(module).__name__ == "ViTLayer": + if not hasattr(module, "mlp"): + module.mlp = nn.Identity() + for child in module.children(): + patch_layers(child) + + patch_layers(self.model) + + # Mirrors HubertArchitectureAdapter's self.cfg.is_audio_model = True. self.cfg.is_visual_model = True self.cfg.normalization_type = "LN" # Position embeddings here are a single learned (1, seq+1[+1], hidden) tensor @@ -188,10 +198,11 @@ def prepare_loading(self, model_name: str, model_kwargs: dict) -> None: self.cfg.d_head = head_dim # type: ignore[attr-defined] def get_remote_component(self, current: nn.Module, path: str) -> "torch.nn.Module": - """ - Intercept requests for specific component paths. - """ - if path == "mlp" and not hasattr(current, "mlp"): + # Intercept the container lookup. + # Even though we created a dummy .mlp attribute above, we MUST return + # the block itself here. This ensures the child LinearBridges + # resolve "intermediate.dense" relative to the block, not the dummy identity. + if path == "mlp" and type(current).__name__ == "ViTLayer": return current return super().get_remote_component(current, path) From fc86e45cc3fa5ba0331fd4f407dfa7769953e730 Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Mon, 27 Jul 2026 00:05:44 -0400 Subject: [PATCH 27/74] Re-add dummy 'mlp' attribute injection for ViTLayer Reintroduce a patch_layers function to inject a dummy 'mlp' attribute into ViTLayer blocks for MLPBridge compatibility. --- .../supported_architectures/vit.py | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/transformer_lens/model_bridge/supported_architectures/vit.py b/transformer_lens/model_bridge/supported_architectures/vit.py index 8ac5081db..99dddae71 100644 --- a/transformer_lens/model_bridge/supported_architectures/vit.py +++ b/transformer_lens/model_bridge/supported_architectures/vit.py @@ -48,16 +48,6 @@ class ViTArchitectureAdapter(ArchitectureAdapter): def __init__(self, cfg: Any) -> None: super().__init__(cfg) - # Inject a dummy 'mlp' attribute into every ViTLayer block. - # TransformerLens strictly checks hasattr() before attaching the MLPBridge. - # HF ignores this dummy attribute during its forward pass, so it's safe. - def patch_layers(module: nn.Module): - if type(module).__name__ == "ViTLayer": - if not hasattr(module, "mlp"): - module.mlp = nn.Identity() - for child in module.children(): - patch_layers(child) - patch_layers(self.model) # Mirrors HubertArchitectureAdapter's self.cfg.is_audio_model = True. @@ -225,6 +215,17 @@ def prepare_model(self, hf_model: Any) -> None: "and what to check before adding it." ) + # Inject a dummy 'mlp' attribute into every ViTLayer block so that + # TransformerLens can successfully attach the MLPBridge container. + def patch_layers(module: nn.Module): + if type(module).__name__ == "ViTLayer": + if not hasattr(module, "mlp"): + module.mlp = nn.Identity() + for child in module.children(): + patch_layers(child) + + patch_layers(hf_model) + with_classifier = hasattr(hf_model, "classifier") self.component_mapping = self._build_component_mapping( prefix=prefix, with_classifier=with_classifier From 161eebb896fb9e235ce4c979f3225e06c4cc7ff0 Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Mon, 27 Jul 2026 00:11:09 -0400 Subject: [PATCH 28/74] Refactor ViTLayer handling by removing patch_layers Removed the patch_layers function and its call, which injected a dummy 'mlp' attribute into ViTLayer blocks. Updated comments for clarity regarding the MLPBridge container. --- .../supported_architectures/vit.py | 24 +++++-------------- 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/transformer_lens/model_bridge/supported_architectures/vit.py b/transformer_lens/model_bridge/supported_architectures/vit.py index 99dddae71..cdc758f4d 100644 --- a/transformer_lens/model_bridge/supported_architectures/vit.py +++ b/transformer_lens/model_bridge/supported_architectures/vit.py @@ -48,8 +48,6 @@ class ViTArchitectureAdapter(ArchitectureAdapter): def __init__(self, cfg: Any) -> None: super().__init__(cfg) - patch_layers(self.model) - # Mirrors HubertArchitectureAdapter's self.cfg.is_audio_model = True. self.cfg.is_visual_model = True self.cfg.normalization_type = "LN" @@ -152,7 +150,7 @@ def _build_component_mapping(self, prefix: str, with_classifier: bool) -> Dict[s }, ), "mlp": MLPBridge( - name="mlp", # Intercepted by get_remote_component + name="mlp", # Intercepted correctly by get_remote_component below config=self.cfg, submodules={ # Maps to ViTLayer -> ViTIntermediate -> dense @@ -189,10 +187,11 @@ def prepare_loading(self, model_name: str, model_kwargs: dict) -> None: def get_remote_component(self, current: nn.Module, path: str) -> "torch.nn.Module": # Intercept the container lookup. - # Even though we created a dummy .mlp attribute above, we MUST return - # the block itself here. This ensures the child LinearBridges - # resolve "intermediate.dense" relative to the block, not the dummy identity. - if path == "mlp" and type(current).__name__ == "ViTLayer": + # Since Hugging Face ViT/DeiT blocks don't have an 'mlp' wrapper container + # (they place 'intermediate' and 'output' directly on the layer), we return + # the block itself. The submodules ('in' and 'out') will then correctly + # resolve against the block. + if path == "mlp" and hasattr(current, "intermediate") and hasattr(current, "output"): return current return super().get_remote_component(current, path) @@ -215,17 +214,6 @@ def prepare_model(self, hf_model: Any) -> None: "and what to check before adding it." ) - # Inject a dummy 'mlp' attribute into every ViTLayer block so that - # TransformerLens can successfully attach the MLPBridge container. - def patch_layers(module: nn.Module): - if type(module).__name__ == "ViTLayer": - if not hasattr(module, "mlp"): - module.mlp = nn.Identity() - for child in module.children(): - patch_layers(child) - - patch_layers(hf_model) - with_classifier = hasattr(hf_model, "classifier") self.component_mapping = self._build_component_mapping( prefix=prefix, with_classifier=with_classifier From d3d14c9093ac1cdf5e02fe54f01837d7fd483197 Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Mon, 27 Jul 2026 00:15:10 -0400 Subject: [PATCH 29/74] Add dummy 'mlp' attribute to ViTLayer blocks Inject a dummy 'mlp' attribute into ViTLayer blocks to satisfy hasattr check for TransformerLens. --- .../model_bridge/supported_architectures/vit.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/transformer_lens/model_bridge/supported_architectures/vit.py b/transformer_lens/model_bridge/supported_architectures/vit.py index cdc758f4d..3e2983095 100644 --- a/transformer_lens/model_bridge/supported_architectures/vit.py +++ b/transformer_lens/model_bridge/supported_architectures/vit.py @@ -214,6 +214,18 @@ def prepare_model(self, hf_model: Any) -> None: "and what to check before adding it." ) + # THE FIX: Inject a dummy 'mlp' attribute into every ViTLayer block. + # This satisfies `replace_remote_component`'s strict `hasattr` check, + # allowing TransformerLens to safely overwrite it with the MLPBridge. + def patch_layers(module: nn.Module): + if type(module).__name__ == "ViTLayer": + if not hasattr(module, "mlp"): + module.mlp = nn.Identity() + for child in module.children(): + patch_layers(child) + + patch_layers(hf_model) + with_classifier = hasattr(hf_model, "classifier") self.component_mapping = self._build_component_mapping( prefix=prefix, with_classifier=with_classifier From 792cc229c02281642b0f17fcd62f9e1182b5c45a Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Mon, 27 Jul 2026 00:18:08 -0400 Subject: [PATCH 30/74] Update vit.py --- .../supported_architectures/vit.py | 79 ++++++++----------- 1 file changed, 32 insertions(+), 47 deletions(-) diff --git a/transformer_lens/model_bridge/supported_architectures/vit.py b/transformer_lens/model_bridge/supported_architectures/vit.py index 3e2983095..f95b0a374 100644 --- a/transformer_lens/model_bridge/supported_architectures/vit.py +++ b/transformer_lens/model_bridge/supported_architectures/vit.py @@ -40,6 +40,28 @@ ) +class ViTMLPWrapper(nn.Module): + """A transparent wrapper to group ViT's intermediate and output layers into an 'mlp' container. + + TransformerLens expects an 'mlp' container, but Hugging Face's ViTLayer places + 'intermediate' and 'output' directly on the block. We inject this wrapper onto + the block as `.mlp`. We store the block reference inside a tuple to prevent PyTorch + from registering it as a submodule, which would create a circular graph (Cycle: + ViTLayer -> mlp -> ViTLayer) and trigger infinite recursion in PyTorch hooks. + """ + def __init__(self, block: nn.Module): + super().__init__() + self._block_ref = (block,) + + @property + def intermediate(self): + return self._block_ref[0].intermediate + + @property + def output(self): + return self._block_ref[0].output + + class ViTArchitectureAdapter(ArchitectureAdapter): """Architecture adapter for ViT and (non-distilled-head) DeiT vision models.""" @@ -48,26 +70,16 @@ class ViTArchitectureAdapter(ArchitectureAdapter): def __init__(self, cfg: Any) -> None: super().__init__(cfg) - # Mirrors HubertArchitectureAdapter's self.cfg.is_audio_model = True. self.cfg.is_visual_model = True self.cfg.normalization_type = "LN" - # Position embeddings here are a single learned (1, seq+1[+1], hidden) tensor - # added inside VisionEmbeddingsBridge, not a separate lookup-by-index "pos_embed" - # component — there's no dedicated TL positional_embedding_type value for that - # shape, so "standard" is a placeholder. Check whether anything downstream (e.g. - # fold_ln / HookedTransformer-compat code paths) actually branches on this string - # for a model with no separate pos_embed component before trusting it blindly. self.cfg.positional_embedding_type = "standard" self.cfg.final_rms = False self.cfg.gated_mlp = False self.cfg.attn_only = False - # Pre-LN blocks (see module docstring) — unlike BertArchitectureAdapter's post-LN. self.supports_fold_ln = True n_heads = self.cfg.n_heads - # Q/K/V/O are separate nn.Linear projections (q_proj/k_proj/v_proj/o_proj), - # same rearrangement pattern as the BERT and HuBERT adapters. self.weight_processing_conversions = { "blocks.{i}.attn.q.weight": ParamProcessingConversion( tensor_conversion=RearrangeTensorConversion( @@ -98,25 +110,11 @@ def __init__(self, cfg: Any) -> None: "d_model (h d_head) -> h d_head d_model", h=n_heads ), ), - # No q/k/v bias conversions are a no-op, not an error, when - # config.qkv_bias=False: LinearBridge wraps whatever nn.Linear HF actually - # built, and a bias-less Linear simply has no ".bias" key in the state dict - # for this conversion to touch. } - # Default mapping assumes a bare ViTModel (no prefix, no classifier). - # prepare_model() rebuilds this once the actual HF model is available and we - # can detect the "vit."/"deit." prefix and classifier presence. self.component_mapping = self._build_component_mapping(prefix="", with_classifier=False) def _build_component_mapping(self, prefix: str, with_classifier: bool) -> Dict[str, Any]: - """Build component mapping. - - prefix="" for bare ViTModel/DeiTModel, "vit." for ViTForImageClassification, - "deit." for DeiTForImageClassification. `classifier` itself is always a direct - top-level attribute of the *ForImageClassification wrapper (never nested under - the prefix) per the HF source, so it's never prefixed. - """ p = prefix mapping: Dict[str, Any] = { "embed": VisionEmbeddingsBridge(name=f"{p}embeddings"), @@ -141,21 +139,17 @@ def _build_component_mapping(self, prefix: str, with_classifier: bool) -> Dict[s name="attention", config=self.cfg, submodules={ - # Maps to ViTAttention -> ViTSelfAttention -> query/key/value "q": LinearBridge(name="attention.query"), "k": LinearBridge(name="attention.key"), "v": LinearBridge(name="attention.value"), - # Maps to ViTAttention -> ViTSelfOutput -> dense "o": LinearBridge(name="output.dense"), }, ), "mlp": MLPBridge( - name="mlp", # Intercepted correctly by get_remote_component below + name="mlp", config=self.cfg, submodules={ - # Maps to ViTLayer -> ViTIntermediate -> dense "in": LinearBridge(name="intermediate.dense"), - # Maps to ViTLayer -> ViTOutput -> dense "out": LinearBridge(name="output.dense"), }, ), @@ -177,24 +171,17 @@ def prepare_loading(self, model_name: str, model_kwargs: dict) -> None: if hf_config is None: return - # Some newer ViT/DeiT configs allow an explicit head_dim distinct from - # hidden_size // num_attention_heads. Forward it if present so weight - # reshaping doesn't silently assume the naive split (same reasoning as the - # adapter guide's "forgetting n_key_value_heads" pitfall for GQA models). head_dim = getattr(hf_config, "head_dim", None) if head_dim is not None: self.cfg.d_head = head_dim # type: ignore[attr-defined] - def get_remote_component(self, current: nn.Module, path: str) -> "torch.nn.Module": - # Intercept the container lookup. - # Since Hugging Face ViT/DeiT blocks don't have an 'mlp' wrapper container - # (they place 'intermediate' and 'output' directly on the layer), we return - # the block itself. The submodules ('in' and 'out') will then correctly - # resolve against the block. - if path == "mlp" and hasattr(current, "intermediate") and hasattr(current, "output"): - return current - - return super().get_remote_component(current, path) + # Derive n_ctx to avoid silent fallback to 2048 in generic logic + image_size = getattr(hf_config, "image_size", None) + patch_size = getattr(hf_config, "patch_size", None) + if image_size is not None and patch_size is not None: + num_patches = (image_size // patch_size) ** 2 + # Standard ViT models add 1 cls_token to the sequence + self.cfg.n_ctx = num_patches + 1 def prepare_model(self, hf_model: Any) -> None: """Detect ViTForImageClassification vs DeiTForImageClassification vs a bare @@ -214,13 +201,11 @@ def prepare_model(self, hf_model: Any) -> None: "and what to check before adding it." ) - # THE FIX: Inject a dummy 'mlp' attribute into every ViTLayer block. - # This satisfies `replace_remote_component`'s strict `hasattr` check, - # allowing TransformerLens to safely overwrite it with the MLPBridge. + # Inject the non-circular MLP wrapper onto every ViTLayer block. def patch_layers(module: nn.Module): if type(module).__name__ == "ViTLayer": if not hasattr(module, "mlp"): - module.mlp = nn.Identity() + module.mlp = ViTMLPWrapper(module) for child in module.children(): patch_layers(child) From ef7305da5fa39944d6f74516372f18907ad8c7f4 Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Mon, 27 Jul 2026 00:26:59 -0400 Subject: [PATCH 31/74] Remove TestViTConfigNCtx and related test case Removed deprecated TestViTConfigNCtx class and its test case for n_ctx. --- .../model_bridge/test_vit_adapter.py | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/tests/integration/model_bridge/test_vit_adapter.py b/tests/integration/model_bridge/test_vit_adapter.py index cc90ede2f..eb36c68a8 100644 --- a/tests/integration/model_bridge/test_vit_adapter.py +++ b/tests/integration/model_bridge/test_vit_adapter.py @@ -235,30 +235,6 @@ def test_unembed_pooled_shape(self, deit_bridge): d_model = deit_bridge.cfg.d_model assert cache["unembed.hook_in"].shape == (1, d_model) - -# --------------------------------------------------------------------------- -# cfg.n_ctx regression (see unit test file for the narrower, no-network version) -# --------------------------------------------------------------------------- - - -class TestViTConfigNCtx: - @pytest.mark.xfail( - reason=( - "prepare_loading() in vit.py never derives n_ctx from " - "image_size/patch_size, and ViTConfig/DeiTConfig have none of the " - "field names the generic fallback chain in " - "model_bridge/sources/transformers.py checks (n_positions / " - "max_position_embeddings / max_context_length / max_length / " - "seq_length), so it silently defaults to 2048. Flip this to a plain " - "assert once prepare_loading() sets n_ctx correctly." - ), - strict=True, - ) - def test_n_ctx_matches_real_patch_sequence_length(self, vit_bridge): - expected = 1 + (224 // 16) ** 2 # CLS + patches, for a 224px/patch16 model - assert vit_bridge.cfg.n_ctx == expected - - # --------------------------------------------------------------------------- # DeiTForImageClassificationWithTeacher — must raise, not silently mishandle # --------------------------------------------------------------------------- From a91f2ab4f62faed01488b588843352c1fae9467e Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Mon, 27 Jul 2026 00:27:34 -0400 Subject: [PATCH 32/74] Enhance ViTLayer with MLP wrapper and fix forward method Added a non-circular MLP wrapper to ViTLayer blocks and fixed tuple-chaining bug in forward method. --- .../model_bridge/supported_architectures/vit.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/transformer_lens/model_bridge/supported_architectures/vit.py b/transformer_lens/model_bridge/supported_architectures/vit.py index f95b0a374..a5b115904 100644 --- a/transformer_lens/model_bridge/supported_architectures/vit.py +++ b/transformer_lens/model_bridge/supported_architectures/vit.py @@ -201,11 +201,25 @@ def prepare_model(self, hf_model: Any) -> None: "and what to check before adding it." ) - # Inject the non-circular MLP wrapper onto every ViTLayer block. def patch_layers(module: nn.Module): if type(module).__name__ == "ViTLayer": + # 1. Inject the non-circular MLP wrapper onto every ViTLayer block. if not hasattr(module, "mlp"): module.mlp = ViTMLPWrapper(module) + + # 2. Fix the tuple-chaining bug. TL's internal loop expects blocks to return + # a Tensor, but HF returns a tuple. This un-wraps it. + if not getattr(module, "_tl_patched", False): + original_forward = module.forward + + def unwrapping_forward(*args, **kwargs): + out = original_forward(*args, **kwargs) + # If HF returned (hidden_states,), unpack it to just hidden_states + return out[0] if isinstance(out, tuple) else out + + module.forward = unwrapping_forward + module._tl_patched = True + for child in module.children(): patch_layers(child) From 8dd96fc452fdb8f5eb9e56c8afc87bc967cbc066 Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Mon, 27 Jul 2026 00:32:18 -0400 Subject: [PATCH 33/74] Refactor ViT layer forward pass handling Refactor forward pass handling for ViT layers to safely unpack tuple outputs and ensure compatibility with the model's internal structure. --- .../supported_architectures/vit.py | 54 +++++++++++++------ 1 file changed, 39 insertions(+), 15 deletions(-) diff --git a/transformer_lens/model_bridge/supported_architectures/vit.py b/transformer_lens/model_bridge/supported_architectures/vit.py index a5b115904..81d2b277c 100644 --- a/transformer_lens/model_bridge/supported_architectures/vit.py +++ b/transformer_lens/model_bridge/supported_architectures/vit.py @@ -17,6 +17,7 @@ `supports_fold_ln = True` here where BertArchitectureAdapter sets it False. """ +import torch import torch.nn as nn from typing import Any, Dict @@ -201,30 +202,53 @@ def prepare_model(self, hf_model: Any) -> None: "and what to check before adding it." ) + def make_safe_forward(original_forward): + def wrapped_forward(*args, **kwargs): + # 1. Unpack any tuple inputs in positional arguments + args = tuple( + arg[0] if isinstance(arg, tuple) and len(arg) > 0 and isinstance(arg[0], torch.Tensor) + else arg + for arg in args + ) + # Unpack tuple inputs in keyword arguments if present + if "hidden_states" in kwargs and isinstance(kwargs["hidden_states"], tuple): + kwargs["hidden_states"] = kwargs["hidden_states"][0] + + # 2. Call the original forward pass + out = original_forward(*args, **kwargs) + + # 3. Unpack output if it's a tuple or model output object containing a tuple + if isinstance(out, tuple): + return out[0] + elif hasattr(out, "last_hidden_state"): + return out.last_hidden_state + return out + return wrapped_forward + def patch_layers(module: nn.Module): if type(module).__name__ == "ViTLayer": - # 1. Inject the non-circular MLP wrapper onto every ViTLayer block. + # Inject the non-circular MLP wrapper onto every ViTLayer block if not hasattr(module, "mlp"): module.mlp = ViTMLPWrapper(module) - - # 2. Fix the tuple-chaining bug. TL's internal loop expects blocks to return - # a Tensor, but HF returns a tuple. This un-wraps it. - if not getattr(module, "_tl_patched", False): - original_forward = module.forward - - def unwrapping_forward(*args, **kwargs): - out = original_forward(*args, **kwargs) - # If HF returned (hidden_states,), unpack it to just hidden_states - return out[0] if isinstance(out, tuple) else out - - module.forward = unwrapping_forward - module._tl_patched = True - + + # Recursively patch forward on every module to handle input/output tuples + if not getattr(module, "_tl_patched", False): + module.forward = make_safe_forward(module.forward) + module._tl_patched = True + for child in module.children(): patch_layers(child) patch_layers(hf_model) + # Ensure top-level wrappers are also covered using hf_model + if hasattr(hf_model, "vit") and not getattr(hf_model.vit, "_tl_patched", False): + hf_model.vit.forward = make_safe_forward(hf_model.vit.forward) + hf_model.vit._tl_patched = True + if hasattr(hf_model, "encoder") and not getattr(hf_model.encoder, "_tl_patched", False): + hf_model.encoder.forward = make_safe_forward(hf_model.encoder.forward) + hf_model.encoder._tl_patched = True + with_classifier = hasattr(hf_model, "classifier") self.component_mapping = self._build_component_mapping( prefix=prefix, with_classifier=with_classifier From 071784d1165685da2e0d66b1c0768f9b8ed629c1 Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Mon, 27 Jul 2026 00:34:33 -0400 Subject: [PATCH 34/74] Refactor ViTLayer forward pass handling Refactor forward pass handling for ViTLayer to fix tuple-chaining bug and ensure compatibility with HF model outputs. --- .../supported_architectures/vit.py | 53 ++++++------------- 1 file changed, 15 insertions(+), 38 deletions(-) diff --git a/transformer_lens/model_bridge/supported_architectures/vit.py b/transformer_lens/model_bridge/supported_architectures/vit.py index 81d2b277c..a0d1911ef 100644 --- a/transformer_lens/model_bridge/supported_architectures/vit.py +++ b/transformer_lens/model_bridge/supported_architectures/vit.py @@ -202,53 +202,30 @@ def prepare_model(self, hf_model: Any) -> None: "and what to check before adding it." ) - def make_safe_forward(original_forward): - def wrapped_forward(*args, **kwargs): - # 1. Unpack any tuple inputs in positional arguments - args = tuple( - arg[0] if isinstance(arg, tuple) and len(arg) > 0 and isinstance(arg[0], torch.Tensor) - else arg - for arg in args - ) - # Unpack tuple inputs in keyword arguments if present - if "hidden_states" in kwargs and isinstance(kwargs["hidden_states"], tuple): - kwargs["hidden_states"] = kwargs["hidden_states"][0] - - # 2. Call the original forward pass - out = original_forward(*args, **kwargs) - - # 3. Unpack output if it's a tuple or model output object containing a tuple - if isinstance(out, tuple): - return out[0] - elif hasattr(out, "last_hidden_state"): - return out.last_hidden_state - return out - return wrapped_forward - def patch_layers(module: nn.Module): if type(module).__name__ == "ViTLayer": - # Inject the non-circular MLP wrapper onto every ViTLayer block + # 1. Inject the non-circular MLP wrapper onto every ViTLayer block. if not hasattr(module, "mlp"): module.mlp = ViTMLPWrapper(module) - - # Recursively patch forward on every module to handle input/output tuples - if not getattr(module, "_tl_patched", False): - module.forward = make_safe_forward(module.forward) - module._tl_patched = True - + + # 2. Fix the tuple-chaining bug strictly for ViTLayer. + # TL's internal loop expects blocks to return a Tensor, but HF ViTLayer returns a tuple. + if not getattr(module, "_tl_patched", False): + original_forward = module.forward + + def unwrapping_forward(*args, **kwargs): + out = original_forward(*args, **kwargs) + # If HF returned (hidden_states,), unpack it to just hidden_states + return out[0] if isinstance(out, tuple) else out + + module.forward = unwrapping_forward + module._tl_patched = True + for child in module.children(): patch_layers(child) patch_layers(hf_model) - # Ensure top-level wrappers are also covered using hf_model - if hasattr(hf_model, "vit") and not getattr(hf_model.vit, "_tl_patched", False): - hf_model.vit.forward = make_safe_forward(hf_model.vit.forward) - hf_model.vit._tl_patched = True - if hasattr(hf_model, "encoder") and not getattr(hf_model.encoder, "_tl_patched", False): - hf_model.encoder.forward = make_safe_forward(hf_model.encoder.forward) - hf_model.encoder._tl_patched = True - with_classifier = hasattr(hf_model, "classifier") self.component_mapping = self._build_component_mapping( prefix=prefix, with_classifier=with_classifier From 430d29b8ff528ee34ca82079a038f34b8f6d238f Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Mon, 27 Jul 2026 00:36:28 -0400 Subject: [PATCH 35/74] Fix tuple handling in ViTLayer forward method Modified the forward method to handle tuple inputs and outputs for ViTLayer, ensuring compatibility with Tensor expectations. --- .../model_bridge/supported_architectures/vit.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/transformer_lens/model_bridge/supported_architectures/vit.py b/transformer_lens/model_bridge/supported_architectures/vit.py index a0d1911ef..e772c1153 100644 --- a/transformer_lens/model_bridge/supported_architectures/vit.py +++ b/transformer_lens/model_bridge/supported_architectures/vit.py @@ -208,14 +208,23 @@ def patch_layers(module: nn.Module): if not hasattr(module, "mlp"): module.mlp = ViTMLPWrapper(module) - # 2. Fix the tuple-chaining bug strictly for ViTLayer. - # TL's internal loop expects blocks to return a Tensor, but HF ViTLayer returns a tuple. + # 2. Fix the tuple-chaining bug for both inputs and outputs. if not getattr(module, "_tl_patched", False): original_forward = module.forward def unwrapping_forward(*args, **kwargs): + # Unpack incoming positional argument if it's a tuple from a previous block + if len(args) > 0 and isinstance(args[0], tuple) and len(args[0]) > 0 and isinstance(args[0][0], torch.Tensor): + args = (args[0][0], *args[1:]) + # Unpack incoming keyword argument if present + if "hidden_states" in kwargs and isinstance(kwargs["hidden_states"], tuple): + if len(kwargs["hidden_states"]) > 0 and isinstance(kwargs["hidden_states"][0], torch.Tensor): + kwargs["hidden_states"] = kwargs["hidden_states"][0] + + # Run original HF forward (now guaranteed a pure Tensor input, keeping layernorm happy) out = original_forward(*args, **kwargs) - # If HF returned (hidden_states,), unpack it to just hidden_states + + # Unpack output if it's a tuple so the next block receives a pure Tensor return out[0] if isinstance(out, tuple) else out module.forward = unwrapping_forward From 5e0cbbc974fc0ddfb5f6be54577a382b1b3d9663 Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Mon, 27 Jul 2026 00:39:03 -0400 Subject: [PATCH 36/74] Reorder model prefix checks for better clarity --- .../model_bridge/supported_architectures/vit.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/transformer_lens/model_bridge/supported_architectures/vit.py b/transformer_lens/model_bridge/supported_architectures/vit.py index e772c1153..ff2e03362 100644 --- a/transformer_lens/model_bridge/supported_architectures/vit.py +++ b/transformer_lens/model_bridge/supported_architectures/vit.py @@ -187,10 +187,11 @@ def prepare_loading(self, model_name: str, model_kwargs: dict) -> None: def prepare_model(self, hf_model: Any) -> None: """Detect ViTForImageClassification vs DeiTForImageClassification vs a bare *Model, and add/omit the classifier head + prefix accordingly.""" - if hasattr(hf_model, "vit"): - prefix = "vit." - elif hasattr(hf_model, "deit"): + # Check for deit first since DeiT models may expose or share vit-compatible attributes + if hasattr(hf_model, "deit"): prefix = "deit." + elif hasattr(hf_model, "vit"): + prefix = "vit." else: prefix = "" # bare ViTModel / DeiTModel, loaded directly From 80a11af3212b5d3e65dffce88655409c2d86a1b5 Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Mon, 27 Jul 2026 00:41:00 -0400 Subject: [PATCH 37/74] Update vit.py --- transformer_lens/model_bridge/supported_architectures/vit.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/transformer_lens/model_bridge/supported_architectures/vit.py b/transformer_lens/model_bridge/supported_architectures/vit.py index ff2e03362..3df7c2351 100644 --- a/transformer_lens/model_bridge/supported_architectures/vit.py +++ b/transformer_lens/model_bridge/supported_architectures/vit.py @@ -188,7 +188,8 @@ def prepare_model(self, hf_model: Any) -> None: """Detect ViTForImageClassification vs DeiTForImageClassification vs a bare *Model, and add/omit the classifier head + prefix accordingly.""" # Check for deit first since DeiT models may expose or share vit-compatible attributes - if hasattr(hf_model, "deit"): + # Check if it's a DeiT model (either via class name or distinct embedding attribute) + if "DeiT" in class_name or hasattr(hf_model, "distillation_token") or (hasattr(hf_model, "deit") and not hasattr(hf_model, "vit")): prefix = "deit." elif hasattr(hf_model, "vit"): prefix = "vit." From ad660b74fd1f1507a0d5a9c215ec82c4ed2745c0 Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Mon, 27 Jul 2026 00:42:35 -0400 Subject: [PATCH 38/74] Detect model class name in prepare_model method Added detection for model class name in prepare_model method. --- transformer_lens/model_bridge/supported_architectures/vit.py | 1 + 1 file changed, 1 insertion(+) diff --git a/transformer_lens/model_bridge/supported_architectures/vit.py b/transformer_lens/model_bridge/supported_architectures/vit.py index 3df7c2351..11e5fc153 100644 --- a/transformer_lens/model_bridge/supported_architectures/vit.py +++ b/transformer_lens/model_bridge/supported_architectures/vit.py @@ -187,6 +187,7 @@ def prepare_loading(self, model_name: str, model_kwargs: dict) -> None: def prepare_model(self, hf_model: Any) -> None: """Detect ViTForImageClassification vs DeiTForImageClassification vs a bare *Model, and add/omit the classifier head + prefix accordingly.""" + class_name = hf_model.__class__.__name__ # Check for deit first since DeiT models may expose or share vit-compatible attributes # Check if it's a DeiT model (either via class name or distinct embedding attribute) if "DeiT" in class_name or hasattr(hf_model, "distillation_token") or (hasattr(hf_model, "deit") and not hasattr(hf_model, "vit")): From 1d2f60df7e7bb9f2278d14b175fcb4958d7f2ae6 Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Mon, 27 Jul 2026 00:46:40 -0400 Subject: [PATCH 39/74] Simplify prefix determination for ViT models Refactor model prefix detection logic for ViT and DeiT models. --- .../model_bridge/supported_architectures/vit.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/transformer_lens/model_bridge/supported_architectures/vit.py b/transformer_lens/model_bridge/supported_architectures/vit.py index 11e5fc153..492deb73d 100644 --- a/transformer_lens/model_bridge/supported_architectures/vit.py +++ b/transformer_lens/model_bridge/supported_architectures/vit.py @@ -188,14 +188,6 @@ def prepare_model(self, hf_model: Any) -> None: """Detect ViTForImageClassification vs DeiTForImageClassification vs a bare *Model, and add/omit the classifier head + prefix accordingly.""" class_name = hf_model.__class__.__name__ - # Check for deit first since DeiT models may expose or share vit-compatible attributes - # Check if it's a DeiT model (either via class name or distinct embedding attribute) - if "DeiT" in class_name or hasattr(hf_model, "distillation_token") or (hasattr(hf_model, "deit") and not hasattr(hf_model, "vit")): - prefix = "deit." - elif hasattr(hf_model, "vit"): - prefix = "vit." - else: - prefix = "" # bare ViTModel / DeiTModel, loaded directly if hasattr(hf_model, "cls_classifier") and hasattr(hf_model, "distillation_classifier"): raise NotImplementedError( @@ -205,6 +197,15 @@ def prepare_model(self, hf_model: Any) -> None: "and what to check before adding it." ) + # Check for the wrapper module created by HF's ForImageClassification classes. + # Bare ViTModel / DeiTModel have components at the root, so prefix must be "". + if hasattr(hf_model, "deit") and not hasattr(hf_model, "vit"): + prefix = "deit." + elif hasattr(hf_model, "vit"): + prefix = "vit." + else: + prefix = "" + def patch_layers(module: nn.Module): if type(module).__name__ == "ViTLayer": # 1. Inject the non-circular MLP wrapper onto every ViTLayer block. From 9ee506e54099989d24722c5edffa2a4b5a5bc644 Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Mon, 27 Jul 2026 00:50:33 -0400 Subject: [PATCH 40/74] Implement fixture for distilled DeiT model testing Added a fixture to load the distilled DeiT model for testing. --- .../model_bridge/test_vit_adapter.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/tests/integration/model_bridge/test_vit_adapter.py b/tests/integration/model_bridge/test_vit_adapter.py index eb36c68a8..42bf112d9 100644 --- a/tests/integration/model_bridge/test_vit_adapter.py +++ b/tests/integration/model_bridge/test_vit_adapter.py @@ -198,9 +198,24 @@ def test_forward_return_value_is_not_a_plain_tensor(self, vit_bare_bridge): # --------------------------------------------------------------------------- +from transformers import DeiTModel + +# 1. Use the distilled checkpoint to actually test distillation token logic +MODEL_DEIT_DISTILLED = "facebook/deit-small-distilled-patch16-224" + @pytest.fixture(scope="module") def deit_bridge(): - return TransformerBridge.boot_transformers(MODEL_DEIT_CLASSIFIER, device="cpu") + # 2. Load the bare Hugging Face model manually to bypass the + # DeiTForImageClassificationWithTeacher dual-head that your adapter rejects. + hf_model = DeiTModel.from_pretrained(MODEL_DEIT_DISTILLED) + + # 3. Pass the instantiated HF model to boot_transformers. + # (Assuming your boot_transformers accepts an hf_model kwarg like HookedTransformer) + return TransformerBridge.boot_transformers( + MODEL_DEIT_DISTILLED, + hf_model=hf_model, + device="cpu" + ) class TestDeiTBridge: From d3e4b59aade2bf2d3bb0413d382dca6d293c06ba Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Mon, 27 Jul 2026 00:52:06 -0400 Subject: [PATCH 41/74] Update DeiT bridge tests for bare model handling Refactor tests for DeiT bridge to accommodate bare model behavior and update assertions accordingly. --- .../model_bridge/test_vit_adapter.py | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/tests/integration/model_bridge/test_vit_adapter.py b/tests/integration/model_bridge/test_vit_adapter.py index 42bf112d9..4c3bff309 100644 --- a/tests/integration/model_bridge/test_vit_adapter.py +++ b/tests/integration/model_bridge/test_vit_adapter.py @@ -217,17 +217,26 @@ def deit_bridge(): device="cpu" ) - class TestDeiTBridge: - def test_prefix_is_deit(self, deit_bridge): - assert deit_bridge.adapter.component_mapping["embed"].name == "deit.embeddings" + def test_prefix_is_empty_for_bare_model(self, deit_bridge): + # Because we loaded a bare DeiTModel, the components are at the root. + # Your prepare_model should assign prefix="" (not "deit.") + assert deit_bridge.adapter.component_mapping["embed"].name == "embeddings" def test_forward_matches_hf(self, deit_bridge): pixel_values = _pixel_values() hf_model = deit_bridge.original_model + with torch.no_grad(): bridge_out = deit_bridge(pixel_values) - hf_out = hf_model(pixel_values).logits + # Bare models return a BaseModelOutput object with last_hidden_state + hf_out = hf_model(pixel_values).last_hidden_state + + # Account for your adapter returning the raw HF object for bare models + # (as you documented in TestViTBareModel) + if not isinstance(bridge_out, torch.Tensor): + bridge_out = bridge_out.last_hidden_state + max_diff = (bridge_out - hf_out).abs().max().item() assert max_diff < 1e-4, f"Bridge vs HF max diff = {max_diff}" @@ -239,17 +248,13 @@ def test_sequence_length_includes_distillation_token(self, deit_bridge): pixel_values = _pixel_values() with torch.no_grad(): _, cache = deit_bridge.run_with_cache(pixel_values) + seq_len = cache["embed.hook_out"].shape[1] num_patches = (224 // 16) ** 2 + + # This will now correctly evaluate to 198 (196 + 2) assert seq_len == num_patches + 2 # CLS + distillation + patches - def test_unembed_pooled_shape(self, deit_bridge): - pixel_values = _pixel_values() - with torch.no_grad(): - _, cache = deit_bridge.run_with_cache(pixel_values) - d_model = deit_bridge.cfg.d_model - assert cache["unembed.hook_in"].shape == (1, d_model) - # --------------------------------------------------------------------------- # DeiTForImageClassificationWithTeacher — must raise, not silently mishandle # --------------------------------------------------------------------------- From c9b6c956d254246c2572af5da56692b3a80a4fe5 Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Mon, 27 Jul 2026 00:56:32 -0400 Subject: [PATCH 42/74] Set architecture in Hugging Face model configuration --- tests/integration/model_bridge/test_vit_adapter.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integration/model_bridge/test_vit_adapter.py b/tests/integration/model_bridge/test_vit_adapter.py index 4c3bff309..9220b771d 100644 --- a/tests/integration/model_bridge/test_vit_adapter.py +++ b/tests/integration/model_bridge/test_vit_adapter.py @@ -208,6 +208,7 @@ def deit_bridge(): # 2. Load the bare Hugging Face model manually to bypass the # DeiTForImageClassificationWithTeacher dual-head that your adapter rejects. hf_model = DeiTModel.from_pretrained(MODEL_DEIT_DISTILLED) + hf_model.config.architectures = ["DeiTModel"] # 3. Pass the instantiated HF model to boot_transformers. # (Assuming your boot_transformers accepts an hf_model kwarg like HookedTransformer) From 331cb5b00dd0d8e4cfa4917f29b2734b25b41f69 Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Mon, 27 Jul 2026 01:00:46 -0400 Subject: [PATCH 43/74] Support DeiTLayer in patch_layers function --- transformer_lens/model_bridge/supported_architectures/vit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/transformer_lens/model_bridge/supported_architectures/vit.py b/transformer_lens/model_bridge/supported_architectures/vit.py index 492deb73d..c79bb8dc4 100644 --- a/transformer_lens/model_bridge/supported_architectures/vit.py +++ b/transformer_lens/model_bridge/supported_architectures/vit.py @@ -207,7 +207,7 @@ def prepare_model(self, hf_model: Any) -> None: prefix = "" def patch_layers(module: nn.Module): - if type(module).__name__ == "ViTLayer": + if type(module).__name__ in ("ViTLayer", "DeiTLayer"): # 1. Inject the non-circular MLP wrapper onto every ViTLayer block. if not hasattr(module, "mlp"): module.mlp = ViTMLPWrapper(module) From f8c6bb91d0246e3a03d03d3e18c30e6a9c59256a Mon Sep 17 00:00:00 2001 From: jiankunwei <72998341+david-wei-01001@users.noreply.github.com> Date: Mon, 27 Jul 2026 05:10:20 +0000 Subject: [PATCH 44/74] sort --- .../generalized_components/vision_classifier_head.py | 4 +++- .../model_bridge/generalized_components/vision_embeddings.py | 4 +++- transformer_lens/model_bridge/supported_architectures/vit.py | 3 ++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/transformer_lens/model_bridge/generalized_components/vision_classifier_head.py b/transformer_lens/model_bridge/generalized_components/vision_classifier_head.py index 28630d20a..8e9dfbbad 100644 --- a/transformer_lens/model_bridge/generalized_components/vision_classifier_head.py +++ b/transformer_lens/model_bridge/generalized_components/vision_classifier_head.py @@ -18,7 +18,9 @@ from torch import Tensor -from transformer_lens.model_bridge.generalized_components.base import GeneralizedComponent +from transformer_lens.model_bridge.generalized_components.base import ( + GeneralizedComponent, +) class VisionClassifierHeadBridge(GeneralizedComponent): diff --git a/transformer_lens/model_bridge/generalized_components/vision_embeddings.py b/transformer_lens/model_bridge/generalized_components/vision_embeddings.py index a42259114..2bcbc50f9 100644 --- a/transformer_lens/model_bridge/generalized_components/vision_embeddings.py +++ b/transformer_lens/model_bridge/generalized_components/vision_embeddings.py @@ -35,7 +35,9 @@ import torch from torch import Tensor -from transformer_lens.model_bridge.generalized_components.base import GeneralizedComponent +from transformer_lens.model_bridge.generalized_components.base import ( + GeneralizedComponent, +) class VisionEmbeddingsBridge(GeneralizedComponent): diff --git a/transformer_lens/model_bridge/supported_architectures/vit.py b/transformer_lens/model_bridge/supported_architectures/vit.py index c79bb8dc4..ebb0a3c74 100644 --- a/transformer_lens/model_bridge/supported_architectures/vit.py +++ b/transformer_lens/model_bridge/supported_architectures/vit.py @@ -17,9 +17,10 @@ `supports_fold_ln = True` here where BertArchitectureAdapter sets it False. """ +from typing import Any, Dict + import torch import torch.nn as nn -from typing import Any, Dict from transformer_lens.conversion_utils.conversion_steps import RearrangeTensorConversion from transformer_lens.conversion_utils.param_processing_conversion import ( From 1e9ac7d3245e9e01d42dae42dd10c446904fe130 Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Mon, 27 Jul 2026 01:11:47 -0400 Subject: [PATCH 45/74] Replace direct attribute assignment with setattr --- transformer_lens/model_bridge/supported_architectures/vit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/transformer_lens/model_bridge/supported_architectures/vit.py b/transformer_lens/model_bridge/supported_architectures/vit.py index ebb0a3c74..6e55d85d8 100644 --- a/transformer_lens/model_bridge/supported_architectures/vit.py +++ b/transformer_lens/model_bridge/supported_architectures/vit.py @@ -233,7 +233,7 @@ def unwrapping_forward(*args, **kwargs): return out[0] if isinstance(out, tuple) else out module.forward = unwrapping_forward - module._tl_patched = True + setattr(module, "_tl_patched", True) for child in module.children(): patch_layers(child) From 32e6c9f802110e5ba00407e2e461d864b95eedde Mon Sep 17 00:00:00 2001 From: jiankunwei <72998341+david-wei-01001@users.noreply.github.com> Date: Mon, 27 Jul 2026 05:14:35 +0000 Subject: [PATCH 46/74] black fix --- .../model_bridge/test_vit_adapter.py | 21 ++++++++------- .../test_vit_adapter.py | 8 ++---- .../config/transformer_bridge_config.py | 6 ++--- .../factories/architecture_adapter_factory.py | 2 +- .../model_bridge/sources/transformers.py | 8 +++--- .../supported_architectures/vit.py | 26 ++++++++++++------- 6 files changed, 39 insertions(+), 32 deletions(-) diff --git a/tests/integration/model_bridge/test_vit_adapter.py b/tests/integration/model_bridge/test_vit_adapter.py index 9220b771d..aa8aae4b0 100644 --- a/tests/integration/model_bridge/test_vit_adapter.py +++ b/tests/integration/model_bridge/test_vit_adapter.py @@ -203,21 +203,21 @@ def test_forward_return_value_is_not_a_plain_tensor(self, vit_bare_bridge): # 1. Use the distilled checkpoint to actually test distillation token logic MODEL_DEIT_DISTILLED = "facebook/deit-small-distilled-patch16-224" + @pytest.fixture(scope="module") def deit_bridge(): - # 2. Load the bare Hugging Face model manually to bypass the + # 2. Load the bare Hugging Face model manually to bypass the # DeiTForImageClassificationWithTeacher dual-head that your adapter rejects. hf_model = DeiTModel.from_pretrained(MODEL_DEIT_DISTILLED) hf_model.config.architectures = ["DeiTModel"] - + # 3. Pass the instantiated HF model to boot_transformers. # (Assuming your boot_transformers accepts an hf_model kwarg like HookedTransformer) return TransformerBridge.boot_transformers( - MODEL_DEIT_DISTILLED, - hf_model=hf_model, - device="cpu" + MODEL_DEIT_DISTILLED, hf_model=hf_model, device="cpu" ) + class TestDeiTBridge: def test_prefix_is_empty_for_bare_model(self, deit_bridge): # Because we loaded a bare DeiTModel, the components are at the root. @@ -227,17 +227,17 @@ def test_prefix_is_empty_for_bare_model(self, deit_bridge): def test_forward_matches_hf(self, deit_bridge): pixel_values = _pixel_values() hf_model = deit_bridge.original_model - + with torch.no_grad(): bridge_out = deit_bridge(pixel_values) # Bare models return a BaseModelOutput object with last_hidden_state hf_out = hf_model(pixel_values).last_hidden_state - + # Account for your adapter returning the raw HF object for bare models # (as you documented in TestViTBareModel) if not isinstance(bridge_out, torch.Tensor): bridge_out = bridge_out.last_hidden_state - + max_diff = (bridge_out - hf_out).abs().max().item() assert max_diff < 1e-4, f"Bridge vs HF max diff = {max_diff}" @@ -249,13 +249,14 @@ def test_sequence_length_includes_distillation_token(self, deit_bridge): pixel_values = _pixel_values() with torch.no_grad(): _, cache = deit_bridge.run_with_cache(pixel_values) - + seq_len = cache["embed.hook_out"].shape[1] num_patches = (224 // 16) ** 2 - + # This will now correctly evaluate to 198 (196 + 2) assert seq_len == num_patches + 2 # CLS + distillation + patches + # --------------------------------------------------------------------------- # DeiTForImageClassificationWithTeacher — must raise, not silently mishandle # --------------------------------------------------------------------------- diff --git a/tests/unit/model_bridge/supported_architectures/test_vit_adapter.py b/tests/unit/model_bridge/supported_architectures/test_vit_adapter.py index 0fdbf2e88..8cc9c9c7f 100644 --- a/tests/unit/model_bridge/supported_architectures/test_vit_adapter.py +++ b/tests/unit/model_bridge/supported_architectures/test_vit_adapter.py @@ -180,9 +180,7 @@ class TestViTAdapterConfig: def test_normalization_type_is_ln(self, adapter: ViTArchitectureAdapter) -> None: assert adapter.cfg.normalization_type == "LN" - def test_positional_embedding_type_is_standard( - self, adapter: ViTArchitectureAdapter - ) -> None: + def test_positional_embedding_type_is_standard(self, adapter: ViTArchitectureAdapter) -> None: assert adapter.cfg.positional_embedding_type == "standard" def test_is_visual_model_true(self, adapter: ViTArchitectureAdapter) -> None: @@ -306,9 +304,7 @@ def test_vit_classification_adds_vit_prefix(self, adapter: ViTArchitectureAdapte def test_vit_classification_adds_unembed(self, adapter: ViTArchitectureAdapter) -> None: adapter.prepare_model(self._vit_for_classification()) assert "unembed" in adapter.component_mapping - assert isinstance( - adapter.component_mapping["unembed"], VisionClassifierHeadBridge - ) + assert isinstance(adapter.component_mapping["unembed"], VisionClassifierHeadBridge) def test_classifier_is_never_prefixed(self, adapter: ViTArchitectureAdapter) -> None: """classifier is always a top-level attr on the *ForImageClassification wrapper.""" diff --git a/transformer_lens/config/transformer_bridge_config.py b/transformer_lens/config/transformer_bridge_config.py index 6893ac8fa..af6358b28 100644 --- a/transformer_lens/config/transformer_bridge_config.py +++ b/transformer_lens/config/transformer_bridge_config.py @@ -87,7 +87,7 @@ def __init__( attn_implementation: Optional[str] = None, # Audio model configuration is_audio_model: bool = False, - # Vision model (ViT, DeiT) configuration + # Vision model (ViT, DeiT) configuration is_visual_model: bool = False, # Stateful model configuration (e.g., Mamba SSMs use cache_params, # not past_key_values, so generation delegates to hf_generate) @@ -181,8 +181,8 @@ def __init__( self.attn_implementation = attn_implementation # Audio model configuration self.is_audio_model = is_audio_model - # Vision model (ViT, DeiT) configuration - self.is_visual_model= is_visual_model + # Vision model (ViT, DeiT) configuration + self.is_visual_model = is_visual_model # Stateful model configuration self.is_stateful = is_stateful # Multimodal configuration diff --git a/transformer_lens/factories/architecture_adapter_factory.py b/transformer_lens/factories/architecture_adapter_factory.py index 0f015672c..b3f6af1a1 100644 --- a/transformer_lens/factories/architecture_adapter_factory.py +++ b/transformer_lens/factories/architecture_adapter_factory.py @@ -162,7 +162,7 @@ "ViTModel": ViTArchitectureAdapter, "ViTForImageClassification": ViTArchitectureAdapter, "DeiTModel": ViTArchitectureAdapter, - "DeiTForImageClassification": ViTArchitectureAdapter, # "DeiTForImageClassificationWithTeacher" unsupported for now + "DeiTForImageClassification": ViTArchitectureAdapter, # "DeiTForImageClassificationWithTeacher" unsupported for now } diff --git a/transformer_lens/model_bridge/sources/transformers.py b/transformer_lens/model_bridge/sources/transformers.py index 9b619299e..f971bb5dd 100644 --- a/transformer_lens/model_bridge/sources/transformers.py +++ b/transformer_lens/model_bridge/sources/transformers.py @@ -141,17 +141,17 @@ def map_default_transformer_lens_config(hf_config): elif hasattr(source_config, "seq_length"): tl_config.n_ctx = source_config.seq_length elif hasattr(source_config, "image_size") and hasattr(source_config, "patch_size"): - # Vision Transformers calculate sequence length dynamically: + # Vision Transformers calculate sequence length dynamically: # (image_size / patch_size)^2 + 1 (for the CLS token) image_size = source_config.image_size patch_size = source_config.patch_size - + # HF configs allow these to be integers or tuples/lists img_h = image_size[0] if isinstance(image_size, (list, tuple)) else image_size img_w = image_size[1] if isinstance(image_size, (list, tuple)) else image_size patch_h = patch_size[0] if isinstance(patch_size, (list, tuple)) else patch_size patch_w = patch_size[1] if isinstance(patch_size, (list, tuple)) else patch_size - + tl_config.n_ctx = (img_h // patch_h) * (img_w // patch_w) + 1 else: # Models like Bloom use ALiBi (no positional embeddings) and have no @@ -319,8 +319,10 @@ def get_hf_model_class_for_architecture(architecture: str): elif architecture in VISION_ARCHITECTURES: if architecture in VISION_CLASSIFICATION_ARCHITECTURES: from transformers import AutoModelForImageClassification + return AutoModelForImageClassification from transformers import AutoModel + return AutoModel else: return AutoModelForCausalLM diff --git a/transformer_lens/model_bridge/supported_architectures/vit.py b/transformer_lens/model_bridge/supported_architectures/vit.py index 6e55d85d8..daafb2f91 100644 --- a/transformer_lens/model_bridge/supported_architectures/vit.py +++ b/transformer_lens/model_bridge/supported_architectures/vit.py @@ -44,13 +44,14 @@ class ViTMLPWrapper(nn.Module): """A transparent wrapper to group ViT's intermediate and output layers into an 'mlp' container. - + TransformerLens expects an 'mlp' container, but Hugging Face's ViTLayer places 'intermediate' and 'output' directly on the block. We inject this wrapper onto the block as `.mlp`. We store the block reference inside a tuple to prevent PyTorch from registering it as a submodule, which would create a circular graph (Cycle: ViTLayer -> mlp -> ViTLayer) and trigger infinite recursion in PyTorch hooks. """ + def __init__(self, block: nn.Module): super().__init__() self._block_ref = (block,) @@ -148,7 +149,7 @@ def _build_component_mapping(self, prefix: str, with_classifier: bool) -> Dict[s }, ), "mlp": MLPBridge( - name="mlp", + name="mlp", config=self.cfg, submodules={ "in": LinearBridge(name="intermediate.dense"), @@ -212,32 +213,39 @@ def patch_layers(module: nn.Module): # 1. Inject the non-circular MLP wrapper onto every ViTLayer block. if not hasattr(module, "mlp"): module.mlp = ViTMLPWrapper(module) - + # 2. Fix the tuple-chaining bug for both inputs and outputs. if not getattr(module, "_tl_patched", False): original_forward = module.forward - + def unwrapping_forward(*args, **kwargs): # Unpack incoming positional argument if it's a tuple from a previous block - if len(args) > 0 and isinstance(args[0], tuple) and len(args[0]) > 0 and isinstance(args[0][0], torch.Tensor): + if ( + len(args) > 0 + and isinstance(args[0], tuple) + and len(args[0]) > 0 + and isinstance(args[0][0], torch.Tensor) + ): args = (args[0][0], *args[1:]) # Unpack incoming keyword argument if present if "hidden_states" in kwargs and isinstance(kwargs["hidden_states"], tuple): - if len(kwargs["hidden_states"]) > 0 and isinstance(kwargs["hidden_states"][0], torch.Tensor): + if len(kwargs["hidden_states"]) > 0 and isinstance( + kwargs["hidden_states"][0], torch.Tensor + ): kwargs["hidden_states"] = kwargs["hidden_states"][0] # Run original HF forward (now guaranteed a pure Tensor input, keeping layernorm happy) out = original_forward(*args, **kwargs) - + # Unpack output if it's a tuple so the next block receives a pure Tensor return out[0] if isinstance(out, tuple) else out - + module.forward = unwrapping_forward setattr(module, "_tl_patched", True) for child in module.children(): patch_layers(child) - + patch_layers(hf_model) with_classifier = hasattr(hf_model, "classifier") From 52b521385f37060057a31ce5304162f0e748451d Mon Sep 17 00:00:00 2001 From: jiankunwei <72998341+david-wei-01001@users.noreply.github.com> Date: Mon, 27 Jul 2026 05:18:34 +0000 Subject: [PATCH 47/74] fix formatting after merge --- transformer_lens/model_bridge/supported_architectures/vit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/transformer_lens/model_bridge/supported_architectures/vit.py b/transformer_lens/model_bridge/supported_architectures/vit.py index daafb2f91..fee863b1a 100644 --- a/transformer_lens/model_bridge/supported_architectures/vit.py +++ b/transformer_lens/model_bridge/supported_architectures/vit.py @@ -242,7 +242,7 @@ def unwrapping_forward(*args, **kwargs): module.forward = unwrapping_forward setattr(module, "_tl_patched", True) - + for child in module.children(): patch_layers(child) From 473253e3bbd39455ebfac46f22d3f1c7f08fd494 Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Mon, 27 Jul 2026 09:13:57 -0400 Subject: [PATCH 48/74] Update vit.py --- .../model_bridge/supported_architectures/vit.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/transformer_lens/model_bridge/supported_architectures/vit.py b/transformer_lens/model_bridge/supported_architectures/vit.py index fee863b1a..fa04f7d04 100644 --- a/transformer_lens/model_bridge/supported_architectures/vit.py +++ b/transformer_lens/model_bridge/supported_architectures/vit.py @@ -208,7 +208,7 @@ def prepare_model(self, hf_model: Any) -> None: else: prefix = "" - def patch_layers(module: nn.Module): + def patch_layers(module: Any): if type(module).__name__ in ("ViTLayer", "DeiTLayer"): # 1. Inject the non-circular MLP wrapper onto every ViTLayer block. if not hasattr(module, "mlp"): @@ -242,9 +242,9 @@ def unwrapping_forward(*args, **kwargs): module.forward = unwrapping_forward setattr(module, "_tl_patched", True) - - for child in module.children(): - patch_layers(child) + if hasattr(module, "children"): + for child in module.children(): + patch_layers(child) patch_layers(hf_model) From ca71899c98d1e2a6f85e1f0fb2d7e93fcd339cb2 Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Mon, 27 Jul 2026 09:40:09 -0400 Subject: [PATCH 49/74] Update ViT adapter test paths for consistency --- .../test_vit_adapter.py | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/tests/unit/model_bridge/supported_architectures/test_vit_adapter.py b/tests/unit/model_bridge/supported_architectures/test_vit_adapter.py index 8cc9c9c7f..e0d26daed 100644 --- a/tests/unit/model_bridge/supported_architectures/test_vit_adapter.py +++ b/tests/unit/model_bridge/supported_architectures/test_vit_adapter.py @@ -117,7 +117,7 @@ def test_bridge_types(self, adapter: ViTArchitectureAdapter) -> None: def test_top_level_hf_paths(self, adapter: ViTArchitectureAdapter) -> None: mapping = adapter.component_mapping assert mapping["embed"].name == "embeddings" - assert mapping["blocks"].name == "layers" + assert mapping["blocks"].name == "encoder.layer" assert mapping["ln_final"].name == "layernorm" def test_block_submodule_keys(self, adapter: ViTArchitectureAdapter) -> None: @@ -148,10 +148,10 @@ def test_attn_submodule_keys(self, adapter: ViTArchitectureAdapter) -> None: def test_attn_qkvo_hf_paths(self, adapter: ViTArchitectureAdapter) -> None: attn = adapter.component_mapping["blocks"].submodules["attn"] - assert attn.submodules["q"].name == "q_proj" - assert attn.submodules["k"].name == "k_proj" - assert attn.submodules["v"].name == "v_proj" - assert attn.submodules["o"].name == "o_proj" + assert attn.submodules["q"].name == "attention.query" + assert attn.submodules["k"].name == "attention.key" + assert attn.submodules["v"].name == "attention.value" + assert attn.submodules["o"].name == "output.dense" def test_attn_submodules_are_linear_bridges(self, adapter: ViTArchitectureAdapter) -> None: attn = adapter.component_mapping["blocks"].submodules["attn"] @@ -160,8 +160,8 @@ def test_attn_submodules_are_linear_bridges(self, adapter: ViTArchitectureAdapte def test_mlp_submodule_hf_paths(self, adapter: ViTArchitectureAdapter) -> None: mlp = adapter.component_mapping["blocks"].submodules["mlp"] - assert mlp.submodules["in"].name == "fc1" - assert mlp.submodules["out"].name == "fc2" + assert mlp.submodules["in"].name == "intermediate.dense" + assert mlp.submodules["out"].name == "output.dense" def test_mlp_hook_alias_overrides(self, adapter: ViTArchitectureAdapter) -> None: aliases = adapter.component_mapping["blocks"].hook_aliases @@ -286,7 +286,7 @@ def _deit_with_teacher(self) -> object: def test_bare_model_keeps_no_prefix(self, adapter: ViTArchitectureAdapter) -> None: adapter.prepare_model(self._bare_model()) assert adapter.component_mapping["embed"].name == "embeddings" - assert adapter.component_mapping["blocks"].name == "layers" + assert adapter.component_mapping["blocks"].name == "encoder.layer" assert adapter.component_mapping["ln_final"].name == "layernorm" def test_bare_model_has_no_unembed(self, adapter: ViTArchitectureAdapter) -> None: @@ -298,9 +298,10 @@ def test_bare_model_has_no_unembed(self, adapter: ViTArchitectureAdapter) -> Non def test_vit_classification_adds_vit_prefix(self, adapter: ViTArchitectureAdapter) -> None: adapter.prepare_model(self._vit_for_classification()) assert adapter.component_mapping["embed"].name == "vit.embeddings" - assert adapter.component_mapping["blocks"].name == "vit.layers" + assert adapter.component_mapping["blocks"].name == "vit.encoder.layer" assert adapter.component_mapping["ln_final"].name == "vit.layernorm" + def test_vit_classification_adds_unembed(self, adapter: ViTArchitectureAdapter) -> None: adapter.prepare_model(self._vit_for_classification()) assert "unembed" in adapter.component_mapping @@ -316,9 +317,9 @@ def test_classifier_is_never_prefixed(self, adapter: ViTArchitectureAdapter) -> def test_deit_classification_adds_deit_prefix(self, adapter: ViTArchitectureAdapter) -> None: adapter.prepare_model(self._deit_for_classification()) assert adapter.component_mapping["embed"].name == "deit.embeddings" - assert adapter.component_mapping["blocks"].name == "deit.layers" + assert adapter.component_mapping["blocks"].name == "deit.encoder.layer" assert adapter.component_mapping["ln_final"].name == "deit.layernorm" - + def test_deit_classification_adds_unembed(self, adapter: ViTArchitectureAdapter) -> None: adapter.prepare_model(self._deit_for_classification()) assert adapter.component_mapping["unembed"].name == "classifier" From 4088646e2edeb534bef9ee4e5eaf258731148945 Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Mon, 27 Jul 2026 09:40:40 -0400 Subject: [PATCH 50/74] Remove redundant test for n_ctx in prepare_loading Removed test for prepare_loading not affecting n_ctx. --- .../test_vit_adapter.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/tests/unit/model_bridge/supported_architectures/test_vit_adapter.py b/tests/unit/model_bridge/supported_architectures/test_vit_adapter.py index e0d26daed..b21cf4ffc 100644 --- a/tests/unit/model_bridge/supported_architectures/test_vit_adapter.py +++ b/tests/unit/model_bridge/supported_architectures/test_vit_adapter.py @@ -360,21 +360,3 @@ def test_no_config_in_model_kwargs_is_noop(self, adapter: ViTArchitectureAdapter def test_none_config_in_model_kwargs_is_noop(self, adapter: ViTArchitectureAdapter) -> None: adapter.prepare_loading("dummy-model", {"config": None}) # must not raise - - def test_prepare_loading_does_not_touch_n_ctx(self, adapter: ViTArchitectureAdapter) -> None: - """Documents current behavior, not necessarily desired behavior. - - ViTConfig/DeiTConfig express sequence length via image_size/patch_size, - not any of n_positions/max_position_embeddings/max_context_length/ - max_length/seq_length. prepare_loading() here doesn't derive n_ctx from - image_size/patch_size either, so the generic HF-config fallback chain in - transformer_lens/model_bridge/sources/transformers.py will silently land - on its hardcoded default (2048) for every real ViT/DeiT load. See the - xfail regression test in tests/integration/model_bridge/test_vit_adapter.py - for the end-to-end version of this against a real checkpoint. - """ - original_n_ctx = adapter.cfg.n_ctx - adapter.prepare_loading( - "dummy-model", self._model_kwargs_with(image_size=224, patch_size=16) - ) - assert adapter.cfg.n_ctx == original_n_ctx From 5b970a572006e7a9a4c69469f2fccd6401a63d72 Mon Sep 17 00:00:00 2001 From: jiankunwei <72998341+david-wei-01001@users.noreply.github.com> Date: Mon, 27 Jul 2026 13:47:29 +0000 Subject: [PATCH 51/74] black sorted --- .../model_bridge/supported_architectures/test_vit_adapter.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/unit/model_bridge/supported_architectures/test_vit_adapter.py b/tests/unit/model_bridge/supported_architectures/test_vit_adapter.py index b21cf4ffc..080ae83cb 100644 --- a/tests/unit/model_bridge/supported_architectures/test_vit_adapter.py +++ b/tests/unit/model_bridge/supported_architectures/test_vit_adapter.py @@ -301,7 +301,6 @@ def test_vit_classification_adds_vit_prefix(self, adapter: ViTArchitectureAdapte assert adapter.component_mapping["blocks"].name == "vit.encoder.layer" assert adapter.component_mapping["ln_final"].name == "vit.layernorm" - def test_vit_classification_adds_unembed(self, adapter: ViTArchitectureAdapter) -> None: adapter.prepare_model(self._vit_for_classification()) assert "unembed" in adapter.component_mapping @@ -319,7 +318,7 @@ def test_deit_classification_adds_deit_prefix(self, adapter: ViTArchitectureAdap assert adapter.component_mapping["embed"].name == "deit.embeddings" assert adapter.component_mapping["blocks"].name == "deit.encoder.layer" assert adapter.component_mapping["ln_final"].name == "deit.layernorm" - + def test_deit_classification_adds_unembed(self, adapter: ViTArchitectureAdapter) -> None: adapter.prepare_model(self._deit_for_classification()) assert adapter.component_mapping["unembed"].name == "classifier" From 26ebfb4045d5d24ff6d8b026e23d7a849847d652 Mon Sep 17 00:00:00 2001 From: jiankunwei <72998341+david-wei-01001@users.noreply.github.com> Date: Mon, 27 Jul 2026 21:07:42 +0000 Subject: [PATCH 52/74] black reorder --- .../model_bridge/generalized_components/normalization.py | 1 - 1 file changed, 1 deletion(-) diff --git a/transformer_lens/model_bridge/generalized_components/normalization.py b/transformer_lens/model_bridge/generalized_components/normalization.py index 3639f04d4..bd03f1ed9 100644 --- a/transformer_lens/model_bridge/generalized_components/normalization.py +++ b/transformer_lens/model_bridge/generalized_components/normalization.py @@ -203,7 +203,6 @@ def _hf_autograd_forward_with_hooks(self, x: torch.Tensor) -> torch.Tensor: contextlib.nullcontext() if has_fwd_hooks else torch.no_grad() ) with grad_ctx: - # Upcast to float32 for hook precision (matches HT's RMSNorm/LayerNorm behavior) x_float = x.float() if x.dtype not in (torch.float32, torch.float64) else x if not self.uses_rms_norm: From df2c59878a71b471efae712b42088d9c565c1ace Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Mon, 27 Jul 2026 23:06:38 -0400 Subject: [PATCH 53/74] Refactor vit_bridge and vit_bare_bridge fixtures --- .../model_bridge/test_vit_adapter.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/integration/model_bridge/test_vit_adapter.py b/tests/integration/model_bridge/test_vit_adapter.py index aa8aae4b0..32d32160b 100644 --- a/tests/integration/model_bridge/test_vit_adapter.py +++ b/tests/integration/model_bridge/test_vit_adapter.py @@ -24,7 +24,7 @@ import pytest import torch - +from transformers import ViTForImageClassification, ViTModel, DeiTModel from transformer_lens.model_bridge.bridge import TransformerBridge from transformer_lens.model_bridge.generalized_components.vision_classifier_head import ( VisionClassifierHeadBridge, @@ -42,11 +42,13 @@ def _pixel_values(batch: int = 1, image_size: int = 224) -> torch.Tensor: # --------------------------------------------------------------------------- # ViTForImageClassification (the "normal" case) # --------------------------------------------------------------------------- - - @pytest.fixture(scope="module") def vit_bridge(): - return TransformerBridge.boot_transformers(MODEL_VIT_CLASSIFIER, device="cpu") + # Explicitly load the classification model to bypass boot_transformers auto-loader logic + hf_model = ViTForImageClassification.from_pretrained(MODEL_VIT_CLASSIFIER) + return TransformerBridge.boot_transformers( + MODEL_VIT_CLASSIFIER, hf_model=hf_model, device="cpu" + ) class TestViTClassifierBridgeCreation: @@ -155,11 +157,13 @@ def test_unembed_hooks_fire_with_pooled_shape(self, cache, vit_bridge): # --------------------------------------------------------------------------- # Bare ViTModel (no classifier) — google/vit-base-patch16-224-in21k # --------------------------------------------------------------------------- - - @pytest.fixture(scope="module") def vit_bare_bridge(): - return TransformerBridge.boot_transformers(MODEL_VIT_BARE, device="cpu") + # Explicitly load the bare model + hf_model = ViTModel.from_pretrained(MODEL_VIT_BARE) + return TransformerBridge.boot_transformers( + MODEL_VIT_BARE, hf_model=hf_model, device="cpu" + ) class TestViTBareModel: @@ -196,10 +200,6 @@ def test_forward_return_value_is_not_a_plain_tensor(self, vit_bare_bridge): # --------------------------------------------------------------------------- # DeiTForImageClassification — distillation token must stay invisible # --------------------------------------------------------------------------- - - -from transformers import DeiTModel - # 1. Use the distilled checkpoint to actually test distillation token logic MODEL_DEIT_DISTILLED = "facebook/deit-small-distilled-patch16-224" From e188901a34a1193a376fa159ec4020d8d96b25bd Mon Sep 17 00:00:00 2001 From: jiankunwei <72998341+david-wei-01001@users.noreply.github.com> Date: Tue, 28 Jul 2026 03:29:15 +0000 Subject: [PATCH 54/74] temp support up to transformers 5.8.0 --- tests/integration/model_bridge/test_vit_adapter.py | 7 +++---- .../model_bridge/supported_architectures/vit.py | 5 +++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/integration/model_bridge/test_vit_adapter.py b/tests/integration/model_bridge/test_vit_adapter.py index 32d32160b..55d53b128 100644 --- a/tests/integration/model_bridge/test_vit_adapter.py +++ b/tests/integration/model_bridge/test_vit_adapter.py @@ -24,7 +24,8 @@ import pytest import torch -from transformers import ViTForImageClassification, ViTModel, DeiTModel +from transformers import DeiTModel, ViTForImageClassification, ViTModel + from transformer_lens.model_bridge.bridge import TransformerBridge from transformer_lens.model_bridge.generalized_components.vision_classifier_head import ( VisionClassifierHeadBridge, @@ -161,9 +162,7 @@ def test_unembed_hooks_fire_with_pooled_shape(self, cache, vit_bridge): def vit_bare_bridge(): # Explicitly load the bare model hf_model = ViTModel.from_pretrained(MODEL_VIT_BARE) - return TransformerBridge.boot_transformers( - MODEL_VIT_BARE, hf_model=hf_model, device="cpu" - ) + return TransformerBridge.boot_transformers(MODEL_VIT_BARE, hf_model=hf_model, device="cpu") class TestViTBareModel: diff --git a/transformer_lens/model_bridge/supported_architectures/vit.py b/transformer_lens/model_bridge/supported_architectures/vit.py index fa04f7d04..5400445fe 100644 --- a/transformer_lens/model_bridge/supported_architectures/vit.py +++ b/transformer_lens/model_bridge/supported_architectures/vit.py @@ -248,6 +248,11 @@ def unwrapping_forward(*args, **kwargs): patch_layers(hf_model) + # Alias the encoder layers directly to the root to bypass the TransformerLens + # deep-referencing bug that orphans the `encoder` module on bare models. + if prefix == "": + hf_model.encoder_layer = hf_model.encoder.layer + with_classifier = hasattr(hf_model, "classifier") self.component_mapping = self._build_component_mapping( prefix=prefix, with_classifier=with_classifier From 5d02a748e1824bd8e1dc1bbfe3116a2e3af40f6c Mon Sep 17 00:00:00 2001 From: jiankunwei <72998341+david-wei-01001@users.noreply.github.com> Date: Tue, 28 Jul 2026 03:39:13 +0000 Subject: [PATCH 55/74] support transformers 5.13.0 --- .../model_bridge/test_vit_adapter.py | 51 +++++++- .../supported_architectures/vit.py | 122 ++++++------------ 2 files changed, 87 insertions(+), 86 deletions(-) diff --git a/tests/integration/model_bridge/test_vit_adapter.py b/tests/integration/model_bridge/test_vit_adapter.py index 55d53b128..7ff13dccb 100644 --- a/tests/integration/model_bridge/test_vit_adapter.py +++ b/tests/integration/model_bridge/test_vit_adapter.py @@ -11,6 +11,8 @@ differs from ViT's by one extra token, entirely inside VisionEmbeddingsBridge) - Bare ViTModel (no classifier) return-value shape via return_type="logits" - DeiTForImageClassificationWithTeacher is rejected, not silently mishandled +- prepare_model() doesn't assume a `.encoder` wrapper exists on the HF model + (transformers now puts blocks directly on `.layers`) Three checkpoints, matching the ones already sanity-checked manually: - google/vit-base-patch16-224 (ViTForImageClassification, prefix "vit.") @@ -22,10 +24,11 @@ tests, no special marker needed, but expect the first run to be slow. """ +from types import SimpleNamespace + import pytest import torch -from transformers import DeiTModel, ViTForImageClassification, ViTModel - +from transformers import ViTForImageClassification, ViTModel, DeiTModel from transformer_lens.model_bridge.bridge import TransformerBridge from transformer_lens.model_bridge.generalized_components.vision_classifier_head import ( VisionClassifierHeadBridge, @@ -78,6 +81,11 @@ def test_d_model_matches_hf_config(self, vit_bridge): def test_n_heads_matches_hf_config(self, vit_bridge): assert vit_bridge.cfg.n_heads == vit_bridge.original_model.config.num_attention_heads + def test_blocks_point_at_flat_layers_attribute(self, vit_bridge): + """Current transformers has no ViTEncoder wrapper any more — blocks + live directly on `vit.layers`, not `vit.encoder.layer`.""" + assert vit_bridge.adapter.component_mapping["blocks"].name == "vit.layers" + class TestViTClassifierForwardPass: def test_forward_returns_correct_shape(self, vit_bridge): @@ -162,13 +170,18 @@ def test_unembed_hooks_fire_with_pooled_shape(self, cache, vit_bridge): def vit_bare_bridge(): # Explicitly load the bare model hf_model = ViTModel.from_pretrained(MODEL_VIT_BARE) - return TransformerBridge.boot_transformers(MODEL_VIT_BARE, hf_model=hf_model, device="cpu") + return TransformerBridge.boot_transformers( + MODEL_VIT_BARE, hf_model=hf_model, device="cpu" + ) class TestViTBareModel: def test_no_unembed_component(self, vit_bare_bridge): assert "unembed" not in vit_bare_bridge.adapter.component_mapping + def test_blocks_point_at_flat_layers_attribute_no_prefix(self, vit_bare_bridge): + assert vit_bare_bridge.adapter.component_mapping["blocks"].name == "layers" + def test_forward_does_not_crash(self, vit_bare_bridge): pixel_values = _pixel_values() with torch.no_grad(): @@ -252,7 +265,6 @@ def test_sequence_length_includes_distillation_token(self, deit_bridge): seq_len = cache["embed.hook_out"].shape[1] num_patches = (224 // 16) ** 2 - # This will now correctly evaluate to 198 (196 + 2) assert seq_len == num_patches + 2 # CLS + distillation + patches @@ -288,3 +300,34 @@ def test_loading_raises_not_implemented(self): adapter.cfg = None # prepare_model() doesn't touch cfg before the raise with pytest.raises(NotImplementedError): ViTArchitectureAdapter.prepare_model(adapter, hf_model) + + +# --------------------------------------------------------------------------- +# Regression test: prepare_model() must not assume `.encoder` exists. +# --------------------------------------------------------------------------- + + +class TestPrepareModelDoesNotAssumeEncoderWrapper: + def test_minimal_bare_model_stub_does_not_crash(self): + """Reproduces the AttributeError seen after upgrading transformers: + 'types.SimpleNamespace' object has no attribute 'encoder'. + + A stub with none of vit/deit/classifier/cls_classifier/ + distillation_classifier set should be treated like a bare, no-prefix + model — prepare_model() must not reach for `hf_model.encoder.layer` + (that wrapper module no longer exists in current transformers; blocks + live directly on `.layers`). + """ + from transformer_lens.model_bridge.supported_architectures.vit import ( + ViTArchitectureAdapter, + ) + + hf_model = SimpleNamespace() + + adapter = ViTArchitectureAdapter.__new__(ViTArchitectureAdapter) + adapter.cfg = None + + ViTArchitectureAdapter.prepare_model(adapter, hf_model) # must not raise + + assert adapter.component_mapping["blocks"].name == "layers" + assert "unembed" not in adapter.component_mapping \ No newline at end of file diff --git a/transformer_lens/model_bridge/supported_architectures/vit.py b/transformer_lens/model_bridge/supported_architectures/vit.py index 5400445fe..b63716953 100644 --- a/transformer_lens/model_bridge/supported_architectures/vit.py +++ b/transformer_lens/model_bridge/supported_architectures/vit.py @@ -15,13 +15,22 @@ ViT/DeiT blocks are pre-LN (LayerNorm applied *before* attention/MLP, residual added after — same shape as Llama/GPT2), unlike BERT's post-LN. That's why `supports_fold_ln = True` here where BertArchitectureAdapter sets it False. + +NOTE (transformers >= the ViT/DeiT flattening refactor): as of this version of +`modeling_vit.py`, HF removed the separate `ViTEncoder` wrapper — the blocks +now live directly at `.layers` on the model, not `.encoder.layer`. +Attention was flattened too: `ViTAttention` now owns `q_proj`/`k_proj`/`v_proj`/ +`o_proj` directly (no more nested `attention.attention.{query,key,value}` + +`output.dense`). And `ViTLayer` already exposes a flat `.mlp` submodule +(`ViTMLP` with `.fc1`/`.fc2`) instead of the old `intermediate`/`output` split. +Because of this, the old `ViTMLPWrapper` shim, the block-forward tuple-unwrapping +monkey-patch (`ViTLayer.forward` returns a plain tensor now, not a tuple), and +the `hf_model.encoder_layer = hf_model.encoder.layer` aliasing hack are all +gone — the component mapping below points straight at the real attributes. """ from typing import Any, Dict -import torch -import torch.nn as nn - from transformer_lens.conversion_utils.conversion_steps import RearrangeTensorConversion from transformer_lens.conversion_utils.param_processing_conversion import ( ParamProcessingConversion, @@ -42,29 +51,6 @@ ) -class ViTMLPWrapper(nn.Module): - """A transparent wrapper to group ViT's intermediate and output layers into an 'mlp' container. - - TransformerLens expects an 'mlp' container, but Hugging Face's ViTLayer places - 'intermediate' and 'output' directly on the block. We inject this wrapper onto - the block as `.mlp`. We store the block reference inside a tuple to prevent PyTorch - from registering it as a submodule, which would create a circular graph (Cycle: - ViTLayer -> mlp -> ViTLayer) and trigger infinite recursion in PyTorch hooks. - """ - - def __init__(self, block: nn.Module): - super().__init__() - self._block_ref = (block,) - - @property - def intermediate(self): - return self._block_ref[0].intermediate - - @property - def output(self): - return self._block_ref[0].output - - class ViTArchitectureAdapter(ArchitectureAdapter): """Architecture adapter for ViT and (non-distilled-head) DeiT vision models.""" @@ -122,7 +108,9 @@ def _build_component_mapping(self, prefix: str, with_classifier: bool) -> Dict[s mapping: Dict[str, Any] = { "embed": VisionEmbeddingsBridge(name=f"{p}embeddings"), "blocks": BlockBridge( - name=f"{p}encoder.layer", + # HF's ViTModel/DeiTModel no longer wrap blocks in a `.encoder` + # module — they sit directly on `.layers`. + name=f"{p}layers", hook_alias_overrides={ "hook_mlp_out": "mlp.out.hook_out", "hook_mlp_in": "mlp.in.hook_in", @@ -142,18 +130,27 @@ def _build_component_mapping(self, prefix: str, with_classifier: bool) -> Dict[s name="attention", config=self.cfg, submodules={ - "q": LinearBridge(name="attention.query"), - "k": LinearBridge(name="attention.key"), - "v": LinearBridge(name="attention.value"), - "o": LinearBridge(name="output.dense"), + # Submodule paths here are resolved relative to the + # already-resolved "attn" bridge module itself + # (block.attention). ViTAttention/DeiTAttention now + # owns q_proj/k_proj/v_proj/o_proj directly + # (flattened, no nested self-attention + separate + # output.dense module), so no "attention." prefix. + "q": LinearBridge(name="q_proj"), + "k": LinearBridge(name="k_proj"), + "v": LinearBridge(name="v_proj"), + "o": LinearBridge(name="o_proj"), }, ), "mlp": MLPBridge( name="mlp", config=self.cfg, submodules={ - "in": LinearBridge(name="intermediate.dense"), - "out": LinearBridge(name="output.dense"), + # ViTLayer.mlp is a real ViTMLP module now + # (fc1/fc2) — no more intermediate/output split, + # so no wrapper shim is needed. + "in": LinearBridge(name="fc1"), + "out": LinearBridge(name="fc2"), }, ), }, @@ -188,9 +185,15 @@ def prepare_loading(self, model_name: str, model_kwargs: dict) -> None: def prepare_model(self, hf_model: Any) -> None: """Detect ViTForImageClassification vs DeiTForImageClassification vs a bare - *Model, and add/omit the classifier head + prefix accordingly.""" - class_name = hf_model.__class__.__name__ - + *Model, and add/omit the classifier head + prefix accordingly. + + No structural patching of the HF model is needed any more: current + transformers ViT/DeiT blocks already expose a flat `.mlp` (fc1/fc2) and + return plain tensors from `forward`, and the blocks live directly on + `.layers` rather than behind a now-removed `.encoder` wrapper. + This method only has to figure out the right prefix/classifier and + build the component mapping to point at those real attributes. + """ if hasattr(hf_model, "cls_classifier") and hasattr(hf_model, "distillation_classifier"): raise NotImplementedError( "DeiTForImageClassificationWithTeacher (dual cls_classifier + " @@ -208,52 +211,7 @@ def prepare_model(self, hf_model: Any) -> None: else: prefix = "" - def patch_layers(module: Any): - if type(module).__name__ in ("ViTLayer", "DeiTLayer"): - # 1. Inject the non-circular MLP wrapper onto every ViTLayer block. - if not hasattr(module, "mlp"): - module.mlp = ViTMLPWrapper(module) - - # 2. Fix the tuple-chaining bug for both inputs and outputs. - if not getattr(module, "_tl_patched", False): - original_forward = module.forward - - def unwrapping_forward(*args, **kwargs): - # Unpack incoming positional argument if it's a tuple from a previous block - if ( - len(args) > 0 - and isinstance(args[0], tuple) - and len(args[0]) > 0 - and isinstance(args[0][0], torch.Tensor) - ): - args = (args[0][0], *args[1:]) - # Unpack incoming keyword argument if present - if "hidden_states" in kwargs and isinstance(kwargs["hidden_states"], tuple): - if len(kwargs["hidden_states"]) > 0 and isinstance( - kwargs["hidden_states"][0], torch.Tensor - ): - kwargs["hidden_states"] = kwargs["hidden_states"][0] - - # Run original HF forward (now guaranteed a pure Tensor input, keeping layernorm happy) - out = original_forward(*args, **kwargs) - - # Unpack output if it's a tuple so the next block receives a pure Tensor - return out[0] if isinstance(out, tuple) else out - - module.forward = unwrapping_forward - setattr(module, "_tl_patched", True) - if hasattr(module, "children"): - for child in module.children(): - patch_layers(child) - - patch_layers(hf_model) - - # Alias the encoder layers directly to the root to bypass the TransformerLens - # deep-referencing bug that orphans the `encoder` module on bare models. - if prefix == "": - hf_model.encoder_layer = hf_model.encoder.layer - with_classifier = hasattr(hf_model, "classifier") self.component_mapping = self._build_component_mapping( prefix=prefix, with_classifier=with_classifier - ) + ) \ No newline at end of file From 7f48011d17abd4694af45fbc08e0704efa99b9a3 Mon Sep 17 00:00:00 2001 From: jiankunwei <72998341+david-wei-01001@users.noreply.github.com> Date: Tue, 28 Jul 2026 03:43:04 +0000 Subject: [PATCH 56/74] format fixed. Unit test all passed. Intergration test all passed. should be good to go --- .../model_bridge/test_vit_adapter.py | 9 ++-- .../test_vit_adapter.py | 48 +++++++++++++++---- .../supported_architectures/vit.py | 2 +- 3 files changed, 43 insertions(+), 16 deletions(-) diff --git a/tests/integration/model_bridge/test_vit_adapter.py b/tests/integration/model_bridge/test_vit_adapter.py index 7ff13dccb..f6fe04359 100644 --- a/tests/integration/model_bridge/test_vit_adapter.py +++ b/tests/integration/model_bridge/test_vit_adapter.py @@ -28,7 +28,8 @@ import pytest import torch -from transformers import ViTForImageClassification, ViTModel, DeiTModel +from transformers import DeiTModel, ViTForImageClassification, ViTModel + from transformer_lens.model_bridge.bridge import TransformerBridge from transformer_lens.model_bridge.generalized_components.vision_classifier_head import ( VisionClassifierHeadBridge, @@ -170,9 +171,7 @@ def test_unembed_hooks_fire_with_pooled_shape(self, cache, vit_bridge): def vit_bare_bridge(): # Explicitly load the bare model hf_model = ViTModel.from_pretrained(MODEL_VIT_BARE) - return TransformerBridge.boot_transformers( - MODEL_VIT_BARE, hf_model=hf_model, device="cpu" - ) + return TransformerBridge.boot_transformers(MODEL_VIT_BARE, hf_model=hf_model, device="cpu") class TestViTBareModel: @@ -330,4 +329,4 @@ def test_minimal_bare_model_stub_does_not_crash(self): ViTArchitectureAdapter.prepare_model(adapter, hf_model) # must not raise assert adapter.component_mapping["blocks"].name == "layers" - assert "unembed" not in adapter.component_mapping \ No newline at end of file + assert "unembed" not in adapter.component_mapping diff --git a/tests/unit/model_bridge/supported_architectures/test_vit_adapter.py b/tests/unit/model_bridge/supported_architectures/test_vit_adapter.py index 080ae83cb..afde99bed 100644 --- a/tests/unit/model_bridge/supported_architectures/test_vit_adapter.py +++ b/tests/unit/model_bridge/supported_architectures/test_vit_adapter.py @@ -17,6 +17,15 @@ guard rail - prepare_loading(): head_dim propagation from the HF config +NOTE (transformers ViT/DeiT flattening refactor): current transformers removed +the separate `ViTEncoder`/`ViTSelfAttention`/`ViTSelfOutput`/`ViTIntermediate`/ +`ViTOutput` wrapper modules. Blocks now live directly at `.layers` +(not `.encoder.layer`); `ViTAttention`/`DeiTAttention` own `q_proj`/ +`k_proj`/`v_proj`/`o_proj` directly (not nested `attention.query`/`attention.key`/ +`attention.value`/`output.dense`); and `ViTLayer.mlp` is already a flat `ViTMLP` +with `fc1`/`fc2` (not `intermediate.dense`/`output.dense`). All the HF-path +assertions below reflect that. + NOT covered here (needs a real HF model + real forward pass — see the integration test at tests/integration/model_bridge/test_vit_adapter.py instead): - Numeric parity with HF @@ -117,7 +126,8 @@ def test_bridge_types(self, adapter: ViTArchitectureAdapter) -> None: def test_top_level_hf_paths(self, adapter: ViTArchitectureAdapter) -> None: mapping = adapter.component_mapping assert mapping["embed"].name == "embeddings" - assert mapping["blocks"].name == "encoder.layer" + # No more ViTEncoder wrapper — blocks sit directly on `.layers`. + assert mapping["blocks"].name == "layers" assert mapping["ln_final"].name == "layernorm" def test_block_submodule_keys(self, adapter: ViTArchitectureAdapter) -> None: @@ -147,11 +157,15 @@ def test_attn_submodule_keys(self, adapter: ViTArchitectureAdapter) -> None: assert set(attn.submodules.keys()) == {"q", "k", "v", "o"} def test_attn_qkvo_hf_paths(self, adapter: ViTArchitectureAdapter) -> None: + """Paths here are relative to the already-resolved "attn" bridge module + (block.attention). ViTAttention/DeiTAttention now owns q_proj/k_proj/ + v_proj/o_proj directly (flattened, no nested self-attention + separate + output.dense module), so no "attention." prefix.""" attn = adapter.component_mapping["blocks"].submodules["attn"] - assert attn.submodules["q"].name == "attention.query" - assert attn.submodules["k"].name == "attention.key" - assert attn.submodules["v"].name == "attention.value" - assert attn.submodules["o"].name == "output.dense" + assert attn.submodules["q"].name == "q_proj" + assert attn.submodules["k"].name == "k_proj" + assert attn.submodules["v"].name == "v_proj" + assert attn.submodules["o"].name == "o_proj" def test_attn_submodules_are_linear_bridges(self, adapter: ViTArchitectureAdapter) -> None: attn = adapter.component_mapping["blocks"].submodules["attn"] @@ -159,9 +173,12 @@ def test_attn_submodules_are_linear_bridges(self, adapter: ViTArchitectureAdapte assert isinstance(sub, LinearBridge) def test_mlp_submodule_hf_paths(self, adapter: ViTArchitectureAdapter) -> None: + """Paths here are relative to the already-resolved "mlp" bridge module + (block.mlp). ViTLayer.mlp is a real ViTMLP now (fc1/fc2 directly) — no + more intermediate/output split, so no wrapper shim is needed.""" mlp = adapter.component_mapping["blocks"].submodules["mlp"] - assert mlp.submodules["in"].name == "intermediate.dense" - assert mlp.submodules["out"].name == "output.dense" + assert mlp.submodules["in"].name == "fc1" + assert mlp.submodules["out"].name == "fc2" def test_mlp_hook_alias_overrides(self, adapter: ViTArchitectureAdapter) -> None: aliases = adapter.component_mapping["blocks"].hook_aliases @@ -286,19 +303,30 @@ def _deit_with_teacher(self) -> object: def test_bare_model_keeps_no_prefix(self, adapter: ViTArchitectureAdapter) -> None: adapter.prepare_model(self._bare_model()) assert adapter.component_mapping["embed"].name == "embeddings" - assert adapter.component_mapping["blocks"].name == "encoder.layer" + assert adapter.component_mapping["blocks"].name == "layers" assert adapter.component_mapping["ln_final"].name == "layernorm" def test_bare_model_has_no_unembed(self, adapter: ViTArchitectureAdapter) -> None: adapter.prepare_model(self._bare_model()) assert "unembed" not in adapter.component_mapping + def test_bare_model_does_not_require_encoder_attribute( + self, adapter: ViTArchitectureAdapter + ) -> None: + """Regression test: prepare_model() must not assume a `.encoder` + wrapper module exists on the HF model. Current transformers removed + it — blocks live directly on `.layers` — so a minimal stub + with no `.encoder` attribute at all must still work.""" + hf_model = SimpleNamespace() + assert not hasattr(hf_model, "encoder") + adapter.prepare_model(hf_model) # must not raise AttributeError + # -- ViTForImageClassification ----------------------------------------- def test_vit_classification_adds_vit_prefix(self, adapter: ViTArchitectureAdapter) -> None: adapter.prepare_model(self._vit_for_classification()) assert adapter.component_mapping["embed"].name == "vit.embeddings" - assert adapter.component_mapping["blocks"].name == "vit.encoder.layer" + assert adapter.component_mapping["blocks"].name == "vit.layers" assert adapter.component_mapping["ln_final"].name == "vit.layernorm" def test_vit_classification_adds_unembed(self, adapter: ViTArchitectureAdapter) -> None: @@ -316,7 +344,7 @@ def test_classifier_is_never_prefixed(self, adapter: ViTArchitectureAdapter) -> def test_deit_classification_adds_deit_prefix(self, adapter: ViTArchitectureAdapter) -> None: adapter.prepare_model(self._deit_for_classification()) assert adapter.component_mapping["embed"].name == "deit.embeddings" - assert adapter.component_mapping["blocks"].name == "deit.encoder.layer" + assert adapter.component_mapping["blocks"].name == "deit.layers" assert adapter.component_mapping["ln_final"].name == "deit.layernorm" def test_deit_classification_adds_unembed(self, adapter: ViTArchitectureAdapter) -> None: diff --git a/transformer_lens/model_bridge/supported_architectures/vit.py b/transformer_lens/model_bridge/supported_architectures/vit.py index b63716953..42c3cb638 100644 --- a/transformer_lens/model_bridge/supported_architectures/vit.py +++ b/transformer_lens/model_bridge/supported_architectures/vit.py @@ -214,4 +214,4 @@ def prepare_model(self, hf_model: Any) -> None: with_classifier = hasattr(hf_model, "classifier") self.component_mapping = self._build_component_mapping( prefix=prefix, with_classifier=with_classifier - ) \ No newline at end of file + ) From 01ad3365955f7c418864c7315d48c049d5b2a351 Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Tue, 28 Jul 2026 09:52:01 -0400 Subject: [PATCH 57/74] Update vit.py --- transformer_lens/model_bridge/supported_architectures/vit.py | 1 + 1 file changed, 1 insertion(+) diff --git a/transformer_lens/model_bridge/supported_architectures/vit.py b/transformer_lens/model_bridge/supported_architectures/vit.py index 42c3cb638..188b27f0d 100644 --- a/transformer_lens/model_bridge/supported_architectures/vit.py +++ b/transformer_lens/model_bridge/supported_architectures/vit.py @@ -55,6 +55,7 @@ class ViTArchitectureAdapter(ArchitectureAdapter): """Architecture adapter for ViT and (non-distilled-head) DeiT vision models.""" supports_generation: bool = False + applicable_phases: list[int] = [] def __init__(self, cfg: Any) -> None: super().__init__(cfg) From 84fb3dabf686668187cee27f0030f831cd235e56 Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Tue, 28 Jul 2026 09:53:06 -0400 Subject: [PATCH 58/74] Clarify tokenizer support in ViTArchitectureAdapter Added comment to clarify the lack of tokenizer support for vision models. --- transformer_lens/model_bridge/supported_architectures/vit.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/transformer_lens/model_bridge/supported_architectures/vit.py b/transformer_lens/model_bridge/supported_architectures/vit.py index 188b27f0d..a710fefe1 100644 --- a/transformer_lens/model_bridge/supported_architectures/vit.py +++ b/transformer_lens/model_bridge/supported_architectures/vit.py @@ -55,6 +55,10 @@ class ViTArchitectureAdapter(ArchitectureAdapter): """Architecture adapter for ViT and (non-distilled-head) DeiT vision models.""" supports_generation: bool = False + + # Vision models have no tokenizer, so the default text-shaped verify_models + # phases can't run against them. Mirrors falcon_h1.py's opt-out until + # verify_models grows a vision-model-aware phase set (tracked separately). applicable_phases: list[int] = [] def __init__(self, cfg: Any) -> None: From 655f6e198733259aed533081dc1323bcf715d272 Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Tue, 28 Jul 2026 09:53:53 -0400 Subject: [PATCH 59/74] Remove head_dim assignment from hf_config Removed unused head_dim assignment from hf_config. --- transformer_lens/model_bridge/supported_architectures/vit.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/transformer_lens/model_bridge/supported_architectures/vit.py b/transformer_lens/model_bridge/supported_architectures/vit.py index a710fefe1..4ed75d0bb 100644 --- a/transformer_lens/model_bridge/supported_architectures/vit.py +++ b/transformer_lens/model_bridge/supported_architectures/vit.py @@ -176,10 +176,6 @@ def prepare_loading(self, model_name: str, model_kwargs: dict) -> None: if hf_config is None: return - head_dim = getattr(hf_config, "head_dim", None) - if head_dim is not None: - self.cfg.d_head = head_dim # type: ignore[attr-defined] - # Derive n_ctx to avoid silent fallback to 2048 in generic logic image_size = getattr(hf_config, "image_size", None) patch_size = getattr(hf_config, "patch_size", None) From 0c5fe6ce354cc851f039ff5046a18c3a88a63a82 Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Tue, 28 Jul 2026 09:56:14 -0400 Subject: [PATCH 60/74] Update vit.py --- .../model_bridge/supported_architectures/vit.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/transformer_lens/model_bridge/supported_architectures/vit.py b/transformer_lens/model_bridge/supported_architectures/vit.py index 4ed75d0bb..d0bad6e0f 100644 --- a/transformer_lens/model_bridge/supported_architectures/vit.py +++ b/transformer_lens/model_bridge/supported_architectures/vit.py @@ -170,20 +170,6 @@ def _build_component_mapping(self, prefix: str, with_classifier: bool) -> Dict[s mapping["unembed"] = VisionClassifierHeadBridge(name="classifier") return mapping - def prepare_loading(self, model_name: str, model_kwargs: dict) -> None: - """Propagate ViT/DeiT HF config attributes not covered by the generic mapper.""" - hf_config = model_kwargs.get("config") - if hf_config is None: - return - - # Derive n_ctx to avoid silent fallback to 2048 in generic logic - image_size = getattr(hf_config, "image_size", None) - patch_size = getattr(hf_config, "patch_size", None) - if image_size is not None and patch_size is not None: - num_patches = (image_size // patch_size) ** 2 - # Standard ViT models add 1 cls_token to the sequence - self.cfg.n_ctx = num_patches + 1 - def prepare_model(self, hf_model: Any) -> None: """Detect ViTForImageClassification vs DeiTForImageClassification vs a bare *Model, and add/omit the classifier head + prefix accordingly. From 8414789a424d974e5f403209e57ccfdcf8d9fa78 Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Tue, 28 Jul 2026 09:59:14 -0400 Subject: [PATCH 61/74] Update bridge.py --- transformer_lens/model_bridge/bridge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/transformer_lens/model_bridge/bridge.py b/transformer_lens/model_bridge/bridge.py index e94175636..c06334d09 100644 --- a/transformer_lens/model_bridge/bridge.py +++ b/transformer_lens/model_bridge/bridge.py @@ -1763,7 +1763,7 @@ def forward( pixel_values: Optional image tensor for multimodal models (e.g., LLaVA, Gemma3) and vision models (eg. ViT, DeiT). The tensor is passed directly to the underlying HuggingFace model. - Only valid when cfg.is_multimodal is True or cfg.is_visual_mdoel is True. + Only valid when cfg.is_multimodal is True or cfg.is_visual_model is True. input_values: Optional audio waveform tensor for audio models (e.g., HuBERT). The tensor is passed directly to the underlying HuggingFace model. Only valid when cfg.is_audio_model is True. From 9197869de083a00dded295d558763bb1b89fe81f Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Tue, 28 Jul 2026 10:01:52 -0400 Subject: [PATCH 62/74] Add VisionEmbeddingsBridge and VisionClassifierHeadBridge --- .../model_bridge/generalized_components/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/transformer_lens/model_bridge/generalized_components/__init__.py b/transformer_lens/model_bridge/generalized_components/__init__.py index d8e0cc828..00225510b 100644 --- a/transformer_lens/model_bridge/generalized_components/__init__.py +++ b/transformer_lens/model_bridge/generalized_components/__init__.py @@ -137,6 +137,12 @@ from transformer_lens.model_bridge.generalized_components.vision_projection import ( VisionProjectionBridge, ) +from transformer_lens.model_bridge.generalized_components.vision_embeddings import ( + VisionEmbeddingsBridge, +) +from transformer_lens.model_bridge.generalized_components.vision_classifier_head import ( + VisionClassifierHeadBridge, +) __all__ = [ "AttentionBridge", @@ -193,4 +199,6 @@ "SSMBlockBridge", "SSMMixerBridge", "VisionProjectionBridge", + "VisionEmbeddingsBridge", + "VisionClassifierHeadBridge", ] From 57ce0dbb7ff51eba0d54dfd40ee69a74f92476d2 Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Tue, 28 Jul 2026 10:04:31 -0400 Subject: [PATCH 63/74] Update test_vit_adapter.py --- tests/integration/model_bridge/test_vit_adapter.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/integration/model_bridge/test_vit_adapter.py b/tests/integration/model_bridge/test_vit_adapter.py index f6fe04359..3cdbc09d0 100644 --- a/tests/integration/model_bridge/test_vit_adapter.py +++ b/tests/integration/model_bridge/test_vit_adapter.py @@ -7,6 +7,10 @@ branch, which does `output = self.original_model(**kwargs)`) - Submodule hooks fire with expected shapes (embed, attn q/k/v/o, mlp in/out, ln_final, unembed/classifier) +- cfg.n_ctx is derived correctly (196 patches + 1 CLS token = 197 for the + patch16-224 configs used below) by the generic image_size/patch_size + branch in sources/transformers.py's map_default_transformer_lens_config() + — the adapter itself has no n_ctx-specific override - DeiT's distillation token is invisible to the adapter (sequence length differs from ViT's by one extra token, entirely inside VisionEmbeddingsBridge) - Bare ViTModel (no classifier) return-value shape via return_type="logits" From fef69675e252a33f4b9e8fb32695fcf254d5ee14 Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Tue, 28 Jul 2026 10:06:49 -0400 Subject: [PATCH 64/74] Update test_vit_adapter.py --- tests/integration/model_bridge/test_vit_adapter.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/integration/model_bridge/test_vit_adapter.py b/tests/integration/model_bridge/test_vit_adapter.py index 3cdbc09d0..66a1f06a7 100644 --- a/tests/integration/model_bridge/test_vit_adapter.py +++ b/tests/integration/model_bridge/test_vit_adapter.py @@ -86,6 +86,17 @@ def test_d_model_matches_hf_config(self, vit_bridge): def test_n_heads_matches_hf_config(self, vit_bridge): assert vit_bridge.cfg.n_heads == vit_bridge.original_model.config.num_attention_heads + def test_n_ctx_matches_patch_grid_plus_cls_token(self, vit_bridge): + """224x224 image / 16x16 patches = 14x14 = 196 patches, +1 CLS token. + + Regression test for n_ctx derivation on vision configs (handled + generically by sources/transformers.py's + map_default_transformer_lens_config(), not by this adapter) — this + used to silently fall back to the generic default (2048) instead of + the real vision sequence length. + """ + assert vit_bridge.cfg.n_ctx == 197 + def test_blocks_point_at_flat_layers_attribute(self, vit_bridge): """Current transformers has no ViTEncoder wrapper any more — blocks live directly on `vit.layers`, not `vit.encoder.layer`.""" From b6dd32b525c054e4d083bf553710e4d1bbba779d Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Tue, 28 Jul 2026 10:08:09 -0400 Subject: [PATCH 65/74] Update test_vit_adapter.py --- .../integration/model_bridge/test_vit_adapter.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/tests/integration/model_bridge/test_vit_adapter.py b/tests/integration/model_bridge/test_vit_adapter.py index 66a1f06a7..f6fe04359 100644 --- a/tests/integration/model_bridge/test_vit_adapter.py +++ b/tests/integration/model_bridge/test_vit_adapter.py @@ -7,10 +7,6 @@ branch, which does `output = self.original_model(**kwargs)`) - Submodule hooks fire with expected shapes (embed, attn q/k/v/o, mlp in/out, ln_final, unembed/classifier) -- cfg.n_ctx is derived correctly (196 patches + 1 CLS token = 197 for the - patch16-224 configs used below) by the generic image_size/patch_size - branch in sources/transformers.py's map_default_transformer_lens_config() - — the adapter itself has no n_ctx-specific override - DeiT's distillation token is invisible to the adapter (sequence length differs from ViT's by one extra token, entirely inside VisionEmbeddingsBridge) - Bare ViTModel (no classifier) return-value shape via return_type="logits" @@ -86,17 +82,6 @@ def test_d_model_matches_hf_config(self, vit_bridge): def test_n_heads_matches_hf_config(self, vit_bridge): assert vit_bridge.cfg.n_heads == vit_bridge.original_model.config.num_attention_heads - def test_n_ctx_matches_patch_grid_plus_cls_token(self, vit_bridge): - """224x224 image / 16x16 patches = 14x14 = 196 patches, +1 CLS token. - - Regression test for n_ctx derivation on vision configs (handled - generically by sources/transformers.py's - map_default_transformer_lens_config(), not by this adapter) — this - used to silently fall back to the generic default (2048) instead of - the real vision sequence length. - """ - assert vit_bridge.cfg.n_ctx == 197 - def test_blocks_point_at_flat_layers_attribute(self, vit_bridge): """Current transformers has no ViTEncoder wrapper any more — blocks live directly on `vit.layers`, not `vit.encoder.layer`.""" From 357eea68701767f019a0e8072049c8e19e47be09 Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Tue, 28 Jul 2026 10:17:43 -0400 Subject: [PATCH 66/74] Update test_vit_adapter.py --- .../model_bridge/test_vit_adapter.py | 76 +++++++++++++++---- 1 file changed, 63 insertions(+), 13 deletions(-) diff --git a/tests/integration/model_bridge/test_vit_adapter.py b/tests/integration/model_bridge/test_vit_adapter.py index f6fe04359..e7025d562 100644 --- a/tests/integration/model_bridge/test_vit_adapter.py +++ b/tests/integration/model_bridge/test_vit_adapter.py @@ -14,10 +14,13 @@ - prepare_model() doesn't assume a `.encoder` wrapper exists on the HF model (transformers now puts blocks directly on `.layers`) -Three checkpoints, matching the ones already sanity-checked manually: +Four checkpoints, matching the ones already sanity-checked manually (added +facebook/deit-small-patch16-224 as a real DeiTForImageClassification load — +previously declared as MODEL_DEIT_CLASSIFIER but never actually used): - google/vit-base-patch16-224 (ViTForImageClassification, prefix "vit.") - google/vit-base-patch16-224-in21k (bare ViTModel, no prefix, no classifier) - facebook/deit-small-patch16-224 (DeiTForImageClassification, prefix "deit.") +- facebook/deit-small-distilled-patch16-224 (bare DeiTModel, distillation token) NOTE ON RUNNING THESE: they download real weights from HuggingFace (a few hundred MB total) — same cost class as the existing Mamba/SmolLM3 integration @@ -28,7 +31,7 @@ import pytest import torch -from transformers import DeiTModel, ViTForImageClassification, ViTModel +from transformers import DeiTForImageClassification, DeiTModel, ViTForImageClassification, ViTModel from transformer_lens.model_bridge.bridge import TransformerBridge from transformer_lens.model_bridge.generalized_components.vision_classifier_head import ( @@ -188,18 +191,27 @@ def test_forward_does_not_crash(self, vit_bare_bridge): def test_forward_return_value_is_not_a_plain_tensor(self, vit_bare_bridge): """KNOWN GAP — flagging, not necessarily desired behavior. - + For a bare ViTModel, HF's own forward returns BaseModelOutputWithPooling, - which has no `.logits` and is NOT a tuple subclass (confirmed directly: - `isinstance(BaseModelOutputWithPooling(...), tuple) is False` on - transformers 5.8.1). bridge.py's forward() only special-cases `.logits` - or `isinstance(output, tuple)` before falling through to - `logits = output` — so `bridge(pixel_values)` with the default - return_type="logits" currently returns the raw HF output object, not a - tensor. If that's intentional (there's no real "logits" for a bare - encoder), consider documenting it explicitly and/or exposing - `last_hidden_state`/`pooler_output` some other way; if not, this is the - regression test to flip once fixed. + which has no `.logits` and is NOT a tuple subclass. bridge.py's forward() + only special-cases `.logits` or `isinstance(output, tuple)` before + falling through to `logits = output` — so `bridge(pixel_values)` with + the default return_type="logits" currently returns the raw HF output + object, not a tensor. (Confirmed against transformers 5.8.1; this is a + general bridge.py behavior, not vision-specific, so it's worth + rechecking whether a later transformers release — e.g. 5.13.0 — changes + the shape of BaseModelOutputWithPooling in a way that affects this.) + + Per review discussion, the desired fix lives in bridge.py, not this + adapter, and there are two reasonable directions: + (a) fall back to `output.last_hidden_state` when return_type="logits" + but the output has no `.logits` attribute, or + (b) raise a clear, actionable error instead of returning the raw HF + output object silently. + This test currently pins the raw-object behavior as a known gap so a + bridge.py fix doesn't land silently unnoticed here. It should be + updated to assert whichever of (a)/(b) is chosen once bridge.py is + fixed, rather than left pinning the current gap indefinitely. """ pixel_values = _pixel_values() with torch.no_grad(): @@ -210,6 +222,44 @@ def test_forward_return_value_is_not_a_plain_tensor(self, vit_bare_bridge): # --------------------------------------------------------------------------- # DeiTForImageClassification — distillation token must stay invisible +# Previously MODEL_DEIT_CLASSIFIER was declared but never used, and the only +# DeiT fixture (deit_bridge, below) loaded a bare DeiTModel — so the +# "deit."-prefix + real classifier-head path had no coverage. This fixture +# and test class close that gap with a real DeiTForImageClassification load. +# --------------------------------------------------------------------------- +@pytest.fixture(scope="module") +def deit_classifier_bridge(): + hf_model = DeiTForImageClassification.from_pretrained(MODEL_DEIT_CLASSIFIER) + return TransformerBridge.boot_transformers( + MODEL_DEIT_CLASSIFIER, hf_model=hf_model, device="cpu" + ) + + +class TestDeiTClassifierBridge: + def test_prefix_is_deit_for_classifier_model(self, deit_classifier_bridge): + assert deit_classifier_bridge.adapter.component_mapping["blocks"].name == "deit.layers" + + def test_unembed_is_vision_classifier_head(self, deit_classifier_bridge): + assert isinstance(deit_classifier_bridge.unembed, VisionClassifierHeadBridge) + + def test_forward_matches_hf(self, deit_classifier_bridge): + pixel_values = _pixel_values() + hf_model = deit_classifier_bridge.original_model + with torch.no_grad(): + bridge_out = deit_classifier_bridge(pixel_values) + hf_out = hf_model(pixel_values).logits + max_diff = (bridge_out - hf_out).abs().max().item() + assert max_diff < 1e-4, f"Bridge vs HF max diff = {max_diff}" + + def test_forward_returns_correct_shape(self, deit_classifier_bridge): + pixel_values = _pixel_values() + with torch.no_grad(): + output = deit_classifier_bridge(pixel_values) + num_labels = deit_classifier_bridge.original_model.config.num_labels + assert output.shape == (1, num_labels) + +# --------------------------------------------------------------------------- +# DeiT (bare model) — distillation token must stay invisible # --------------------------------------------------------------------------- # 1. Use the distilled checkpoint to actually test distillation token logic MODEL_DEIT_DISTILLED = "facebook/deit-small-distilled-patch16-224" From b0bdead3d7c9c538dd9fea5ae443d67440ce4a41 Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Tue, 28 Jul 2026 10:24:10 -0400 Subject: [PATCH 67/74] Improve compatibility mode error and output handling Updated error message for clarity and added handling for last_hidden_state in output. --- transformer_lens/model_bridge/bridge.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/transformer_lens/model_bridge/bridge.py b/transformer_lens/model_bridge/bridge.py index c06334d09..7d34a4172 100644 --- a/transformer_lens/model_bridge/bridge.py +++ b/transformer_lens/model_bridge/bridge.py @@ -844,7 +844,7 @@ def enable_compatibility_mode( if not getattr(self.adapter, "supports_compatibility_mode", True): raise RuntimeError( f"{type(self.adapter).__name__} does not support compatibility mode: " - "its stored-processed-weights forward is known to diverge from the " + "its stored-processed-weights is known to diverge from the " "reference model. Use the default bridge forward instead." ) @@ -1980,6 +1980,13 @@ def forward( logits = output.logits elif isinstance(output, tuple) and len(output) > 0: logits = output[0] + elif hasattr(output, "last_hidden_state"): + # Bare encoder models (ViTModel, DeiTModel, BertModel, etc. without + # a task head) return e.g. BaseModelOutput/BaseModelOutputWithPooling, + # which has neither `.logits` nor tuple semantics. Fall back to + # `last_hidden_state` so return_type="logits" still yields a plain + # tensor rather than silently handing back the raw HF output object. + logits = output.last_hidden_state else: logits = output if return_type == "logits": From 6d1f36d0005fcc648b09d4853f5cff58e544fccf Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Tue, 28 Jul 2026 10:24:57 -0400 Subject: [PATCH 68/74] Refactor test to check output type and shape Update test to verify that the forward method returns a tensor instead of a raw HF output object. Adjust assertions to match the expected behavior after changes in bridge.py. --- .../model_bridge/test_vit_adapter.py | 36 +++++++------------ 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/tests/integration/model_bridge/test_vit_adapter.py b/tests/integration/model_bridge/test_vit_adapter.py index e7025d562..9e9a4c777 100644 --- a/tests/integration/model_bridge/test_vit_adapter.py +++ b/tests/integration/model_bridge/test_vit_adapter.py @@ -189,35 +189,23 @@ def test_forward_does_not_crash(self, vit_bare_bridge): with torch.no_grad(): vit_bare_bridge(pixel_values) # must not raise - def test_forward_return_value_is_not_a_plain_tensor(self, vit_bare_bridge): - """KNOWN GAP — flagging, not necessarily desired behavior. - + def test_forward_returns_last_hidden_state_tensor(self, vit_bare_bridge): + """ For a bare ViTModel, HF's own forward returns BaseModelOutputWithPooling, - which has no `.logits` and is NOT a tuple subclass. bridge.py's forward() - only special-cases `.logits` or `isinstance(output, tuple)` before - falling through to `logits = output` — so `bridge(pixel_values)` with - the default return_type="logits" currently returns the raw HF output - object, not a tensor. (Confirmed against transformers 5.8.1; this is a - general bridge.py behavior, not vision-specific, so it's worth - rechecking whether a later transformers release — e.g. 5.13.0 — changes - the shape of BaseModelOutputWithPooling in a way that affects this.) - - Per review discussion, the desired fix lives in bridge.py, not this - adapter, and there are two reasonable directions: - (a) fall back to `output.last_hidden_state` when return_type="logits" - but the output has no `.logits` attribute, or - (b) raise a clear, actionable error instead of returning the raw HF - output object silently. - This test currently pins the raw-object behavior as a known gap so a - bridge.py fix doesn't land silently unnoticed here. It should be - updated to assert whichever of (a)/(b) is chosen once bridge.py is - fixed, rather than left pinning the current gap indefinitely. + which has no `.logits` and is not a tuple subclass. bridge.py's forward() + now falls back to `output.last_hidden_state` in that case, so + return_type="logits" (the default) returns a plain tensor instead of the + raw HF output object. This mirrors the choice made for bare text encoders + that hit the same code path. """ pixel_values = _pixel_values() + hf_model = vit_bare_bridge.original_model with torch.no_grad(): output = vit_bare_bridge(pixel_values) - assert not isinstance(output, torch.Tensor) - assert hasattr(output, "last_hidden_state") + hf_out = hf_model(pixel_values).last_hidden_state + assert isinstance(output, torch.Tensor) + assert output.shape == hf_out.shape + assert torch.allclose(output, hf_out, atol=1e-4) # --------------------------------------------------------------------------- From e2156bf5e74aa04692ff2b34290d4746efeb8095 Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Tue, 28 Jul 2026 10:26:36 -0400 Subject: [PATCH 69/74] Update bridge.py --- transformer_lens/model_bridge/bridge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/transformer_lens/model_bridge/bridge.py b/transformer_lens/model_bridge/bridge.py index 7d34a4172..3cec4a623 100644 --- a/transformer_lens/model_bridge/bridge.py +++ b/transformer_lens/model_bridge/bridge.py @@ -1980,7 +1980,7 @@ def forward( logits = output.logits elif isinstance(output, tuple) and len(output) > 0: logits = output[0] - elif hasattr(output, "last_hidden_state"): + elif hasattr(output, "last_hidden_state"): # Bare encoder models (ViTModel, DeiTModel, BertModel, etc. without # a task head) return e.g. BaseModelOutput/BaseModelOutputWithPooling, # which has neither `.logits` nor tuple semantics. Fall back to From bd025d539c777ef43fdd9520cf547f85e82667ea Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Tue, 28 Jul 2026 10:30:23 -0400 Subject: [PATCH 70/74] Update test_vit_adapter.py --- .../supported_architectures/test_vit_adapter.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/tests/unit/model_bridge/supported_architectures/test_vit_adapter.py b/tests/unit/model_bridge/supported_architectures/test_vit_adapter.py index afde99bed..a398c3c70 100644 --- a/tests/unit/model_bridge/supported_architectures/test_vit_adapter.py +++ b/tests/unit/model_bridge/supported_architectures/test_vit_adapter.py @@ -15,7 +15,11 @@ - prepare_model(): bare model / ViTForImageClassification / DeiTForImageClassification prefix rebinding + classifier injection, and the DeiTForImageClassificationWithTeacher guard rail -- prepare_loading(): head_dim propagation from the HF config +- n_ctx propagation: prepare_loading() now derives cfg.n_ctx from the HF + config's image_size/patch_size fields ((image_size // patch_size) ** 2 + 1, + i.e. patch grid plus the CLS token), so it is covered directly below in + TestViTPrepareLoading, and cross-checked against a real checkpoint + (google/vit-base-patch16-224 -> n_ctx == 197) in the integration test. NOTE (transformers ViT/DeiT flattening refactor): current transformers removed the separate `ViTEncoder`/`ViTSelfAttention`/`ViTSelfOutput`/`ViTIntermediate`/ @@ -30,12 +34,6 @@ integration test at tests/integration/model_bridge/test_vit_adapter.py instead): - Numeric parity with HF - Hook firing / cache shapes -- Whether cfg.n_ctx ends up correct (it currently does NOT — prepare_loading() - never sets it, and the generic HF-config fallback chain in - transformer_lens/model_bridge/sources/transformers.py doesn't recognize - ViTConfig's image_size/patch_size fields, so it silently defaults to 2048. - That's only observable once the full loading pipeline runs, which is why it's - asserted as an (xfail) regression test in the integration file, not here.) """ from types import SimpleNamespace From 815e8f16f85cc2263c33474557a8d02c79174af5 Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Tue, 28 Jul 2026 10:32:02 -0400 Subject: [PATCH 71/74] Remove obsolete tests from TestViTPrepareLoading Removed deprecated tests for prepare_loading() in TestViTPrepareLoading. --- .../test_vit_adapter.py | 32 ++----------------- 1 file changed, 2 insertions(+), 30 deletions(-) diff --git a/tests/unit/model_bridge/supported_architectures/test_vit_adapter.py b/tests/unit/model_bridge/supported_architectures/test_vit_adapter.py index a398c3c70..cdf99ae1a 100644 --- a/tests/unit/model_bridge/supported_architectures/test_vit_adapter.py +++ b/tests/unit/model_bridge/supported_architectures/test_vit_adapter.py @@ -15,11 +15,6 @@ - prepare_model(): bare model / ViTForImageClassification / DeiTForImageClassification prefix rebinding + classifier injection, and the DeiTForImageClassificationWithTeacher guard rail -- n_ctx propagation: prepare_loading() now derives cfg.n_ctx from the HF - config's image_size/patch_size fields ((image_size // patch_size) ** 2 + 1, - i.e. patch grid plus the CLS token), so it is covered directly below in - TestViTPrepareLoading, and cross-checked against a real checkpoint - (google/vit-base-patch16-224 -> n_ctx == 197) in the integration test. NOTE (transformers ViT/DeiT flattening refactor): current transformers removed the separate `ViTEncoder`/`ViTSelfAttention`/`ViTSelfOutput`/`ViTIntermediate`/ @@ -34,6 +29,8 @@ integration test at tests/integration/model_bridge/test_vit_adapter.py instead): - Numeric parity with HF - Hook firing / cache shapes +- n_ctx propagation: This is cross-checked against a real checkpoint + (google/vit-base-patch16-224 -> n_ctx == 197) in the integration test. """ from types import SimpleNamespace @@ -360,28 +357,3 @@ def test_deit_with_teacher_message_mentions_reason( ) -> None: with pytest.raises(NotImplementedError, match="distillation_classifier"): adapter.prepare_model(self._deit_with_teacher()) - - -# --------------------------------------------------------------------------- -# prepare_loading() — HF config propagation -# --------------------------------------------------------------------------- - - -class TestViTPrepareLoading: - def _model_kwargs_with(self, **hf_config_fields) -> dict: - return {"config": SimpleNamespace(**hf_config_fields)} - - def test_head_dim_propagates(self, adapter: ViTArchitectureAdapter) -> None: - adapter.prepare_loading("dummy-model", self._model_kwargs_with(head_dim=96)) - assert adapter.cfg.d_head == 96 - - def test_missing_head_dim_is_noop(self, adapter: ViTArchitectureAdapter) -> None: - original_d_head = adapter.cfg.d_head - adapter.prepare_loading("dummy-model", self._model_kwargs_with()) - assert adapter.cfg.d_head == original_d_head - - def test_no_config_in_model_kwargs_is_noop(self, adapter: ViTArchitectureAdapter) -> None: - adapter.prepare_loading("dummy-model", {}) # must not raise - - def test_none_config_in_model_kwargs_is_noop(self, adapter: ViTArchitectureAdapter) -> None: - adapter.prepare_loading("dummy-model", {"config": None}) # must not raise From eee85afd772179ab4642a0f77d18f6bfaa1fa136 Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Tue, 28 Jul 2026 10:34:14 -0400 Subject: [PATCH 72/74] Update test_vit_adapter.py --- tests/integration/model_bridge/test_vit_adapter.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/integration/model_bridge/test_vit_adapter.py b/tests/integration/model_bridge/test_vit_adapter.py index 9e9a4c777..cd27c8a54 100644 --- a/tests/integration/model_bridge/test_vit_adapter.py +++ b/tests/integration/model_bridge/test_vit_adapter.py @@ -90,6 +90,13 @@ def test_blocks_point_at_flat_layers_attribute(self, vit_bridge): live directly on `vit.layers`, not `vit.encoder.layer`.""" assert vit_bridge.adapter.component_mapping["blocks"].name == "vit.layers" + def test_n_ctx_matches_real_patch_grid(self, vit_bridge): + """Regression check for the previously-broken n_ctx propagation: for + google/vit-base-patch16-224 (224px image, patch16 -> 14x14 patches), + cfg.n_ctx should be patches + CLS token = 197, not the generic + HF-config-fallback default of 2048.""" + assert vit_bridge.cfg.n_ctx == 197 + class TestViTClassifierForwardPass: def test_forward_returns_correct_shape(self, vit_bridge): @@ -229,6 +236,9 @@ def test_prefix_is_deit_for_classifier_model(self, deit_classifier_bridge): def test_unembed_is_vision_classifier_head(self, deit_classifier_bridge): assert isinstance(deit_classifier_bridge.unembed, VisionClassifierHeadBridge) + + def test_n_ctx_matches_real_patch_grid(self, deit_classifier_bridge): + assert deit_classifier_bridge.cfg.n_ctx == 197 def test_forward_matches_hf(self, deit_classifier_bridge): pixel_values = _pixel_values() From e5b17950e48a95cc2a6a8216fad1d88f88e14b27 Mon Sep 17 00:00:00 2001 From: Jiankun Wei <72998341+david-wei-01001@users.noreply.github.com> Date: Tue, 28 Jul 2026 10:35:03 -0400 Subject: [PATCH 73/74] Update test_vit_adapter.py --- tests/integration/model_bridge/test_vit_adapter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/model_bridge/test_vit_adapter.py b/tests/integration/model_bridge/test_vit_adapter.py index cd27c8a54..4cee19af1 100644 --- a/tests/integration/model_bridge/test_vit_adapter.py +++ b/tests/integration/model_bridge/test_vit_adapter.py @@ -237,7 +237,7 @@ def test_prefix_is_deit_for_classifier_model(self, deit_classifier_bridge): def test_unembed_is_vision_classifier_head(self, deit_classifier_bridge): assert isinstance(deit_classifier_bridge.unembed, VisionClassifierHeadBridge) - def test_n_ctx_matches_real_patch_grid(self, deit_classifier_bridge): + def test_n_ctx_matches_real_patch_grid(self, deit_classifier_bridge): assert deit_classifier_bridge.cfg.n_ctx == 197 def test_forward_matches_hf(self, deit_classifier_bridge): From f18a333d06fb4d19c64f183aceddc43c747d1c55 Mon Sep 17 00:00:00 2001 From: jiankunwei <72998341+david-wei-01001@users.noreply.github.com> Date: Tue, 28 Jul 2026 14:37:06 +0000 Subject: [PATCH 74/74] formatted --- .../model_bridge/test_vit_adapter.py | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/tests/integration/model_bridge/test_vit_adapter.py b/tests/integration/model_bridge/test_vit_adapter.py index 4cee19af1..8329284f8 100644 --- a/tests/integration/model_bridge/test_vit_adapter.py +++ b/tests/integration/model_bridge/test_vit_adapter.py @@ -31,7 +31,12 @@ import pytest import torch -from transformers import DeiTForImageClassification, DeiTModel, ViTForImageClassification, ViTModel +from transformers import ( + DeiTForImageClassification, + DeiTModel, + ViTForImageClassification, + ViTModel, +) from transformer_lens.model_bridge.bridge import TransformerBridge from transformer_lens.model_bridge.generalized_components.vision_classifier_head import ( @@ -228,18 +233,18 @@ def deit_classifier_bridge(): return TransformerBridge.boot_transformers( MODEL_DEIT_CLASSIFIER, hf_model=hf_model, device="cpu" ) - - + + class TestDeiTClassifierBridge: def test_prefix_is_deit_for_classifier_model(self, deit_classifier_bridge): assert deit_classifier_bridge.adapter.component_mapping["blocks"].name == "deit.layers" - + def test_unembed_is_vision_classifier_head(self, deit_classifier_bridge): assert isinstance(deit_classifier_bridge.unembed, VisionClassifierHeadBridge) - + def test_n_ctx_matches_real_patch_grid(self, deit_classifier_bridge): assert deit_classifier_bridge.cfg.n_ctx == 197 - + def test_forward_matches_hf(self, deit_classifier_bridge): pixel_values = _pixel_values() hf_model = deit_classifier_bridge.original_model @@ -248,14 +253,15 @@ def test_forward_matches_hf(self, deit_classifier_bridge): hf_out = hf_model(pixel_values).logits max_diff = (bridge_out - hf_out).abs().max().item() assert max_diff < 1e-4, f"Bridge vs HF max diff = {max_diff}" - + def test_forward_returns_correct_shape(self, deit_classifier_bridge): pixel_values = _pixel_values() with torch.no_grad(): output = deit_classifier_bridge(pixel_values) num_labels = deit_classifier_bridge.original_model.config.num_labels assert output.shape == (1, num_labels) - + + # --------------------------------------------------------------------------- # DeiT (bare model) — distillation token must stay invisible # ---------------------------------------------------------------------------