Xunzhuo commited on
Commit
d5727fb
·
verified ·
1 Parent(s): eb42120

Load Vela Reranker directly with Transformers

Browse files
Files changed (3) hide show
  1. README.md +17 -0
  2. config.json +18 -1
  3. modeling_vela_reranker.py +271 -0
README.md CHANGED
@@ -35,4 +35,21 @@ Vela Reranker ranks passages by their relevance to a query.
35
 
36
  Higher scores indicate greater relevance. The default uses the 22-layer, 768-dimensional exit.
37
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  [Explore the Vela model collection](https://huggingface.co/collections/llm-semantic-router/vela-10-router-models-6aa555ba70cc6997d6d67798)
 
35
 
36
  Higher scores indicate greater relevance. The default uses the 22-layer, 768-dimensional exit.
37
 
38
+ ## Quick start
39
+
40
+ Install `torch`, `transformers`, and `safetensors`.
41
+
42
+ ```python
43
+ import torch
44
+ from transformers import AutoModel, AutoTokenizer
45
+
46
+ model_id = "llm-semantic-router/Vela-1.0-Encoder-307M-Reranker"
47
+ tokenizer = AutoTokenizer.from_pretrained(model_id)
48
+ model = AutoModel.from_pretrained(model_id, trust_remote_code=True).eval()
49
+ inputs = tokenizer(["When does the library open?"], ["The library opens in the morning."], padding=True, truncation=False, return_tensors="pt")
50
+ with torch.inference_mode():
51
+ scores = model(**inputs).logits
52
+ print(scores)
53
+ ```
54
+
55
  [Explore the Vela model collection](https://huggingface.co/collections/llm-semantic-router/vela-10-router-models-6aa555ba70cc6997d6d67798)
config.json CHANGED
@@ -57,5 +57,22 @@
57
  "sparse_pred_ignore_index": -100,
58
  "sparse_prediction": false,
59
  "transformers_version": "4.57.6",
60
- "vocab_size": 256000
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  }
 
57
  "sparse_pred_ignore_index": -100,
58
  "sparse_prediction": false,
59
  "transformers_version": "4.57.6",
60
+ "vocab_size": 256000,
61
+ "layer_indices": [
62
+ 3,
63
+ 6,
64
+ 11,
65
+ 22
66
+ ],
67
+ "dim_indices": [
68
+ 768,
69
+ 512,
70
+ 256,
71
+ 128,
72
+ 64
73
+ ],
74
+ "auto_map": {
75
+ "AutoConfig": "modeling_vela_reranker.VelaRerankerConfig",
76
+ "AutoModel": "modeling_vela_reranker.VelaReranker"
77
+ }
78
  }
