Coverage for transformer_lens/model_bridge/supported_architectures/rwkv7.py: 94%
46 statements
« prev ^ index » next coverage.py v7.10.1, created at 2026-09-21 19:27 +0000
« prev ^ index » next coverage.py v7.10.1, created at 2026-09-21 19:27 +0000
1"""RWKV-7 ("Goose") architecture adapter (RWKV7ForCausalLM).
3Family ``fla-hub/rwkv7-*``: attention-free recurrent LM from the
4flash-linear-attention library, loaded via remote code. Blocks pair a
5time-mixing (generalized delta rule) and a token-shifted squared-ReLU
6channel-mixing sublayer under biased pre-LN; no positional embeddings.
8Adapter decisions:
9- Full delegation: recurrence, token shift, LoRAs, and cross-block ``v_first``
10 threading all run inside the fla forward (v_first is managed by the HF
11 model-level forward, so delegated blocks get it for free).
12 ``weight_processing_conversions = {}``.
13- ``OpaqueBlockBridge``: BlockBridge's hook aliases hardcode the standard
14 pre-norm attention flow; only ``hook_in``/``hook_out`` are sound here.
15- ``ffn_norm`` is a fused add-and-norm ``(normed, residual) = ffn_norm(x, res,
16 True)`` under ``config.fuse_norm`` — wrapped as a delegating
17 ``GeneralizedComponent`` because ``NormalizationBridge`` can't express it.
18- ``head_dim`` is read but never assigned (aliases the read-only ``d_head``).
19"""
21from typing import Any
23from transformer_lens.model_bridge.architecture_adapter import ArchitectureAdapter
24from transformer_lens.model_bridge.generalized_components import (
25 EmbeddingBridge,
26 LinearBridge,
27 NormalizationBridge,
28 OpaqueBlockBridge,
29 UnembeddingBridge,
30)
31from transformer_lens.model_bridge.generalized_components.base import (
32 GeneralizedComponent,
33)
34from transformer_lens.model_bridge.supported_architectures._remote_code_compat import (
35 force_import_remote_class,
36 iter_remote_modeling_modules,
37 patch_init_weights_skip_loaded,
38 retie_weights_keys_v5,
39)
42class RWKV7ArchitectureAdapter(ArchitectureAdapter):
43 """Architecture adapter for RWKV7ForCausalLM (RWKV-7 "Goose").
45 Attention-free recurrent decoder: a flat stack of pre-norm blocks, each a
46 generalized-delta-rule time-mixing sublayer plus a token-shifted squared-ReLU
47 channel-mixing sublayer, wrapped by standard biased LayerNorm. The recurrence
48 and the cross-block ``v_first`` threading live inside the ``fla`` remote-code
49 forward, which the bridge delegates to; see the module docstring for the full
50 set of adapter decisions.
51 """
53 # Attention-free and recurrent — off the transformer-shaped verify_models
54 # path; correctness lives in the integration tests (bridge-vs-HF parity).
55 applicable_phases: list[int] = []
56 # fla threads recurrence through its own Cache object, not past_key_values;
57 # generation recomputes the full prefix per step (exact, O(n^2)).
58 supports_kv_cache: bool = False
59 # The fla forward ignores attention_mask, so left-padding would silently
60 # poison the recurrent state instead of being masked out.
61 supports_batched_generation: bool = False
63 def __init__(self, cfg: Any) -> None:
64 """Initialize the RWKV-7 architecture adapter."""
65 super().__init__(cfg)
67 # Standard biased LayerNorm, no positional embeddings, ungated FFN.
68 self.cfg.normalization_type = "LN"
69 self.cfg.uses_rms_norm = False
70 self.cfg.positional_embedding_type = "none"
71 self.cfg.final_rms = False
72 self.cfg.attn_only = False
73 self.cfg.gated_mlp = False
75 # fla drives recurrent decode state via its own Cache, not the
76 # cache_params convention, so the adapter does not advertise a cache.
77 setattr(self.cfg, "is_stateful", False)
79 # Surface the RWKV-7 shape attributes on cfg so they are present on both
80 # the HF-boot path (also via _HF_PASSTHROUGH_ATTRS) and the synthetic
81 # config path used by the unit tests. getattr-with-default keeps a bare
82 # TransformerBridgeConfig from raising.
83 # head_dim is a read-only property on TransformerBridgeConfig (aliases
84 # d_head), so it is read but never assigned. num_heads defaults to
85 # d_model // head_dim.
86 head_dim = getattr(cfg, "head_dim", 64) or 64
87 num_heads = getattr(cfg, "num_heads", None) or max(1, self.cfg.d_model // head_dim)
88 value_dim = getattr(cfg, "value_dim", None) or [self.cfg.d_model] * self.cfg.n_layers
89 # setattr (not direct assignment) for the RWKV-7-specific names so mypy
90 # does not flag them as undeclared on TransformerBridgeConfig.
91 setattr(self.cfg, "num_heads", num_heads)
92 setattr(self.cfg, "value_dim", value_dim)
93 setattr(self.cfg, "decay_low_rank_dim", getattr(cfg, "decay_low_rank_dim", 64))
94 setattr(self.cfg, "gate_low_rank_dim", getattr(cfg, "gate_low_rank_dim", 128))
95 setattr(self.cfg, "a_low_rank_dim", getattr(cfg, "a_low_rank_dim", 64))
96 setattr(self.cfg, "v_low_rank_dim", getattr(cfg, "v_low_rank_dim", 16))
97 setattr(self.cfg, "norm_first", getattr(cfg, "norm_first", True))
98 setattr(self.cfg, "norm_bias", getattr(cfg, "norm_bias", True))
99 setattr(self.cfg, "fuse_norm", getattr(cfg, "fuse_norm", True))
100 setattr(self.cfg, "attn_mode", getattr(cfg, "attn_mode", "chunk"))
101 setattr(self.cfg, "hidden_act", getattr(cfg, "hidden_act", "sqrelu"))
102 # LayerNorm epsilon for the reimplementing NormalizationBridge path (eps
103 # is a real config field, so direct assignment is fine).
104 self.cfg.eps = getattr(cfg, "norm_eps", getattr(cfg, "eps", 1e-5))
106 # Full delegation to the fla forward — no HT-format weight reshaping.
107 self.weight_processing_conversions = {}
109 self.component_mapping = {
110 "embed": EmbeddingBridge(name="model.embeddings"),
111 "blocks": OpaqueBlockBridge(
112 name="model.layers",
113 submodules={
114 # Pre-norm before time-mixing (standard single-input LayerNorm).
115 "attn_norm": NormalizationBridge(
116 name="attn_norm", config=self.cfg, uses_rms_norm=False
117 ),
118 # Time-mixing: generalized delta rule. Delegated passthrough;
119 # only the four projections are exposed (LoRAs / GroupNorm /
120 # lerp params stay inside the fla forward).
121 "attn": GeneralizedComponent(
122 name="attn",
123 config=self.cfg,
124 submodules={
125 "r_proj": LinearBridge(name="r_proj"),
126 "k_proj": LinearBridge(name="k_proj"),
127 "v_proj": LinearBridge(name="v_proj"),
128 "o_proj": LinearBridge(name="o_proj"),
129 },
130 ),
131 # Pre-norm before channel-mixing. Under config.fuse_norm the
132 # block calls this as ffn_norm(x, residual, True) -> (normed,
133 # residual), a fused signature the reimplementing
134 # NormalizationBridge can't express, so delegate any-signature
135 # to the live HF module with I/O hooks.
136 "ffn_norm": GeneralizedComponent(name="ffn_norm", config=self.cfg),
137 # Channel-mixing: token-shifted squared-ReLU MLP. Delegated
138 # passthrough exposing the up ("key") and down ("value")
139 # projections. HF confusingly names the output proj "value".
140 # NOT MLPBridge (cf. rwkv4): its hook_in pre-fire would
141 # suppress key.hook_in, the post-token-shift input — the one
142 # interesting tensor here. Aliases give hook_pre/hook_post.
143 "ffn": GeneralizedComponent(
144 name="ffn",
145 config=self.cfg,
146 hook_alias_overrides={
147 "hook_pre": "key.hook_out",
148 "hook_post": "value.hook_in",
149 },
150 submodules={
151 "key": LinearBridge(name="key"),
152 "value": LinearBridge(name="value"),
153 },
154 ),
155 },
156 ),
157 "ln_final": NormalizationBridge(
158 name="model.norm", config=self.cfg, uses_rms_norm=False
159 ),
160 "unembed": UnembeddingBridge(name="lm_head"),
161 }
163 def prepare_loading(self, model_name: str, model_kwargs: dict) -> None:
164 """Patch fla's RWKV-7 remote code for transformers v5 compatibility.
166 Two defensive patches, mirroring raven:
168 1. Tied-weights format. ``RWKV7ForCausalLM._tied_weights_keys`` is a list
169 (``["lm_head.weight"]``, the 4.x form). v5's ``tie_weights`` ->
170 ``get_expanded_tied_weights_keys`` calls ``.keys()`` on the mapping,
171 which raises ``AttributeError`` on a list. Rewrite it to the v5 dict
172 form ``{"lm_head.weight": "model.embeddings.weight"}``. RWKV-7 defaults
173 ``tie_word_embeddings=False`` (so the list path is usually short-
174 circuited before ``.keys()``), but the rewrite is harmless when untied
175 and prevents the crash on any tied checkpoint.
177 2. Weight re-init. Under v5's meta-device load-then-materialise flow,
178 ``PreTrainedModel._init_weights`` is invoked on modules that already
179 hold checkpoint weights, re-randomising them. Guard it to skip modules
180 whose parameters are already on a real (non-meta) device — the same
181 defensive patch openelm.py / raven.py apply.
183 Args:
184 model_name: The HuggingFace model name/path.
185 model_kwargs: The kwargs dict for from_pretrained().
186 """
187 # Force-import the fla RWKV-7 modeling module so its classes appear in
188 # sys.modules to patch. fla is normally a pip package; fall back to the
189 # dynamic-module route for genuinely bundled remote code.
190 try:
191 import fla.models.rwkv7.modeling_rwkv7 # noqa: F401
192 except Exception:
193 if force_import_remote_class(model_name, "modeling_rwkv7.RWKV7ForCausalLM") is None: 193 ↛ 194line 193 didn't jump to line 194 because the condition on line 193 was never true
194 return
196 # Patch every loaded RWKV-7 modeling module (each remote revision gets its
197 # own module object in sys.modules).
198 for module in iter_remote_modeling_modules("rwkv7"):
199 # Patch 1: tied-weights keys list -> v5 dict form.
200 retie_weights_keys_v5(
201 getattr(module, "RWKV7ForCausalLM", None),
202 {"lm_head.weight": "model.embeddings.weight"},
203 )
204 # Patch 2: don't re-randomise already-loaded weights.
205 pretrained_class = getattr(module, "RWKV7PreTrainedModel", None)
206 if pretrained_class is not None: 206 ↛ 198line 206 didn't jump to line 198 because the condition on line 206 was always true
207 patch_init_weights_skip_loaded(pretrained_class)