Coverage for transformer_lens/model_bridge/supported_architectures/ouro.py: 57%

47 statements  

« prev     ^ index     » next       coverage.py v7.10.1, created at 2026-08-11 18:50 +0000

1"""Ouro architecture adapter.""" 

2 

3import sys 

4from typing import Any, Optional 

5 

6import torch 

7 

8from transformer_lens.model_bridge.architecture_adapter import ArchitectureAdapter 

9from transformer_lens.model_bridge.generalized_components import ( 

10 BlockBridge, 

11 EmbeddingBridge, 

12 GatedMLPBridge, 

13 LinearBridge, 

14 PositionEmbeddingsAttentionBridge, 

15 RMSNormalizationBridge, 

16 RotaryEmbeddingBridge, 

17 UnembeddingBridge, 

18) 

19 

20 

21def _compute_default_rope_parameters( 

22 config: Any, 

23 device: Optional[torch.device] = None, 

24 seq_len: Optional[int] = None, 

25 **rope_kwargs: Any, 

26) -> tuple[torch.Tensor, float]: 

27 """Standard (unscaled) RoPE inverse frequencies, as transformers v4 defined them. 

28 

29 Transformers v5 removed the "default" entry from ROPE_INIT_FUNCTIONS (standard 

30 RoPE moved to a per-model static method), but Ouro's remote code still looks it 

31 up. Signature and return match the v4 contract the remote code calls with: 

32 (config, device) -> (inv_freq, attention_scaling). 

33 """ 

34 base = config.rope_theta 

35 partial_rotary_factor = getattr(config, "partial_rotary_factor", None) or 1.0 

36 head_dim = getattr(config, "head_dim", None) or config.hidden_size // config.num_attention_heads 

37 dim = int(head_dim * partial_rotary_factor) 

38 inv_freq = 1.0 / ( 

39 base 

40 ** (torch.arange(0, dim, 2, dtype=torch.int64).to(device=device, dtype=torch.float) / dim) 

41 ) 

42 return inv_freq, 1.0 

43 

44 

45class OuroArchitectureAdapter(ArchitectureAdapter): 

46 """Architecture adapter for ByteDance Ouro (LoopLM) models. 

47 

48 Ouro is a looped-depth ("Universal Transformer") decoder: the remote-code 

49 ``OuroModel.forward`` applies the same ``num_hidden_layers``-deep stack 

50 ``total_ut_steps`` times (4 for the released checkpoints) within a single 

51 forward pass, applying ``model.norm`` after every pass. The loop lives 

52 entirely inside the HF forward, which the bridge delegates to, so logits 

53 and generation are correct with no loop handling here. ``n_layers`` counts 

54 the physical layers; each block's hooks fire once per loop step, and a 

55 cache records the final step's value. The same holds for ``ln_final`` 

56 (``model.norm``): it runs after EVERY UT pass, so its hooks fire 

57 ``total_ut_steps`` times per forward and ``run_with_cache`` keeps only the 

58 last pass. 

59 

60 The backbone is Qwen2/Llama-shaped (RoPE, no-bias q/k/v/o projections, 

61 SwiGLU gate/up/down MLP, untied lm_head) with one twist: sandwich 

62 normalization. Each decoder layer has FOUR RMSNorms; the extra two 

63 (``input_layernorm_2``, ``post_attention_layernorm_2``) apply to the 

64 sublayer outputs before the residual add, exactly like Gemma2's 

65 ``ln1_post``/``ln2_post`` but without Gemma's +1.0 RMSNorm offset. 

66 

67 Deliberately not mapped by this adapter: 

68 

69 - per-loop-step hooks (a cache holds the final UT step only) 

70 - ``model.early_exit_gate``, the adaptive-exit halting head 

71 - the ``UniversalTransformerCache`` slot layout (``step * n_layers + layer``) 

72 

73 Loading requires ``trust_remote_code=True`` (``auto_map`` to 

74 ``modeling_ouro``). 

75 

76 Optional Parameters (may not exist in state_dict): 

77 ------------------------------------------------- 

78 Ouro models do NOT have biases on any mapped linear layers: 

79 

80 - blocks.{i}.attn.b_Q / b_K / b_V / b_O - no attention biases 

81 - blocks.{i}.mlp.b_gate / b_in / b_out - no MLP biases 

82 - blocks.{i}.ln1.b / ln1_post.b / ln2.b / ln2_post.b - RMSNorm has no bias 

83 - ln_final.b - RMSNorm has no bias 

84 

85 Weight processing must handle these missing biases gracefully using 

86 ProcessWeights._safe_get_tensor() or by checking for None values. 

87 """ 

88 

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

90 """Initialize the Ouro architecture adapter.""" 

91 super().__init__(cfg) 

92 

93 # Set config variables for weight processing 

94 self.cfg.normalization_type = "RMS" 

95 self.cfg.positional_embedding_type = "rotary" 

96 self.cfg.final_rms = True 

97 self.cfg.gated_mlp = True 

98 self.cfg.attn_only = False 

99 self.cfg.uses_rms_norm = True 

100 # default_prepend_bos stays at the framework default: the GPT2-style BPE 

101 # tokenizer (bos == eos == <|endoftext|>) does not prepend BOS itself. 

102 

103 # ln_final (model.norm) is applied after EVERY UT pass, feeding the next 

104 # pass and the early-exit gate, so it is not a final-only norm. Folding 

105 # it into W_U resets the live module's norm weight the loop reuses and 

