Text Ranking
sentence-transformers
Safetensors
Transformers
multilingual
t5gemma2
text2text-generation
reranker
encoder-decoder
FBNL
Retrieval
RAG

Add sentence-transformers CrossEncoder support

#2
by cosyy - opened
config_sentence_transformers.json ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_type": "CrossEncoder",
3
+ "__version__": {
4
+ "sentence_transformers": "5.6.0",
5
+ "transformers": "5.3.0",
6
+ "pytorch": "2.6.0"
7
+ },
8
+ "prompts": {
9
+ "retrieval": "Given a query, retrieve documents that answer the query."
10
+ },
11
+ "default_prompt_name": "retrieval",
12
+ "activation_fn": "torch.nn.modules.activation.Sigmoid"
13
+ }
kalm_cross_encoder.py ADDED
@@ -0,0 +1,298 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, ClassVar
4
+
5
+ try:
6
+ from typing import Self
7
+ except ImportError:
8
+ from typing_extensions import Self
9
+
10
+ import torch
11
+ from sentence_transformers.base.modules import InputModule
12
+ from transformers import AutoConfig, AutoModelForSeq2SeqLM, AutoTokenizer
13
+
14
+ from .kalm_reranker_utils import (
15
+ DEFAULT_INSTRUCTION,
16
+ DEFAULT_SYSTEM_INSTRUCTION,
17
+ answer_token_id,
18
+ build_decoder_text,
19
+ cast_floating_parameters,
20
+ extract_yes_no_logits,
21
+ forward_reranker_model,
22
+ normalize_requested_dtype,
23
+ validate_text_pairs,
24
+ )
25
+
26
+
27
+ class KaLMCrossEncoderModule(InputModule):
28
+ """Sentence Transformers input module for KaLM encoder-decoder rerankers."""
29
+
30
+ config_file_name = "kalm_cross_encoder_config.json"
31
+ config_keys: ClassVar[list[str]] = [
32
+ "query_max_length",
33
+ "document_max_length",
34
+ "encoder_chunk_size",
35
+ "system_instruction",
36
+ ]
37
+ save_in_root = True
38
+
39
+ def __init__(
40
+ self,
41
+ model_name_or_path: str,
42
+ *,
43
+ query_max_length: int = 512,
44
+ document_max_length: int = 1024,
45
+ encoder_chunk_size: int | None = 4,
46
+ system_instruction: str = DEFAULT_SYSTEM_INSTRUCTION,
47
+ model_kwargs: dict[str, Any] | None = None,
48
+ processor_kwargs: dict[str, Any] | None = None,
49
+ config_kwargs: dict[str, Any] | None = None,
50
+ backend: str = "torch",
51
+ ) -> None:
52
+ super().__init__()
53
+ if backend != "torch":
54
+ raise ValueError(
55
+ "KaLMCrossEncoderModule only supports backend='torch'; "
56
+ f"received {backend!r}."
57
+ )
58
+ if not isinstance(model_name_or_path, str) or not model_name_or_path:
59
+ raise ValueError("model_name_or_path must be a non-empty string.")
60
+ if not isinstance(query_max_length, int) or query_max_length <= 0:
61
+ raise ValueError("query_max_length must be a positive integer.")
62
+ if not isinstance(document_max_length, int) or document_max_length <= 0:
63
+ raise ValueError("document_max_length must be a positive integer.")
64
+ if encoder_chunk_size is not None and (
65
+ not isinstance(encoder_chunk_size, int) or encoder_chunk_size <= 0
66
+ ):
67
+ raise ValueError("encoder_chunk_size must be a positive integer or None.")
68
+ if not isinstance(system_instruction, str):
69
+ raise TypeError("system_instruction must be a string.")
70
+
71
+ self.query_max_length = query_max_length
72
+ self.max_seq_length = document_max_length
73
+ self.encoder_chunk_size = encoder_chunk_size
74
+ self.system_instruction = system_instruction
75
+ self.backend = backend
76
+
77
+ model_kwargs = dict(model_kwargs or {})
78
+ processor_kwargs = dict(processor_kwargs or {})
79
+ config_kwargs = dict(config_kwargs or {})
80
+
81
+ num_labels = config_kwargs.pop("num_labels", 1)
82
+ if num_labels != 1:
83
+ raise ValueError(
84
+ "KaLM reranking produces one relevance score; num_labels must be 1."
85
+ )
86
+
87
+ config = AutoConfig.from_pretrained(model_name_or_path, **config_kwargs)
88
+ self.tokenizer = AutoTokenizer.from_pretrained(
89
+ model_name_or_path, **processor_kwargs
90
+ )
91
+ if self.tokenizer.pad_token_id is None:
92
+ if self.tokenizer.eos_token_id is None:
93
+ raise ValueError(
94
+ "The tokenizer must define a pad token or an EOS token."
95
+ )
96
+ self.tokenizer.pad_token = self.tokenizer.eos_token
97
+ self.tokenizer.padding_side = "right"
98
+ self.processor = self.tokenizer
99
+
100
+ requested_dtype = normalize_requested_dtype(
101
+ model_kwargs.get("dtype", model_kwargs.get("torch_dtype"))
102
+ )
103
+ self.model = AutoModelForSeq2SeqLM.from_pretrained(
104
+ model_name_or_path,
105
+ config=config,
106
+ **model_kwargs,
107
+ )
108
+ cast_floating_parameters(self.model, requested_dtype)
109
+
110
+ self.yes_token_id = answer_token_id(self.tokenizer, "yes")
111
+ self.no_token_id = answer_token_id(self.tokenizer, "no")
112
+
113
+ @property
114
+ def document_max_length(self) -> int:
115
+ return self.max_seq_length
116
+
117
+ @document_max_length.setter
118
+ def document_max_length(self, value: int) -> None:
119
+ if not isinstance(value, int) or value <= 0:
120
+ raise ValueError("document_max_length must be a positive integer.")
121
+ self.max_seq_length = value
122
+
123
+ @property
124
+ def encoder_chunk_size(self) -> int | None:
125
+ return self._encoder_chunk_size
126
+
127
+ @encoder_chunk_size.setter
128
+ def encoder_chunk_size(self, value: int | None) -> None:
129
+ if value is not None and (not isinstance(value, int) or value <= 0):
130
+ raise ValueError("encoder_chunk_size must be a positive integer or None.")
131
+ self._encoder_chunk_size = value
132
+
133
+ @property
134
+ def chunk_size(self) -> int | None:
135
+ """Alias for the encoder token mean-pooling compression rate."""
136
+ return self.encoder_chunk_size
137
+
138
+ @chunk_size.setter
139
+ def chunk_size(self, value: int | None) -> None:
140
+ self.encoder_chunk_size = value
141
+
142
+ def preprocess(
143
+ self,
144
+ inputs: list[Any],
145
+ prompt: str | None = None,
146
+ **kwargs: Any,
147
+ ) -> dict[str, torch.Tensor]:
148
+ pairs = validate_text_pairs(inputs)
149
+ if not pairs:
150
+ return {}
151
+
152
+ instruction = DEFAULT_INSTRUCTION if prompt is None else prompt
153
+ if not isinstance(instruction, str):
154
+ raise TypeError("prompt must be a string or None.")
155
+
156
+ encoder_texts = [f"<Document>: {document}" for _, document in pairs]
157
+ decoder_texts = [
158
+ build_decoder_text(
159
+ self.tokenizer,
160
+ query,
161
+ instruction,
162
+ self.system_instruction,
163
+ self.query_max_length,
164
+ )
165
+ for query, _ in pairs
166
+ ]
167
+
168
+ encoder_batch = self.tokenizer(
169
+ encoder_texts,
170
+ padding=True,
171
+ truncation=True,
172
+ max_length=self.document_max_length,
173
+ add_special_tokens=False,
174
+ return_tensors="pt",
175
+ )
176
+ decoder_batch = self.tokenizer(
177
+ decoder_texts,
178
+ padding=True,
179
+ pad_to_multiple_of=8,
180
+ add_special_tokens=False,
181
+ return_tensors="pt",
182
+ )
183
+ return {
184
+ "input_ids": encoder_batch["input_ids"],
185
+ "attention_mask": encoder_batch["attention_mask"],
186
+ "decoder_input_ids": decoder_batch["input_ids"],
187
+ "decoder_attention_mask": decoder_batch["attention_mask"],
188
+ }
189
+
190
+ def forward(
191
+ self,
192
+ features: dict[str, torch.Tensor | Any],
193
+ **kwargs: Any,
194
+ ) -> dict[str, torch.Tensor | Any]:
195
+ outputs = forward_reranker_model(
196
+ self.model,
197
+ input_ids=features["input_ids"],
198
+ attention_mask=features["attention_mask"],
199
+ decoder_input_ids=features["decoder_input_ids"],
200
+ decoder_attention_mask=features["decoder_attention_mask"],
201
+ encoder_chunk_size=self.chunk_size,
202
+ )
203
+ yes_no_logits = extract_yes_no_logits(
204
+ outputs.logits,
205
+ features["decoder_attention_mask"],
206
+ self.yes_token_id,
207
+ self.no_token_id,
208
+ )
209
+ features["scores"] = (yes_no_logits[:, 0] - yes_no_logits[:, 1]).unsqueeze(1)
210
+ return features
211
+
212
+ def save(
213
+ self,
214
+ output_path: str,
215
+ *args: Any,
216
+ safe_serialization: bool = True,
217
+ **kwargs: Any,
218
+ ) -> None:
219
+ self.model.save_pretrained(output_path, safe_serialization=safe_serialization)
220
+ self.tokenizer.save_pretrained(output_path)
221
+ self.save_config(output_path)
222
+
223
+ @classmethod
224
+ def load(
225
+ cls,
226
+ model_name_or_path: str,
227
+ subfolder: str = "",
228
+ token: bool | str | None = None,
229
+ cache_folder: str | None = None,
230
+ revision: str | None = None,
231
+ local_files_only: bool = False,
232
+ trust_remote_code: bool = False,
233
+ model_kwargs: dict[str, Any] | None = None,
234
+ processor_kwargs: dict[str, Any] | None = None,
235
+ config_kwargs: dict[str, Any] | None = None,
236
+ backend: str = "torch",
237
+ **kwargs: Any,
238
+ ) -> Self:
239
+ module_config = cls.load_config(
240
+ model_name_or_path,
241
+ subfolder=subfolder,
242
+ token=token,
243
+ cache_folder=cache_folder,
244
+ revision=revision,
245
+ local_files_only=local_files_only,
246
+ )
247
+
248
+ supplied_model_kwargs = dict(model_kwargs or {})
249
+ supplied_config_kwargs = dict(config_kwargs or {})
250
+ supplied_module_kwargs = dict(kwargs)
251
+ chunk_size_values: list[tuple[str, int | None]] = []
252
+ for source_name, source in (
253
+ ("model_kwargs", supplied_model_kwargs),
254
+ ("config_kwargs", supplied_config_kwargs),
255
+ ("module kwargs", supplied_module_kwargs),
256
+ ):
257
+ for key in ("chunk_size", "encoder_chunk_size"):
258
+ if key in source:
259
+ chunk_size_values.append((f"{source_name}.{key}", source.pop(key)))
260
+ if chunk_size_values:
261
+ first_name, first_value = chunk_size_values[0]
262
+ for current_name, current_value in chunk_size_values[1:]:
263
+ if current_value != first_value:
264
+ raise ValueError(
265
+ "Conflicting encoder chunk sizes: "
266
+ f"{first_name}={first_value!r}, "
267
+ f"{current_name}={current_value!r}."
268
+ )
269
+ module_config["encoder_chunk_size"] = first_value
270
+
271
+ hub_kwargs = {
272
+ "subfolder": subfolder,
273
+ "token": token,
274
+ "cache_dir": cache_folder,
275
+ "revision": revision,
276
+ "local_files_only": local_files_only,
277
+ "trust_remote_code": trust_remote_code,
278
+ }
279
+ effective_model_kwargs = {**hub_kwargs, **supplied_model_kwargs}
280
+ effective_processor_kwargs = {**hub_kwargs, **(processor_kwargs or {})}
281
+ effective_config_kwargs = {**hub_kwargs, **supplied_config_kwargs}
282
+
283
+ if "model_max_length" in effective_processor_kwargs:
284
+ module_config["document_max_length"] = effective_processor_kwargs[
285
+ "model_max_length"
286
+ ]
287
+
288
+ return cls(
289
+ model_name_or_path,
290
+ model_kwargs=effective_model_kwargs,
291
+ processor_kwargs=effective_processor_kwargs,
292
+ config_kwargs=effective_config_kwargs,
293
+ backend=backend,
294
+ **module_config,
295
+ )
296
+
297
+
298
+ __all__ = ["KaLMCrossEncoderModule"]
kalm_cross_encoder_config.json ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ {
2
+ "query_max_length": 512,
3
+ "document_max_length": 1024,
4
+ "encoder_chunk_size": 4,
5
+ "system_instruction": "Judge whether the Document meets the requirements based on the Query and the Instruct provided. Note that the answer can only be \"yes\" or \"no\"."
6
+ }
kalm_reranker.py CHANGED
@@ -4,16 +4,34 @@ from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
4
 
