Skip to content

fix: apply BitNet's ffn_sub_norm on the processed-weights MLP path - #2004

Open
flyingdoubleG wants to merge 1 commit into
TransformerLensOrg:devfrom
flyingdoubleG:fix/bitnet-compat-ffn-sub-norm
Open

flyingdoubleG wants to merge 1 commit into
TransformerLensOrg:devfrom
flyingdoubleG:fix/bitnet-compat-ffn-sub-norm

Conversation

@flyingdoubleG

Copy link
Copy Markdown
Contributor

Description

BitNet applies an RMSNorm right before each output projection:

# BitNetMLP.forward
down_proj(ffn_sub_norm(act_fn(gate_proj(x)) * up_proj(x)))

_BitNetAttentionBridge already re-applies attn_sub_norm through _pre_output_projection on the processed-weights path. The MLP has no such step: GatedMLPBridge's processed-weights forward goes straight from act(gate) * up to down_proj. So after enable_compatibility_mode(), every BitNet MLP skips ffn_sub_norm, and nothing reports it:

tiny random BitNet (2 layers, sub-norm weights perturbed) dev this PR
compatibility-mode log-probs vs HF, max abs diff 0.58 1e-6
blocks.0.mlp.hook_post vs the unprocessed bridge (rel.) 0.99 0
blocks.1.hook_resid_post vs the unprocessed bridge (rel.) 0.84 0

Fix. GatedMLPBridge gets a _pre_output_projection(hidden) step between the gating product and out on the processed-weights path. It is the identity by default, like the attention bridge's. An adapter-local _BitNetMLPBridge overrides it to apply the HF MLP's ffn_sub_norm. hook_post (out.hook_in) sees the normalized activation, as on the unprocessed path, where it is the down_proj input. Other gated MLPs are unchanged.

I left applicable_phases = [1, 2, 4] as it is: I did not run the Phase 3 benchmark on a real checkpoint.

Type of change

  • Bug fix (non-breaking change which fixes an issue)

Checklist:

  • I have commented my code, particularly in hard-to-understand areas

  • I have made corresponding changes to the documentation

  • My changes generate no new warnings

  • I have added tests that prove my fix is effective or that my feature works

  • New and existing unit tests pass locally with my changes

  • I have not rewritten tests relating to key interfaces which would affect backward compatibility

  • New tests/integration/model_bridge/test_bitnet_compatibility_mode.py. It builds a tiny random BitNetForCausalLM without hub access and checks that:

    • compatibility-mode log-probs match HF;
    • mlp.hook_post, hook_mlp_out and resid_post match the unprocessed bridge;
    • get_full_resid_decomposition(expand_neurons=True) sums to the residual stream.

    The first four fail on dev.

  • test_bitnet_adapter.py: the MLP is a _BitNetMLPBridge with the usual gate_proj / up_proj / down_proj mapping, its _pre_output_projection applies ffn_sub_norm, and the generic one is the identity.

Local runs (Windows, CPU, torch 2.9.1, transformers 5.20.2, --jaxtyping-packages=transformer_lens,beartype.beartype):

black (the locked 23.12.1), isort --check-only and mypy pass on the changed files. In my environment mypy reports three errors in other, unchanged files (_model_state.py, moe.py, olmo_hybrid.py); they also appear on dev.

This was prepared with AI assistance (Claude); I reviewed and tested the change.

GatedMLPBridge's processed-weights forward went straight from act(gate) * up to
down_proj, so BitNet in compatibility mode skipped ffn_sub_norm. Add a
_pre_output_projection seam (identity by default), as on the attention bridge,
and override it in an adapter-local BitNet MLP bridge.

@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 putting this together @flyingdoubleG, just a couple comments before we can approve and merge this

# 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"]

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.

The attention path changes its class using _attention_bridge_cls. Here, the MLP is built as a plain GatedMLPBridge and then replaced, so a reader of _gated_mlp cannot see that BitNet overrides it and the next gated MLP will copy the swap. Can we create a mirror path for the MLP class, either by overriding _gated_mlp or by giving ArchitectureAdapter a _gated_mlp_bridge_cls to mirror the attention one?

down_proj; BitNet inserts an RMSNorm in between.
"""

def _pre_output_projection(self, hidden: torch.Tensor) -> torch.Tensor:

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.

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 does F.linear on the fp32 master weights, meaning after this fix compatibility mode on that checkpoint disagrees with HF by about 24 in log-prob, starting at mlp.hook_pre.

The attention bridge goes through its LinearBridges rather than F.linear, could the MLP do the same through gate, in and out with no loss? Nothing is folded for BitNet, so this should be safe. Take a look and let me know what you think

_testing_eager = "config"

# Sub-layer norms are incompatible with HT-style processed-weight
# attention, so compatibility-mode equivalence (Phase 3) is out of scope.

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 comment is now out of date, can we update it to reflect the updated reasoning for Phase 3 being out of scope?

This branch has not been deployed

No deployments
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