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 10, 21:30 UTC
[Bugfix][Frontend] Fix Jina reranker multimodal input compatibility (#31445)
Signed-off-by: tianwenjing <tianwenjing@jfgenius.com> Signed-off-by: twj <151701930+twjww@users.noreply.github.com> Co-authored-by: tianwenjing <tianwenjing@jfgenius.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
twj committed
Dec 29, 2025 at 17:13 UTC
bf73a3e4d7e16d03c89ac068de9cf80664f8195f
2 files changed
+316
-138
tests/models/multimodal/pooling/test_jinavl_reranker.py
+313
-137
@@ -1,194 +1,370 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+from typing import cast
4
5
import pytest
6
from transformers import AutoModel
7
7
-from vllm.entrypoints.chat_utils import ChatCompletionContentPartImageParam
8
+from vllm.entrypoints.chat_utils import (
9
+ ChatCompletionContentPartImageEmbedsParam,
10
+ ChatCompletionContentPartImageParam,
11
+ ChatCompletionContentPartTextParam,
12
+)
13
from vllm.entrypoints.score_utils import ScoreMultiModalParam
14
15
from ....conftest import HfRunner, VllmRunner
16
12
-model_name = "jinaai/jina-reranker-m0"
17
+MODELS = ["jinaai/jina-reranker-m0"]
18
14
-mm_processor_kwargs = {
19
+MM_PROCESSOR_KWARGS = {
20
"min_pixels": 3136,
21
"max_pixels": 602112,
22
}
23
19
-limit_mm_per_prompt = {"image": 2}
24
+LIMIT_MM_PER_PROMPT = {"image": 2}
25
26
+CHECKPOINT_TO_HF_MAPPER = {
27
+ "visual.": "model.visual.",
28
+ "model.": "model.language_model.",
29
+}
30
+
31
+# Shared long text for test data
32
+LONG_TEXT_DOC = """We present ReaderLM-v2, a compact 1.5 billion parameter language model designed for efficient
33
+web content extraction. Our model processes documents up to 512K tokens, transforming messy HTML
34
+into clean Markdown or JSON formats with high accuracy -- making it an ideal tool for grounding
35
+large language models. The models effectiveness results from two key innovations: (1) a three-stage
36
+data synthesis pipeline that generates high quality, diverse training data by iteratively drafting,
37
+refining, and critiquing web content extraction; and (2) a unified training framework combining
38
+continuous pre-training with multi-objective optimization. Intensive evaluation demonstrates that
39
+ReaderLM-v2 outperforms GPT-4o-2024-08-06 and other larger models by 15-20% on carefully curated
40
+benchmarks, particularly excelling at documents exceeding 100K tokens, while maintaining significantly
41
+lower computational requirements.""" # noqa: E501
42
+
43
+# Test data for different scenarios
44
+TEXT_IMAGE_TEST_DATA = {
45
+ "query": [{"text": "slm markdown"}],
46
+ "documents": [
47
+ {
48
+ "image": "https://raw.githubusercontent.com/jina-ai/multimodal-reranker-test/main/handelsblatt-preview.png"
49
+ },
50
+ {
51
+ "image": "https://raw.githubusercontent.com/jina-ai/multimodal-reranker-test/main/paper-11.png"
52
+ },
53
+ ],
54
+}
55
+
56
+TEXT_TEXT_TEST_DATA = {
57
+ "query": [{"text": "slm markdown"}],
58
+ "documents": [
59
+ {"text": LONG_TEXT_DOC},
60
+ {"text": "数据提取么?为什么不用正则啊,你用正则不就全解决了么?"},
61
+ ],
62
+}
63
+
64
+IMAGE_TEXT_TEST_DATA = {
65
+ "query": [
66
+ {
67
+ "image": "https://raw.githubusercontent.com/jina-ai/multimodal-reranker-test/main/paper-11.png"
68
+ }
69
+ ],
70
+ "documents": [
71
+ {"text": LONG_TEXT_DOC},
72
+ {"text": "数据提取么?为什么不用正则啊,你用正则不就全解决了么?"},
73
+ ],
74
+}
75
+
76
+IMAGE_IMAGE_TEST_DATA = {
77
+ "query": [
78
+ {
79
+ "image": "https://raw.githubusercontent.com/jina-ai/multimodal-reranker-test/main/paper-11.png"
80
+ }
81
+ ],
82
+ "documents": [
83
+ {
84
+ "image": "https://raw.githubusercontent.com/jina-ai/multimodal-reranker-test/main/handelsblatt-preview.png"
85
+ },
86
+ {
87
+ "image": "https://raw.githubusercontent.com/jina-ai/multimodal-reranker-test/main/paper-11.png"
88
+ },
89
+ ],
90
+}
91
22
-def vllm_reranker(
92
+TEXT_MIXED_DOCS_TEST_DATA = {
93
+ "query": [{"text": "slm markdown"}],
94
+ "documents": [
95
+ {"text": LONG_TEXT_DOC},
96
+ {
97
+ "image": "https://raw.githubusercontent.com/jina-ai/multimodal-reranker-test/main/paper-11.png"
98
+ },
99
+ {"text": "数据提取么?为什么不用正则啊,你用正则不就全解决了么?"},
100
+ {
101
+ "image": "https://raw.githubusercontent.com/jina-ai/multimodal-reranker-test/main/handelsblatt-preview.png"
102
+ },
103
+ ],
104
+}
105
+
106
+
107
+def _normalize_image(image_val: str) -> str:
108
+ """Normalize image value to proper format for HF model."""
109
+ return (
110
+ image_val
111
+ if image_val.startswith(("http://", "https://"))
112
+ else f"data:image/png;base64,{image_val}"
113
+ )
114
+
115
+
116
+def create_score_multimodal_param(
117
+ content_parts: list[dict],
118
+) -> ScoreMultiModalParam:
119
+ """
120
+ Create a ScoreMultiModalParam from a list of content dictionaries.
121
+
122
+ Each dict supports the following formats:
123
+ - Text: {'text': 'content'}
124
+ - Image URL: {'image': 'https://...'}
125
+ - Image Base64: {'image': 'base64_str'}
126
+ """
127
+ formatted_content = []
128
+
129
+ for part in content_parts:
130
+ if "text" in part:
131
+ formatted_content.append(
132
+ ChatCompletionContentPartTextParam(
133
+ type="text",
134
+ text=part["text"],
135
+ )
136
+ )
137
+ elif "image" in part:
138
+ image_val = part["image"]
139
+ if image_val.startswith(("http://", "https://")):
140
+ formatted_content.append(
141
+ ChatCompletionContentPartImageParam(
142
+ type="image_url",
143
+ image_url={"url": image_val},
144
+ )
145
+ )
146
+ else:
147
+ formatted_content.append(
148
+ ChatCompletionContentPartImageEmbedsParam(
149
+ type="image_embeds", image_embeds=image_val
150
+ )
151
+ )
152
+
153
+ return ScoreMultiModalParam(content=formatted_content)
154
+
155
+
156
+def _run_vllm(
157
vllm_runner: type[VllmRunner],
24
- model_name: str,
158
+ model: str,
159
dtype: str,
26
- query_strs: list[str],
27
- document_strs: list[str],
28
- query_type: str = "text",
29
- doc_type: str = "text",
30
-):
31
- def create_image_param(url: str) -> ChatCompletionContentPartImageParam:
32
- return {"type": "image_url", "image_url": {"url": f"{url}"}}
33
-
34
- query: list[str] | ScoreMultiModalParam
35
- if query_type == "text":
36
- query = query_strs
37
- elif query_type == "image":
38
- query = ScoreMultiModalParam(
39
- content=[create_image_param(url) for url in query_strs]
40
- )
41
-
42
- documents: list[str] | ScoreMultiModalParam
43
- if doc_type == "text":
44
- documents = document_strs
45
- elif doc_type == "image":
46
- documents = ScoreMultiModalParam(
47
- content=[create_image_param(url) for url in document_strs]
48
- )
160
+ query_strs: list[dict[str, str]],
161
+ document_strs: list[dict[str, str]],
162
+) -> list[float]:
163
+ """Run vLLM reranker and return scores."""
164
+ query = create_score_multimodal_param(query_strs)
165
+ documents = create_score_multimodal_param(document_strs)
166
167
with vllm_runner(
51
- model_name,
168
+ model,
169
runner="pooling",
170
dtype=dtype,
171
max_num_seqs=2,
172
max_model_len=2048,
56
- mm_processor_kwargs=mm_processor_kwargs,
57
- limit_mm_per_prompt=limit_mm_per_prompt,
173
+ mm_processor_kwargs=MM_PROCESSOR_KWARGS,
174
+ limit_mm_per_prompt=LIMIT_MM_PER_PROMPT,
175
) as vllm_model:
176
outputs = vllm_model.llm.score(query, documents)
177
178
return [output.outputs.score for output in outputs]
179
180
64
-def hf_reranker(
181
+def _run_hf(
182
hf_runner: type[HfRunner],
66
- model_name: str,
183
+ model: str,
184
dtype: str,
68
- query_strs: list[str],
69
- document_strs: list[str],
70
- query_type: str = "text",
71
- doc_type: str = "text",
72
-):
73
- checkpoint_to_hf_mapper = {
74
- "visual.": "model.visual.",
75
- "model.": "model.language_model.",
76
- }
77
-
78
- data_pairs = [[query_strs[0], d] for d in document_strs]
185
+ query_strs: list[dict[str, str]],
186
+ document_strs: list[dict[str, str]],
187
+) -> list[float]:
188
+ """Run HuggingFace reranker and return scores."""
189
+ query = query_strs[0]
190
+ if "text" in query:
191
+ query_type = "text"
192
+ query_data = query["text"]
193
+ elif "image" in query:
194
+ query_type = "image"
195
+ query_data = _normalize_image(query["image"])
196
+ else:
197
+ raise ValueError("Unsupported query format")
198
+
199
+ # Separate documents by type
200
+ text_docs: list[str] = []
201
+ image_docs: list[str] = []
202
+ text_indices: list[int] = []
203
+ image_indices: list[int] = []
204
+
205
+ for idx, doc in enumerate(document_strs):
206
+ if "text" in doc:
207
+ text_docs.append(doc["text"])
208
+ text_indices.append(idx)
209
+ elif "image" in doc:
210
+ image_docs.append(_normalize_image(doc["image"]))
211
+ image_indices.append(idx)
212
+ else:
213
+ raise ValueError(f"Unsupported document format at index {idx}")
214
+
215
+ scores: list[None | float] = [None] * len(document_strs)
216
217
with hf_runner(
81
- model_name,
218
+ model,
219
dtype=dtype,
220
trust_remote_code=True,
221
auto_cls=AutoModel,
85
- model_kwargs={"key_mapping": checkpoint_to_hf_mapper},
222
+ model_kwargs={"key_mapping": CHECKPOINT_TO_HF_MAPPER},
223
) as hf_model:
87
- return hf_model.model.compute_score(
88
- data_pairs, max_length=2048, query_type=query_type, doc_type=doc_type
89
- )
224
+ # Score text documents
225
+ if text_docs:
226
+ text_scores = hf_model.model.compute_score(
227
+ [[query_data, d] for d in text_docs],
228
+ max_length=2048,
229
+ query_type=query_type,
230
+ doc_type="text",
231
+ )
232
+ for i, s in zip(text_indices, text_scores):
233
+ scores[i] = s
234
235
+ # Score image documents
236
+ if image_docs:
237
+ image_scores = hf_model.model.compute_score(
238
+ [[query_data, d] for d in image_docs],
239
+ max_length=2048,
240
+ query_type=query_type,
241
+ doc_type="image",
242
+ )
243
+ for i, s in zip(image_indices, image_scores):
244
+ scores[i] = s
245
92
-# Visual Documents Reranking
93
-@pytest.mark.parametrize("model_name", [model_name])
94
-@pytest.mark.parametrize("dtype", ["half"])
95
-def test_model_text_image(hf_runner, vllm_runner, model_name, dtype):
96
- query = ["slm markdown"]
97
- documents = [
98
- "https://raw.githubusercontent.com/jina-ai/multimodal-reranker-test/main/handelsblatt-preview.png",
99
- "https://raw.githubusercontent.com/jina-ai/multimodal-reranker-test/main/paper-11.png",
100
- ]
101
-
102
- hf_outputs = hf_reranker(
103
- hf_runner, model_name, dtype, query, documents, "text", "image"
104
- )
105
- vllm_outputs = vllm_reranker(
106
- vllm_runner, model_name, dtype, query, documents, "text", "image"
107
- )
246
+ assert all(s is not None for s in scores)
247
+ return cast(list[float], scores)
248
109
- assert hf_outputs[0] == pytest.approx(vllm_outputs[0], rel=0.02)
110
- assert hf_outputs[1] == pytest.approx(vllm_outputs[1], rel=0.02)
249
250
+def _run_test(
251
+ hf_runner: type[HfRunner],
252
+ vllm_runner: type[VllmRunner],
253
+ model: str,
254
+ dtype: str,
255
+ query_strs: list[dict[str, str]],
256
+ document_strs: list[dict[str, str]],
257
+) -> None:
258
+ """Run comparison test between vLLM and HuggingFace implementations."""
259
+ # NOTE: take care of the order. run vLLM first, and then run HF.
260
+ # vLLM needs a fresh new process without cuda initialization.
261
+ # if we run HF first, the cuda initialization will be done and it
262
+ # will hurt multiprocessing backend with fork method (the default method).
263
113
-# Textual Documents Reranking
114
-@pytest.mark.parametrize("model_name", [model_name])
115
-@pytest.mark.parametrize("dtype", ["half"])
116
-def test_model_text_text(hf_runner, vllm_runner, model_name, dtype):
117
- query = ["slm markdown"]
118
- documents = [
119
- """We present ReaderLM-v2, a compact 1.5 billion parameter language model designed for efficient
120
- web content extraction. Our model processes documents up to 512K tokens, transforming messy HTML
121
- into clean Markdown or JSON formats with high accuracy -- making it an ideal tool for grounding
122
- large language models. The models effectiveness results from two key innovations: (1) a three-stage
123
- data synthesis pipeline that generates high quality, diverse training data by iteratively drafting,
124
- refining, and critiquing web content extraction; and (2) a unified training framework combining
125
- continuous pre-training with multi-objective optimization. Intensive evaluation demonstrates that
126
- ReaderLM-v2 outperforms GPT-4o-2024-08-06 and other larger models by 15-20% on carefully curated
127
- benchmarks, particularly excelling at documents exceeding 100K tokens, while maintaining significantly
128
- lower computational requirements.""", # noqa: E501
129
- "数据提取么?为什么不用正则啊,你用正则不就全解决了么?",
130
- ]
131
- hf_outputs = hf_reranker(
132
- hf_runner, model_name, dtype, query, documents, "text", "text"
133
- )
134
- vllm_outputs = vllm_reranker(
135
- vllm_runner, model_name, dtype, query, documents, "text", "text"
264
+ vllm_outputs = _run_vllm(vllm_runner, model, dtype, query_strs, document_strs)
265
+ hf_outputs = _run_hf(hf_runner, model, dtype, query_strs, document_strs)
266
+
267
+ # Compare outputs
268
+ assert len(hf_outputs) == len(vllm_outputs), (
269
+ f"Output length mismatch: HF={len(hf_outputs)}, vLLM={len(vllm_outputs)}"
270
)
271
138
- assert hf_outputs[0] == pytest.approx(vllm_outputs[0], rel=0.02)
139
- assert hf_outputs[1] == pytest.approx(vllm_outputs[1], rel=0.02)
272
+ for i, (hf_score, vllm_score) in enumerate(zip(hf_outputs, vllm_outputs)):
273
+ assert hf_score == pytest.approx(vllm_score, rel=0.02), (
274
+ f"Score mismatch at index {i}: HF={hf_score}, vLLM={vllm_score}"
275
+ )
276
277
142
-# Image Querying for Textual Documents
143
-@pytest.mark.parametrize("model_name", [model_name])
278
+@pytest.mark.parametrize("model", MODELS)
279
@pytest.mark.parametrize("dtype", ["half"])
145
-def test_model_image_text(hf_runner, vllm_runner, model_name, dtype):
146
- query = [
147
- "https://raw.githubusercontent.com/jina-ai/multimodal-reranker-test/main/paper-11.png"
148
- ]
149
- documents = [
150
- """We present ReaderLM-v2, a compact 1.5 billion parameter language model designed for efficient
151
- web content extraction. Our model processes documents up to 512K tokens, transforming messy HTML
152
- into clean Markdown or JSON formats with high accuracy -- making it an ideal tool for grounding
153
- large language models. The models effectiveness results from two key innovations: (1) a three-stage
154
- data synthesis pipeline that generates high quality, diverse training data by iteratively drafting,
155
- refining, and critiquing web content extraction; and (2) a unified training framework combining
156
- continuous pre-training with multi-objective optimization. Intensive evaluation demonstrates that
157
- ReaderLM-v2 outperforms GPT-4o-2024-08-06 and other larger models by 15-20% on carefully curated
158
- benchmarks, particularly excelling at documents exceeding 100K tokens, while maintaining significantly
159
- lower computational requirements.""", # noqa: E501
160
- "数据提取么?为什么不用正则啊,你用正则不就全解决了么?",
161
- ]
162
-
163
- hf_outputs = hf_reranker(
164
- hf_runner, model_name, dtype, query, documents, "image", "text"
165
- )
166
- vllm_outputs = vllm_reranker(
167
- vllm_runner, model_name, dtype, query, documents, "image", "text"
280
+def test_model_text_image(
281
+ hf_runner,
282
+ vllm_runner,
283
+ model: str,
284
+ dtype: str,
285
+) -> None:
286
+ """Visual Documents Reranking"""
287
+ _run_test(
288
+ hf_runner,
289
+ vllm_runner,
290
+ model,
291
+ dtype,
292
+ TEXT_IMAGE_TEST_DATA["query"],
293
+ TEXT_IMAGE_TEST_DATA["documents"],
294
)
295
170
- assert hf_outputs[0] == pytest.approx(vllm_outputs[0], rel=0.02)
171
- assert hf_outputs[1] == pytest.approx(vllm_outputs[1], rel=0.02)
296
+
297
+@pytest.mark.parametrize("model", MODELS)
298
+@pytest.mark.parametrize("dtype", ["half"])
299
+def test_model_text_text(
300
+ hf_runner,
301
+ vllm_runner,
302
+ model: str,
303
+ dtype: str,
304
+) -> None:
305
+ """Textual Documents Reranking"""
306
+ _run_test(
307
+ hf_runner,
308
+ vllm_runner,
309
+ model,
310
+ dtype,
311
+ TEXT_TEXT_TEST_DATA["query"],
312
+ TEXT_TEXT_TEST_DATA["documents"],
313
+ )
314
315
174
-# Image Querying for Image Documents
175
-@pytest.mark.parametrize("model_name", [model_name])
316
+@pytest.mark.parametrize("model", MODELS)
317
@pytest.mark.parametrize("dtype", ["half"])
177
-def test_model_image_image(hf_runner, vllm_runner, model_name, dtype):
178
- query = [
179
- "https://raw.githubusercontent.com/jina-ai/multimodal-reranker-test/main/paper-11.png"
180
- ]
181
- documents = [
182
- "https://raw.githubusercontent.com/jina-ai/multimodal-reranker-test/main/handelsblatt-preview.png",
183
- "https://raw.githubusercontent.com/jina-ai/multimodal-reranker-test/main/paper-11.png",
184
- ]
185
-
186
- hf_outputs = hf_reranker(
187
- hf_runner, model_name, dtype, query, documents, "image", "image"
318
+def test_model_image_text(
319
+ hf_runner,
320
+ vllm_runner,
321
+ model: str,
322
+ dtype: str,
323
+) -> None:
324
+ """Image Querying for Textual Documents"""
325
+ _run_test(
326
+ hf_runner,
327
+ vllm_runner,
328
+ model,
329
+ dtype,
330
+ IMAGE_TEXT_TEST_DATA["query"],
331
+ IMAGE_TEXT_TEST_DATA["documents"],
332
)
189
- vllm_outputs = vllm_reranker(
190
- vllm_runner, model_name, dtype, query, documents, "image", "image"
333
+
334
+
335
+@pytest.mark.parametrize("model", MODELS)
336
+@pytest.mark.parametrize("dtype", ["half"])
337
+def test_model_image_image(
338
+ hf_runner,
339
+ vllm_runner,
340
+ model: str,
341
+ dtype: str,
342
+) -> None:
343
+ """Image Querying for Image Documents"""
344
+ _run_test(
345
+ hf_runner,
346
+ vllm_runner,
347
+ model,
348
+ dtype,
349
+ IMAGE_IMAGE_TEST_DATA["query"],
350
+ IMAGE_IMAGE_TEST_DATA["documents"],
351
)
352
193
- assert hf_outputs[0] == pytest.approx(vllm_outputs[0], rel=0.02)
194
- assert hf_outputs[1] == pytest.approx(vllm_outputs[1], rel=0.02)
353
+
354
+@pytest.mark.parametrize("model", MODELS)
355
+@pytest.mark.parametrize("dtype", ["half"])
356
+def test_model_text_mixed_documents(
357
+ hf_runner,
358
+ vllm_runner,
359
+ model: str,
360
+ dtype: str,
361
+) -> None:
362
+ """Text Query for Mixed Text and Image Documents"""
363
+ _run_test(
364
+ hf_runner,
365
+ vllm_runner,
366
+ model,
367
+ dtype,
368
+ TEXT_MIXED_DOCS_TEST_DATA["query"],
369
+ TEXT_MIXED_DOCS_TEST_DATA["documents"],
370
+ )
vllm/entrypoints/score_utils.py
+3
-1
@@ -24,7 +24,9 @@ from vllm.outputs import PoolingRequestOutput
24
from vllm.tokenizers import TokenizerLike
25
26
ScoreContentPartParam: TypeAlias = (
27
- ChatCompletionContentPartImageParam | ChatCompletionContentPartImageEmbedsParam
27
+ ChatCompletionContentPartImageParam
28
+ | ChatCompletionContentPartImageEmbedsParam
29
+ | ChatCompletionContentPartTextParam
30
)
31
32