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

60 statements  

« prev     ^ index     » next       coverage.py v7.10.1, created at 2026-08-11 18:50 +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 "ffn": GeneralizedComponent( 

146 name="ffn", 

147 config=self.cfg, 

148 submodules={ 

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

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

151 }, 

152 ), 

153 }, 

154 ), 

155 "ln_final": NormalizationBridge( 

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

157 ), 

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

159 } 

160 

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

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

163 

164 Two defensive patches, mirroring raven: 

165 

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

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

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

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

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

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

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

173 and prevents the crash on any tied checkpoint. 

174 

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

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

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

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

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

180 

181 Args: 

182 model_name: The HuggingFace model name/path. 

183 model_kwargs: The kwargs dict for from_pretrained(). 

184 """ 

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

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

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

188 try: 

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

190 except Exception: 

191 try: 

192 from transformers.dynamic_module_utils import ( 

193 get_class_from_dynamic_module, 

194 ) 

195 

196 get_class_from_dynamic_module( 

197 "modeling_rwkv7.RWKV7ForCausalLM", 

198 model_name, 

199 ) 

200 except Exception: 

201 return 

202 

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

204 # own module object in sys.modules). 

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

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

207 continue 

208 module = sys.modules[key] 

209 

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

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

212 if causal_lm_class is not None and isinstance( 

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

214 ): 

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

216 

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

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

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

220 continue 

221 original_init_weights = pretrained_class._init_weights 

222 

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

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

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

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

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

228 return 

229 _original(self, mod) 

230 

231 pretrained_class._init_weights = safe_init_weights 

232 pretrained_class._tl_patched = True