A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 5, 07:15 UTC
[New Model]: jinaai/jina-reranker-v3 (#38800)
Signed-off-by: wang.yuqi <yuqi.wang@daocloud.io>
wang.yuqi committed
Apr 10, 2026 at 23:20 UTC
cb5f7501cbc80a394afbce0c877593be8cf6175c
14 files changed
+660
-19
docs/models/pooling_models/token_embed.md
+8
@@ -71,6 +71,14 @@ Models of any architecture can be converted into embedding models using `--conve
71
72
If your model is not in the above list, we will try to automatically convert the model using [as_embedding_model][vllm.model_executor.models.adapters.as_embedding_model].
73
74
+### Special models
75
+
76
+| Architecture | Models | Example HF Models | [LoRA](../../features/lora.md) | [PP](../../serving/parallelism_scaling.md) |
77
+| ------------ | ------ | ----------------- | -------------------- | ------------------------- |
78
+| `JinaForRanking` | Qwen3-based | `jinaai/jina-reranker-v3` | | |
79
+
80
+jina-reranker-v3 is a listwise document reranker model with a novel `last but not late interaction` architecture. More information can be found at: [examples/pooling/token_embed/jina_reranker_v3_offline.py](../../../examples/pooling/token_embed/jina_reranker_v3_offline.py)
81
+
82
--8<-- [end:supported-token-embed-models]
83
84
## Offline Inference
examples/pooling/token_embed/jina_reranker_v3_offline.py
new
+56
@@ -0,0 +1,56 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+# ruff: noqa: E501
4
+
5
+import torch.nn.functional as F
6
+
7
+from vllm import LLM
8
+
9
+query = "What are the health benefits of green tea?"
10
+documents = [
11
+ "Green tea contains antioxidants called catechins that may help reduce inflammation and protect cells from damage.",
12
+ "El precio del café ha aumentado un 20% este año debido a problemas en la cadena de suministro.",
13
+ "Studies show that drinking green tea regularly can improve brain function and boost metabolism.",
14
+ "Basketball is one of the most popular sports in the United States.",
15
+ "绿茶富含儿茶素等抗氧化剂,可以降低心脏病风险,还有助于控制体重。",
16
+ "Le thé vert est riche en antioxydants et peut améliorer la fonction cérébrale.",
17
+]
18
+
19
+
20
+def main():
21
+ # Initialize model
22
+ llm = LLM(
23
+ model="jinaai/jina-reranker-v3",
24
+ runner="pooling",
25
+ )
26
+
27
+ # Generate scores.
28
+ outputs = llm.score(query, documents)
29
+
30
+ # Print the outputs.
31
+ print("\nGenerated Outputs:\n" + "-" * 60)
32
+ for document, output in zip(documents, outputs):
33
+ score = output.outputs.score
34
+ print(f"Pair: {[query, document]!r} \nScore: {score}")
35
+ print("-" * 60)
36
+
37
+ # Generate embeddings.
38
+ # The JinaForRanking model concatenates docs first, then query.
39
+ # Let's stay consistent with this novel design.
40
+ outputs = llm.encode(documents + [query], pooling_task="token_embed")
41
+ embeds = outputs[0].outputs.data.float()
42
+
43
+ doc_embeds = embeds[:-1]
44
+ query_embeds = embeds[-1]
45
+
46
+ scores = F.cosine_similarity(query_embeds, doc_embeds)
47
+
48
+ # Print the outputs.
49
+ print("\nGenerated Outputs:\n" + "-" * 60)
50
+ for document, score in zip(documents, scores):
51
+ print(f"Pair: {[query, document]!r} \nScore: {score}")
52
+ print("-" * 60)
53
+
54
+
55
+if __name__ == "__main__":
56
+ main()
pyproject.toml
+2
-1
@@ -120,7 +120,8 @@ python = "./.venv"
120
[tool.typos.files]
121
# these files may be written in non english words
122
extend-exclude = ["tests/models/fixtures/*", "tests/prompts/*", "tests/tokenizers_/*",
123
- "benchmarks/sonnet.txt", "tests/lora/data/*", "examples/pooling/token_embed/*", "build/*",
123
+ "benchmarks/sonnet.txt", "tests/lora/data/*", "build/*",
124
+ "examples/pooling/token_embed/*", "tests/models/language/pooling/*",
125
"vllm/third_party/*", "vllm/entrypoints/serve/instrumentator/static/*", "tests/entrypoints/openai/speech_to_text/test_transcription_validation.py",
126
"docs/governance/process.md", "tests/v1/engine/test_fast_incdec_prefix_err.py", ".git/*"]
127
ignore-hidden = false
tests/models/language/pooling/test_jina_reranker_v3.py
new
+275
@@ -0,0 +1,275 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+# ruff: noqa: E501
4
+import pytest
5
+import requests
6
+import torch
7
+import torch.nn.functional as F
8
+
9
+from tests.utils import RemoteOpenAIServer
10
+from vllm.entrypoints.pooling.pooling.protocol import PoolingResponse
11
+from vllm.entrypoints.pooling.scoring.protocol import ScoreResponse
12
+
13
+model_name = "jinaai/jina-reranker-v3"
14
+query = "What are the health benefits of green tea?"
15
+documents = [
16
+ "Green tea contains antioxidants called catechins that may help reduce inflammation and protect cells from damage.",
17
+ "El precio del café ha aumentado un 20% este año debido a problemas en la cadena de suministro.",
18
+ "Studies show that drinking green tea regularly can improve brain function and boost metabolism.",
19
+ "Basketball is one of the most popular sports in the United States.",
20
+ "绿茶富含儿茶素等抗氧化剂,可以降低心脏病风险,还有助于控制体重。",
21
+ "Le thé vert est riche en antioxydants et peut améliorer la fonction cérébrale.",
22
+]
23
+
24
+EMBEDDING_SIZE = 512
25
+REFERENCE_1_VS_1 = [
26
+ 0.345703125,
27
+ -0.10498046,
28
+ 0.314453125,
29
+ -0.1376953125,
30
+ 0.3398437500,
31
+ 0.2539062,
32
+]
33
+REFERENCE_1_VS_N = [
34
+ 0.294921875,
35
+ -0.16015625,
36
+ 0.189453125,
37
+ -0.1708984375,
38
+ 0.2255859375,
39
+ 0.1640625,
40
+]
41
+TOL = 0.01
42
+
43
+
44
+def test_offline(vllm_runner):
45
+ with vllm_runner(model_name, runner="pooling") as llm_runner:
46
+ llm = llm_runner.get_llm()
47
+ _test_offline_1_v_1(llm)
48
+ _test_offline_1_v_n(llm)
49
+ _test_offline_n_v_n(llm)
50
+ _test_offline_token_embed_illegal_inputs(llm)
51
+ assert llm.model_config.embedding_size == EMBEDDING_SIZE
52
+
53
+
54
+def test_online():
55
+ with RemoteOpenAIServer(model_name, ["--runner", "pooling"]) as server:
56
+ _test_online_1_v_1(server)
57
+ _test_online_1_v_n(server)
58
+ _test_online_n_v_n(server)
59
+ _test_online_token_embed_illegal_inputs(server)
60
+
61
+
62
+def _test_offline_1_v_1(llm):
63
+ # test llm.score
64
+ outputs = llm.score(query, documents[0])
65
+ assert len(outputs) == 1
66
+ assert outputs[0].outputs.score == pytest.approx(REFERENCE_1_VS_1[0], abs=TOL)
67
+
68
+ # test llm.encode
69
+ outputs = llm.encode(documents[:1] + [query], pooling_task="token_embed")
70
+ embeds = outputs[0].outputs.data.float()
71
+ assert embeds.shape[0] == 2
72
+ assert embeds.shape[-1] == EMBEDDING_SIZE
73
+
74
+ doc_embeds = embeds[:-1]
75
+ query_embeds = embeds[-1]
76
+
77
+ scores = F.cosine_similarity(query_embeds, doc_embeds)
78
+ assert scores[0] == pytest.approx(REFERENCE_1_VS_1[0], abs=TOL)
79
+
80
+
81
+def _test_offline_1_v_n(llm):
82
+ # test llm.score
83
+ outputs = llm.score(query, documents)
84
+ assert len(outputs) == len(documents)
85
+
86
+ for expected, output in zip(REFERENCE_1_VS_N, outputs):
87
+ actual = output.outputs.score
88
+ assert actual == pytest.approx(expected, abs=TOL)
89
+
90
+ # test llm.encode
91
+ outputs = llm.encode(documents + [query], pooling_task="token_embed")
92
+ embeds = outputs[0].outputs.data.float()
93
+ assert embeds.shape[0] == len(documents) + 1
94
+
95
+ doc_embeds = embeds[:-1]
96
+ query_embeds = embeds[-1]
97
+
98
+ scores = F.cosine_similarity(query_embeds, doc_embeds)
99
+
100
+ assert len(scores) == len(documents)
101
+ for expected, actual in zip(REFERENCE_1_VS_N, scores):
102
+ assert actual == pytest.approx(expected, abs=TOL)
103
+
104
+
105
+def _test_offline_n_v_n(llm):
106
+ # test llm.score
107
+ outputs = llm.score([query] * len(documents), documents)
108
+ assert len(outputs) == len(documents)
109
+
110
+ for expected, output in zip(REFERENCE_1_VS_1, outputs):
111
+ actual = output.outputs.score
112
+ assert actual == pytest.approx(expected, abs=TOL)
113
+
114
+ # test llm.encode
115
+ for doc, expected in zip(documents, REFERENCE_1_VS_1):
116
+ outputs = llm.encode([doc, query], pooling_task="token_embed")
117
+ embeds = outputs[0].outputs.data.float()
118
+ assert embeds.shape[0] == 2
119
+
120
+ doc_embeds = embeds[:-1]
121
+ query_embeds = embeds[-1]
122
+
123
+ scores = F.cosine_similarity(query_embeds, doc_embeds)
124
+ assert scores[0] == pytest.approx(expected, abs=TOL)
125
+
126
+
127
+def _test_offline_token_embed_illegal_inputs(llm):
128
+ with pytest.raises(
129
+ ValueError, match="The JinaForRanking model requires at least 2 inputs."
130
+ ):
131
+ llm.encode([query], pooling_task="token_embed")
132
+
133
+ with pytest.raises(
134
+ ValueError, match="The JinaForRanking model only supports text as input."
135
+ ):
136
+ llm.encode([1, 2, 3], pooling_task="token_embed")
137
+
138
+
139
+def _get_scores(server, query, document):
140
+ score_response = requests.post(
141
+ server.url_for("score"),
142
+ json={
143
+ "model": model_name,
144
+ "queries": query,
145
+ "documents": document,
146
+ },
147
+ )
148
+
149
+ score_response.raise_for_status()
150
+ score = ScoreResponse.model_validate(score_response.json())
151
+
152
+ return [d.score for d in score.data]
153
+
154
+
155
+def _get_embeds(server, prompts: list[str]):
156
+ response = requests.post(
157
+ server.url_for("pooling"),
158
+ json={
159
+ "model": model_name,
160
+ "task": "token_embed",
161
+ "input": prompts,
162
+ "encoding_format": "float",
163
+ },
164
+ )
165
+ response.raise_for_status()
166
+ poolings = PoolingResponse.model_validate(response.json())
167
+
168
+ return torch.as_tensor([d.data for d in poolings.data][0]).float()
169
+
170
+
171
+def _test_online_1_v_1(server):
172
+ # test scoring api
173
+ scores = _get_scores(server, query, documents[0])
174
+ assert len(scores) == 1
175
+ assert scores[0] == pytest.approx(REFERENCE_1_VS_1[0], abs=TOL)
176
+
177
+ # test pooling api
178
+ embeds = _get_embeds(server, [documents[0], query])
179
+ assert embeds.shape[0] == 2
180
+ assert embeds.shape[-1] == EMBEDDING_SIZE
181
+
182
+ doc_embeds = embeds[:-1]
183
+ query_embeds = embeds[-1]
184
+
185
+ scores = F.cosine_similarity(query_embeds, doc_embeds)
186
+ assert scores[0] == pytest.approx(REFERENCE_1_VS_1[0], abs=TOL)
187
+
188
+
189
+def _test_online_1_v_n(server):
190
+ # test scoring api
191
+ scores = _get_scores(server, query, documents)
192
+ assert len(scores) == len(documents)
193
+
194
+ for expected, actual in zip(REFERENCE_1_VS_N, scores):
195
+ assert actual == pytest.approx(expected, abs=TOL)
196
+
197
+ # test pooling api
198
+ embeds = _get_embeds(server, documents + [query])
199
+ assert embeds.shape[0] == len(documents) + 1
200
+
201
+ doc_embeds = embeds[:-1]
202
+ query_embeds = embeds[-1]
203
+
204
+ scores = F.cosine_similarity(query_embeds, doc_embeds)
205
+
206
+ assert len(scores) == len(documents)
207
+ for expected, actual in zip(REFERENCE_1_VS_N, scores):
208
+ assert actual == pytest.approx(expected, abs=TOL)
209
+
210
+
211
+def _test_online_n_v_n(server):
212
+ # test scoring api
213
+ scores = _get_scores(server, [query] * len(documents), documents)
214
+ assert len(scores) == len(documents)
215
+
216
+ for expected, actual in zip(REFERENCE_1_VS_1, scores):
217
+ assert actual == pytest.approx(expected, abs=TOL)
218
+
219
+ # test pooling api
220
+ for doc, expected in zip(documents, REFERENCE_1_VS_1):
221
+ embeds = _get_embeds(server, [doc, query])
222
+ assert embeds.shape[0] == 2
223
+
224
+ doc_embeds = embeds[:-1]
225
+ query_embeds = embeds[-1]
226
+
227
+ scores = F.cosine_similarity(query_embeds, doc_embeds)
228
+ assert len(scores) == 1
229
+ assert scores[0] == pytest.approx(expected, abs=TOL)
230
+
231
+
232
+def _test_online_token_embed_illegal_inputs(server):
233
+ response = requests.post(
234
+ server.url_for("pooling"),
235
+ json={
236
+ "model": model_name,
237
+ "task": "token_embed",
238
+ "input": [query],
239
+ "encoding_format": "float",
240
+ },
241
+ )
242
+ assert response.json()["error"]["message"].startswith(
243
+ "The JinaForRanking model requires at least 2 inputs."
244
+ )
245
+
246
+ response = requests.post(
247
+ server.url_for("pooling"),
248
+ json={
249
+ "model": model_name,
250
+ "task": "token_embed",
251
+ "input": [1, 2, 3],
252
+ "encoding_format": "float",
253
+ },
254
+ )
255
+ assert response.json()["error"]["message"].startswith(
256
+ "The JinaForRanking model only supports text as input."
257
+ )
258
+
259
+ response = requests.post(
260
+ server.url_for("pooling"),
261
+ json={
262
+ "model": model_name,
263
+ "task": "token_embed",
264
+ "messages": [
265
+ {
266
+ "role": "user",
267
+ "content": "The cat sat on the mat.",
268
+ }
269
+ ],
270
+ "encoding_format": "float",
271
+ },
272
+ )
273
+ assert response.json()["error"]["message"].startswith(
274
+ "The JinaForRanking does not support chat Request."
275
+ )
tests/models/registry.py
+1
@@ -645,6 +645,7 @@ _LATE_INTERACTION_EXAMPLE_MODELS = {
645
trust_remote_code=True,
646
hf_overrides={"architectures": ["ColBERTLfm2Model"]},
647
),
648
+ "JinaForRanking": _HfExamplesInfo("jinaai/jina-reranker-v3"),
649
# [Multimodal]
650
"ColModernVBertForRetrieval": _HfExamplesInfo(
651
"ModernVBERT/colmodernvbert-merged",
vllm/entrypoints/pooling/embed/io_processor.py
+52
-1
@@ -24,7 +24,13 @@ from vllm.entrypoints.pooling.embed.protocol import (
24
EmbeddingChatRequest,
25
EmbeddingCompletionRequest,
26
)
27
-from vllm.entrypoints.pooling.typing import PoolingServeContext
27
+from vllm.entrypoints.pooling.scoring.io_processor import JinaRankingIOProcessorMixin
28
+from vllm.entrypoints.pooling.typing import (
29
+ OfflineInputsContext,
30
+ PoolingChatLikeRequest,
31
+ PoolingCompletionLikeRequest,
32
+ PoolingServeContext,
33
+)
34
from vllm.inputs import EngineInput, tokens_input
35
from vllm.logger import init_logger
36
from vllm.outputs import PoolingOutput, PoolingRequestOutput
@@ -553,3 +559,48 @@ class EmbedIOProcessor(PoolingIOProcessor):
559
560
class TokenEmbedIOProcessor(PoolingIOProcessor):
561
name = "token_embed"
562
+
563
+
564
+class JinaRankingTokenEmbedIOProcessor(
565
+ TokenEmbedIOProcessor, JinaRankingIOProcessorMixin
566
+):
567
+ def pre_process_online(self, ctx: PoolingServeContext):
568
+ request = ctx.request
569
+ if isinstance(request, PoolingCompletionLikeRequest):
570
+ prompts = request.input
571
+ if not isinstance(prompts, Sequence) or len(prompts) < 2:
572
+ raise ValueError("The JinaForRanking model requires at least 2 inputs.")
573
+
574
+ text_prompts = self.ensure_str(prompts)
575
+
576
+ # The JinaForRanking model concatenates docs first, then query.
577
+ # Let's stay consistent with this novel design.
578
+ prompt_input = self.format_docs_prompts_func(
579
+ query=text_prompts[-1], docs=text_prompts[:-1]
580
+ )
581
+
582
+ engine_inputs = self._preprocess_completion_online(
583
+ request,
584
+ prompt_input=prompt_input,
585
+ prompt_embeds=None,
586
+ )
587
+ elif isinstance(request, PoolingChatLikeRequest):
588
+ raise ValueError("The JinaForRanking does not support chat Request.")
589
+ else:
590
+ raise ValueError(f"Invalid {self.name} request type")
591
+
592
+ ctx.engine_inputs = engine_inputs
593
+
594
+ def pre_process_offline(self, ctx: OfflineInputsContext) -> Sequence[EngineInput]:
595
+ if not isinstance(ctx.prompts, Sequence) or len(ctx.prompts) < 2:
596
+ raise ValueError("The JinaForRanking model requires at least 2 inputs.")
597
+
598
+ text_prompts = self.ensure_str(ctx.prompts)
599
+
600
+ # The JinaForRanking model concatenates docs first, then query.
601
+ # Let's stay consistent with this novel design.
602
+ ctx.prompts = self.format_docs_prompts_func(
603
+ query=text_prompts[-1], docs=text_prompts[:-1]
604
+ )
605
+
606
+ return super().pre_process_offline(ctx)
vllm/entrypoints/pooling/io_processor_factories.py
+7
@@ -59,6 +59,13 @@ def init_pooling_io_processors(
59
if score_type is not None and score_type in ScoringIOProcessors:
60
processors[score_type] = ScoringIOProcessors[score_type]
61
62
+ if model_config.architecture == "JinaForRanking":
63
+ from .embed.io_processor import JinaRankingTokenEmbedIOProcessor
64
+ from .scoring.io_processor import ScoringIOProcessors
65
+
66
+ processors["token_embed"] = JinaRankingTokenEmbedIOProcessor
67
+ processors["late-interaction"] = ScoringIOProcessors["jina-reranking-scoring"]
68
+
69
return {
70
task: processor_cls(
71
vllm_config=vllm_config,
vllm/entrypoints/pooling/scoring/io_processor.py
+126
@@ -416,11 +416,137 @@ class CrossEncoderIOProcessor(ScoringIOProcessor):
416
return full_prompt, engine_prompt
417
418
419
+class JinaRankingIOProcessorMixin:
420
+ @staticmethod
421
+ def sanitize_input(text: str, special_tokens: dict[str, str]) -> str:
422
+ for token in special_tokens.values():
423
+ text = text.replace(token, "")
424
+ return text
425
+
426
+ @staticmethod
427
+ def format_docs_prompts_func(
428
+ query: str,
429
+ docs: list[str],
430
+ special_tokens: dict[str, str] | None = None,
431
+ instruction: str | None = None,
432
+ no_thinking: bool = True,
433
+ ) -> str:
434
+ # TODO: Try converting the code below into a chat template.
435
+
436
+ default_special_tokens = {
437
+ "query_embed_token": "<|rerank_token|>",
438
+ "doc_embed_token": "<|embed_token|>",
439
+ }
440
+ if special_tokens is None:
441
+ special_tokens = default_special_tokens
442
+
443
+ query = JinaRankingIOProcessorMixin.sanitize_input(query, special_tokens)
444
+ docs = [
445
+ JinaRankingIOProcessorMixin.sanitize_input(doc, special_tokens)
446
+ for doc in docs
447
+ ]
448
+
449
+ prefix = (
450
+ "<|im_start|>system\n"
451
+ "You are a search relevance expert who can determine a ranking of the passages based on how relevant they are to the query. " # noqa: E501
452
+ "If the query is a question, how relevant a passage is depends on how well it answers the question. " # noqa: E501
453
+ "If not, try to analyze the intent of the query and assess how well each passage satisfies the intent. " # noqa: E501
454
+ "If an instruction is provided, you should follow the instruction when determining the ranking." # noqa: E501
455
+ "<|im_end|>\n<|im_start|>user\n"
456
+ )
457
+ suffix = "<|im_end|>\n<|im_start|>assistant\n"
458
+ if no_thinking:
459
+ suffix += "<think>\n\n</think>\n\n"
460
+
461
+ doc_emb_token = special_tokens["doc_embed_token"]
462
+ query_emb_token = special_tokens["query_embed_token"]
463
+
464
+ prompt = (
465
+ f"I will provide you with {len(docs)} passages, each indicated by a numerical identifier. " # noqa: E501
466
+ f"Rank the passages based on their relevance to query: {query}\n"
467
+ )
468
+
469
+ if instruction:
470
+ prompt += f"<instruct>\n{instruction}\n</instruct>\n"
471
+
472
+ doc_prompts = [
473
+ f'<passage id="{i}">\n{doc}{doc_emb_token}\n</passage>'
474
+ for i, doc in enumerate(docs)
475
+ ]
476
+ prompt += "\n".join(doc_prompts) + "\n"
477
+ prompt += f"<query>\n{query}{query_emb_token}\n</query>"
478
+
479
+ return prefix + prompt + suffix
480
+
481
+ @staticmethod
482
+ def ensure_str(data: Sequence[Any]) -> list[str]:
483
+ text: list[str] = []
484
+ for prompt in data:
485
+ if not isinstance(prompt, str):
486
+ raise ValueError(
487
+ "The JinaForRanking model only supports text as input."
488
+ )
489
+ text.append(prompt)
490
+ return text
491
+
492
+
493
+class JinaRankingIOProcessor(LateInteractionIOProcessor, JinaRankingIOProcessorMixin):
494
+ name = "jina-reranking-scoring"
495
+ pooling_task: PoolingTask = "token_embed"
496
+
497
+ def _pre_process(
498
+ self,
499
+ scoring_data: ScoringData,
500
+ tok_params: TokenizeParams,
501
+ prompt_extras: dict[str, Any] | None = None,
502
+ ) -> Sequence[EngineInput]:
503
+ queries = self.ensure_str(scoring_data.data_1)
504
+ docs = self.ensure_str(scoring_data.data_2)
505
+
506
+ if len(queries) == 1:
507
+ prompts = [self.format_docs_prompts_func(query=queries[0], docs=docs)]
508
+ else:
509
+ prompts = [
510
+ self.format_docs_prompts_func(query=q, docs=[d])
511
+ for q, d in zip(queries, docs)
512
+ ]
513
+
514
+ return self._preprocess_completion_offline(
515
+ prompts=prompts, tok_params=tok_params, prompt_extras=prompt_extras
516
+ )
517
+
518
+ def _post_process(self, outputs: list[PoolingRequestOutput], n_queries: int):
519
+ final_res_batch: list[PoolingRequestOutput] = []
520
+
521
+ for i in range(len(outputs)):
522
+ embeds = outputs[i].outputs.data.float()
523
+
524
+ # The JinaForRanking model concatenates docs first, then query.
525
+ # Let's stay consistent with this novel design.
526
+ query_embeds = embeds[-1]
527
+ doc_embeds = embeds[:-1]
528
+
529
+ scores = F.cosine_similarity(query_embeds, doc_embeds)
530
+
531
+ for score in scores:
532
+ final_res_batch.append(
533
+ PoolingRequestOutput(
534
+ request_id=outputs[i].request_id,
535
+ outputs=score,
536
+ prompt_token_ids=outputs[i].prompt_token_ids,
537
+ num_cached_tokens=outputs[i].num_cached_tokens,
538
+ finished=True,
539
+ )
540
+ )
541
+ return final_res_batch
542
+
543
+
544
ScoringIOProcessors: dict[str, type[ScoringIOProcessor]] = {
545
p.name: p
546
for p in [
547
BiEncoderIOProcessor,
548
LateInteractionIOProcessor,
549
+ JinaRankingIOProcessor,
550
FlashLateInteractionIOProcessor,
551
CrossEncoderIOProcessor,
552
]
vllm/entrypoints/pooling/scoring/serving.py
+11
-14
@@ -4,7 +4,6 @@
4
from fastapi.responses import JSONResponse, Response
5
6
from vllm import PoolingParams
7
-from vllm.config import VllmConfig
7
from vllm.engine.protocol import EngineClient
8
from vllm.entrypoints.openai.engine.protocol import UsageInfo
9
from vllm.entrypoints.pooling.base.io_processor import PoolingIOProcessor
@@ -42,25 +41,23 @@ class ServingScores(PoolingServing):
41
enable_flash_late_interaction: bool = True,
42
**kwargs,
43
):
45
- self.score_type = engine_client.model_config.score_type
44
+ self.io_processor_name: str = engine_client.model_config.score_type
45
self.enable_flash_late_interaction = (
47
- self.score_type == "late-interaction" and enable_flash_late_interaction
46
+ self.io_processor_name == "late-interaction"
47
+ and enable_flash_late_interaction
48
)
49
50
- super().__init__(engine_client, *args, **kwargs)
50
+ if self.enable_flash_late_interaction:
51
+ self.io_processor_name = "flash-late-interaction"
52
52
- def init_io_processor(
53
- self, vllm_config: VllmConfig, *args, **kwargs
54
- ) -> PoolingIOProcessor:
55
- model_config = vllm_config.model_config
53
+ if engine_client.model_config.architecture == "JinaForRanking":
54
+ self.io_processor_name = "jina-reranking-scoring"
55
+ self.enable_flash_late_interaction = False
56
57
- score_type: str = model_config.score_type
58
- if self.enable_flash_late_interaction:
59
- score_type = "flash-late-interaction"
57
+ super().__init__(engine_client, *args, **kwargs)
58
61
- assert score_type in ScoringIOProcessors
62
- processor_cls = ScoringIOProcessors[score_type]
63
- return processor_cls(vllm_config, *args, **kwargs)
59
+ def init_io_processor(self, *args, **kwargs) -> PoolingIOProcessor:
60
+ return ScoringIOProcessors[self.io_processor_name](*args, **kwargs)
61
62
async def __call__(self, *args, **kwargs) -> Response:
63
if not self.enable_flash_late_interaction:
vllm/model_executor/layers/pooler/tokwise/methods.py
+1
-1
@@ -100,7 +100,7 @@ class StepPool(AllPool):
100
):
101
# for unfinished chunked prefill
102
if data is None:
103
- pass
103
+ pooled_data.append(None)
104
else:
105
step_tag_id = pooling_param.step_tag_id
106
returned_token_ids = pooling_param.returned_token_ids
vllm/model_executor/layers/pooler/tokwise/poolers.py
+3
-2
@@ -58,7 +58,7 @@ class TokenPooler(Pooler):
58
def __init__(
59
self,
60
pooling: TokenPoolingMethod | TokenPoolingFn,
61
- head: TokenPoolerHead | TokenPoolingHeadFn,
61
+ head: TokenPoolerHead | TokenPoolingHeadFn | None = None,
62
) -> None:
63
super().__init__()
64
@@ -89,7 +89,8 @@ class TokenPooler(Pooler):
89
pooling_metadata: PoolingMetadata,
90
) -> TokenPoolerOutput:
91
pooled_data = self.pooling(hidden_states, pooling_metadata)
92
- pooled_data = self.head(pooled_data, pooling_metadata)
92
+ if self.head is not None:
93
+ pooled_data = self.head(pooled_data, pooling_metadata)
94
return pooled_data
95
96
vllm/model_executor/models/config.py
+7
@@ -192,6 +192,12 @@ class JambaForSequenceClassificationConfig(VerifyAndUpdateConfig):
192
pooler_config.use_activation = False
193
194
195
+class JinaForRankingConfig(VerifyAndUpdateConfig):
196
+ @staticmethod
197
+ def verify_and_update_model_config(model_config: "ModelConfig") -> None:
198
+ model_config.hf_config.embedding_size = 512
199
+
200
+
201
class JinaRobertaModelConfig(VerifyAndUpdateConfig):
202
@staticmethod
203
def verify_and_update_model_config(model_config: "ModelConfig") -> None:
@@ -612,6 +618,7 @@ MODELS_CONFIG_MAP: dict[str, type[VerifyAndUpdateConfig]] = {
618
"GteNewForSequenceClassification": GteNewModelConfig,
619
"GteNewModel": GteNewModelConfig,
620
"JambaForSequenceClassification": JambaForSequenceClassificationConfig,
621
+ "JinaForRanking": JinaForRankingConfig,
622
"JinaVLForRanking": JinaVLForSequenceClassificationConfig,
623
"LlamaBidirectionalForSequenceClassification": LlamaBidirectionalConfig,
624
"LlamaBidirectionalModel": LlamaBidirectionalConfig,
vllm/model_executor/models/jina.py
new
+110
@@ -0,0 +1,110 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+# Adapted from https://huggingface.co/jinaai/jina-reranker-v3/blob/main/modeling.py
4
+from collections.abc import Iterable
5
+
6
+import torch
7
+from torch import nn
8
+
9
+from vllm.config import VllmConfig
10
+from vllm.sequence import IntermediateTensors
11
+from vllm.tasks import PoolingTask
12
+from vllm.v1.pool.metadata import PoolingMetadata
13
+
14
+from ..layers.pooler import DispatchPooler
15
+from ..layers.pooler.tokwise import (
16
+ StepPool,
17
+ TokenPooler,
18
+ TokenPoolingMethodOutputItem,
19
+)
20
+from .interfaces import SupportsLateInteraction
21
+from .qwen3 import Qwen3Model
22
+from .utils import AutoWeightsLoader, maybe_prefix
23
+
24
+
25
+class JinaForRanking(nn.Module, SupportsLateInteraction):
26
+ is_pooling_model = True
27
+
28
+ def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
29
+ super().__init__()
30
+ config = vllm_config.model_config.hf_config
31
+ quant_config = vllm_config.quant_config
32
+
33
+ self.config = config
34
+ self.projector_dim: int = config.embedding_size
35
+
36
+ self.vllm_config = vllm_config
37
+ self.quant_config = quant_config
38
+ self.model = Qwen3Model(
39
+ vllm_config=vllm_config, prefix=maybe_prefix(prefix, "model")
40
+ )
41
+
42
+ self.projector = nn.Sequential(
43
+ nn.Linear(config.hidden_size, config.hidden_size // 2, bias=False),
44
+ nn.ReLU(),
45
+ nn.Linear(config.hidden_size // 2, self.projector_dim, bias=False),
46
+ )
47
+
48
+ self.pooler = DispatchPooler(
49
+ {
50
+ "token_embed": TokenPooler(
51
+ pooling=JinaForRankingPool(self.projector),
52
+ )
53
+ }
54
+ )
55
+
56
+ def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
57
+ return self.model.embed_input_ids(input_ids)
58
+
59
+ def forward(
60
+ self,
61
+ input_ids: torch.Tensor | None,
62
+ positions: torch.Tensor,
63
+ intermediate_tensors: IntermediateTensors | None = None,
64
+ inputs_embeds: torch.Tensor | None = None,
65
+ ) -> torch.Tensor | IntermediateTensors:
66
+ hidden_states = self.model(
67
+ input_ids, positions, intermediate_tensors, inputs_embeds
68
+ )
69
+ return hidden_states
70
+
71
+ def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
72
+ loader = AutoWeightsLoader(self, skip_prefixes=(["lm_head."]))
73
+ return loader.load_weights(weights)
74
+
75
+
76
+class JinaForRankingPool(StepPool):
77
+ def __init__(self, projector: nn.Sequential):
78
+ super().__init__()
79
+
80
+ self.doc_token_id = 151670
81
+ self.query_token_id = 151671
82
+ self.projector = projector
83
+
84
+ def get_supported_tasks(self) -> set[PoolingTask]:
85
+ return {"token_embed"}
86
+
87
+ def forward(
88
+ self,
89
+ hidden_states: torch.Tensor,
90
+ pooling_metadata: PoolingMetadata,
91
+ ) -> list[TokenPoolingMethodOutputItem]:
92
+ pooled_data_lst = super().forward(hidden_states, pooling_metadata)
93
+ prompt_token_ids = pooling_metadata.get_prompt_token_ids()
94
+
95
+ embeds_list = list[torch.Tensor | None]()
96
+ for data, token_ids in zip(pooled_data_lst, prompt_token_ids):
97
+ # for unfinished chunked prefill
98
+ if data is None:
99
+ embeds_list.append(None)
100
+ else:
101
+ docs_indexes = torch.where(torch.eq(token_ids, self.doc_token_id))[0]
102
+ query_indexes = torch.where(torch.eq(token_ids, self.query_token_id))[0]
103
+
104
+ # The JinaForRanking model concatenates docs first, then query.
105
+ # Let's stay consistent with this novel design.
106
+ indexes = torch.cat([docs_indexes, query_indexes])
107
+ embeds = self.projector(data[indexes])
108
+ embeds_list.append(embeds)
109
+
110
+ return embeds_list
vllm/model_executor/models/registry.py
+1
@@ -273,6 +273,7 @@ _LATE_INTERACTION_MODELS = {
273
"ColBERTModernBertModel": ("colbert", "ColBERTModernBertModel"),
274
"ColBERTJinaRobertaModel": ("colbert", "ColBERTJinaRobertaModel"),
275
"ColBERTLfm2Model": ("colbert", "ColBERTLfm2Model"),
276
+ "JinaForRanking": ("jina", "JinaForRanking"),
277
# [Multimodal]
278
"ColModernVBertForRetrieval": ("colmodernvbert", "ColModernVBertForRetrieval"),
279
"ColPaliForRetrieval": ("colpali", "ColPaliModel"),