-
Notifications
You must be signed in to change notification settings - Fork 626
Added HRM-Text two-timescale recurrent adapter #1487
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
MdSadiqMd
wants to merge
12
commits into
TransformerLensOrg:dev
Choose a base branch
from
MdSadiqMd:sadiq/HrmTextForCausalLM-adapter
base: dev
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
2751cfc
feat: respect prepend_bos and add return_input_tokens flag
MdSadiqMd fead8b5
Merge branch 'TransformerLensOrg:main' into main
MdSadiqMd 65b726a
feat: Added HRM-Text two-timescale recurrent adapter
MdSadiqMd b76e086
fixed broken tests and architecture
MdSadiqMd 8e0ac56
docs update
MdSadiqMd 8fb89b9
my bad
MdSadiqMd 642ce16
Merge branch 'dev' into sadiq/HrmTextForCausalLM-adapter
jlarson4 d2d2b4a
pipeline fix
MdSadiqMd d809974
adapter changes
MdSadiqMd d428c96
pipeline fix
MdSadiqMd 7d2a7ae
pipeline fix
MdSadiqMd a3c9b70
dropping encoder and decoder blocks
MdSadiqMd File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
240 changes: 240 additions & 0 deletions
240
tests/integration/model_bridge/test_hrm_text_adapter.py
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,240 @@ | ||
| """Integration tests for the HRM-Text architecture adapter. | ||
|
|
||
| Builds a tiny HrmTextForCausalLM from scratch, wraps it in a TransformerBridge, | ||
| and verifies: | ||
| - Bridge creation with correct component structure | ||
| - Forward pass parity with HF | ||
| - Hook firing with expected shapes | ||
| - HRM-specific config attr propagation | ||
| """ | ||
|
|
||
| import pytest | ||
| import torch | ||
|
|
||
| from transformer_lens.model_bridge.bridge import TransformerBridge | ||
|
|
||
| _HRM_TEXT_AVAILABLE = True | ||
| try: | ||
| from transformers import HrmTextConfig, HrmTextForCausalLM | ||
| except ImportError: | ||
| _HRM_TEXT_AVAILABLE = False | ||
|
|
||
|
|
||
| def _make_tiny_hf_model(): | ||
| """Tiny HRM-Text model: 2 layers per stack, 1 H_cycle, 2 L_cycles.""" | ||
| cfg = HrmTextConfig( | ||
| hidden_size=64, | ||
| num_hidden_layers=2, | ||
| num_attention_heads=4, | ||
| head_dim=16, | ||
| intermediate_size=128, | ||
| vocab_size=128, | ||
| max_position_embeddings=64, | ||
| rms_norm_eps=1e-6, | ||
| H_cycles=1, | ||
| L_cycles=2, | ||
| L_bp_cycles=[0, 2], | ||
| embedding_scale=8.0, | ||
| prefix_lm=False, | ||
| _attn_implementation="eager", | ||
| ) | ||
| cfg.tie_word_embeddings = False | ||
| model = HrmTextForCausalLM(cfg) | ||
| model.eval() | ||
| return model | ||
|
|
||
|
|
||
| def _make_bridge(hf_model): | ||
| from unittest.mock import MagicMock | ||
|
|
||
| from transformer_lens.config.transformer_bridge_config import ( | ||
| TransformerBridgeConfig, | ||
| ) | ||
| from transformer_lens.model_bridge.supported_architectures.hrm_text import ( | ||
| HrmTextArchitectureAdapter, | ||
| ) | ||
|
|
||
| bridge_cfg = TransformerBridgeConfig( | ||
| d_model=64, | ||
| d_head=16, | ||
| n_heads=4, | ||
| n_layers=2, | ||
| n_ctx=64, | ||
| d_vocab=128, | ||
| d_mlp=128, | ||
| architecture="HrmTextForCausalLM", | ||
| ) | ||
| # HRM-Text passthrough attrs must be propagated to bridge_cfg so the adapter | ||
| # can read them. In production this happens via _HF_PASSTHROUGH_ATTRS in | ||
| # _bridge_builder.py / transformers.py, but in this test we construct the | ||
| # config manually. | ||
| for attr in ("H_cycles", "L_cycles", "L_bp_cycles", "embedding_scale", "prefix_lm"): | ||
| setattr(bridge_cfg, attr, getattr(hf_model.config, attr)) | ||
| adapter = HrmTextArchitectureAdapter(bridge_cfg) | ||
| return TransformerBridge(hf_model, adapter, tokenizer=MagicMock()) | ||
|
|
||
|
|
||
| @pytest.mark.skipif( | ||
| not _HRM_TEXT_AVAILABLE, | ||
| reason="HrmTextForCausalLM not available in installed transformers", | ||
| ) | ||
| class TestHrmTextBridgeCreation: | ||
| @pytest.fixture(scope="class") | ||
| def bridge(self): | ||
| return _make_bridge(_make_tiny_hf_model()) | ||
|
|
||
| @pytest.fixture(scope="class") | ||
| def hf_model(self): | ||
| return _make_tiny_hf_model() | ||
|
|
||
| def test_has_correct_block_structure(self, bridge): | ||
| assert hasattr(bridge, "L_blocks") | ||
| assert hasattr(bridge, "H_blocks") | ||
| assert len(bridge.L_blocks) == 2 | ||
| assert len(bridge.H_blocks) == 2 | ||
|
|
||
| def test_has_core_components(self, bridge): | ||
| assert hasattr(bridge, "embed") | ||
| assert hasattr(bridge, "unembed") | ||
| assert hasattr(bridge, "L_ln_final") | ||
| assert hasattr(bridge, "H_ln_final") | ||
|
|
||
| def test_hook_names_present(self, bridge): | ||
| hook_keys = set(bridge.hook_dict.keys()) | ||
| assert "L_blocks.0.hook_resid_pre" in hook_keys | ||
| assert "L_blocks.0.hook_resid_post" in hook_keys | ||
| assert "H_blocks.0.hook_resid_pre" in hook_keys | ||
| assert "H_blocks.0.hook_resid_post" in hook_keys | ||
|
|
||
| def test_L_block_submodule_hooks(self, bridge): | ||
| hook_keys = set(bridge.hook_dict.keys()) | ||
| assert any("L_blocks.0.ln1" in k for k in hook_keys) | ||
| assert any("L_blocks.0.ln2" in k for k in hook_keys) | ||
| assert any("L_blocks.0.attn" in k for k in hook_keys) | ||
| assert any("L_blocks.0.mlp" in k for k in hook_keys) | ||
|
|
||
| def test_H_block_submodule_hooks(self, bridge): | ||
| hook_keys = set(bridge.hook_dict.keys()) | ||
| assert any("H_blocks.0.ln1" in k for k in hook_keys) | ||
| assert any("H_blocks.0.ln2" in k for k in hook_keys) | ||
| assert any("H_blocks.0.attn" in k for k in hook_keys) | ||
| assert any("H_blocks.0.mlp" in k for k in hook_keys) | ||
|
|
||
| def test_config_flags(self, bridge): | ||
| assert bridge.cfg.normalization_type == "RMS" | ||
| assert bridge.cfg.positional_embedding_type == "rotary" | ||
| assert bridge.cfg.gated_mlp is True | ||
| assert bridge.cfg.final_rms is True | ||
|
|
||
| def test_hr_config_propagated(self, bridge, hf_model): | ||
| assert bridge.cfg.H_cycles == hf_model.config.H_cycles | ||
| assert bridge.cfg.L_cycles == hf_model.config.L_cycles | ||
| assert bridge.cfg.embedding_scale == hf_model.config.embedding_scale | ||
| assert bridge.cfg.prefix_lm == hf_model.config.prefix_lm | ||
|
|
||
|
|
||
| @pytest.mark.skipif( | ||
| not _HRM_TEXT_AVAILABLE, | ||
| reason="HrmTextForCausalLM not available in installed transformers", | ||
| ) | ||
| class TestHrmTextForwardPass: | ||
| @pytest.fixture(scope="class") | ||
| def hf_model(self): | ||
| """Independent HF model, NOT wrapped by the bridge.""" | ||
| return _make_tiny_hf_model() | ||
|
|
||
| @pytest.fixture(scope="class") | ||
| def bridge(self, hf_model): | ||
| """Bridge built from a model copy with the same weights as hf_model.""" | ||
| model_copy = _make_tiny_hf_model() | ||
| model_copy.load_state_dict(hf_model.state_dict()) | ||
| return _make_bridge(model_copy) | ||
|
|
||
| def test_forward_returns_logits(self, bridge): | ||
| tokens = torch.randint(0, 128, (1, 4)) | ||
| with torch.no_grad(): | ||
| output = bridge(tokens, use_cache=False) | ||
| assert output.shape == (1, 4, 128) | ||
| assert not torch.isnan(output).any() | ||
|
|
||
| def test_forward_matches_hf(self, bridge, hf_model): | ||
| tokens = torch.randint(0, 128, (1, 4)) | ||
| with torch.no_grad(): | ||
| hf_logits = hf_model(tokens, use_cache=False).logits | ||
| bridge_logits = bridge(tokens, use_cache=False) | ||
| assert hf_logits.shape == bridge_logits.shape | ||
| torch.testing.assert_close(hf_logits, bridge_logits, atol=1e-5, rtol=1e-5) | ||
|
|
||
| def test_hook_activation_shapes(self, bridge): | ||
| captured = [] | ||
|
|
||
| def capture_hook(tensor, hook): | ||
| captured.append(tensor.detach().clone()) | ||
| return tensor | ||
|
|
||
| tokens = torch.randint(0, 128, (1, 4)) | ||
| with torch.no_grad(): | ||
| bridge.run_with_hooks( | ||
| tokens, | ||
| fwd_hooks=[("L_blocks.0.mlp.hook_out", capture_hook)], | ||
| use_cache=False, | ||
| ) | ||
| # Each L_cycle fires the hook, so with L_cycles=2 we expect 2 firings | ||
| assert len(captured) == 2, f"Expected 2 hook firings (one per L_cycle), got {len(captured)}" | ||
| for output in captured: | ||
| assert output.shape == (1, 4, 64), f"Expected (1, 4, 64), got {output.shape}" | ||
|
|
||
| def test_hook_on_H_block_fires(self, bridge): | ||
| captured = [] | ||
|
|
||
| def capture_hook(tensor, hook): | ||
| captured.append(tensor.detach().clone()) | ||
| return tensor | ||
|
|
||
| tokens = torch.randint(0, 128, (1, 4)) | ||
| with torch.no_grad(): | ||
| bridge.run_with_hooks( | ||
| tokens, | ||
| fwd_hooks=[("H_blocks.0.attn.hook_out", capture_hook)], | ||
| use_cache=False, | ||
| ) | ||
| # H_blocks fire once per H_cycle (H_cycles=1) | ||
| assert len(captured) == 1, "H_blocks hook must fire" | ||
|
|
||
| def test_gate_hook_fires(self, bridge): | ||
| firing_count = [0] | ||
|
|
||
| def count_hook(value, hook): | ||
| firing_count[0] += 1 | ||
| return value | ||
|
|
||
| tokens = torch.randint(0, 128, (1, 4)) | ||
| with torch.no_grad(): | ||
| bridge.run_with_hooks( | ||
| tokens, | ||
| fwd_hooks=[("L_blocks.0.attn.hook_gate", count_hook)], | ||
| use_cache=False, | ||
| ) | ||
| assert firing_count[0] > 0, ( | ||
| "L_blocks.0.attn.hook_gate did not fire — the gated-attention path " | ||
| "is dead code and the forward-parity test is tautological." | ||
| ) | ||
|
|
||
| def test_forward_with_cache_does_not_crash(self, bridge): | ||
| tokens = torch.randint(0, 128, (1, 4)) | ||
| with torch.no_grad(): | ||
| output = bridge(tokens, use_cache=True) | ||
| assert output.shape == (1, 4, 128) | ||
| assert not torch.isnan(output).any() | ||
|
|
||
| def test_forward_with_cache_and_past_key_values(self, bridge): | ||
| tokens = torch.randint(0, 128, (1, 4)) | ||
| with torch.no_grad(): | ||
| output, past_kv = bridge(tokens, use_cache=True, return_type="logits_and_cache") | ||
| assert output.shape == (1, 4, 128) | ||
| assert past_kv is not None | ||
| second_token = torch.randint(0, 128, (1, 1)) | ||
| with torch.no_grad(): | ||
| output2 = bridge(second_token, use_cache=True, past_key_values=past_kv) | ||
| assert output2.shape == (1, 1, 128) | ||
| assert not torch.isnan(output2).any() | ||
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
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.