[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