Add TransformerBridge adapter for lightweight decoder-only pretrainin…#1516
Add TransformerBridge adapter for lightweight decoder-only pretrainin…#1516Cacapice wants to merge 1 commit into
Conversation
11c3685 to
4877ee1
Compare
|
@Cacapice Could you please fix the formatting of your PR comment? I will review once that is done, thank you! |
0f75ec7 to
4dcd7b6
Compare
|
@jlarson4 Good to go! Thank you for your review. I look forward to any feedback or discussion on the design decisions. Katherine |
|
Hi @Cacapice! Great work on this, an exceptionally well created adapter. I have just one small note related to naming: Can we rename the |
4dcd7b6 to
9fa30dc
Compare
|
Due to a local Git repository issue while recovering my branch history, I closed this PR and recreated it with the same changes on a branch correctly based on dev. The replacement PR is #1519. Thank you for the review and apologies for the extra churn. |
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, normalizes its output contract, and ensures bridge
mode changes propagate to the source model (see Scope boundaries).
Direct
build_bridge_from_moduleuse still works for advanced cases."SwiGLUMoEForCausalLM"inSUPPORTED_ARCHITECTURESandINTENTIONAL_EXCLUDES(no HF Hub checkpoint backs it).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.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, and (via asmall generated subclass) train/eval mode all still reach the wrapped
source model — see Scope boundaries for why train/eval needed a
workaround here.
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 a plain
AttentionBridgedelegating 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.
Worth flagging for discussion:
build_pretrain_bridgereassignsbridge.__class__to propagate train/eval mode (see code comments).Reassigning
__class__at runtime is unconventional; aTransformerBridge.train()fix upstream would be cleaner if maintainersare open to that scope.
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: 77 tests passed (26 adapter unit, 34 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 (a cached generated subclass,
rather than per-instance method closures); 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).
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.
Checklist: