[Bugfix][Scoring] Warn when serving original Qwen3 reranker without chat template (#56017)
Signed-off-by: 김재억 <gadian88@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
김재억 committed
Sep 11, 2026 at 21:52 UTC
912dfb37581b98c0b6eaeca5758d1e1462b7feac
2 files changed
+168
tests/entrypoints/pooling/test_io_processor.py
+147
@@ -74,3 +74,150 @@ def test_rejects_conflicting_pooling_task(processor: PoolingIOProcessor):
74
)
75
assert exc_info.value.parameter is None
76
assert exc_info.value.value is None
77
+
78
+
79
+def test_qwen3_reranker_warns_without_chat_template(monkeypatch):
80
+ from unittest.mock import MagicMock
81
+
82
+ from vllm.entrypoints.pooling.scoring.io_processor import CrossEncoderIOProcessor
83
+
84
+ monkeypatch.setattr(
85
+ "vllm.model_executor.model_loader.get_model_cls", lambda *_: MagicMock()
86
+ )
87
+ monkeypatch.setattr(
88
+ "vllm.model_executor.models.interfaces.supports_score_template",
89
+ lambda *_: False,
90
+ )
91
+ monkeypatch.setattr(
92
+ "vllm.entrypoints.pooling.scoring.io_processor.is_mistral_tokenizer",
93
+ lambda *_: False,
94
+ )
95
+
96
+ mock_logger = MagicMock()
97
+ monkeypatch.setattr(
98
+ "vllm.entrypoints.pooling.scoring.io_processor.logger",
99
+ mock_logger,
100
+ )
101
+
102
+ model_config = MagicMock()
103
+ model_config.architecture = "Qwen3ForSequenceClassification"
104
+ model_config.model = "Qwen/Qwen3-Reranker-0.6B"
105
+ model_config.hf_config.is_original_qwen3_reranker = True
106
+ model_config.use_sep_token = False
107
+ model_config.is_multimodal_model = False
108
+ model_config.max_model_len = 8192
109
+
110
+ vllm_config = MagicMock(model_config=model_config)
111
+ tokenizer = MagicMock()
112
+ tokenizer.pad_token_id = 0
113
+ renderer = MagicMock()
114
+ renderer.get_tokenizer.return_value = tokenizer
115
+ chat_template_config = MagicMock(chat_template=None)
116
+
117
+ CrossEncoderIOProcessor(
118
+ vllm_config=vllm_config,
119
+ renderer=renderer,
120
+ chat_template_config=chat_template_config,
121
+ )
122
+
123
+ mock_logger.warning.assert_called_once()
124
+ warning_call = mock_logger.warning.call_args
125
+ assert "Qwen/Qwen3-Reranker-0.6B" in warning_call[0][1]
126
+ assert "qwen3_reranker.jinja" in warning_call[0][2]
127
+
128
+
129
+def test_qwen3_vl_reranker_warns_with_vl_template(monkeypatch):
130
+ from unittest.mock import MagicMock
131
+
132
+ from vllm.entrypoints.pooling.scoring.io_processor import CrossEncoderIOProcessor
133
+
134
+ monkeypatch.setattr(
135
+ "vllm.model_executor.model_loader.get_model_cls", lambda *_: MagicMock()
136
+ )
137
+ monkeypatch.setattr(
138
+ "vllm.model_executor.models.interfaces.supports_score_template",
139
+ lambda *_: False,
140
+ )
141
+ monkeypatch.setattr(
142
+ "vllm.entrypoints.pooling.scoring.io_processor.is_mistral_tokenizer",
143
+ lambda *_: False,
144
+ )
145
+
146
+ mock_logger = MagicMock()
147
+ monkeypatch.setattr(
148
+ "vllm.entrypoints.pooling.scoring.io_processor.logger",
149
+ mock_logger,
150
+ )
151
+
152
+ model_config = MagicMock()
153
+ model_config.architecture = "Qwen3VLForSequenceClassification"
154
+ model_config.model = "Qwen/Qwen3-VL-Reranker-2B"
155
+ model_config.hf_config.is_original_qwen3_reranker = True
156
+ model_config.use_sep_token = False
157
+ model_config.is_multimodal_model = True
158
+ model_config.max_model_len = 8192
159
+
160
+ vllm_config = MagicMock(model_config=model_config)
161
+ tokenizer = MagicMock()
162
+ tokenizer.pad_token_id = 0
163
+ renderer = MagicMock()
164
+ renderer.get_tokenizer.return_value = tokenizer
165
+ chat_template_config = MagicMock(chat_template=None)
166
+
167
+ CrossEncoderIOProcessor(
168
+ vllm_config=vllm_config,
169
+ renderer=renderer,
170
+ chat_template_config=chat_template_config,
171
+ )
172
+
173
+ mock_logger.warning.assert_called_once()
174
+ warning_call = mock_logger.warning.call_args
175
+ assert "Qwen/Qwen3-VL-Reranker-2B" in warning_call[0][1]
176
+ assert "qwen3_vl_reranker.jinja" in warning_call[0][2]
177
+
178
+
179
+def test_qwen3_reranker_no_warning_when_template_provided(monkeypatch):
180
+ from unittest.mock import MagicMock
181
+
182
+ from vllm.entrypoints.pooling.scoring.io_processor import CrossEncoderIOProcessor
183
+
184
+ monkeypatch.setattr(
185
+ "vllm.model_executor.model_loader.get_model_cls", lambda *_: MagicMock()
186
+ )
187
+ monkeypatch.setattr(
188
+ "vllm.model_executor.models.interfaces.supports_score_template",
189
+ lambda *_: False,
190
+ )
191
+ monkeypatch.setattr(
192
+ "vllm.entrypoints.pooling.scoring.io_processor.is_mistral_tokenizer",
193
+ lambda *_: False,
194
+ )
195
+
196
+ mock_logger = MagicMock()
197
+ monkeypatch.setattr(
198
+ "vllm.entrypoints.pooling.scoring.io_processor.logger",
199
+ mock_logger,
200
+ )
201
+
202
+ model_config = MagicMock()
203
+ model_config.architecture = "Qwen3ForSequenceClassification"
204
+ model_config.model = "Qwen/Qwen3-Reranker-0.6B"
205
+ model_config.hf_config.is_original_qwen3_reranker = True
206
+ model_config.use_sep_token = False
207
+ model_config.is_multimodal_model = False
208
+ model_config.max_model_len = 8192
209
+
210
+ vllm_config = MagicMock(model_config=model_config)
211
+ tokenizer = MagicMock()
212
+ tokenizer.pad_token_id = 0
213
+ renderer = MagicMock()
214
+ renderer.get_tokenizer.return_value = tokenizer
215
+ chat_template_config = MagicMock(chat_template="custom template")
216
+
217
+ CrossEncoderIOProcessor(
218
+ vllm_config=vllm_config,
219
+ renderer=renderer,
220
+ chat_template_config=chat_template_config,
221
+ )
222
+
223
+ mock_logger.warning.assert_not_called()
vllm/entrypoints/pooling/scoring/io_processor.py
+21
@@ -7,6 +7,7 @@ from typing import Any, TypeAlias, cast
7
import torch.nn.functional as F
8
9
from vllm import PoolingParams, PoolingRequestOutput, TokensPrompt
10
+from vllm.logger import init_logger
11
from vllm.renderers import TokenizeParams
12
from vllm.renderers.hf import safe_apply_chat_template
13
from vllm.renderers.inputs.preprocess import (
@@ -47,6 +48,8 @@ from .utils import (
48
validate_score_input,
49
)
50
51
+logger = init_logger(__name__)
52
+
53
ScoringServeContext: TypeAlias = PoolingServeContext[ScoringRequest]
54
55
@@ -425,6 +428,24 @@ class CrossEncoderIOProcessor(ScoringIOProcessor):
428
self.model = model if self.supports_score_template else None
429
self.use_sep_token = self.model_config.use_sep_token
430
431
+ if (
432
+ getattr(self.model_config.hf_config, "is_original_qwen3_reranker", False)
433
+ and self.chat_template is None
434
+ ):
435
+ suggested_template = (
436
+ "examples/pooling/score/template/qwen3_vl_reranker.jinja"
437
+ if self.model_config.is_multimodal_model
438
+ else "examples/pooling/score/template/qwen3_reranker.jinja"
439
+ )
440
+ logger.warning(
441
+ "Serving an original Qwen3 reranker (%s) without a "
442
+ "--chat-template. The model was trained with a specific prompt "
443
+ "template; running without it may produce inaccurate relevance "
444
+ "scores. Consider specifying `--chat-template %s`.",
445
+ self.model_config.model,
446
+ suggested_template,
447
+ )
448
+
449
#######################################
450
# online APIs
451