Skip to content

fix(bridge): make native TransformerBridge state_dict()/load_state_dict() true inverses - #1598

Merged
jlarson4 merged 2 commits into
TransformerLensOrg:dev-4.xfrom
LightWork666:fix/bridge-state-dict-roundtrip
Aug 4, 2026
Merged

fix(bridge): make native TransformerBridge state_dict()/load_state_dict() true inverses#1598
jlarson4 merged 2 commits into
TransformerLensOrg:dev-4.xfrom
LightWork666:fix/bridge-state-dict-roundtrip

Conversation

@LightWork666

Copy link
Copy Markdown

Fixes #1587.

The bug

On a native TransformerBridge, state_dict() returns TL-renamed keys (embed.weight, blocks.0.attn.q.weight, ...), but load_state_dict() only knew how to match raw native parameter names. So bridge.load_state_dict(bridge.state_dict()) silently did nothing — no error, no warning, params just stayed whatever they were before. Worse, strict=True was silently downgraded to strict=False whenever the key counts didn't line up, so there was no way to even notice the round trip had failed.

The fix

load_state_dict now builds the inverse of the TL-key renaming that state_dict() applies (_tl_key_to_actual_keys), so it can map TL-format keys back to the underlying native parameter paths before handing them to the wrapped model's own load_state_dict. One wrinkle: some bridge components expose the same underlying parameter through more than one attribute path — e.g. GPT-2's q/k/v are views into the wrapped module's combined c_attn weight, reachable both through a block-level shortcut and through the nested _original_component chain. Writing to only one of those paths leaves the model's actual forward pass untouched even though state_dict() looks fine, so the mapping keeps every alias for a given TL key, not just the first one found.

The silent strict=True → False downgrade is gone. Missing/unexpected keys are now computed properly (scoped to what the TL state dict actually needs) and raise a real RuntimeError under strict=True, matching how HookedTransformer already behaves.

The raw-key loading path used by tracr (make_tracr_transformer_bridge_state_dict) still works — raw native keys are matched directly before falling through to the TL-key path.

Testing

  • New tests in tests/unit/model_bridge/test_state_dict_round_trip.py: round-trip actually overwrites params (not a no-op), strict=True raises on both missing and unexpected keys, strict=False doesn't raise on a partial dict, the tracr raw-key path still loads, and a real GPT-2 case checking forward-pass logits match after a zero-and-reload cycle (this last one is what caught the aliasing issue above — a naive key-rename fix passes the round-trip-key-equality check but still produces different logits, because it never touches the aliased storage).
  • uv run mypy . — clean.
  • Ran the full tests/unit/model_bridge/ suite (207 files) in isolated batches; everything passes except a handful of pre-existing generate()/KV-cache crashes already tracked as an upstream PyTorch/HF bug on Apple Silicon in tests/QUARANTINES.md — confirmed via git stash that those reproduce identically on unmodified dev-4.x, unrelated to this change.

Note on #1595

I noticed after finishing this that #1595 is already open for the same issue, taking a related but different approach (it detects aliasing by comparing live tensor identity rather than by TL-key-name collisions, which is arguably a more principled check). I'm submitting this anyway since it was already done and transparency seemed better than not mentioning it. Happy to have the maintainers pick whichever they prefer, or close this if #1595 is the better fix.

…ct() true inverses

state_dict() emits TL-renamed keys, but load_state_dict() only matched
raw native names, so a round trip silently loaded nothing and
strict=True was silently downgraded to strict=False. Adds the inverse
key mapping (including aliased parameters reachable via multiple
attribute paths, e.g. GPT-2's split q/k/v views into c_attn) and
proper missing/unexpected key accounting that raises under strict=True.

Fixes TransformerLensOrg#1587

@jlarson4 jlarson4 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for taking a look at this @LightWork666! My intention is to take this implementation as our primary solution, and keep a couple tests and other small elements from #1591 and #1595. I do have a couple change requests, please take a look when you have a moment. Your hard work on this is much appreciated

clean_key = actual_key.replace("._original_component", "")
clean_to_actual[clean_key] = actual_key
actual_to_clean[actual_key] = clean_key
clean_to_actual[actual_key.replace("._original_component", "")] = actual_key

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A complete raw-HF-format gpt2 checkpoint raises under strict=True (337 missing) even though strict=False restores the forward pass exactly. The clean branch maps one alias while required_actual_keys demands the union (lines 3546-3549). Can the strict accounting treat a required key as satisfied when any alias of its tensor is written?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, thanks for catching this. The bug was that required_actual_keys demanded every alias in a TL key's group be present, but the clean-key branch only ever writes one alias per key. Since aliases of the same TL key share the underlying storage, writing any one of them already updates what forward() reads for all of them, so treating the group as satisfied by any single alias is correct. Fixed in the latest commit: missing_keys now checks any(k in mapped_state_dict for k in actual_keys) per group instead of requiring the full union. Verified against your exact repro (gpt2, clean-key dict, strict=True): 0 missing, 0 unexpected now.

assert torch.equal(reloaded_raw[key], value), f"{key} did not round-trip"


@pytest.mark.slow

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the only test in any of the three competing PRs that exercises real HF-key conversion, but slow is deselected by make unit-test and the MPS job only runs for main-targeted PRs. Can you add an unmarked boot_native test that builds a multi-alias clean-key dict at strict=True?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added test_native_clean_key_dict_with_partial_aliases_does_not_raise_strict. No HF download needed; it uses boot_native's own _original_component wrapping, which turns out to alias internally too (not just gpt2's c_attn split), so it exercises the same bug without needing the slow tier. Also added a second slow test (test_boot_transformers_clean_key_dict_does_not_raise_strict) that's your exact repro on real gpt2, so there's a permanent regression test for the specific case you hit too.

…s written

load_state_dict's strict=True missing-keys accounting required every
actual-key alias of a shared-storage TL key to be present in the
input, even though writing any one alias already updates what
forward() reads for all of them (they're views onto the same
Parameter). A complete raw-HF-format checkpoint using clean keys -
which map to exactly one alias per key - triggered false "missing
key" errors under strict=True despite loading correctly.

Reported by @jlarson4 on gpt2 (337 false missing keys); reproduced
and fixed here, plus a fast boot_native regression test that doesn't
need a real model download.
@jlarson4

jlarson4 commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

@LightWork666 thanks for the updates! Looks good, merging now

@jlarson4
jlarson4 merged commit a6e0033 into TransformerLensOrg:dev-4.x Aug 4, 2026
25 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants