Coverage for transformer_lens/model_bridge/supported_architectures/rwkv7.py: 50%

60 statements  

« prev     ^ index     » next       coverage.py v7.10.1, created at 2026-09-01 16:23 +0000

1"""RWKV-7 ("Goose") architecture adapter (RWKV7ForCausalLM). 

2 

3Model family: ``fla-hub/rwkv7-*`` (e.g. ``fla-hub/rwkv7-0.1B-g1``), an 

4attention-free recurrent language model from the flash-linear-attention (``fla``) 

5library. Loaded via remote code (``trust_remote_code=True``); the modeling 

6classes live in ``fla.models.rwkv7``. 

7 

8Architecture overview 

9--------------------- 

10RWKV-7 is a flat stack of recurrent blocks over a shared residual width, wrapped 

11by standard (biased) LayerNorm rather than RMSNorm and with no positional 

12embeddings:: 

13 

14 embeddings -> [ RWKV7Block x N ] -> norm -> lm_head 

15 

16Each ``RWKV7Block`` is a pre-norm pair of a time-mixing and a channel-mixing 

17sublayer:: 

18 

19 x = x + attn(attn_norm(x)) # time-mixing (RWKV7Attention) 

20 x = x + ffn(ffn_norm(x)) # channel-mixing (RWKV7FeedForward) 

21 

22with an extra ``pre_norm`` LayerNorm on layer 0 only (``config.norm_first``). 

23The time-mixing sublayer is the RWKV-7 "generalized delta rule": token-shifted 

24lerp coefficients (``x_r``..``x_g``), receptance / key / value / output 

25projections (``r_proj`` / ``k_proj`` / ``v_proj`` / ``o_proj``), low-rank LoRAs 

26for the log-space decay / value blend / a-coefficient / gate, and a GroupNorm. 

27The channel-mixing sublayer is a token-shifted squared-ReLU MLP (``key`` up, 

28``value`` down). 

29 

30Cross-block ``v_first`` threading: ``RWKV7Model.forward`` initialises 

31``v_first = torch.zeros_like(hidden_states)`` and threads it through every block 

32(layer 0 fills it; later layers blend their value with it). This is managed 

33entirely by the HF model-level forward, so the bridge — which delegates each 

34block's forward to HF — gets it for free. 

35""" 

36 

37import sys 

38from typing import Any 

39 

40from transformer_lens.model_bridge.architecture_adapter import ArchitectureAdapter 

41from transformer_lens.model_bridge.generalized_components import ( 

42 EmbeddingBridge, 

43 LinearBridge, 

44 NormalizationBridge, 

45 OpaqueBlockBridge, 

46 UnembeddingBridge, 

47) 

48from transformer_lens.model_bridge.generalized_components.base import ( 

49 GeneralizedComponent, 

50) 

51 

52 

53class RWKV7ArchitectureAdapter(ArchitectureAdapter): 

54 """Architecture adapter for RWKV7ForCausalLM (RWKV-7 "Goose"). 

55 

56 Attention-free recurrent decoder: a flat stack of pre-norm blocks, each a 

57 generalized-delta-rule time-mixing sublayer plus a token-shifted squared-ReLU 

58 channel-mixing sublayer, wrapped by standard biased LayerNorm. The recurrence 

59 and the cross-block ``v_first`` threading live inside the ``fla`` remote-code 

60 forward, which the bridge delegates to; see the module docstring for the full 

61 set of adapter decisions. 

62 """ 

63 

64 # Attention-free and recurrent — off the transformer-shaped verify_models 

65 # path; correctness lives in the integration tests (bridge-vs-HF parity). 

66 applicable_phases: list[int] = [] 

67 

68 def __init__(self, cfg: Any) -> None: 

69 """Initialize the RWKV-7 architecture adapter.""" 

70 super().__init__(cfg) 

71 

72 # Standard biased LayerNorm, no positional embeddings, ungated FFN. 

73 self.cfg.normalization_type = "LN" 

74 self.cfg.uses_rms_norm = False 

75 self.cfg.positional_embedding_type = "none" 

76 self.cfg.final_rms = False 

77 self.cfg.attn_only = False 

78 self.cfg.gated_mlp = False 

79 

80 # fla drives recurrent decode state via its own Cache, not the 

81 # cache_params convention, so the adapter does not advertise a cache. 

82 setattr(self.cfg, "is_stateful", False) 

83 

84 # Surface the RWKV-7 shape attributes on cfg so they are present on both 

85 # the HF-boot path (also via _HF_PASSTHROUGH_ATTRS) and the synthetic 

86 # config path used by the unit tests. getattr-with-default keeps a bare 

87 # TransformerBridgeConfig from raising. 

88 # head_dim is a read-only property on TransformerBridgeConfig (aliases 

89 # d_head), so it is read but never assigned. num_heads defaults to 

90 # d_model // head_dim. 

91 head_dim = getattr(cfg, "head_dim", 64) or 64 

92 num_heads = getattr(cfg, "num_heads", None) or max(1, self.cfg.d_model // head_dim) 

93 value_dim = getattr(cfg, "value_dim", None) or [self.cfg.d_model] * self.cfg.n_layers 

94 # setattr (not direct assignment) for the RWKV-7-specific names so mypy 

95 # does not flag them as undeclared on TransformerBridgeConfig. 

96 setattr(self.cfg, "num_heads", num_heads) 

97 setattr(self.cfg, "value_dim", value_dim) 

98 setattr(self.cfg, "decay_low_rank_dim", getattr(cfg, "decay_low_rank_dim", 64)) 

99 setattr(self.cfg, "gate_low_rank_dim", getattr(cfg, "gate_low_rank_dim", 128)) 

100 setattr(self.cfg, "a_low_rank_dim", getattr(cfg, "a_low_rank_dim", 64)) 

101 setattr(self.cfg, "v_low_rank_dim", getattr(cfg, "v_low_rank_dim", 16)) 

102 setattr(self.cfg, "norm_first", getattr(cfg, "norm_first", True)) 

103 setattr(self.cfg, "norm_bias", getattr(cfg, "norm_bias", True)) 

104 setattr(self.cfg, "fuse_norm", getattr(cfg, "fuse_norm", True)) 

105 setattr(self.cfg, "attn_mode", getattr(cfg, "attn_mode", "chunk")) 

106 setattr(self.cfg, "hidden_act", getattr(cfg, "hidden_act", "sqrelu")) 

107 # LayerNorm epsilon for the reimplementing NormalizationBridge path (eps 

108 # is a real config field, so direct assignment is fine). 

109 self.cfg.eps = getattr(cfg, "norm_eps", getattr(cfg, "eps", 1e-5)) 

110 

111 # Full delegation to the fla forward — no HT-format weight reshaping. 

112 self.weight_processing_conversions = {} 

113 

114 self.component_mapping = { 

115 "embed": EmbeddingBridge(name="model.embeddings"), 

116 "blocks": OpaqueBlockBridge( 

117 name="model.layers", 

118 submodules={ 

119 # Pre-norm before time-mixing (standard single-input LayerNorm). 

120 "attn_norm": NormalizationBridge( 

121 name="attn_norm", config=self.cfg, uses_rms_norm=False 

122 ), 

123 # Time-mixing: generalized delta rule. Delegated passthrough; 

124 # only the four projections are exposed (LoRAs / GroupNorm / 

125 # lerp params stay inside the fla forward). 

126 "attn": GeneralizedComponent( 

127 name="attn", 

128 config=self.cfg, 

129 submodules={ 

130 "r_proj": LinearBridge(name="r_proj"), 

131 "k_proj": LinearBridge(name="k_proj"), 

132 "v_proj": LinearBridge(name="v_proj"), 

133 "o_proj": LinearBridge(name="o_proj"), 

134 }, 

135 ), 

136 # Pre-norm before channel-mixing. Under config.fuse_norm the 

137 # block calls this as ffn_norm(x, residual, True) -> (normed, 

138 # residual), a fused signature the reimplementing 

139 # NormalizationBridge can't express, so delegate any-signature 

140 # to the live HF module with I/O hooks. 

141 "ffn_norm": GeneralizedComponent(name="ffn_norm", config=self.cfg), 

142 # Channel-mixing: token-shifted squared-ReLU MLP. Delegated 

143 # passthrough exposing the up ("key") and down ("value") 

144 # projections. HF confusingly names the output proj "value". 

145 # NOT MLPBridge (cf. rwkv4): its hook_in pre-fire would 

146 # suppress key.hook_in, the post-token-shift input — the one 

147 # interesting tensor here. Aliases give hook_pre/hook_post. 

148 "ffn": GeneralizedComponent( 

149 name="ffn", 

150 config=self.cfg, 

151 hook_alias_overrides={ 

152 "hook_pre": "key.hook_out", 

153 "hook_post": "value.hook_in", 

154 }, 

155 submodules={ 

156 "key": LinearBridge(name="key"), 

157 "value": LinearBridge(name="value"), 

158 }, 

159 ), 

160 }, 

161 ), 

162 "ln_final": NormalizationBridge( 

163 name="model.norm", config=self.cfg, uses_rms_norm=False 

164 ), 

165 "unembed": UnembeddingBridge(name="lm_head"), 

166 } 

167 

168 def prepare_loading(self, model_name: str, model_kwargs: dict) -> None: 

169 """Patch fla's RWKV-7 remote code for transformers v5 compatibility. 

170 

171 Two defensive patches, mirroring raven: 

172 

173 1. Tied-weights format. ``RWKV7ForCausalLM._tied_weights_keys`` is a list 

174 (``["lm_head.weight"]``, the 4.x form). v5's ``tie_weights`` -> 

175 ``get_expanded_tied_weights_keys`` calls ``.keys()`` on the mapping, 

176 which raises ``AttributeError`` on a list. Rewrite it to the v5 dict 

177 form ``{"lm_head.weight": "model.embeddings.weight"}``. RWKV-7 defaults 

178 ``tie_word_embeddings=False`` (so the list path is usually short- 

179 circuited before ``.keys()``), but the rewrite is harmless when untied 

180 and prevents the crash on any tied checkpoint. 

181 

182 2. Weight re-init. Under v5's meta-device load-then-materialise flow, 

183 ``PreTrainedModel._init_weights`` is invoked on modules that already 

184 hold checkpoint weights, re-randomising them. Guard it to skip modules 

185 whose parameters are already on a real (non-meta) device — the same 

186 defensive patch openelm.py / raven.py apply. 

187 

188 Args: 

189 model_name: The HuggingFace model name/path. 

190 model_kwargs: The kwargs dict for from_pretrained(). 

191 """ 

192 # Force-import the fla RWKV-7 modeling module so its classes appear in 

193 # sys.modules to patch. fla is normally a pip package; fall back to the 

194 # dynamic-module route for genuinely bundled remote code. 

195 try: 

196 import fla.models.rwkv7.modeling_rwkv7 # noqa: F401 

197 except Exception: 

198 try: 

199 from transformers.dynamic_module_utils import ( 

200 get_class_from_dynamic_module, 

201 ) 

202 

203 get_class_from_dynamic_module( 

204 "modeling_rwkv7.RWKV7ForCausalLM", 

205 model_name, 

206 ) 

207 except Exception: 

208 return 

209 

210 # Patch every loaded RWKV-7 modeling module (each remote revision gets its 

211 # own module object in sys.modules). 

212 for key in list(sys.modules.keys()): 

213 if "rwkv7" not in key.lower() or "modeling" not in key.lower(): 

214 continue 

215 module = sys.modules[key] 

216 

217 # Patch 1: tied-weights keys list -> v5 dict form. 

218 causal_lm_class = getattr(module, "RWKV7ForCausalLM", None) 

219 if causal_lm_class is not None and isinstance( 

220 getattr(causal_lm_class, "_tied_weights_keys", None), list 

221 ): 

222 causal_lm_class._tied_weights_keys = {"lm_head.weight": "model.embeddings.weight"} 

223 

224 # Patch 2: don't re-randomise already-loaded weights. 

225 pretrained_class = getattr(module, "RWKV7PreTrainedModel", None) 

226 if pretrained_class is None or getattr(pretrained_class, "_tl_patched", False): 

227 continue 

228 original_init_weights = pretrained_class._init_weights 

229 

230 def safe_init_weights(self, mod, _original=original_init_weights): 

231 # Only initialise modules still on meta device (pre-loading); 

232 # never re-randomise weights already read from the checkpoint. 

233 first_param = next(mod.parameters(), None) 

234 if first_param is not None and first_param.device.type != "meta": 

235 return 

236 _original(self, mod) 

237 

238 pretrained_class._init_weights = safe_init_weights 

239 pretrained_class._tl_patched = True