You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
TransformerBridge behaves like a standard torch.nn.Module when used directly, but it is not composable as a child of another nn.Module.
TransformerBridge.__init__() stores the live source model with:
self.__dict__["original_model"] =model
This intentionally bypasses nn.Module.__setattr__() and keeps original_model outside the registered _modules tree. Bridge then restores several root-level operations through explicit overrides, including parameters(), named_parameters(), to(), train(), state_dict(), and load_state_dict().
Those overrides make direct calls on the Bridge work, but a parent module does not use them for ordinary recursive traversal:
parameter discovery walks registered modules and parameters;
parent.to(...), .half(), and .bfloat16() recurse through _apply();
parent.requires_grad_(...), optimizers, and zero_grad() use the parent's recursively discovered parameters;
parent checkpointing passes a shared destination and prefix through recursive state_dict() calls and ignores a child's replacement return object.
The Bridge component tree re-registers only the source modules represented by the adapter mapping. Unmapped parts of the hidden source model are still used by forward, but are invisible to the parent module. As a result, parent optimizers and lifecycle operations can silently omit live parameters, parent dtype conversion can create a mixed-dtype model that crashes, and nested checkpoint round-trips fail.
Code example
This reproduction constructs tiny random models from config only and performs no downloads:
42 37 5
[
'bert.embeddings.token_type_embeddings.weight',
'bert.embeddings.LayerNorm.weight',
'bert.embeddings.LayerNorm.bias',
'cls.predictions.transform.dense.weight',
'cls.predictions.transform.dense.bias',
]
{torch.float64, torch.float32}
RuntimeError: mixed dtype (CPU): all inputs must share same datatype.
The direct controls work correctly:
bridge.to(torch.float64)
all 42 parameters are float64
forward returns float64 logits
bridge.requires_grad_(False)
no source-model parameter still requires gradients
The corresponding parent operations miss the same five parameters:
parent.to(torch.float64)
37 parameters are float64
5 parameters remain float32
parent.requires_grad_(False)
the same 5 source-model parameters still require gradients
There is also a nested checkpoint symptom. With a tiny GPT-2 Bridge:
returned is destination = False
destination first key = bridge.transformer.wte._original_component.weight
returned first key = bridge.transformer.wte.weight
When the Bridge is registered under Parent, parent.state_dict() retains the raw recursive entries rather than the returned TL-key dictionary, contains no W_Q key, and parent.load_state_dict(parent.state_dict()) fails under the default strict load.
Expected behavior
A TransformerBridge registered as a child module should participate in standard PyTorch ownership and recursion:
the parent should discover every unique live parameter used by Bridge forward;
parent-level dtype/device conversion and freezing should reach all live parameters and buffers;
optimizers built from parent.parameters() should not silently omit source-model parameters;
nested state_dict() should honor the supplied destination and prefix and strictly round-trip through the parent.
Same-day searches for parent modules, nested nn.Module, optimizer traversal, requires_grad, FSDP, original_model, and state-dict destination/prefix found no issue or PR covering this parent-recursion defect.
Suggested fix direction
Adding more root-level delegation methods is not a complete fix. An _apply() override could repair direct dtype/device movement but would not make parent.parameters() complete; a state_dict() patch alone would not repair parent optimizers or freezing.
The implementation needs one authoritative registered ownership graph for the parameters and buffers used by forward. One possible direction is to register the source model as the owning graph and make Bridge components non-owning views/proxies over it. Naively registering both the complete source model and all current Bridge component wrappers may create duplicate serialization paths, so parameter identity, state-dict keys, hooks, and existing raw/TL checkpoint behavior need to be considered together.
It may also be cleaner to keep standard recursive PyTorch state_dict() semantics separate from an explicitly named TransformerLens-format export API, rather than making one method serve both parent recursion and key conversion.
Suggested regression coverage:
A parent containing a tiny BERT-family Bridge sees the same unique parameter identities as direct bridge.parameters().
parent.to(device/dtype), .half(), and .bfloat16() reach every live forward parameter and buffer without mixed-dtype execution.
parent.requires_grad_(False) and parent.zero_grad() reach all source-model parameters.
An optimizer created from parent.parameters() can update every intended trainable parameter.
bridge.state_dict(destination=dest, prefix="bridge.") returns dest and writes the intended prefixed keys into it.
A parent containing a Bridge can strictly load its own state_dict().
Parent-level save/mutate/load restores the values actually used by forward.
Existing direct Bridge checkpoint behavior, TransformerLens-format analysis access, and Tracr raw-key loading remain supported.
If DDP/FSDP composition is intended to be supported, add a lightweight wrapper smoke test based on standard parameter enumeration; the current report does not claim a full distributed reproduction.
System Info
Installed from source in the repository uv environment
Windows 11 / PowerShell
Python 3.12.10
TransformerLens dev-4.x commit ac4f7f134b12
CPU-only reproduction; no model or tokenizer download
Additional context
The visible symptom varies by architecture because adapter mappings re-register different fractions of each source-model tree. BERT is a useful control because it leaves five forward parameters outside the Bridge's registered component graph while direct Bridge delegation still sees all of them.
Checklist
I have checked that there is no similar issue in the repo (required)
Describe the bug
TransformerBridgebehaves like a standardtorch.nn.Modulewhen used directly, but it is not composable as a child of anothernn.Module.TransformerBridge.__init__()stores the live source model with:This intentionally bypasses
nn.Module.__setattr__()and keepsoriginal_modeloutside the registered_modulestree. Bridge then restores several root-level operations through explicit overrides, includingparameters(),named_parameters(),to(),train(),state_dict(), andload_state_dict().Those overrides make direct calls on the Bridge work, but a parent module does not use them for ordinary recursive traversal:
parent.to(...),.half(), and.bfloat16()recurse through_apply();parent.requires_grad_(...), optimizers, andzero_grad()use the parent's recursively discovered parameters;destinationandprefixthrough recursivestate_dict()calls and ignores a child's replacement return object.The Bridge component tree re-registers only the source modules represented by the adapter mapping. Unmapped parts of the hidden source model are still used by forward, but are invisible to the parent module. As a result, parent optimizers and lifecycle operations can silently omit live parameters, parent dtype conversion can create a mixed-dtype model that crashes, and nested checkpoint round-trips fail.
Code example
This reproduction constructs tiny random models from config only and performs no downloads:
On current
dev-4.x(ac4f7f134b12), the output is:The direct controls work correctly:
The corresponding parent operations miss the same five parameters:
There is also a nested checkpoint symptom. With a tiny GPT-2 Bridge:
produces:
When the Bridge is registered under
Parent,parent.state_dict()retains the raw recursive entries rather than the returned TL-key dictionary, contains noW_Qkey, andparent.load_state_dict(parent.state_dict())fails under the default strict load.Expected behavior
A
TransformerBridgeregistered as a child module should participate in standard PyTorch ownership and recursion:parent.parameters()should not silently omit source-model parameters;state_dict()should honor the supplied destination and prefix and strictly round-trip through the parent.Related issues and duplicate boundary
bridge.parameters()andnamed_parameters()to delegate to the source model. Parent parameter recursion does not call those child overrides, so this remains unfixed.TransformerBridge.train()propagation because the hiddenoriginal_modelis outside_modules. It confirms the lifecycle-delegation pattern but does not cover parent composition.bridge.state_dict()/load_state_dict()inversion and strictness. They do not cover a supplieddestination, a non-emptyprefix, or a Bridge nested under another module.load_state_dict(assign=True)breaking split-parameter view relationships. That is independent of the incomplete parent ownership graph reported here.nn.Module, optimizer traversal,requires_grad, FSDP,original_model, and state-dict destination/prefix found no issue or PR covering this parent-recursion defect.Suggested fix direction
Adding more root-level delegation methods is not a complete fix. An
_apply()override could repair direct dtype/device movement but would not makeparent.parameters()complete; astate_dict()patch alone would not repair parent optimizers or freezing.The implementation needs one authoritative registered ownership graph for the parameters and buffers used by forward. One possible direction is to register the source model as the owning graph and make Bridge components non-owning views/proxies over it. Naively registering both the complete source model and all current Bridge component wrappers may create duplicate serialization paths, so parameter identity, state-dict keys, hooks, and existing raw/TL checkpoint behavior need to be considered together.
It may also be cleaner to keep standard recursive PyTorch
state_dict()semantics separate from an explicitly named TransformerLens-format export API, rather than making one method serve both parent recursion and key conversion.Suggested regression coverage:
bridge.parameters().parent.to(device/dtype),.half(), and.bfloat16()reach every live forward parameter and buffer without mixed-dtype execution.parent.requires_grad_(False)andparent.zero_grad()reach all source-model parameters.parent.parameters()can update every intended trainable parameter.bridge.state_dict(destination=dest, prefix="bridge.")returnsdestand writes the intended prefixed keys into it.state_dict().System Info
uvenvironmentdev-4.xcommitac4f7f134b12Additional context
The visible symptom varies by architecture because adapter mappings re-register different fractions of each source-model tree. BERT is a useful control because it leaves five forward parameters outside the Bridge's registered component graph while direct Bridge delegation still sees all of them.
Checklist