modeling_vela_reranker.py ADDED
@@ -0,0 +1,271 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Vela's 20-exit reranker, loaded through Transformers AutoModel."""
2
+
3
+ from dataclasses import dataclass
4
+ import json
5
+ from pathlib import Path
6
+ import shutil
7
+
8
+ from huggingface_hub import snapshot_download
9
+ from safetensors.torch import load_file, save_file
10
+
11
+ import torch
12
+ from torch import nn
13
+ from transformers import ModernBertConfig, ModernBertModel, PreTrainedModel
14
+ from transformers.utils import ModelOutput
15
+
16
+
17
+ class VelaRerankerConfig(ModernBertConfig):
18
+ model_type = "modernbert"
19
+
20
+ def __init__(self, layer_indices=None, dim_indices=None, **kwargs):
21
+ super().__init__(**kwargs)
22
+ self.layer_indices = [3, 6, 11, 22] if layer_indices is None else layer_indices
23
+ self.dim_indices = (
24
+ [768, 512, 256, 128, 64] if dim_indices is None else dim_indices
25
+ )
26
+
27
+
28
+ @dataclass
29
+ class VelaRerankerOutput(ModelOutput):
30
+ logits: torch.Tensor | None = None
31
+ all_scores: dict[str, torch.Tensor] | None = None
32
+
33
+
34
+ class VelaReranker(PreTrainedModel):
35
+ config_class = VelaRerankerConfig
36
+ base_model_prefix = "encoder"
37
+ _supports_sdpa = True
38
+ _no_split_modules = ["ModernBertEncoderLayer"]
39
+ _keep_in_fp32_modules = ["layer_heads"]
40
+
41
+ def __init__(self, config, encoder=None):
42
+ super().__init__(config)
43
+ expected = {
44
+ "version": 1,
45
+ "intermediate_normalization": "final_norm",
46
+ "final_normalization": "final_norm",
47
+ "pooling": "cls",
48
+ "head_dtype": "float32",
49
+ }
50
+ if getattr(config, "representation_contract", None) != expected:
51
+ raise ValueError("Unsupported reranker representation contract")
52
+ self.layer_indices = list(config.layer_indices)
53
+ self.dim_indices = list(config.dim_indices)
54
+ if (
55
+ self.layer_indices != sorted(set(self.layer_indices))
56
+ or not self.layer_indices
57
+ or self.layer_indices[-1] != config.num_hidden_layers
58
+ or any(type(x) is not int or x < 1 for x in self.layer_indices)
59
+ ):
60
+ raise ValueError("Invalid reranker layer indices")
61
+ if (
62
+ self.dim_indices != sorted(set(self.dim_indices), reverse=True)
63
+ or not self.dim_indices
64
+ or self.dim_indices[0] > config.hidden_size
65
+ or any(type(x) is not int or x < 2 for x in self.dim_indices)
66
+ ):
67
+ raise ValueError("Invalid reranker dimensions")
68
+ native = config.to_dict()
69
+ for key in ("auto_map", "architectures", "layer_indices", "dim_indices"):
70
+ native.pop(key, None)
71
+ native["model_type"] = "modernbert"
72
+ encoder_config = ModernBertConfig.from_dict(native)
73
+ if hasattr(encoder_config, "reference_compile"):
74
+ encoder_config.reference_compile = False
75
+ encoder_config._attn_implementation = config._attn_implementation or "sdpa"
76
+ self.encoder = ModernBertModel(encoder_config) if encoder is None else encoder
77
+ self.layer_heads = nn.ModuleDict(
78
+ {
79
+ str(layer): nn.ModuleDict(
80
+ {
81
+ str(dim): nn.Sequential(
82
+ nn.Linear(dim, dim // 2),
83
+ nn.GELU(),
84
+ nn.Dropout(0.1),
85
+ nn.Linear(dim // 2, 1),
86
+ )
87
+ for dim in self.dim_indices
88
+ }
89
+ )
90
+ for layer in self.layer_indices
91
+ }
92
+ )
93
+ self.post_init()
94
+
95
+ @classmethod
96
+ def from_pretrained(
97
+ cls,
98
+ pretrained_model_name_or_path,
99
+ *model_args,
100
+ config=None,
101
+ revision=None,
102
+ cache_dir=None,
103
+ token=None,
104
+ local_files_only=False,
105
+ force_download=False,
106
+ torch_dtype=None,
107
+ dtype=None,
108
+ attn_implementation="sdpa",
109
+ **kwargs,
110
+ ):
111
+ """Load the encoder and every trained head from one immutable snapshot."""
112
+ for key in (
113
+ "_from_auto",
114
+ "_commit_hash",
115
+ "trust_remote_code",
116
+ "adapter_kwargs",
117
+ ):
118
+ kwargs.pop(key, None)
119
+ if model_args or kwargs:
120
+ raise TypeError(f"Unsupported loading arguments: {sorted(kwargs)}")
121
+ path = Path(pretrained_model_name_or_path)
122
+ if not path.is_dir():
123
+ pinned = getattr(config, "_commit_hash", None) or revision
124
+ path = Path(
125
+ snapshot_download(
126
+ pretrained_model_name_or_path,
127
+ revision=pinned,
128
+ cache_dir=cache_dir,
129
+ token=token,
130
+ local_files_only=local_files_only,
131
+ force_download=force_download,
132
+ allow_patterns=[
133
+ "config.json",
134
+ "model.safetensors",
135
+ "model-*.safetensors",
136
+ "model.safetensors.index.json",
137
+ "classification_heads.safetensors",
138
+ "matryoshka_config.json",
139
+ ],
140
+ )
141
+ )
142
+ if config is None:
143
+ config = VelaRerankerConfig.from_pretrained(path, local_files_only=True)
144
+ saved = json.loads((path / "matryoshka_config.json").read_text())
145
+ if (
146
+ saved["layer_indices"] != config.layer_indices
147
+ or saved["dim_indices"] != config.dim_indices
148
+ or saved.get("pooling_strategy") != "cls"
149
+ or saved.get("representation_contract") != config.representation_contract
150
+ ):
151
+ raise ValueError("Head metadata does not match the model configuration")
152
+ native = config.to_dict()
153
+ for key in ("auto_map", "architectures", "layer_indices", "dim_indices"):
154
+ native.pop(key, None)
155
+ encoder_config = ModernBertConfig.from_dict(native)
156
+ if hasattr(encoder_config, "reference_compile"):
157
+ encoder_config.reference_compile = False
158
+ precision = dtype if dtype is not None else torch_dtype
159
+ encoder, loading = ModernBertModel.from_pretrained(
160
+ path,
161
+ config=encoder_config,
162
+ local_files_only=True,
163
+ torch_dtype=precision or torch.float32,
164
+ attn_implementation=attn_implementation or "sdpa",
165
+ output_loading_info=True,
166
+ )
167
+ if any(
168
+ loading.get(key)
169
+ for key in (
170
+ "missing_keys",
171
+ "unexpected_keys",
172
+ "mismatched_keys",
173
+ "error_msgs",
174
+ )
175
+ ):
176
+ raise ValueError("Encoder checkpoint is incomplete or incompatible")
177
+ model = cls(config, encoder=encoder)
178
+ heads = load_file(path / "classification_heads.safetensors", device="cpu")
179
+ if any(tensor.dtype != torch.float32 for tensor in heads.values()):
180
+ raise ValueError("Saved reranker heads must be float32")
181
+ model.layer_heads.load_state_dict(heads, strict=True)
182
+ return model.eval()
183
+
184
+ def save_pretrained(self, save_directory, **kwargs):
185
+ """Retain the native encoder and separate-head artifact layout."""
186
+ path = Path(save_directory)
187
+ path.mkdir(parents=True, exist_ok=True)
188
+ self.encoder.save_pretrained(path, **kwargs)
189
+ state = {
190
+ key: value.detach().cpu().contiguous()
191
+ for key, value in self.layer_heads.state_dict().items()
192
+ }
193
+ if any(value.dtype != torch.float32 for value in state.values()):
194
+ raise ValueError("Reranker heads must retain float32 precision")
195
+ save_file(state, path / "classification_heads.safetensors")
196
+ metadata = {
197
+ "layer_indices": self.layer_indices,
198
+ "dim_indices": self.dim_indices,
199
+ "hidden_size": self.config.hidden_size,
200
+ "num_layers": self.config.num_hidden_layers,
201
+ "pooling_strategy": "cls",
202
+ "representation_contract": self.config.representation_contract,
203
+ }
204
+ (path / "matryoshka_config.json").write_text(
205
+ json.dumps(metadata, indent=2) + "\n"
206
+ )
207
+ self.config.auto_map = {
208
+ "AutoConfig": "modeling_vela_reranker.VelaRerankerConfig",
209
+ "AutoModel": "modeling_vela_reranker.VelaReranker",
210
+ }
211
+ self.config.architectures = ["ModernBertModel"]
212
+ self.config.save_pretrained(path)
213
+ target = path / "modeling_vela_reranker.py"
214
+ if Path(__file__).resolve() != target.resolve():
215
+ shutil.copyfile(__file__, target)
216
+
217
+ def forward(
218
+ self,
219
+ input_ids,
220
+ attention_mask=None,
221
+ position_ids=None,
222
+ layer_idx=None,
223
+ dim_idx=None,
224
+ return_all_scores=False,
225
+ ):
226
+ if (
227
+ input_ids.ndim != 2
228
+ or not 1 <= input_ids.shape[1] <= self.config.max_position_embeddings
229
+ ):
230
+ raise ValueError("Input must fit the configured token capacity")
231
+ if attention_mask is None:
232
+ attention_mask = torch.ones_like(input_ids)
233
+ if attention_mask.shape != input_ids.shape:
234
+ raise ValueError("Attention mask shape must match input_ids")
235
+ layers = self.layer_indices if layer_idx is None else [layer_idx]
236
+ dims = self.dim_indices if dim_idx is None else [dim_idx]
237
+ if any(layer not in self.layer_indices for layer in layers) or any(
238
+ dim not in self.dim_indices for dim in dims
239
+ ):
240
+ raise ValueError("Requested exit is not present in this checkpoint")
241
+ if any(
242
+ parameter.dtype != torch.float32
243
+ for parameter in self.layer_heads.parameters()
244
+ ):
245
+ raise ValueError("Reranker heads must retain float32 precision")
246
+ scores = {}
247
+ with torch.autocast(device_type=input_ids.device.type, enabled=False):
248
+ outputs = self.encoder(
249
+ input_ids=input_ids,
250
+ attention_mask=attention_mask,
251
+ position_ids=position_ids,
252
+ output_hidden_states=True,
253
+ return_dict=True,
254
+ )
255
+ if len(outputs.hidden_states) != self.config.num_hidden_layers + 1:
256
+ raise ValueError("Unexpected encoder hidden-state layout")
257
+ for layer in layers:
258
+ hidden = (
259
+ outputs.last_hidden_state
260
+ if layer == self.config.num_hidden_layers
261
+ else self.encoder.final_norm(outputs.hidden_states[layer])
262
+ )
263
+ pooled = hidden[:, 0]
264
+ for dim in dims:
265
+ scores[f"layer_{layer}_dim_{dim}"] = self.layer_heads[str(layer)][
266
+ str(dim)
267
+ ](pooled[:, :dim].float()).squeeze(-1)
268
+ primary = f"layer_{layers[-1]}_dim_{dims[0]}"
269
+ return VelaRerankerOutput(
270
+ logits=scores[primary], all_scores=scores if return_all_scores else None
271
+ )