Add TransformerBridge adapter for lightweight decoder-only pretraining models#1519
Open
Cacapice wants to merge 2 commits into
Open
Add TransformerBridge adapter for lightweight decoder-only pretraining models#1519Cacapice wants to merge 2 commits into
Cacapice wants to merge 2 commits into
Conversation
Cacapice
force-pushed
the
add-pretrain-bridge-adapter
branch
from
July 17, 2026 02:09
0f39d13 to
e6d19bd
Compare
10 tasks
Cacapice
force-pushed
the
add-pretrain-bridge-adapter
branch
from
July 17, 2026 03:03
ba5a849 to
d9a4cc0
Compare
Cacapice
force-pushed
the
add-pretrain-bridge-adapter
branch
from
July 17, 2026 03:51
d9a4cc0 to
e8dd803
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Add TransformerBridge adapter for lightweight decoder-only pretraining models
Description
Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change.
Summary
Adds an interoperability adapter for live model modules loaded from
lightweight decoder-only pretraining checkpoints (RoPE, RMSNorm, SwiGLU,
optional MoE), enabling mechanistic interpretability experiments on
models trained from scratch.
Rather than converting checkpoints into a separate TransformerLens
implementation, the adapter wraps the existing module and delegates
execution to its native
forward, preserving the source model's semantics(including RoPE conventions) while exposing the standard TransformerBridge
interface.
Motivation
Interpretability research on small from-scratch models (circuit discovery,
induction heads, toy superposition models, controlled probing) needs
precise control over architecture, data, and checkpoints. This PR adds
that interoperability path: train a small decoder-only transformer, wrap
it with
build_pretrain_bridge, confirm the wrapping is transparent (thebridge delegates to and matches the source model's own forward exactly,
rather than running a separately reimplemented TransformerLens forward),
then use TransformerLens's caching, hooks, and patching. The goal is
architecture interoperability, not a general-purpose training framework.
What changed
PretrainArchitectureAdapter(
transformer_lens/model_bridge/supported_architectures/pretrain.py)maps token embeddings, decoder blocks (RoPE attention, RMSNorm, gated
SwiGLU MLP), final norm, and tied/untied unembedding.
DenseOrMoEFeedForwardBridgedispatches each block's MLPstructurally (
router+expertsversusgate+up+down), sodense, MoE, and mixed models use the same component mapping.
prevent duplicate named hook points. Structural validation requires
projection/router fields to be modules and
expertsto be a registeredModuleListorModuleDict, producing clear construction-time errorsfor unsupported module shapes.
build_pretrain_bridge(model, cfg): the public entry point. Wrapsthe source model and normalizes its output contract. Direct
build_bridge_from_moduleuse still works for advanced cases.TransformerBridge.train()fix (bug fix, affects all bridges):original_modelis intentionally stored outside the registered moduletree (assigned via
self.__dict__rather thannn.Module.__setattr__);consequently, inherited
nn.Module.train()-- which only recurses intoregistered submodules -- never reached the wrapped source model, so
bridge.train()/bridge.eval()silently left mode-dependent layers(dropout etc.) in whatever mode the model was in at wrap time.
TransformerBridge.train()nowalso sets mode on
original_model;eval()needs no override sincenn.Module.eval()callsself.train(False). Note this propagates toevery bridge, including HF-backed ones -- consistent with standard
nn.Modulesemantics and with the bridge's delegation of otherlifecycle operations (
to/cuda/cpu/mps,state_dict,parameters) tooriginal_model-- but flagged here so theblast radius is reviewed consciously. Authoritative regression coverage
lives at the bridge level in
tests/unit/model_bridge/test_bridge_train_mode_propagation.py(
TestTrainEvalModePropagation),against a minimal architecture-independent stub bridge (propagation both
directions,
train()/eval()returningselfpernn.Modulechainingconvention, direct-on-model mode setting staying in sync); one
end-to-end smoke test through
build_pretrain_bridgeremains in thecontainer test module. The bug is reproducible on released
transformer-lens 3.5.1 (the propagation tests fail there without the
override), so this is a live-release fix, not one only reachable
through the new adapter. No
HookedTransformercounterpart change isneeded: it has no wrapped
original_model-- its components areordinary registered submodules, which inherited
nn.Module.train()already reaches.
"TransformerLensPretrain"inSUPPORTED_ARCHITECTURESandadded it to the model-registry synchronization test's internal-only
exclusion set, since no Hugging Face Hub architecture or checkpoint
backs this key.
nanogpt.py, the adapter mutates the passed-incfgin place rather than copying it.test_pretrain_adapter.py(mapping,dispatch, tied embeddings) and
test_pretrain_model_container.py(container, kwarg filtering, output normalization, hooks, train/eval,
dtype, persistence -- see Scope boundaries), sharing mocks from
_pretrain_mocks.py. Integration tests use areal reference model (
tests/mocks/tiny_pretrain_model.py) with genuineadjacent-pair RoPE/RMSNorm math for logit and hook-placement parity
(see the integration test module docstring for the caveat on what
logit parity does and doesn't prove).
MoEBridgedefensive validation: added validation for tupleoutputs -- rejecting empty tuples and non-tensor first elements with a
clear
TypeError(instead of a lower-levelHookPointtype-checkerror), and guarding
hook_router_scoresso it is only invoked fortensor-valued router scores. Covered directly by new
MoEBridgetests(empty tuple, non-tensor first element, non-tensor metadata preserved
without firing the router hook, tensor router scores still firing it),
not just indirectly through the pretrain adapter's tests. This is a
small compatibility and error-reporting improvement discovered while
integrating the pretrain adapter and does not change the behavior of
standard
(hidden_states, router_scores)MoE outputs.NativeForwardAttentionBridge: a thinAttentionBridgesubclassused for this adapter's opaque attention wrap. Plain
AttentionBridgedeclares class-level
hook_aliases(hook_q/hook_k/hook_v/hook_z) andproperty_aliases(W_Q/W_K/W_V/W_O) that assumemapped Q/K/V/O submodules; this adapter's wrap deliberately has none
(see Hook coverage), so those aliases could never resolve and would
either emit spurious "did not resolve" warnings or misrepresent the
adapter's actual supported interface. The subclass clears both alias
dicts and sets
supports_split_qkv_fork = Falseso it doesn'tadvertise per-head hooks, weight aliases, or split-QKV-fork machinery
it can't back.
DelegatedAttentionBlockBridge, not plainBlockBridge:reuses an existing abstraction rather than adding another special-case
block, since it was written precisely for architectures where attention
is delegated wholesale. It complements
NativeForwardAttentionBridge.supports_split_qkv_fork = False(whichprevents the split-QKV-fork machinery and its associated
hook_attn_in/hook_q_input/hook_k_input/hook_v_inputHookPointsfrom being exposed for this attention component) by also removing the
block-level aliases that would otherwise dangle, pointing at hooks that
aren't exposed.
hook_attn_outis untouched by either change -- theattention component still fires its own
hook_outnormally, and theblock-level alias to it still resolves.
Supported behavior
build_pretrain_bridge()wraps the supplied model in place. Callers thatneed a raw source-model checkpoint should capture its
state_dict()before bridge construction.
Requires a specific module protocol (documented in the adapter file's
docstring) rather than any decoder-only architecture generally. Within
that protocol:
execution, since the adapter delegates rather than reimplementing it —
the tradeoff is that this adapter does not expose per-head attention
hooks (
hook_q/hook_k/hook_v/hook_pattern);forwardwith or without a**kwargscatch-all (only
output_attentionsis stripped — anything else,including a caller typo, passes through and raises naturally);
"logits"dict, tensor, tensor-first tuple, or.logits-exposing object, all validated, with clear errors onmalformed shapes.
Container wrapping
PretrainModelContainerresolves attribute-name collisions withTransformerBridgeand normalizes output to the.logitscontract(raising immediately on missing/malformed output). Full mechanics are in
the class docstring. Kept local rather than generalizing
TransformerBridge's output handling, to keep this PR architecture-scoped— a candidate for later centralization, not addressed here.
The dense/MoE delegate is excluded from the public module tree to avoid
duplicate hook points; forward execution, gradients, dtype/device
conversion,
state_dict-based reconstruction, hook lifecycle, andtrain/eval mode (via the
TransformerBridge.train()fix above) all stillreach the wrapped source model.
Hook coverage
Block-level hooks only (
resid_pre/resid_mid/resid_post, MLPhook_in/hook_out) — no per-headhook_q/hook_k/hook_v/hook_pattern. TransformerLens's HF-oriented attention bridges userotate-half RoPE, which does not match this adapter's adjacent-pair
convention and would break numerical equivalence, so it uses an opaque
NativeForwardAttentionBridge(a thinAttentionBridgesubclass with noQ/K/V/O submodules, and with
hook_aliases/property_aliasescleared soit doesn't advertise per-head hooks or weight aliases it can't back)
delegating to the source model's own attention instead. Per-head hooks
would need a new attention bridge built for that convention.
Scope boundaries
No training loop, optimizers/schedules, dataset/tokenizer infrastructure,
experiment tracking, distributed training, or checkpoint-shard
reconstruction — those stay with the source framework. This PR only adds
the TransformerBridge mapping and its tests.
Train/eval mode propagation is fixed upstream in
TransformerBridge.train()(see What changed) rather than worked aroundin the adapter -- an earlier draft reassigned
bridge.__class__to agenerated subclass; that machinery is gone, and
build_pretrain_bridgenow returns the bridge from
build_bridge_from_moduleunchanged.Persistence is tested by saving the source model's pre-bridge
state_dict, reconstructing the source model and bridge fromconfiguration, and verifying output and mode-propagation equivalence.
Whole-object pickling is not asserted because it depends on unrelated
TransformerBridgeinternals, including locally defined attentionhook-conversion classes. A standardized
TransformerBridgecheckpointinterface could provide a stable, versioned save/load contract,
improving usability and scalability without requiring whole-object
serialization.
Testing
Locally: 82 tests passed (4 bridge-level unit tests
(
TransformerBridge.train()mode propagation, stub bridge), 30adapter unit, 31 container unit, 9 integration, 4
generalized-component unit tests (MoEBridge tuple-output
validation), and 4 registry-sync tests). Covers: component
mapping and MLP dispatch (dense/MoE/mixed, checking actual
run_with_cachekeys, not justisinstance, plus basictype validation when an attribute name matches the MoE/gated-MLP protocol
but the value doesn't); logit and residual-stream parity; that
integration fixtures supply a
cfgtruthfully describing the model theybuild (
build_pretrain_bridgeitself performs no cfg-vs-modelvalidation, so a mismatch silently reports wrong numbers on
bridge.cfg-- see
TestIntegrationFixturesSupplyATruthfulConfig); activationcaching and hook interventions; kwarg filtering and output-normalization
edge cases; hook registration, gradients, dtype/device,
state_dict-basedreconstruction; train/eval mode propagation (authoritative
architecture-independent coverage at the bridge level in
test_bridge_train_mode_propagation.py, plus one end-to-end smoke throughbuild_pretrain_bridge); malformed-architecture errors;MoEBridge's own tuple-output validation and router-score hook gating(empty tuple, non-tensor first element, non-tensor metadata preserved
without firing the hook, tensor router scores still firing it);
NativeForwardAttentionBridge's clearedhook_aliases/property_aliases/supports_split_qkv_fork, and thatDelegatedAttentionBlockBridgecorrectly removes the now-unexposed split-QKV-fork block-level aliases
while leaving
hook_attn_out(both as the block-level alias and theattention component's own
hook_out) resolvable.Exact logit parity mainly demonstrates that wrapping does not alter the
delegated forward; it does not independently establish that the source
RoPE implementation is correct. The residual-stream decomposition and
hook-intervention tests provide the stronger evidence on hook placement.
Numerical parity is tested at float32 on CPU; float64 is tested for
preservation through construction only, not full parity at that dtype.
Type of change
Please delete options that are not relevant.
TransformerBridge.train()/eval()not reaching the wrappedoriginal_model)Checklist: