Repository navigation
fix: apply BitNet's ffn_sub_norm on the processed-weights MLP path #2004
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
base: dev
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| """BitNet in compatibility mode, on a tiny random model built without hub access. | ||
|
|
||
| BitNet applies an RMSNorm (attn_sub_norm / ffn_sub_norm) right before each output | ||
| projection. The processed-weights path rebuilds attention and the MLP from weights, | ||
| so it has to apply both norms to match HF. | ||
| """ | ||
| from unittest.mock import MagicMock | ||
|
|
||
| import pytest | ||
| import torch | ||
|
|
||
| from transformer_lens.model_bridge.bridge import TransformerBridge | ||
| from transformer_lens.model_bridge.sources._bridge_builder import ( | ||
| build_bridge_config_from_hf, | ||
| ) | ||
|
|
||
| TOKENS = torch.tensor([[1, 2, 3, 4, 5, 6, 7, 8], [8, 7, 6, 5, 4, 3, 2, 1]]) | ||
|
|
||
|
|
||
| def _hf_bitnet(): | ||
| from transformers import AutoModelForCausalLM, BitNetConfig | ||
|
|
||
| torch.manual_seed(0) | ||
| c = BitNetConfig( | ||
| vocab_size=128, | ||
| hidden_size=64, | ||
| intermediate_size=128, | ||
| num_hidden_layers=2, | ||
| num_attention_heads=4, | ||
| num_key_value_heads=2, | ||
| max_position_embeddings=64, | ||
| ) | ||
| c.architectures = ["BitNetForCausalLM"] | ||
| hf = AutoModelForCausalLM.from_config(c, attn_implementation="eager").eval() | ||
| with torch.no_grad(): | ||
| # Non-trivial sub-norm weights, so skipping a norm changes the output. | ||
| for name, param in hf.named_parameters(): | ||
| if "norm" in name: | ||
| param.normal_(1.0, 0.3) | ||
| return hf | ||
|
|
||
|
|
||
| def _bridge(hf) -> TransformerBridge: | ||
| from transformer_lens.factories.architecture_adapter_factory import ( | ||
| ArchitectureAdapterFactory, | ||
| ) | ||
|
|
||
| cfg = build_bridge_config_from_hf(hf.config, "BitNetForCausalLM", "tiny", torch.float32) | ||
| adapter = ArchitectureAdapterFactory.select_architecture_adapter(cfg) | ||
| return TransformerBridge(model=hf, adapter=adapter, tokenizer=MagicMock()) | ||
|
|
||
|
|
||
| @pytest.fixture(scope="module") | ||
| def reference(): | ||
| hf = _hf_bitnet() | ||
| with torch.no_grad(): | ||
| logits = hf(TOKENS).logits | ||
| _, cache = _bridge(hf).run_with_cache(TOKENS) | ||
| return logits, cache | ||
|
|
||
|
|
||
| @pytest.fixture(scope="module") | ||
| def compat(): | ||
| bridge = _bridge(_hf_bitnet()) | ||
| bridge.enable_compatibility_mode() | ||
| with torch.no_grad(): | ||
| logits, cache = bridge.run_with_cache(TOKENS) | ||
| return bridge, logits, cache | ||
|
|
||
|
|
||
| def test_compatibility_mode_matches_hf(reference, compat): | ||
| ref_logits, _ = reference | ||
| _, logits, _ = compat | ||
| torch.testing.assert_close( | ||
| logits.log_softmax(-1), ref_logits.log_softmax(-1), atol=1e-5, rtol=0 | ||
| ) | ||
|
|
||
|
|
||
| @pytest.mark.parametrize( | ||
| "hook", ["blocks.0.mlp.hook_post", "blocks.0.hook_mlp_out", "blocks.1.hook_resid_post"] | ||
| ) | ||
| def test_mlp_hooks_match_the_unprocessed_bridge(reference, compat, hook): | ||
| """hook_post is the down_proj input, after ffn_sub_norm, on both paths.""" | ||
| _, ref_cache = reference | ||
| _, _, cache = compat | ||
| torch.testing.assert_close(cache[hook], ref_cache[hook], atol=1e-5, rtol=1e-5) | ||
|
|
||
|
|
||
| def test_neuron_decomposition_sums_to_residual(compat): | ||
| bridge, _, cache = compat | ||
| final = cache[f"blocks.{bridge.cfg.n_layers - 1}.hook_resid_post"] | ||
| stack = cache.get_full_resid_decomposition(expand_neurons=True) | ||
| torch.testing.assert_close(stack.sum(0), final, atol=1e-5, rtol=0) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,13 @@ | ||
| """BitNet b1.58 (``BitNetForCausalLM``) adapter: llama layout plus attn/ffn | ||
| sub-layer RMSNorms (attn_sub_norm reapplied by an adapter-local attention bridge).""" | ||
| sub-layer RMSNorms (attn_sub_norm and ffn_sub_norm reapplied by adapter-local | ||
| attention and MLP bridges on the processed-weights path).""" | ||
|
|
||
| from typing import Any | ||
|
|
||
| import torch | ||
|
|
||
| from transformer_lens.model_bridge.generalized_components import ( | ||
| GatedMLPBridge, | ||
| PositionEmbeddingsAttentionBridge, | ||
| ) | ||
| from transformer_lens.model_bridge.supported_architectures.llama import ( | ||
|
|
@@ -29,6 +31,21 @@ def _pre_output_projection(self, attn_output: torch.Tensor) -> torch.Tensor: | |
| return attn_output | ||
|
|
||
|
|
||
| class _BitNetMLPBridge(GatedMLPBridge): | ||
| """Applies BitNet's ffn_sub_norm before the down projection. | ||
|
|
||
| The generic processed-weights path goes straight from ``act(gate) * up`` to | ||
| down_proj; BitNet inserts an RMSNorm in between. | ||
| """ | ||
|
|
||
| def _pre_output_projection(self, hidden: torch.Tensor) -> torch.Tensor: | ||
| oc = self.original_component | ||
| sub_norm = getattr(oc, "ffn_sub_norm", None) if oc is not None else None | ||
| if isinstance(sub_norm, torch.nn.Module): | ||
| hidden = sub_norm(hidden) | ||
| return hidden | ||
|
|
||
|
|
||
| class BitNetArchitectureAdapter(LlamaArchitectureAdapter): | ||
| """Architecture adapter for BitNetForCausalLM models.""" | ||
|
|
||
|
|
@@ -47,6 +64,11 @@ def __init__(self, cfg: Any) -> None: | |
| # standard LN folding and W_O centering do not model them. | ||
| self.supports_fold_ln = False | ||
| self.supports_center_writing_weights = False | ||
| blocks = self.components["blocks"] | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The attention path changes its class using |
||
| mlp = blocks.submodules["mlp"] | ||
| blocks.submodules["mlp"] = _BitNetMLPBridge( | ||
| name=mlp.name, config=self.cfg, submodules=mlp.submodules | ||
| ) | ||
|
|
||
| def prepare_model(self, hf_model: Any) -> None: | ||
| """Refuse packed 1.58-bit checkpoints, which need BitNet dequant kernels. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tested parity with
microsoft/bitnet-b1.58-2B-4T-bf16. For this model, HF ternarizes the weight and int8-quantizes the activation on each call. The processed-weights path this bridge inherits doesF.linearon the fp32 master weights, meaning after this fix compatibility mode on that checkpoint disagrees with HF by about 24 in log-prob, starting atmlp.hook_pre.The attention bridge goes through its
LinearBridgesrather thanF.linear, could the MLP do the same throughgate,inandoutwith no loss? Nothing is folded for BitNet, so this should be safe. Take a look and let me know what you think