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 15, 03:30 UTC
[Model][3/N] Automatic conversion of CrossEncoding model (#20168)
Signed-off-by: wang.yuqi <noooop@126.com>
wang.yuqi committed
Jul 4, 2025 at 20:47 UTC
2e26f9156ac1eb4cc402221ffb7aa98fe10d9907
8 files changed
+234
-133
docs/models/supported_models.md
+15
-6
@@ -477,12 +477,20 @@ If your model is not in the above list, we will try to automatically convert the
477
478
Specified using `--task score`.
479
480
-| Architecture | Models | Example HF Models | [V1](gh-issue:8779) |
481
-|---------------------------------------|-------------------|--------------------------------------------------------------------------------------|-----------------------|
482
-| `BertForSequenceClassification` | BERT-based | `cross-encoder/ms-marco-MiniLM-L-6-v2`, etc. | |
483
-| `Qwen3ForSequenceClassification` | Qwen3-based | `tomaarsen/Qwen3-Reranker-0.6B-seq-cls`, `Qwen/Qwen3-Reranker-0.6B` (see note), etc. | ✅︎ |
484
-| `RobertaForSequenceClassification` | RoBERTa-based | `cross-encoder/quora-roberta-base`, etc. | |
485
-| `XLMRobertaForSequenceClassification` | XLM-RoBERTa-based | `BAAI/bge-reranker-v2-m3`, etc. | |
480
+| Architecture | Models | Example HF Models | [V1](gh-issue:8779) |
481
+|---------------------------------------|-------------------|--------------------------------------------------------------------------------------|---------------------|
482
+| `BertForSequenceClassification` | BERT-based | `cross-encoder/ms-marco-MiniLM-L-6-v2`, etc. | |
483
+| `Qwen2ForSequenceClassification` | Qwen2-based | `mixedbread-ai/mxbai-rerank-base-v2` (see note), etc. | ✅︎ |
484
+| `Qwen3ForSequenceClassification` | Qwen3-based | `tomaarsen/Qwen3-Reranker-0.6B-seq-cls`, `Qwen/Qwen3-Reranker-0.6B` (see note), etc. | ✅︎ |
485
+| `RobertaForSequenceClassification` | RoBERTa-based | `cross-encoder/quora-roberta-base`, etc. | |
486
+| `XLMRobertaForSequenceClassification` | XLM-RoBERTa-based | `BAAI/bge-reranker-v2-m3`, etc. | |
487
+
488
+!!! note
489
+ Load the official original `mxbai-rerank-v2` by using the following command.
490
+
491
+ ```bash
492
+ vllm serve mixedbread-ai/mxbai-rerank-base-v2 --hf_overrides '{"architectures": ["Qwen2ForSequenceClassification"],"classifier_from_token": ["0", "1"], "method": "from_2_way_softmax"}'
493
+ ```
494
495
!!! note
496
Load the official original `Qwen3 Reranker` by using the following command. More information can be found at: <gh-file:examples/offline_inference/qwen3_reranker.py>.
@@ -490,6 +498,7 @@ Specified using `--task score`.
498
```bash
499
vllm serve Qwen/Qwen3-Reranker-0.6B --hf_overrides '{"architectures": ["Qwen3ForSequenceClassification"],"classifier_from_token": ["no", "yes"],"is_original_qwen3_reranker": true}'
500
```
501
+
502
[](){ #supported-mm-models }
503
504
## List of Multimodal Language Models
tests/models/language/pooling/test_embedding.py
+9
-1
@@ -1,6 +1,7 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
import os
4
+from typing import Optional
5
6
import pytest
7
@@ -74,6 +75,13 @@ def test_models(
75
vllm_extra_kwargs["override_pooler_config"] = \
76
PoolerConfig(pooling_type="MEAN", normalize=False)
77
78
+ max_model_len: Optional[int] = 512
79
+ if model in [
80
+ "sentence-transformers/all-MiniLM-L12-v2",
81
+ "sentence-transformers/stsb-roberta-base-v2"
82
+ ]:
83
+ max_model_len = None
84
+
85
# The example_prompts has ending "\n", for example:
86
# "Write a short story about a robot that dreams for the first time.\n"
87
# sentence_transformers will strip the input texts, see:
@@ -87,7 +95,7 @@ def test_models(
95
96
with vllm_runner(model,
97
task="embed",
90
- max_model_len=512,
98
+ max_model_len=max_model_len,
99
**vllm_extra_kwargs) as vllm_model:
100
vllm_outputs = vllm_model.embed(example_prompts)
101
tests/models/language/pooling/test_gte.py
+12
-4
@@ -56,10 +56,16 @@ MODELS = [
56
enable_test=False),
57
]
58
59
+V1FlashAttentionImpNotSupported = [
60
+ "Alibaba-NLP/gte-Qwen2-1.5B-instruct", "Alibaba-NLP/gte-modernbert-base"
61
+]
62
+
63
64
@pytest.mark.parametrize("model_info", MODELS)
61
-def test_embed_models_mteb(hf_runner, vllm_runner,
62
- model_info: EmbedModelInfo) -> None:
65
+def test_embed_models_mteb(hf_runner, vllm_runner, model_info: EmbedModelInfo,
66
+ monkeypatch) -> None:
67
+ if model_info.name in V1FlashAttentionImpNotSupported:
68
+ monkeypatch.setenv("VLLM_USE_V1", "0")
69
70
vllm_extra_kwargs: dict[str, Any] = {}
71
if model_info.architecture == "GteNewModel":
@@ -71,8 +77,10 @@ def test_embed_models_mteb(hf_runner, vllm_runner,
77
78
@pytest.mark.parametrize("model_info", MODELS)
79
def test_embed_models_correctness(hf_runner, vllm_runner,
74
- model_info: EmbedModelInfo,
75
- example_prompts) -> None:
80
+ model_info: EmbedModelInfo, example_prompts,
81
+ monkeypatch) -> None:
82
+ if model_info.name in V1FlashAttentionImpNotSupported:
83
+ monkeypatch.setenv("VLLM_USE_V1", "0")
84
85
vllm_extra_kwargs: dict[str, Any] = {}
86
if model_info.architecture == "GteNewModel":
tests/models/language/pooling/test_mxbai_rerank.py
new
+84
@@ -0,0 +1,84 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+from typing import Any
4
+
5
+import pytest
6
+import torch
7
+
8
+from tests.conftest import HfRunner
9
+
10
+from .mteb_utils import RerankModelInfo, mteb_test_rerank_models
11
+
12
+RERANK_MODELS = [
13
+ RerankModelInfo("mixedbread-ai/mxbai-rerank-base-v2",
14
+ architecture="Qwen2ForSequenceClassification",
15
+ dtype="float32",
16
+ enable_test=True),
17
+ RerankModelInfo("mixedbread-ai/mxbai-rerank-large-v2",
18
+ architecture="Qwen2ForSequenceClassification",
19
+ dtype="float32",
20
+ enable_test=False)
21
+]
22
+
23
+
24
+class MxbaiRerankerHfRunner(HfRunner):
25
+
26
+ def __init__(self,
27
+ model_name: str,
28
+ dtype: str = "auto",
29
+ *args: Any,
30
+ **kwargs: Any) -> None:
31
+ from transformers import AutoModelForCausalLM, AutoTokenizer
32
+ super().__init__(model_name, dtype, auto_cls=AutoModelForCausalLM)
33
+
34
+ self.tokenizer = AutoTokenizer.from_pretrained(model_name,
35
+ padding_side='left')
36
+ self.yes_loc = self.tokenizer.convert_tokens_to_ids("1")
37
+ self.no_loc = self.tokenizer.convert_tokens_to_ids("0")
38
+
39
+ def predict(self, prompts: list[list[str]], *args,
40
+ **kwargs) -> torch.Tensor:
41
+
42
+ def process_inputs(pairs):
43
+ inputs = self.tokenizer(pairs,
44
+ padding=False,
45
+ truncation='longest_first',
46
+ return_attention_mask=False)
47
+ for i, ele in enumerate(inputs['input_ids']):
48
+ inputs['input_ids'][i] = ele
49
+ inputs = self.tokenizer.pad(inputs,
50
+ padding=True,
51
+ return_tensors="pt")
52
+ for key in inputs:
53
+ inputs[key] = inputs[key].to(self.model.device)
54
+ return inputs
55
+
56
+ @torch.no_grad()
57
+ def compute_logits(inputs):
58
+ logits = self.model(**inputs).logits[:, -1, :]
59
+ yes_logits = logits[:, self.yes_loc]
60
+ no_logits = logits[:, self.no_loc]
61
+ logits = yes_logits - no_logits
62
+ scores = logits.float().sigmoid()
63
+ return scores
64
+
65
+ scores = []
66
+ for prompt in prompts:
67
+ inputs = process_inputs([prompt])
68
+ score = compute_logits(inputs)
69
+ scores.append(score[0].item())
70
+ return torch.Tensor(scores)
71
+
72
+
73
+@pytest.mark.parametrize("model_info", RERANK_MODELS)
74
+def test_rerank_models_mteb(vllm_runner, model_info: RerankModelInfo) -> None:
75
+ vllm_extra_kwargs: dict[str, Any] = {}
76
+ if model_info.architecture == "Qwen2ForSequenceClassification":
77
+ vllm_extra_kwargs["hf_overrides"] = {
78
+ "architectures": ["Qwen2ForSequenceClassification"],
79
+ "classifier_from_token": ["0", "1"],
80
+ "method": "from_2_way_softmax",
81
+ }
82
+
83
+ mteb_test_rerank_models(MxbaiRerankerHfRunner, vllm_runner, model_info,
84
+ vllm_extra_kwargs)
vllm/config.py
+10
-3
@@ -466,6 +466,9 @@ class ModelConfig:
466
"affect the random state of the Python process that "
467
"launched vLLM.", self.seed)
468
469
+ # Keep set served_model_name before maybe_model_redirect(self.model)
470
+ self.served_model_name = get_served_model_name(self.model,
471
+ self.served_model_name)
472
self.model = maybe_model_redirect(self.model)
473
# The tokenizer is consistent with the model by default.
474
if self.tokenizer is None:
@@ -609,8 +612,6 @@ class ModelConfig:
612
613
self.original_max_model_len = self.max_model_len
614
self.max_model_len = self.get_and_verify_max_len(self.max_model_len)
612
- self.served_model_name = get_served_model_name(self.model,
613
- self.served_model_name)
615
self.multimodal_config = self._init_multimodal_config()
616
if not self.skip_tokenizer_init:
617
self._verify_tokenizer_mode()
@@ -1420,7 +1421,7 @@ class ModelConfig:
1421
1422
@property
1423
def is_cross_encoder(self) -> bool:
1423
- return self.registry.is_cross_encoder_model(self.architectures)
1424
+ return self.task == "classify"
1425
1426
@property
1427
def use_mla(self) -> bool:
@@ -4762,6 +4763,12 @@ class VllmConfig:
4763
if cls is not None:
4764
cls.verify_and_update_config(self)
4765
4766
+ if self.model_config.task == "classify":
4767
+ # Maybe convert ForCausalLM into ForSequenceClassification model.
4768
+ from vllm.model_executor.models.adapters import (
4769
+ SequenceClassificationConfig)
4770
+ SequenceClassificationConfig.verify_and_update_config(self)
4771
+
4772
def __str__(self):
4773
return (
4774
f"model={self.model_config.model!r},"
vllm/model_executor/models/adapters.py
+99
-3
@@ -2,14 +2,17 @@
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
from collections.abc import Iterable
5
-from typing import TYPE_CHECKING, Any, Optional, TypeVar, Union
5
+from typing import TYPE_CHECKING, Any, Optional, TypeVar, Union, cast
6
7
import torch
8
import torch.nn as nn
9
10
+from vllm.model_executor.models.config import VerifyAndUpdateConfig
11
+
12
from .interfaces_base import VllmModelForPooling, is_pooling_model
13
14
if TYPE_CHECKING:
15
+ from vllm.config import VllmConfig
16
from vllm.model_executor.layers.pooler import PoolingType
17
18
_T = TypeVar("_T", bound=type[nn.Module])
@@ -39,7 +42,6 @@ def _create_pooling_model_cls(
42
default_softmax: bool,
43
) -> _T:
44
# Lazy import
42
- from vllm.config import VllmConfig
45
from vllm.model_executor.layers.pooler import Pooler, PoolerOutput
46
from vllm.model_executor.pooling_metadata import PoolingMetadata
47
@@ -162,7 +164,6 @@ def as_seq_cls_model(cls: _T) -> _T:
164
return cls
165
166
# Lazy import
165
- from vllm.config import VllmConfig
167
from vllm.model_executor.layers.linear import RowParallelLinear
168
from vllm.model_executor.layers.pooler import PoolerOutput, PoolingType
169
from vllm.model_executor.models.interfaces import SupportsCrossEncoding
@@ -193,6 +194,7 @@ def as_seq_cls_model(cls: _T) -> _T:
194
config = vllm_config.model_config.hf_config
195
quant_config = vllm_config.quant_config
196
197
+ self.vllm_config = vllm_config
198
self.task = vllm_config.model_config.task
199
self.pooling_type = (
200
vllm_config.model_config.pooler_config.pooling_type)
@@ -242,6 +244,17 @@ def as_seq_cls_model(cls: _T) -> _T:
244
]
245
return PoolerOutput(outputs=pooled_outputs)
246
247
+ def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]):
248
+ tokens = getattr(self.config, "classifier_from_token", None)
249
+ method = getattr(self.config, "method", None)
250
+
251
+ if tokens is None and method is None:
252
+ return super().load_weights(weights)
253
+ else:
254
+ # Online convert ForCausalLM into
255
+ # ForSequenceClassification model.
256
+ return seq_cls_model_loader(self, weights)
257
+
258
259
ModelForSequenceClassification.__name__ = \
260
_get_pooling_model_name(cls.__name__, "ForSequenceClassification")
@@ -277,3 +290,86 @@ def as_reward_model(cls: _T) -> _T:
290
_get_pooling_model_name(cls.__name__, "ForReward")
291
292
return ModelForReward # type: ignore
293
+
294
+
295
+class SequenceClassificationConfig(VerifyAndUpdateConfig):
296
+
297
+ @staticmethod
298
+ def verify_and_update_config(vllm_config: "VllmConfig") -> None:
299
+ config = vllm_config.model_config.hf_config
300
+ method = getattr(config, "method", None)
301
+ tokens = getattr(config, "classifier_from_token", None)
302
+
303
+ if method is None:
304
+ return
305
+
306
+ assert tokens is not None
307
+ assert method in SEQ_CLS_LOAD_METHODS, f"method {method} not supported"
308
+
309
+ if method == "from_2_way_softmax":
310
+ assert len(tokens) == 2
311
+ config.num_labels = 1
312
+ else:
313
+ config.num_labels = len(tokens)
314
+
315
+
316
+def load_weights_using_from_2_way_softmax(
317
+ model, weights: Iterable[tuple[str, torch.Tensor]]):
318
+ # refer to https://huggingface.co/Qwen/Qwen3-Reranker-0.6B/discussions/3
319
+ from vllm.model_executor.layers.vocab_parallel_embedding import (
320
+ ParallelLMHead)
321
+ from vllm.model_executor.models.utils import AutoWeightsLoader
322
+
323
+ model_config = model.vllm_config.model_config
324
+ tokens = getattr(model.config, "classifier_from_token", [])
325
+ tokens = cast(list[int], tokens)
326
+ assert len(tokens) == 2
327
+
328
+ device = model.score.weight.device
329
+
330
+ if model.config.tie_word_embeddings:
331
+ model.lm_head = model.model.embed_tokens
332
+ else:
333
+ model.lm_head = ParallelLMHead(model.config.vocab_size,
334
+ model.config.hidden_size,
335
+ quant_config=model.quant_config)
336
+
337
+ loader = AutoWeightsLoader(model)
338
+ loaded_weights = loader.load_weights(weights)
339
+
340
+ from vllm.transformers_utils.tokenizer import get_tokenizer
341
+ tokenizer = get_tokenizer(model_config.tokenizer,
342
+ revision=model_config.tokenizer_revision,
343
+ tokenizer_mode=model_config.tokenizer_mode,
344
+ trust_remote_code=model_config.trust_remote_code)
345
+
346
+ false_id = tokenizer.convert_tokens_to_ids(tokens[0])
347
+ true_id = tokenizer.convert_tokens_to_ids(tokens[1])
348
+ weight = model.lm_head.weight.data[true_id].to(device).to(
349
+ torch.float32) - model.lm_head.weight.data[false_id].to(device).to(
350
+ torch.float32)
351
+ model.score.weight.data.copy_(weight)
352
+
353
+ del model.lm_head
354
+ loaded_weights.add("score.weight")
355
+ loaded_weights.discard("lm_head.weight")
356
+ return loaded_weights
357
+
358
+
359
+SEQ_CLS_LOAD_METHODS = {
360
+ "from_2_way_softmax": load_weights_using_from_2_way_softmax,
361
+}
362
+
363
+
364
+def seq_cls_model_loader(model, weights: Iterable[tuple[str, torch.Tensor]]):
365
+ # Online convert ForCausalLM into ForSequenceClassification model.
366
+ # - from_2_way_softmax:
367
+ # - Qwen3ForCausalLM
368
+ # - Qwen3-Reranker
369
+ # - Qwen2ForCausalLM
370
+ # - mxbai-rerank-v2
371
+
372
+ config = model.vllm_config.model_config.hf_config
373
+ method = getattr(config, "method", None)
374
+ assert method in SEQ_CLS_LOAD_METHODS, f"method {method} not supported"
375
+ return SEQ_CLS_LOAD_METHODS[method](model, weights)
vllm/model_executor/models/config.py
+1
-1
@@ -167,7 +167,7 @@ class Qwen3ForSequenceClassificationConfig(VerifyAndUpdateConfig):
167
assert tokens is not None and len(tokens) == 2, \
168
("Try loading the original Qwen3 Reranker?, see: "
169
"https://github.com/vllm-project/vllm/tree/main/examples/offline_inference/qwen3_reranker.py")
170
- config.num_labels = 1
170
+ vllm_config.model_config.hf_config.method = "from_2_way_softmax"
171
172
173
class SnowflakeGteNewModelConfig(VerifyAndUpdateConfig):
vllm/model_executor/models/qwen3.py
+4
-115
@@ -38,15 +38,14 @@ from vllm.model_executor.layers.layernorm import RMSNorm
38
from vllm.model_executor.layers.linear import (QKVParallelLinear,
39
RowParallelLinear)
40
from vllm.model_executor.layers.logits_processor import LogitsProcessor
41
-from vllm.model_executor.layers.pooler import Pooler, PoolingType
41
from vllm.model_executor.layers.quantization import QuantizationConfig
42
from vllm.model_executor.layers.rotary_embedding import get_rope
43
from vllm.model_executor.layers.vocab_parallel_embedding import ParallelLMHead
45
-from vllm.model_executor.pooling_metadata import PoolingMetadata
44
from vllm.model_executor.sampling_metadata import SamplingMetadata
47
-from vllm.sequence import IntermediateTensors, PoolerOutput
45
+from vllm.sequence import IntermediateTensors
46
49
-from .interfaces import SupportsCrossEncoding, SupportsLoRA, SupportsPP
47
+from .adapters import as_seq_cls_model
48
+from .interfaces import SupportsLoRA, SupportsPP
49
from .qwen2 import Qwen2MLP as Qwen3MLP
50
from .qwen2 import Qwen2Model
51
from .utils import AutoWeightsLoader, PPMissingLayer, maybe_prefix
@@ -323,114 +322,4 @@ class Qwen3ForCausalLM(nn.Module, SupportsLoRA, SupportsPP):
322
return loader.load_weights(weights)
323
324
326
-class Qwen3ForSequenceClassification(nn.Module, SupportsLoRA,
327
- SupportsCrossEncoding):
328
-
329
- def __init__(
330
- self,
331
- vllm_config: "VllmConfig",
332
- prefix: str = "",
333
- ) -> None:
334
- super().__init__()
335
-
336
- config = vllm_config.model_config.hf_config
337
- quant_config = vllm_config.quant_config
338
- pooler_config = vllm_config.model_config.pooler_config
339
-
340
- self.vllm_config = vllm_config
341
- self.config = config
342
- self.quant_config = quant_config
343
- self.prefix = prefix
344
- self.model = Qwen3Model(vllm_config=vllm_config,
345
- prefix=maybe_prefix(prefix, "model"))
346
- self.score = RowParallelLinear(config.hidden_size,
347
- config.num_labels,
348
- quant_config=quant_config,
349
- input_is_parallel=False,
350
- bias=False,
351
- prefix=maybe_prefix(prefix, "score"))
352
-
353
- self._pooler = Pooler.from_config_with_defaults(
354
- pooler_config,
355
- pooling_type=PoolingType.LAST,
356
- normalize=False,
357
- softmax=True)
358
-
359
- def forward(
360
- self,
361
- input_ids: torch.Tensor,
362
- positions: torch.Tensor,
363
- intermediate_tensors: Optional[IntermediateTensors] = None,
364
- inputs_embeds: Optional[torch.Tensor] = None,
365
- ) -> torch.Tensor:
366
- return self.model(input_ids=input_ids,
367
- positions=positions,
368
- inputs_embeds=inputs_embeds,
369
- intermediate_tensors=intermediate_tensors)
370
-
371
- def pooler(
372
- self,
373
- hidden_states: torch.Tensor,
374
- pooling_metadata: PoolingMetadata,
375
- ) -> Optional[PoolerOutput]:
376
- hidden_states = self._pooler.extract_states(hidden_states,
377
- pooling_metadata)
378
-
379
- if isinstance(hidden_states, list):
380
- logits = [self.score(state)[0] for state in hidden_states]
381
- else:
382
- logits, _ = self.score(hidden_states)
383
-
384
- pooled_data = self._pooler.head(logits, pooling_metadata)
385
- pooled_outputs = [
386
- self._pooler.build_output(data.squeeze(-1)) for data in pooled_data
387
- ]
388
- return PoolerOutput(outputs=pooled_outputs)
389
-
390
- def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]):
391
- is_original_qwen3_reranker = getattr(self.config,
392
- "is_original_qwen3_reranker",
393
- False)
394
-
395
- if not is_original_qwen3_reranker:
396
- loader = AutoWeightsLoader(self)
397
- return loader.load_weights(weights)
398
-
399
- return self.load_weights_from_original_qwen3_reranker(weights)
400
-
401
- def load_weights_from_original_qwen3_reranker(
402
- self, weights: Iterable[tuple[str, torch.Tensor]]):
403
-
404
- model_config = self.vllm_config.model_config
405
- tokens = getattr(self.config, "classifier_from_token", None)
406
- device = self.score.weight.device
407
-
408
- if self.config.tie_word_embeddings:
409
- self.lm_head = self.model.embed_tokens
410
- else:
411
- self.lm_head = ParallelLMHead(self.config.vocab_size,
412
- self.config.hidden_size,
413
- quant_config=self.quant_config,
414
- prefix=maybe_prefix(
415
- self.prefix, "lm_head"))
416
-
417
- loader = AutoWeightsLoader(self)
418
- loaded_weights = loader.load_weights(weights)
419
-
420
- from vllm.transformers_utils.tokenizer import get_tokenizer
421
- tokenizer = get_tokenizer(
422
- model_config.tokenizer,
423
- revision=model_config.tokenizer_revision,
424
- tokenizer_mode=model_config.tokenizer_mode,
425
- trust_remote_code=model_config.trust_remote_code)
426
-
427
- a = tokenizer.convert_tokens_to_ids(tokens[0])
428
- b = tokenizer.convert_tokens_to_ids(tokens[1])
429
- weight = self.lm_head.weight.data[b].to(
430
- device) - self.lm_head.weight.data[a].to(device)
431
- self.score.weight.data.copy_(weight)
432
-
433
- del self.lm_head
434
- loaded_weights.add("score.weight")
435
- loaded_weights.discard("lm_head.weight")
436
- return loaded_weights
325
+Qwen3ForSequenceClassification = as_seq_cls_model(Qwen3ForCausalLM)