106 # corrupts UT passes 1..N-1. 

107 self.supports_fold_ln = False 

108 

109 self.weight_processing_conversions = { 

110 **self._qkvo_weight_conversions(), 

111 } 

112 self.component_mapping = { 

113 "embed": EmbeddingBridge(name="model.embed_tokens"), 

114 "rotary_emb": RotaryEmbeddingBridge(name="model.rotary_emb"), 

115 "blocks": BlockBridge( 

116 name="model.layers", 

117 config=self.cfg, 

118 submodules={ 

119 "ln1": RMSNormalizationBridge(name="input_layernorm", config=self.cfg), 

120 "ln1_post": RMSNormalizationBridge(name="input_layernorm_2", config=self.cfg), 

121 "ln2": RMSNormalizationBridge(name="post_attention_layernorm", config=self.cfg), 

122 "ln2_post": RMSNormalizationBridge( 

123 name="post_attention_layernorm_2", config=self.cfg 

124 ), 

125 "attn": PositionEmbeddingsAttentionBridge( 

126 name="self_attn", 

127 config=self.cfg, 

128 submodules={ 

129 "q": LinearBridge(name="q_proj"), 

130 "k": LinearBridge(name="k_proj"), 

131 "v": LinearBridge(name="v_proj"), 

132 "o": LinearBridge(name="o_proj"), 

133 }, 

134 requires_attention_mask=True, 

135 requires_position_embeddings=True, 

136 ), 

137 "mlp": GatedMLPBridge( 

138 name="mlp", 

139 config=self.cfg, 

140 submodules={ 

141 "gate": LinearBridge(name="gate_proj"), 

142 "in": LinearBridge(name="up_proj"), 

143 "out": LinearBridge(name="down_proj"), 

144 }, 

145 ), 

146 }, 

147 ), 

148 "ln_final": RMSNormalizationBridge(name="model.norm", config=self.cfg), 

149 "unembed": UnembeddingBridge(name="lm_head", config=self.cfg), 

150 } 

151 

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

153 """Patch Ouro's remote code for compatibility with transformers v5. 

154 

155 Ouro's modeling code was written against transformers 4.55, where 

156 standard RoPE lived in ROPE_INIT_FUNCTIONS["default"]. Transformers v5 

157 removed that key and instead expects each *RotaryEmbedding class to 

158 carry a compute_default_rope_parameters static method. Two call sites 

159 break, so two patches: 

160 

161 1. OuroRotaryEmbedding.__init__ does ROPE_INIT_FUNCTIONS["default"] 

162 (KeyError). Rebind the module-level name inside the imported 

163 modeling_ouro module(s) to a copy with "default" restored; the 

164 shared transformers dict is left untouched. 

165 2. v5's PreTrainedModel._init_weights re-initializes RotaryEmbedding 

166 buffers via module.compute_default_rope_parameters(config) 

167 (AttributeError). Attach the same function as a static method. 

168 

169 Args: 

170 model_name: The HuggingFace model name/path 

171 model_kwargs: The kwargs dict for from_pretrained() 

172 """ 

173 # Force-import the modeling module so we can patch it 

174 try: 

175 from transformers.dynamic_module_utils import get_class_from_dynamic_module 

176 

177 get_class_from_dynamic_module( 

178 "modeling_ouro.OuroForCausalLM", 

179 model_name, 

180 ) 

181 except Exception: 

182 return 

183 

184 # Each checkpoint revision gets its own module in sys.modules, so patch 

185 # every imported Ouro modeling module (same idiom as openelm.py). 

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

187 if "ouro" in key.lower() and "modeling" in key.lower(): 

188 module = sys.modules[key] 

189 rope_functions = getattr(module, "ROPE_INIT_FUNCTIONS", None) 

190 if rope_functions is not None and "default" not in rope_functions: 

191 setattr( 

192 module, 

193 "ROPE_INIT_FUNCTIONS", 

194 {**rope_functions, "default": _compute_default_rope_parameters}, 

195 ) 

196 rope_class = getattr(module, "OuroRotaryEmbedding", None) 

197 if rope_class is not None and not hasattr( 

198 rope_class, "compute_default_rope_parameters" 

199 ): 

200 rope_class.compute_default_rope_parameters = staticmethod( 

201 _compute_default_rope_parameters 

202 ) 

203 

204 def setup_component_testing(self, hf_model: Any, bridge_model: Any = None) -> None: 

205 """Set up rotary embedding references for Ouro component testing. 

206 

207 Ouro uses RoPE (Rotary Position Embeddings) with a single shared 

208 ``model.rotary_emb``. We set the rotary_emb reference on all attention 

209 bridge instances for component testing. 

210 

211 Args: 

212 hf_model: The HuggingFace Ouro model instance 

213 bridge_model: The TransformerBridge model (if available, set rotary_emb on actual instances) 

214 """ 

215 # Get rotary embedding instance from the model 

216 rotary_emb = hf_model.model.rotary_emb 

217 

218 # Set rotary_emb on actual bridge instances in bridge_model if available 

219 if bridge_model is not None and hasattr(bridge_model, "blocks"): 

220 # Set on each layer's actual attention bridge instance 

221 for block in bridge_model.blocks: 

222 if hasattr(block, "attn"): 

223 block.attn.set_rotary_emb(rotary_emb) 

224 

225 # Also set on the template for get_generalized_component() calls 

226 attn_bridge = self.get_generalized_component("blocks.0.attn") 

227 attn_bridge.set_rotary_emb(rotary_emb)