5
  import numpy as np
6
  import torch
7
- import torch.nn.functional as F
8
  from transformers import AutoModelForSeq2SeqLM, AutoTokenizer
9
- from transformers.modeling_outputs import BaseModelOutput
10
 
11
-
12
- DEFAULT_INSTRUCTION = "Given a query, retrieve documents that answer the query."
13
- DEFAULT_SYSTEM_INSTRUCTION = (
14
- "Judge whether the Document meets the requirements based on the Query and "
15
- 'the Instruct provided. Note that the answer can only be "yes" or "no".'
16
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
 
19
  class KaLMReranker:
@@ -60,9 +78,11 @@ class KaLMReranker:
60
  self.tokenizer = AutoTokenizer.from_pretrained(model_name_or_path)
61
  if self.tokenizer.pad_token_id is None:
62
  if self.tokenizer.eos_token_id is None:
63
- raise ValueError("The tokenizer must define a pad token or an EOS token.")
 
 
64
  self.tokenizer.pad_token = self.tokenizer.eos_token
65
-
66
  self.tokenizer.padding_side = "right"
67
 
68
  self.model = AutoModelForSeq2SeqLM.from_pretrained(
@@ -70,10 +90,7 @@ class KaLMReranker:
70
  dtype=self.dtype,
71
  **model_kwargs,
72
  )
73
-
74
- for parameter in self.model.parameters():
75
- if parameter.is_floating_point() and parameter.dtype != self.dtype:
76
- parameter.data = parameter.data.to(dtype=self.dtype)
77
  self.model.to(device=self.device)
78
  self.model.eval()
79
 
@@ -98,7 +115,9 @@ class KaLMReranker:
98
  if isinstance(dtype, torch.dtype):
99
  return dtype
100
  if not isinstance(dtype, str):
101
- raise TypeError("dtype must be a torch.dtype or a string such as 'bfloat16'.")
 
 
102
  normalized = dtype.lower().removeprefix("torch.")
103
  supported = {
104
  "bfloat16": torch.bfloat16,
@@ -113,17 +132,10 @@ class KaLMReranker:
113
  return supported[normalized]
114
 
115
  def _answer_token_id(self, answer: str) -> int:
116
- token_ids = self.tokenizer(answer, add_special_tokens=False)["input_ids"]
117
- if not token_ids:
118
- raise ValueError(f"Failed to tokenize the answer {answer!r}.")
119
- return token_ids[-1]
120
 
121
  def _get_encoder(self):
122
- if hasattr(self.model, "get_encoder"):
123
- return self.model.get_encoder()
124
- if hasattr(self.model, "encoder"):
125
- return self.model.encoder
126
- raise AttributeError(f"Cannot find the encoder on {type(self.model).__name__}.")
127
 
128
  @staticmethod
129
  def _pool_encoder_chunks(
@@ -131,64 +143,22 @@ class KaLMReranker:
131
  attention_mask: torch.Tensor,
132
  chunk_size: int,
133
  ) -> Tuple[torch.Tensor, torch.Tensor]:
134
- batch_size, sequence_length, hidden_size = hidden_states.shape
135
- num_chunks = (sequence_length + chunk_size - 1) // chunk_size
136
- padded_length = num_chunks * chunk_size
137
- pad_length = padded_length - sequence_length
138
-
139
- if pad_length:
140
- hidden_states = F.pad(hidden_states, (0, 0, 0, pad_length))
141
- attention_mask = F.pad(attention_mask, (0, pad_length))
142
-
143
- hidden_states = hidden_states.view(
144
- batch_size, num_chunks, chunk_size, hidden_size
145
- )
146
- chunk_mask = attention_mask.view(batch_size, num_chunks, chunk_size)
147
- expanded_mask = chunk_mask.unsqueeze(-1).to(hidden_states.dtype)
148
- pooled_hidden = (hidden_states * expanded_mask).sum(dim=2)
149
- pooled_hidden = pooled_hidden / chunk_mask.sum(dim=2).clamp(min=1).unsqueeze(-1)
150
- pooled_mask = (chunk_mask.sum(dim=2) > 0).to(attention_mask.dtype)
151
- return pooled_hidden, pooled_mask
152
 
153
  def _decoder_text(self, query: str, instruction: str) -> str:
154
- query_ids = self.tokenizer(
 
155
  query,
156
- add_special_tokens=False,
157
- truncation=True,
158
- max_length=self.query_max_length,
159
- )["input_ids"]
160
- truncated_query = self.tokenizer.decode(
161
- query_ids,
162
- skip_special_tokens=False,
163
- clean_up_tokenization_spaces=False,
164
- )
165
- return (
166
- "<bos><start_of_turn>user\n"
167
- f"{self.system_instruction}\n\n"
168
- f"<Instruct>: {instruction}\n"
169
- f"<Query>: {truncated_query}<end_of_turn>\n"
170
- "<start_of_turn>model\n\n\n\n"
171
  )
172
 
173
  @staticmethod
174
  def _validate_pairs(
175
  pairs: Sequence[Tuple[str, str]],
176
  ) -> List[Tuple[str, str]]:
177
- if isinstance(pairs, (str, bytes)) or not isinstance(pairs, Sequence):
178
- raise TypeError("pairs must be a sequence of (query, document) pairs.")
179
- validated: List[Tuple[str, str]] = []
180
- for index, pair in enumerate(pairs):
181
- if (
182
- isinstance(pair, (str, bytes))
183
- or not isinstance(pair, Sequence)
184
- or len(pair) != 2
185
- ):
186
- raise ValueError(f"pairs[{index}] must contain exactly two strings.")
187
- query, document = pair
188
- if not isinstance(query, str) or not isinstance(document, str):
189
- raise TypeError(f"pairs[{index}] must contain exactly two strings.")
190
- validated.append((query, document))
191
- return validated
192
 
193
  @torch.inference_mode()
194
  def _predict_batch(
@@ -213,49 +183,20 @@ class KaLMReranker:
213
  return_tensors="pt",
214
  ).to(self.device)
215
 
216
- if self.chunk_size is None:
217
- outputs = self.model(
218
- input_ids=encoder_batch["input_ids"],
219
- attention_mask=encoder_batch["attention_mask"],
220
- decoder_input_ids=decoder_batch["input_ids"],
221
- decoder_attention_mask=decoder_batch["attention_mask"],
222
- return_dict=True,
223
- )
224
- else:
225
- encoder_outputs = self._get_encoder()(
226
- input_ids=encoder_batch["input_ids"],
227
- attention_mask=encoder_batch["attention_mask"],
228
- return_dict=True,
229
- )
230
- pooled_hidden, pooled_mask = self._pool_encoder_chunks(
231
- encoder_outputs.last_hidden_state,
232
- encoder_batch["attention_mask"],
233
- self.chunk_size,
234
- )
235
- outputs = self.model(
236
- encoder_outputs=BaseModelOutput(last_hidden_state=pooled_hidden),
237
- attention_mask=pooled_mask,
238
- decoder_input_ids=decoder_batch["input_ids"],
239
- decoder_attention_mask=decoder_batch["attention_mask"],
240
- return_dict=True,
241
- )
242
-
243
- sequence_lengths = decoder_batch["attention_mask"].sum(dim=1) - 1
244
- batch_indices = torch.arange(outputs.logits.shape[0], device=self.device)
245
- last_logits = outputs.logits[batch_indices, sequence_lengths]
246
- yes_no_logits = torch.stack(
247
- (
248
- last_logits[:, self.yes_token_id],
249
- last_logits[:, self.no_token_id],
250
- ),
251
- dim=-1,
252
- ).float()
253
- if not torch.isfinite(yes_no_logits).all():
254
- bad_count = (~torch.isfinite(yes_no_logits).all(dim=-1)).sum().item()
255
- raise RuntimeError(
256
- f"The model produced non-finite yes/no logits for {bad_count} input(s). "
257
- "Use bfloat16 or float32 instead of float16."
258
- )
259
  return torch.softmax(yes_no_logits, dim=-1)[:, 0].cpu().tolist()
260
 
261
  def predict(
@@ -276,7 +217,6 @@ class KaLMReranker:
276
  if not isinstance(effective_batch_size, int) or effective_batch_size <= 0:
277
  raise ValueError("batch_size must be a positive integer.")
278
 
279
-
280
  length_sorted_indices = np.argsort(
281
  [-(len(query) + len(document)) for query, document in validated_pairs]
282
  )
@@ -296,11 +236,6 @@ class KaLMReranker:
296
  torch.cuda.empty_cache()
297
  tested_batch_size = max(1, tested_batch_size * 3 // 4)
298
 
299
- # The while loop's condition (`> 1`) means batch size 1 is never
300
- # actually probed. If every size down to 2 OOMs, it exits without a
301
- # successful probe. Only skip ahead to `tested_batch_size` when the
302
- # probe actually ran; otherwise fall back to starting at 0 like the
303
- # loop below always did originally, or the first item(s) get dropped.
304
  if first_batch_scores is None:
305
  sorted_scores: List[float] = []
306
  loop_start = 0
@@ -319,8 +254,8 @@ class KaLMReranker:
319
  if torch.cuda.is_available():
320
  torch.cuda.empty_cache()
321
  raise RuntimeError(
322
- "CUDA ran out of memory during reranking. Retry with a smaller batch_size "
323
- "or shorter max_length."
324
  ) from error
325
  inverse_indices = np.argsort(length_sorted_indices)
326
  return [sorted_scores[index] for index in inverse_indices]
@@ -357,4 +292,4 @@ class KaLMReranker:
357
  return rankings if top_k is None else rankings[:top_k]
358
 
359
 
360
- __all__ = ["KaLMReranker"]
 
4
 
5
  import numpy as np
6
  import torch
 
7
  from transformers import AutoModelForSeq2SeqLM, AutoTokenizer
 
8
 
9
+ try:
10
+ from .kalm_reranker_utils import (
11
+ DEFAULT_INSTRUCTION,
12
+ DEFAULT_SYSTEM_INSTRUCTION,
13
+ answer_token_id,
14
+ build_decoder_text,
15
+ cast_floating_parameters,
16
+ extract_yes_no_logits,
17
+ forward_reranker_model,
18
+ get_encoder,
19
+ pool_encoder_chunks,
20
+ validate_text_pairs,
21
+ )
22
+ except ImportError: # Support ``from kalm_reranker import KaLMReranker``.
23
+ from kalm_reranker_utils import (
24
+ DEFAULT_INSTRUCTION,
25
+ DEFAULT_SYSTEM_INSTRUCTION,
26
+ answer_token_id,
27
+ build_decoder_text,
28
+ cast_floating_parameters,
29
+ extract_yes_no_logits,
30
+ forward_reranker_model,
31
+ get_encoder,
32
+ pool_encoder_chunks,
33
+ validate_text_pairs,
34
+ )
35
 
36
 
37
  class KaLMReranker:
 
78
  self.tokenizer = AutoTokenizer.from_pretrained(model_name_or_path)
79
  if self.tokenizer.pad_token_id is None:
80
  if self.tokenizer.eos_token_id is None:
81
+ raise ValueError(
82
+ "The tokenizer must define a pad token or an EOS token."
83
+ )
84
  self.tokenizer.pad_token = self.tokenizer.eos_token
85
+ # Final decoder-token indexing assumes right padding, matching training.
86
  self.tokenizer.padding_side = "right"
87
 
88
  self.model = AutoModelForSeq2SeqLM.from_pretrained(
 
90
  dtype=self.dtype,
91
  **model_kwargs,
92
  )
93
+ cast_floating_parameters(self.model, self.dtype)
 
 
 
94
  self.model.to(device=self.device)
95
  self.model.eval()
96
 
 
115
  if isinstance(dtype, torch.dtype):
116
  return dtype
117
  if not isinstance(dtype, str):
118
+ raise TypeError(
119
+ "dtype must be a torch.dtype or a string such as 'bfloat16'."
120
+ )
121
  normalized = dtype.lower().removeprefix("torch.")
122
  supported = {
123
  "bfloat16": torch.bfloat16,
 
132
  return supported[normalized]
133
 
134
  def _answer_token_id(self, answer: str) -> int:
135
+ return answer_token_id(self.tokenizer, answer)
 
 
 
136
 
137
  def _get_encoder(self):
138
+ return get_encoder(self.model)
 
 
 
 
139
 
140
  @staticmethod
141
  def _pool_encoder_chunks(
 
143
  attention_mask: torch.Tensor,
144
  chunk_size: int,
145
  ) -> Tuple[torch.Tensor, torch.Tensor]:
146
+ return pool_encoder_chunks(hidden_states, attention_mask, chunk_size)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
 
148
  def _decoder_text(self, query: str, instruction: str) -> str:
149
+ return build_decoder_text(
150
+ self.tokenizer,
151
  query,
152
+ instruction,
153
+ self.system_instruction,
154
+ self.query_max_length,
 
 
 
 
 
 
 
 
 
 
 
 
155
  )
156
 
157
  @staticmethod
158
  def _validate_pairs(
159
  pairs: Sequence[Tuple[str, str]],
160
  ) -> List[Tuple[str, str]]:
161
+ return validate_text_pairs(pairs)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
 
163
  @torch.inference_mode()
164
  def _predict_batch(
 
183
  return_tensors="pt",
184
  ).to(self.device)
185
 
186
+ outputs = forward_reranker_model(
187
+ self.model,
188
+ input_ids=encoder_batch["input_ids"],
189
+ attention_mask=encoder_batch["attention_mask"],
190
+ decoder_input_ids=decoder_batch["input_ids"],
191
+ decoder_attention_mask=decoder_batch["attention_mask"],
192
+ encoder_chunk_size=self.chunk_size,
193
+ )
194
+ yes_no_logits = extract_yes_no_logits(
195
+ outputs.logits,
196
+ decoder_batch["attention_mask"],
197
+ self.yes_token_id,
198
+ self.no_token_id,
199
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
200
  return torch.softmax(yes_no_logits, dim=-1)[:, 0].cpu().tolist()
201
 
202
  def predict(
 
217
  if not isinstance(effective_batch_size, int) or effective_batch_size <= 0:
218
  raise ValueError("batch_size must be a positive integer.")
219
 
 
220
  length_sorted_indices = np.argsort(
221
  [-(len(query) + len(document)) for query, document in validated_pairs]
222
  )
 
236
  torch.cuda.empty_cache()
237
  tested_batch_size = max(1, tested_batch_size * 3 // 4)
238
 
 
 
 
 
 
239
  if first_batch_scores is None:
240
  sorted_scores: List[float] = []
241
  loop_start = 0
 
254
  if torch.cuda.is_available():
255
  torch.cuda.empty_cache()
256
  raise RuntimeError(
257
+ "CUDA ran out of memory during reranking. Retry with a smaller "
258
+ "batch_size or shorter max_length."
259
  ) from error
260
  inverse_indices = np.argsort(length_sorted_indices)
261
  return [sorted_scores[index] for index in inverse_indices]
 
292
  return rankings if top_k is None else rankings[:top_k]
293
 
294
 
295
+ __all__ = ["KaLMReranker"]
kalm_reranker_utils.py ADDED
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Sequence
4
+ from typing import Any
5
+
6
+ import torch
7
+ import torch.nn.functional as F
8
+ from transformers.modeling_outputs import BaseModelOutput
9
+
10
+ DEFAULT_INSTRUCTION = "Given a query, retrieve documents that answer the query."
11
+ DEFAULT_SYSTEM_INSTRUCTION = (
12
+ "Judge whether the Document meets the requirements based on the Query and "
13
+ 'the Instruct provided. Note that the answer can only be "yes" or "no".'
14
+ )
15
+
16
+
17
+ def validate_text_pairs(inputs: Sequence[Sequence[str]]) -> list[tuple[str, str]]:
18
+ """Validate and normalize a batch of ``(query, document)`` pairs."""
19
+ if isinstance(inputs, (str, bytes)) or not isinstance(inputs, Sequence):
20
+ raise TypeError("inputs must be a sequence of (query, document) pairs.")
21
+
22
+ validated: list[tuple[str, str]] = []
23
+ for index, pair in enumerate(inputs):
24
+ if (
25
+ isinstance(pair, (str, bytes))
26
+ or not isinstance(pair, Sequence)
27
+ or len(pair) != 2
28
+ ):
29
+ raise ValueError(f"inputs[{index}] must contain exactly two strings.")
30
+ query, document = pair
31
+ if not isinstance(query, str) or not isinstance(document, str):
32
+ raise TypeError(f"inputs[{index}] must contain exactly two strings.")
33
+ validated.append((query, document))
34
+ return validated
35
+
36
+
37
+ def answer_token_id(tokenizer: Any, answer: str) -> int:
38
+ """Return the single vocabulary token used to score an answer."""
39
+ token_ids = tokenizer(answer, add_special_tokens=False)["input_ids"]
40
+ if len(token_ids) != 1:
41
+ raise ValueError(
42
+ f"The answer {answer!r} must tokenize to exactly one token, "
43
+ f"got {token_ids!r}."
44
+ )
45
+ return token_ids[0]
46
+
47
+
48
+ def build_decoder_text(
49
+ tokenizer: Any,
50
+ query: str,
51
+ instruction: str,
52
+ system_instruction: str,
53
+ query_max_length: int,
54
+ ) -> str:
55
+ """Build the decoder-side instruction/query prompt used during training."""
56
+ query_ids = tokenizer(
57
+ query,
58
+ add_special_tokens=False,
59
+ truncation=True,
60
+ max_length=query_max_length,
61
+ )["input_ids"]
62
+ truncated_query = tokenizer.decode(
63
+ query_ids,
64
+ skip_special_tokens=False,
65
+ clean_up_tokenization_spaces=False,
66
+ )
67
+ return (
68
+ "<bos><start_of_turn>user\n"
69
+ f"{system_instruction}\n\n"
70
+ f"<Instruct>: {instruction}\n"
71
+ f"<Query>: {truncated_query}<end_of_turn>\n"
72
+ "<start_of_turn>model\n\n\n\n"
73
+ )
74
+
75
+
76
+ def get_encoder(model: torch.nn.Module) -> torch.nn.Module:
77
+ if hasattr(model, "get_encoder"):
78
+ return model.get_encoder()
79
+ if hasattr(model, "encoder"):
80
+ return model.encoder
81
+ raise AttributeError(f"Cannot find the encoder on {type(model).__name__}.")
82
+
83
+
84
+ def pool_encoder_chunks(
85
+ hidden_states: torch.Tensor,
86
+ attention_mask: torch.Tensor,
87
+ chunk_size: int,
88
+ ) -> tuple[torch.Tensor, torch.Tensor]:
89
+ """Mean-pool consecutive encoder tokens while respecting padding."""
90
+ if chunk_size <= 0:
91
+ raise ValueError("chunk_size must be positive.")
92
+
93
+ batch_size, sequence_length, hidden_size = hidden_states.shape
94
+ num_chunks = (sequence_length + chunk_size - 1) // chunk_size
95
+ padded_length = num_chunks * chunk_size
96
+ pad_length = padded_length - sequence_length
97
+
98
+ if pad_length:
99
+ hidden_states = F.pad(hidden_states, (0, 0, 0, pad_length))
100
+ attention_mask = F.pad(attention_mask, (0, pad_length))
101
+
102
+ hidden_states = hidden_states.view(batch_size, num_chunks, chunk_size, hidden_size)
103
+ chunk_mask = attention_mask.view(batch_size, num_chunks, chunk_size)
104
+ expanded_mask = chunk_mask.unsqueeze(-1).to(hidden_states.dtype)
105
+ pooled_hidden = (hidden_states * expanded_mask).sum(dim=2)
106
+ pooled_hidden = pooled_hidden / chunk_mask.sum(dim=2).clamp(min=1).unsqueeze(-1)
107
+ pooled_mask = (chunk_mask.sum(dim=2) > 0).to(attention_mask.dtype)
108
+ return pooled_hidden, pooled_mask
109
+
110
+
111
+ def forward_reranker_model(
112
+ model: torch.nn.Module,
113
+ *,
114
+ input_ids: torch.Tensor,
115
+ attention_mask: torch.Tensor,
116
+ decoder_input_ids: torch.Tensor,
117
+ decoder_attention_mask: torch.Tensor,
118
+ encoder_chunk_size: int | None,
119
+ ):
120
+ """Run the encoder-decoder model with optional encoder token compression."""
121
+ if encoder_chunk_size is None:
122
+ return model(
123
+ input_ids=input_ids,
124
+ attention_mask=attention_mask,
125
+ decoder_input_ids=decoder_input_ids,
126
+ decoder_attention_mask=decoder_attention_mask,
127
+ return_dict=True,
128
+ )
129
+
130
+ encoder_outputs = get_encoder(model)(
131
+ input_ids=input_ids,
132
+ attention_mask=attention_mask,
133
+ return_dict=True,
134
+ )
135
+ pooled_hidden, pooled_mask = pool_encoder_chunks(
136
+ encoder_outputs.last_hidden_state,
137
+ attention_mask,
138
+ encoder_chunk_size,
139
+ )
140
+ return model(
141
+ encoder_outputs=BaseModelOutput(last_hidden_state=pooled_hidden),
142
+ attention_mask=pooled_mask,
143
+ decoder_input_ids=decoder_input_ids,
144
+ decoder_attention_mask=decoder_attention_mask,
145
+ return_dict=True,
146
+ )
147
+
148
+
149
+ def extract_yes_no_logits(
150
+ logits: torch.Tensor,
151
+ decoder_attention_mask: torch.Tensor,
152
+ yes_token_id: int,
153
+ no_token_id: int,
154
+ ) -> torch.Tensor:
155
+ """Extract float32 yes/no logits at each sample's final non-padding token."""
156
+ if decoder_attention_mask.ndim != 2:
157
+ raise ValueError("decoder_attention_mask must have shape [batch, sequence].")
158
+ sequence_lengths = decoder_attention_mask.sum(dim=1) - 1
159
+ if (sequence_lengths < 0).any():
160
+ raise ValueError(
161
+ "Every decoder input must contain at least one non-padding token."
162
+ )
163
+
164
+ batch_indices = torch.arange(logits.shape[0], device=logits.device)
165
+ last_logits = logits[batch_indices, sequence_lengths]
166
+ yes_no_logits = torch.stack(
167
+ (last_logits[:, yes_token_id], last_logits[:, no_token_id]), dim=-1
168
+ ).float()
169
+ if not torch.isfinite(yes_no_logits).all():
170
+ bad_count = (~torch.isfinite(yes_no_logits).all(dim=-1)).sum().item()
171
+ raise RuntimeError(
172
+ f"The model produced non-finite yes/no logits for {bad_count} input(s). "
173
+ "Use bfloat16 or float32 instead of float16."
174
+ )
175
+ return yes_no_logits
176
+
177
+
178
+ def normalize_requested_dtype(dtype: Any) -> torch.dtype | None:
179
+ """Normalize a caller-provided dtype without changing the ``auto`` behavior."""
180
+ if dtype is None or dtype == "auto":
181
+ return None
182
+ if isinstance(dtype, torch.dtype):
183
+ return dtype
184
+ if not isinstance(dtype, str):
185
+ return None
186
+ normalized = dtype.lower().removeprefix("torch.")
187
+ return {
188
+ "bfloat16": torch.bfloat16,
189
+ "bf16": torch.bfloat16,
190
+ "float16": torch.float16,
191
+ "fp16": torch.float16,
192
+ "float32": torch.float32,
193
+ "fp32": torch.float32,
194
+ }.get(normalized)
195
+
196
+
197
+ def cast_floating_parameters(model: torch.nn.Module, dtype: torch.dtype | None) -> None:
198
+ """Cast model parameters while preserving checkpoint buffer dtypes."""
199
+ if dtype is None:
200
+ return
201
+ for parameter in model.parameters():
202
+ if parameter.is_floating_point() and parameter.dtype != dtype:
203
+ parameter.data = parameter.data.to(dtype=dtype)
204
+
205
+
206
+ __all__ = [
207
+ "DEFAULT_INSTRUCTION",
208
+ "DEFAULT_SYSTEM_INSTRUCTION",
209
+ "answer_token_id",
210
+ "build_decoder_text",
211
+ "cast_floating_parameters",
212
+ "extract_yes_no_logits",
213
+ "forward_reranker_model",
214
+ "get_encoder",
215
+ "normalize_requested_dtype",
216
+ "pool_encoder_chunks",
217
+ "validate_text_pairs",
218
+ ]
modules.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "idx": 0,
4
+ "name": "0",
5
+ "path": "",
6
+ "type": "kalm_cross_encoder.KaLMCrossEncoderModule"
7
+ }
8
+ ]