Coverage for transformer_lens/model_bridge/supported_architectures/zamba2.py: 100%
29 statements
« prev ^ index » next coverage.py v7.10.1, created at 2026-08-11 18:50 +0000
« prev ^ index » next coverage.py v7.10.1, created at 2026-08-11 18:50 +0000
1"""Zamba2 hybrid Mamba2-Transformer architecture adapter.
3Supports Zamba2ForCausalLM (e.g. Zyphra/Zamba2-1.2B, Zamba2-7B, Zamba2-7B-Instruct).
5Architecture overview:
6- Heterogeneous layers defined by ``config.layers_block_type`` — surfaced with
7 canonical names: ``"linear_attention"`` (pure Mamba-2 SSM, HF ``"mamba"``) or
8 ``"hybrid"`` (Mamba-2 + shared global-attention block).
9- Most layers are ``Zamba2MambaDecoderLayer``: a single pre-norm
10 (``input_layernorm``) followed by a Mamba-2 mixer (``.mamba``).
11- A recurring subset are ``Zamba2HybridLayer``: each wraps a Mamba-2 decoder
12 layer plus a SHARED ``Zamba2AttentionDecoderLayer``. The shared attention
13 block's weights are tied across all hybrid layers, cycling through
14 ``config.num_mem_blocks`` unique blocks. When
15 ``config.use_shared_attention_adapter=True``, each hybrid layer carries
16 an independent per-layer LoRA adapter on top of the shared attention.
17- No model-level rotary embedding module is wired by the bridge — the
18 attention block handles RoPE internally via ``position_ids``.
19- Generation runs on the standard KV-cache path: HF threads a single unified
20 ``Zamba2HybridDynamicCache`` via ``past_key_values`` (carrying both KV-cache
21 entries for attention and SSM conv/recurrent states for Mamba-2), so the
22 bridge does not use the Mamba-specific ``cache_params`` stateful path.
24Key adapter decisions:
25- ``SSMBlockBridge`` is used for all layers. Its forward delegates entirely to
26 the HF layer, giving ``hook_in`` / ``hook_out`` on every layer regardless
27 of type.
28- For Mamba layers: ``norm`` (-> ``.input_layernorm``) and ``mixer``
29 (-> ``.mamba``) are declared as submodules and expose inner hooks
30 (in_proj, conv1d, inner_norm, out_proj).
31- For Hybrid layers: ``norm`` and ``mixer`` are marked ``optional=True`` so
32 component_setup skips them gracefully (Hybrid layers have no top-level
33 ``.input_layernorm`` or ``.mamba``). Block-level ``hook_in``/``hook_out``
34 still fire on every layer.
35- ``applicable_phases = [1, 2, 3, 4]``: P1 is exact vs raw HF (pure
36 passthrough); P2/P3 skip without a HookedTransformer; P4 exercises
37 ``past_key_values`` cache threading across Mamba-2 and attention layers.
38"""
40from typing import Any
42from transformer_lens.model_bridge.architecture_adapter import ArchitectureAdapter
43from transformer_lens.model_bridge.generalized_components import (
44 DepthwiseConv1DBridge,
45 EmbeddingBridge,
46 GatedRMSNormBridge,
47 LinearBridge,
48 RMSNormalizationBridge,
49 SSM2MixerBridge,
50 SSMBlockBridge,
51 UnembeddingBridge,
52)
53from transformer_lens.model_bridge.generalized_components.base import (
54 GeneralizedComponent,
55)
58def _make_optional(component: "GeneralizedComponent") -> "GeneralizedComponent":
59 """Mark a GeneralizedComponent submodule as optional.
61 ``component_setup.py`` reads ``getattr(submodule, 'optional', False)`` at
62 setup time, so setting the attribute directly is safe regardless of whether
63 the bridge class's ``__init__`` accepts an ``optional`` keyword argument.
64 """
65 component.optional = True
66 return component
69class Zamba2ArchitectureAdapter(ArchitectureAdapter):
70 """Architecture adapter for Zamba2ForCausalLM.
72 Hybrid Mamba-2 + shared global-attention model. Most layers are pure
73 Mamba-2 SSM (``"mamba"``); a recurring subset are hybrid layers
74 (``"hybrid"``) that route through a shared attention block before the
75 Mamba-2 step.
76 """
78 # P1: exact passthrough vs raw HF; P2/P3: skip without HookedTransformer;
79 # P4: generation with past_key_values cache threading.
80 applicable_phases: list[int] = [1, 2, 3, 4]
82 def __init__(self, cfg: Any) -> None:
83 super().__init__(cfg)
85 self.cfg.normalization_type = "RMS"
86 self.cfg.uses_rms_norm = True
87 # RoPE is handled internally by the attention block (position_ids are
88 # threaded through HF layers); no model-level rotary bridge needed.
89 self.cfg.positional_embedding_type = "none"
90 # MLP inside the shared attention block uses GELU, not SwiGLU.
91 self.cfg.gated_mlp = False
92 self.cfg.attn_only = False
93 self.cfg.final_rms = True
94 # NOTE: is_stateful stays False even though Mamba-2 layers carry SSM
95 # state. In this bridge, ``is_stateful=True`` selects the *Mamba* cache
96 # path, which threads the cache as ``cache_params=`` and drives a
97 # conv-kernel ``cache_position``. Zamba2's HF forward instead threads a
98 # single unified ``Zamba2HybridDynamicCache`` via ``past_key_values=``
99 # (both KV entries and SSM conv/recurrent states live in that one
100 # object). The standard KV-cache generation path already threads
101 # ``past_key_values`` correctly and matches HF ``generate`` bit-for-bit,
102 # so we use it rather than the Mamba path (whose ``cache_params`` kwarg
103 # collides with Zamba2's own ``cache_params=past_key_values`` call).
104 self.cfg.is_stateful = False
106 # Expose the per-layer type list so analysis tools can identify which
107 # layers are Mamba-only vs hybrid: "linear_attention" | "hybrid".
108 setattr(self.cfg, "layers_block_type", self._canonical_layer_types(cfg))
110 # Number of unique shared attention weight blocks (hybrid layers cycle
111 # through num_mem_blocks independent attention weight sets).
112 setattr(self.cfg, "num_mem_blocks", getattr(cfg, "num_mem_blocks", 1))
114 # Whether per-layer LoRA adapters are active on the shared attention.
115 setattr(
116 self.cfg,
117 "use_shared_attention_adapter",
118 getattr(cfg, "use_shared_attention_adapter", False),
119 )
121 # Mamba-2 dimensional config (mirrors Mamba2ArchitectureAdapter /
122 # NemotronHArchitectureAdapter patterns).
123 mamba_intermediate_size = int(getattr(cfg, "mamba_expand", 2) * self.cfg.d_model)
124 n_groups = getattr(cfg, "mamba_ngroups", 1)
125 ssm_state_size = getattr(cfg, "mamba_d_state", 64)
126 conv_dim = mamba_intermediate_size + 2 * n_groups * ssm_state_size
127 setattr(self.cfg, "mamba_intermediate_size", mamba_intermediate_size)
128 setattr(self.cfg, "conv_dim", conv_dim)
130 self.weight_processing_conversions = {}
132 self.component_mapping = {
133 "embed": EmbeddingBridge(name="model.embed_tokens"),
134 "blocks": SSMBlockBridge(
135 name="model.layers",
136 submodules={
137 # Pre-norm: present on Zamba2MambaDecoderLayer (.input_layernorm),
138 # absent on Zamba2HybridLayer (no top-level pre-norm) -> optional.
139 "norm": _make_optional(
140 RMSNormalizationBridge(name="input_layernorm", config=self.cfg)
141 ),
142 # Mamba-2 mixer: present on Zamba2MambaDecoderLayer (.mamba),
143 # absent at the top level of Zamba2HybridLayer -> optional.
144 "mixer": _make_optional(
145 SSM2MixerBridge(
146 name="mamba",
147 config=self.cfg,
148 submodules={
149 # -- Mamba-2 inner submodules (all optional) --
150 "in_proj": LinearBridge(name="in_proj", optional=True),
151 "conv1d": DepthwiseConv1DBridge(name="conv1d", optional=True),
152 # HF names the gated RMS norm "norm" inside the
153 # mixer; TL uses "inner_norm" to avoid colliding
154 # with the block-level norm declared above.
155 "inner_norm": _make_optional(GatedRMSNormBridge(name="norm")),
156 "out_proj": LinearBridge(name="out_proj", optional=True),
157 },
158 )
159 ),
160 },
161 ),
162 "ln_final": RMSNormalizationBridge(name="model.final_layernorm", config=self.cfg),
163 "unembed": UnembeddingBridge(name="lm_head"),
164 }