Repository navigation
[Proposal] RoBERTa masked-LM adapter for TransformerBridge #1870
Description
[Proposal] RoBERTa masked-LM adapter for TransformerBridge
Proposal
Add a RobertaArchitectureAdapter so RobertaForMaskedLM checkpoints (roberta-base, roberta-large, distilroberta-base and the many fine-tunes that keep the MLM head) load through TransformerBridge.boot_transformers.
Right now this fails at adapter selection:
ValueError: Unsupported architecture: RobertaForMaskedLM
even though transformer_lens/utilities/architectures.py already lists RobertaForMaskedLM under MASKED_LM_ARCHITECTURES and routes it to the masked-LM loader. The classifier knows about the family; but the factory doesn't.
Motivation
BERT is the only encoder-only text model the bridge supports. For anything that compares pretraining recipes, or reuses an existing probing / patching setup on a model that isn't BERT, RoBERTa is usually the next model people reach for, and it's currently a dead end in both HookedEncoder and the bridge. The concrete case is running the same activation-patching or probing code on bert-base-cased, roberta-base and distilroberta-base without changing anything but the model id.
I searched issues and PRs for "roberta" (open and closed) and found nothing. #918 added DistilBERT to the legacy loading_from_pretrained path and was closed unmerged in May 2025; it never touched the bridge, so I don't think it counts as prior art here.
Pitch
This is close to a prefix rename of the existing BERT adapter. I instantiated a tiny RobertaForMaskedLM and diffed its module tree against what bert.py maps:
| BERT | RoBERTa |
|---|---|
bert.embeddings.word_embeddings |
roberta.embeddings.word_embeddings |
bert.embeddings.position_embeddings |
roberta.embeddings.position_embeddings |
bert.encoder.layer |
roberta.encoder.layer |
cls.predictions.transform.LayerNorm |
lm_head.layer_norm |
cls.predictions.decoder |
lm_head.decoder |
Everything inside a layer (attention.self.{query,key,value}, attention.output.dense, both LayerNorms, intermediate.dense, output.dense) is identical, so the per-block submodules, the q/k/v/o rearrange conversions, supports_fold_ln = False for post-LN and supports_generation = False all carry over as-is. The decoder weight is tied to the word embeddings in both models.
So the plan would be:
supported_architectures/roberta.pywithRobertaArchitectureAdapter(BertArchitectureAdapter)that only overridescomponent_mappingwith the new names. Same shape asOlmo3ArchitectureAdapter(Olmo2ArchitectureAdapter).- The four registration sites from
supported_architectures/AGENTS.md:__init__.py,SUPPORTED_ARCHITECTURESin the factory,HF_SUPPORTED_ARCHITECTURES+CANONICAL_AUTHORS_BY_ARCH(["FacebookAI", "distilbert"]) intools/model_registry/__init__.py, andARCHITECTURE_DESCRIPTIONSingenerate_report.py(that one already has a RoBERTa string, it just isn't reachable). supported_models.jsonentries forFacebookAI/roberta-baseanddistilbert/distilroberta-basewithstatus: 0, then averify_modelsrun on both. distilroberta is 6 layers / 82M params so it's cheap to iterate on; roberta-base is the one people will actually use.- Tests: a unit adapter test next to
test_bert_adapter.py, and an integration test asserting HF logit parity, hook caching and a basic intervention on distilroberta. There is no bridge-side BERT integration test to copy (the BERT ones undertests/integrationandtests/acceptanceareHookedEncodertests), so this would be the first one for an encoder.
Two things that are not a rename and need a decision:
n_ctx.sources/transformers.pysetsn_ctx = max_position_embeddings, which is 514 for RoBERTa because HF reserves positions 0 and 1 (padding_idx + 1offset). Usable context is 512. The adapter should correct this son_ctxmatches what you can actually feed the model. Related:hook_pos_embedwill see position ids starting at 2, not 0, since HF computes them insideRobertaEmbeddings. The bridge runs the HF forward so the values are right, but anyone comparing position-embedding activations against BERT should know about the offset; Worth a line in the adapter docstring.- Tokenizer policy. RoBERTa is byte-level BPE with
<s>/</s>rather than WordPiece with[CLS]/[SEP]. Per the per-model tokenizer rule insupported_architectures/AGENTS.md,default_prepend_bosand padding side should come from each checkpoint'stokenizer_config.json, not be inherited from the BERT adapter.
Alternatives
- Load through
transformersdirectly and register forward hooks by hand. Works, but it's exactly the thing the bridge exists to avoid, and you loserun_with_cache, the hook aliases, and compatibility mode. - Port RoBERTa to
HookedEncoder. AGENTS.md says the bridge is the default for new work, and the BERT NSP note inmigrating_to_v3.mdalready points people at the bridge for new encoder features, so this seems like the wrong direction. - A single generic "BERT-like" adapter keyed on prefix., which is tempting, but the head module names differ enough between BERT, RoBERTa, ALBERT and ELECTRA that I think one subclass per family is clearer and matches how the repo handles other sibling families.
Additional context
distilbert/distilroberta-baseandFacebookAI/roberta-baseboth declarearchitectures: ["RobertaForMaskedLM"],model_type: roberta,pad_token_id: 1,type_vocab_size: 1,max_position_embeddings: 514. They differ only innum_hidden_layers(6 vs 12).token_type_embeddingsexists in both BERT and RoBERTa but withtype_vocab_size = 1in RoBERTa it's a constant. The BERT adapter doesn't map it either, so no change there.- Happy to do the PR, based on
dev, following the adapter workflow insupported_architectures/AGENTS.md. I can run verification for both models locally. Mostly opening this to check nobody else has it in flight and that aBertArchitectureAdaptersubclass is the shape you want.
Checklist
- I have checked that there is no similar issue in the repo (required)
Activity
jlarson4 commentedon Oct 8, 2026
@Canonik sounds good to me, PR away! A couple detail notes below:
There is a bridge-side BERT integration test to copy.
tests/integration/model_bridge/test_bridge_vs_hf_bert_parity.pybootsbert-base-casedthroughTransformerBridge.boot_transformers, checks logit parity against an independent eager HF reference, and asserts a set of cache names. Use that as your base for testingThe BERT adapter does map
token_type_embeddings,token_type_embedis inbert.py. Keep it in your mapping asroberta.embeddings.token_type_embeddings, the module is real(shape [1, d_model])even attype_vocab_size = 1.Olmo3ArchitectureAdapteris a pure pass subclass, you can't follow its precedent exactly. You will need to callsuper().__init__(cfg)and reassignself.component_mapping. When you do, you'll need to preserve thehook_alias_overridesonBlockBridge(RobertaLayerhas the same no-single-MLP-module layout asBertLayer, sohook_mlp_out/hook_mlp_inneed the same redirects), and note that the inheritedprepare_modelis a safe no-op onRobertaForMaskedLMFor
n_ctx: The default comes fromsources/_hf_format.py, which mapsmax_position_embeddings→ 514.TransformerBridge.W_posalready handles offset checkpoints, both OPT & BART allocaten_ctx + 2rows and the accessor slices them off, but that branch keys onw.shape[0] == cfg.n_ctx + 2, and RoBERTa reports the row count (514) where OPT reports usable context (2048). Withn_ctx = 514the branch is dead andW_pos/W_E_poscarry the two reserved rows at the wrong offsets. Setn_ctx = 512in the adapter and that branch will fire, giving a[512, d_model]W_poswhose row i is what content tokens will get at position i.Let me know if you have any questions, I look forward to reviewing your PR