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, 09:45 UTC
[Frontend][4/n] Improve pooling entrypoints | pooling. (#39153)
Signed-off-by: wang.yuqi <yuqi.wang@daocloud.io>
wang.yuqi committed
Apr 9, 2026 at 18:09 UTC
66c079ae83f5d5200306da28acb8a0949a6a9a45
43 files changed
+549
-728
tests/entrypoints/openai/chat_completion/test_chat_error.py
-3
@@ -87,7 +87,6 @@ def _build_serving_chat(engine: AsyncLLM) -> OpenAIServingChat:
87
serving_render = OpenAIServingRender(
88
model_config=engine.model_config,
89
renderer=engine.renderer,
90
- io_processor=engine.io_processor,
90
model_registry=models.registry,
91
request_logger=None,
92
chat_template=None,
@@ -123,7 +122,6 @@ async def test_chat_error_non_stream():
122
mock_engine.errored = False
123
mock_engine.model_config = MockModelConfig()
124
mock_engine.input_processor = MagicMock()
126
- mock_engine.io_processor = MagicMock()
125
mock_engine.renderer = _build_renderer(mock_engine.model_config)
126
127
serving_chat = _build_serving_chat(mock_engine)
@@ -173,7 +171,6 @@ async def test_chat_error_stream():
171
mock_engine.errored = False
172
mock_engine.model_config = MockModelConfig()
173
mock_engine.input_processor = MagicMock()
176
- mock_engine.io_processor = MagicMock()
174
mock_engine.renderer = _build_renderer(mock_engine.model_config)
175
176
serving_chat = _build_serving_chat(mock_engine)
tests/entrypoints/openai/chat_completion/test_serving_chat.py
-15
@@ -567,7 +567,6 @@ def _build_serving_render(
567
return OpenAIServingRender(
568
model_config=engine.model_config,
569
renderer=engine.renderer,
570
- io_processor=engine.io_processor,
570
model_registry=model_registry,
571
request_logger=None,
572
chat_template=CHAT_TEMPLATE,
@@ -599,7 +598,6 @@ def _build_serving_chat(engine: AsyncLLM) -> OpenAIServingChat:
598
class MockEngine:
599
model_config: MockModelConfig = field(default_factory=MockModelConfig)
600
input_processor: MagicMock = field(default_factory=MagicMock)
602
- io_processor: MagicMock = field(default_factory=MagicMock)
601
renderer: MagicMock = field(default_factory=MagicMock)
602
603
@@ -632,7 +630,6 @@ async def test_serving_chat_returns_correct_model_name():
630
mock_engine.errored = False
631
mock_engine.model_config = MockModelConfig()
632
mock_engine.input_processor = MagicMock()
635
- mock_engine.io_processor = MagicMock()
633
mock_engine.renderer = _build_renderer(mock_engine.model_config)
634
635
serving_chat = _build_serving_chat(mock_engine)
@@ -662,7 +659,6 @@ async def test_serving_chat_should_set_correct_max_tokens():
659
mock_engine.errored = False
660
mock_engine.model_config = MockModelConfig()
661
mock_engine.input_processor = MagicMock()
665
- mock_engine.io_processor = MagicMock()
662
mock_engine.renderer = _build_renderer(mock_engine.model_config)
663
664
serving_chat = _build_serving_chat(mock_engine)
@@ -693,7 +689,6 @@ async def test_serving_chat_should_set_correct_max_tokens():
689
mock_engine.errored = False
690
mock_engine.model_config = mock_model_config
691
mock_engine.input_processor = MagicMock()
696
- mock_engine.io_processor = MagicMock()
692
mock_engine.renderer = _build_renderer(mock_engine.model_config)
693
694
# Initialize the serving chat
@@ -737,7 +732,6 @@ async def test_serving_chat_should_set_correct_max_tokens():
732
mock_engine.errored = False
733
mock_engine.model_config = mock_model_config
734
mock_engine.input_processor = MagicMock()
740
- mock_engine.io_processor = MagicMock()
735
mock_engine.renderer = _build_renderer(mock_engine.model_config)
736
737
serving_chat = _build_serving_chat(mock_engine)
@@ -779,7 +773,6 @@ async def test_serving_chat_should_set_correct_max_tokens():
773
mock_engine.errored = False
774
mock_engine.model_config = mock_model_config
775
mock_engine.input_processor = MagicMock()
782
- mock_engine.io_processor = MagicMock()
776
mock_engine.renderer = _build_renderer(mock_engine.model_config)
777
778
# Initialize the serving chat
@@ -823,7 +816,6 @@ async def test_serving_chat_mistral_token_ids_prompt_is_validated():
816
mock_engine.errored = False
817
mock_engine.model_config = MockModelConfig(skip_tokenizer_init=True)
818
mock_engine.input_processor = MagicMock()
826
- mock_engine.io_processor = MagicMock()
819
820
mock_tokenizer = MagicMock(spec=MistralTokenizer)
821
mock_renderer = MistralRenderer(
@@ -863,7 +855,6 @@ async def test_serving_chat_mistral_token_ids_prompt_too_long_is_rejected():
855
mock_engine.errored = False
856
mock_engine.model_config = MockModelConfig(skip_tokenizer_init=True)
857
mock_engine.input_processor = MagicMock()
866
- mock_engine.io_processor = MagicMock()
858
859
mock_tokenizer = MagicMock(spec=MistralTokenizer)
860
mock_renderer = MistralRenderer(
@@ -906,7 +897,6 @@ async def test_serving_chat_could_load_correct_generation_config():
897
mock_engine.errored = False
898
mock_engine.model_config = mock_model_config
899
mock_engine.input_processor = MagicMock()
909
- mock_engine.io_processor = MagicMock()
900
mock_engine.renderer = _build_renderer(mock_engine.model_config)
901
902
# Initialize the serving chat
@@ -952,7 +942,6 @@ async def test_serving_chat_did_set_correct_cache_salt(model_type):
942
mock_engine.errored = False
943
mock_engine.model_config = mock_model_config
944
mock_engine.input_processor = MagicMock()
955
- mock_engine.io_processor = MagicMock()
945
mock_engine.renderer = _build_renderer(mock_engine.model_config)
946
947
serving_chat = _build_serving_chat(mock_engine)
@@ -1003,7 +992,6 @@ async def test_serving_chat_data_parallel_rank_extraction():
992
mock_engine.errored = False
993
mock_engine.model_config = MockModelConfig()
994
mock_engine.input_processor = MagicMock()
1006
- mock_engine.io_processor = MagicMock()
995
mock_engine.renderer = _build_renderer(mock_engine.model_config)
996
997
# Mock the generate method to return an async generator
@@ -1095,7 +1083,6 @@ class TestServingChatWithHarmony:
1083
mock_engine.errored = False
1084
mock_engine.model_config = MockModelConfig()
1085
mock_engine.input_processor = MagicMock()
1098
- mock_engine.io_processor = MagicMock()
1086
mock_engine.renderer = _build_renderer(mock_engine.model_config)
1087
return mock_engine
1088
@@ -1732,7 +1719,6 @@ async def test_tool_choice_validation_without_parser():
1719
mock_engine.errored = False
1720
mock_engine.model_config = MockModelConfig()
1721
mock_engine.input_processor = MagicMock()
1735
- mock_engine.io_processor = MagicMock()
1722
mock_engine.renderer = _build_renderer(mock_engine.model_config)
1723
1724
models = OpenAIServingModels(
@@ -1802,7 +1788,6 @@ async def test_streaming_n_gt1_independent_tool_parsers():
1788
mock_engine.errored = False
1789
mock_engine.model_config = MockModelConfig()
1790
mock_engine.input_processor = MagicMock()
1805
- mock_engine.io_processor = MagicMock()
1791
mock_engine.renderer = _build_renderer(mock_engine.model_config)
1792
1793
models = OpenAIServingModels(
tests/entrypoints/openai/completion/test_completion_error.py
-3
@@ -79,7 +79,6 @@ def _build_serving_completion(engine: AsyncLLM) -> OpenAIServingCompletion:
79
serving_render = OpenAIServingRender(
80
model_config=engine.model_config,
81
renderer=engine.renderer,
82
- io_processor=engine.io_processor,
82
model_registry=models.registry,
83
request_logger=None,
84
chat_template=None,
@@ -107,7 +106,6 @@ async def test_completion_error_non_stream():
106
mock_engine.errored = False
107
mock_engine.model_config = MockModelConfig()
108
mock_engine.input_processor = MagicMock()
110
- mock_engine.io_processor = MagicMock()
109
mock_engine.renderer = _build_renderer(mock_engine.model_config)
110
111
serving_completion = _build_serving_completion(mock_engine)
@@ -157,7 +155,6 @@ async def test_completion_error_stream():
155
mock_engine.errored = False
156
mock_engine.model_config = MockModelConfig()
157
mock_engine.input_processor = MagicMock()
160
- mock_engine.io_processor = MagicMock()
158
mock_engine.renderer = _build_renderer(mock_engine.model_config)
159
160
serving_completion = _build_serving_completion(mock_engine)
tests/entrypoints/openai/completion/test_lora_resolvers.py
-2
@@ -137,7 +137,6 @@ def mock_serving_setup():
137
138
mock_engine.model_config = MockModelConfig()
139
mock_engine.input_processor = MagicMock()
140
- mock_engine.io_processor = MagicMock()
140
mock_engine.renderer = _build_renderer(mock_engine.model_config)
141
142
models = OpenAIServingModels(
@@ -148,7 +147,6 @@ def mock_serving_setup():
147
serving_render = OpenAIServingRender(
148
model_config=mock_engine.model_config,
149
renderer=mock_engine.renderer,
151
- io_processor=mock_engine.io_processor,
150
model_registry=models.registry,
151
request_logger=None,
152
chat_template=None,
tests/entrypoints/openai/generative_scoring/test_generative_scoring.py
-1
@@ -77,7 +77,6 @@ def _create_mock_engine():
77
mock_engine.errored = False
78
mock_engine.model_config = MockModelConfig()
79
mock_engine.input_processor = MagicMock()
80
- mock_engine.io_processor = MagicMock()
80
81
# renderer is accessed by OpenAIServing.__init__ and serving.py
82
mock_renderer = MagicMock()
tests/entrypoints/openai/responses/test_serving_responses.py
-4
@@ -218,7 +218,6 @@ class TestInitializeToolSessions:
218
engine_client.model_config = model_config
219
220
engine_client.input_processor = MagicMock()
221
- engine_client.io_processor = MagicMock()
221
engine_client.renderer = MagicMock()
222
223
models = MagicMock()
@@ -307,7 +306,6 @@ class TestValidateGeneratorInput:
306
engine_client.model_config = model_config
307
308
engine_client.input_processor = MagicMock()
310
- engine_client.io_processor = MagicMock()
309
engine_client.renderer = MagicMock()
310
311
models = MagicMock()
@@ -369,7 +367,6 @@ async def test_reasoning_tokens_counted_for_text_reasoning_model(monkeypatch):
367
model_config.get_diff_sampling_param.return_value = {}
368
engine_client.model_config = model_config
369
engine_client.input_processor = MagicMock()
372
- engine_client.io_processor = MagicMock()
370
engine_client.renderer = MagicMock()
371
372
tokenizer = FakeTokenizer()
@@ -672,7 +669,6 @@ def _make_serving_instance_with_reasoning():
669
model_config.get_diff_sampling_param.return_value = {}
670
engine_client.model_config = model_config
671
engine_client.input_processor = MagicMock()
675
- engine_client.io_processor = MagicMock()
672
engine_client.renderer = MagicMock()
673
674
models = MagicMock()
tests/entrypoints/pooling/classify/test_offline.py
+5
-2
@@ -110,8 +110,11 @@ def test_score_api(llm: LLM):
110
llm.score("ping", "pong", use_tqdm=False)
111
112
113
-@pytest.mark.parametrize("task", ["embed", "token_embed"])
113
+@pytest.mark.parametrize("task", ["embed", "token_embed", "plugin"])
114
def test_unsupported_tasks(llm: LLM, task: PoolingTask):
115
- err_msg = "Embedding API is not supported by this model.+"
115
+ if task == "plugin":
116
+ err_msg = "No IOProcessor plugin installed."
117
+ else:
118
+ err_msg = "Embedding API is not supported by this model.+"
119
with pytest.raises(ValueError, match=err_msg):
120
llm.encode(prompt, pooling_task=task, use_tqdm=False)
tests/entrypoints/pooling/classify/test_online.py
+5
-1
@@ -469,4 +469,8 @@ async def test_pooling_not_supported(
469
},
470
)
471
assert response.json()["error"]["type"] == "BadRequestError"
472
- assert response.json()["error"]["message"].startswith(f"Unsupported task: {task!r}")
472
+ if task == "plugin":
473
+ err_msg = "No IOProcessor plugin installed."
474
+ else:
475
+ err_msg = f"Unsupported task: {task!r}"
476
+ assert response.json()["error"]["message"].startswith(err_msg)
tests/entrypoints/pooling/embed/test_offline.py
+5
-2
@@ -107,8 +107,11 @@ def test_pooling_params(llm: LLM):
107
)
108
109
110
-@pytest.mark.parametrize("task", ["token_classify", "classify"])
110
+@pytest.mark.parametrize("task", ["token_classify", "classify", "plugin"])
111
def test_unsupported_tasks(llm: LLM, task: PoolingTask):
112
- err_msg = "Classification API is not supported by this model.+"
112
+ if task == "plugin":
113
+ err_msg = "No IOProcessor plugin installed."
114
+ else:
115
+ err_msg = "Classification API is not supported by this model.+"
116
with pytest.raises(ValueError, match=err_msg):
117
llm.encode(prompt, pooling_task=task, use_tqdm=False)
tests/entrypoints/pooling/embed/test_online.py
+5
-1
@@ -767,4 +767,8 @@ async def test_pooling_not_supported(
767
},
768
)
769
assert response.json()["error"]["type"] == "BadRequestError"
770
- assert response.json()["error"]["message"].startswith(f"Unsupported task: {task!r}")
770
+ if task == "plugin":
771
+ err_msg = "No IOProcessor plugin installed."
772
+ else:
773
+ err_msg = f"Unsupported task: {task!r}"
774
+ assert response.json()["error"]["message"].startswith(err_msg)
tests/entrypoints/pooling/scoring/test_bi_encoder_online.py
+5
-1
@@ -411,4 +411,8 @@ async def test_pooling_not_supported(server: RemoteOpenAIServer, task: str):
411
},
412
)
413
assert response.json()["error"]["type"] == "BadRequestError"
414
- assert response.json()["error"]["message"].startswith(f"Unsupported task: {task!r}")
414
+ if task == "plugin":
415
+ err_msg = "No IOProcessor plugin installed."
416
+ else:
417
+ err_msg = f"Unsupported task: {task!r}"
418
+ assert response.json()["error"]["message"].startswith(err_msg)
tests/entrypoints/pooling/scoring/test_cross_encoder_online.py
+5
-1
@@ -484,4 +484,8 @@ async def test_pooling_not_supported(server: RemoteOpenAIServer, task: str):
484
},
485
)
486
assert response.json()["error"]["type"] == "BadRequestError"
487
- assert response.json()["error"]["message"].startswith(f"Unsupported task: {task!r}")
487
+ if task == "plugin":
488
+ err_msg = "No IOProcessor plugin installed."
489
+ else:
490
+ err_msg = f"Unsupported task: {task!r}"
491
+ assert response.json()["error"]["message"].startswith(err_msg)
tests/entrypoints/pooling/token_classify/test_offline.py
+5
-2
@@ -65,14 +65,17 @@ def test_score_api(llm: LLM):
65
llm.score("ping", "pong", use_tqdm=False)
66
67
68
-@pytest.mark.parametrize("task", ["classify", "embed", "token_embed"])
68
+@pytest.mark.parametrize("task", ["classify", "embed", "token_embed", "plugin"])
69
def test_unsupported_tasks(llm: LLM, task: PoolingTask, caplog_vllm):
70
if task == "classify":
71
with caplog_vllm.at_level(level=logging.WARNING, logger="vllm"):
72
llm.encode(prompt, pooling_task=task, use_tqdm=False)
73
assert "deprecated" in caplog_vllm.text
74
else:
75
- err_msg = "Embedding API is not supported by this model.+"
75
+ if task == "plugin":
76
+ err_msg = "No IOProcessor plugin installed."
77
+ else:
78
+ err_msg = "Embedding API is not supported by this model.+"
79
80
with pytest.raises(ValueError, match=err_msg):
81
llm.encode(prompt, pooling_task=task, use_tqdm=False)
tests/entrypoints/pooling/token_classify/test_online.py
+5
-4
@@ -50,7 +50,7 @@ async def test_pooling_token_classify(server: RemoteOpenAIServer, model_name: st
50
51
@pytest.mark.asyncio
52
@pytest.mark.parametrize("model_name", [MODEL_NAME])
53
-@pytest.mark.parametrize("task", ["classify", "embed", "token_embed", "plugin"])
53
+@pytest.mark.parametrize("task", ["embed", "token_embed", "plugin"])
54
async def test_pooling_not_supported(
55
server: RemoteOpenAIServer, model_name: str, task: str
56
):
@@ -64,7 +64,8 @@ async def test_pooling_not_supported(
64
},
65
)
66
67
- if task != "classify":
68
- assert response.json()["error"]["type"] == "BadRequestError"
67
+ if task == "plugin":
68
+ err_msg = "No IOProcessor plugin installed."
69
+ else:
70
err_msg = f"Unsupported task: {task!r}"
70
- assert response.json()["error"]["message"].startswith(err_msg)
71
+ assert response.json()["error"]["message"].startswith(err_msg)
tests/entrypoints/pooling/token_embed/test_offline.py
+5
-2
@@ -62,14 +62,17 @@ def test_token_ids_prompts(llm: LLM):
62
assert outputs[0].outputs.data.shape == (11, 384)
63
64
65
-@pytest.mark.parametrize("task", ["embed", "classify", "token_classify"])
65
+@pytest.mark.parametrize("task", ["embed", "classify", "token_classify", "plugin"])
66
def test_unsupported_tasks(llm: LLM, task: PoolingTask, caplog_vllm):
67
if task == "embed":
68
with caplog_vllm.at_level(level=logging.WARNING, logger="vllm"):
69
llm.encode(prompt, pooling_task=task, use_tqdm=False)
70
assert "deprecated" in caplog_vllm.text
71
else:
72
- err_msg = "Classification API is not supported by this model.+"
72
+ if task == "plugin":
73
+ err_msg = "No IOProcessor plugin installed."
74
+ else:
75
+ err_msg = "Classification API is not supported by this model.+"
76
77
with pytest.raises(ValueError, match=err_msg):
78
llm.encode(prompt, pooling_task=task, use_tqdm=False)
tests/entrypoints/pooling/token_embed/test_online.py
+5
-4
@@ -73,7 +73,7 @@ async def test_pooling_token_embed(server: RemoteOpenAIServer, model_name: str):
73
74
@pytest.mark.asyncio
75
@pytest.mark.parametrize("model_name", [MODEL_NAME])
76
-@pytest.mark.parametrize("task", ["embed", "classify", "token_classify", "plugin"])
76
+@pytest.mark.parametrize("task", ["classify", "token_classify", "plugin"])
77
async def test_pooling_not_supported(
78
server: RemoteOpenAIServer, model_name: str, task: str
79
):
@@ -87,7 +87,8 @@ async def test_pooling_not_supported(
87
},
88
)
89
90
- if task != "embed":
91
- assert response.json()["error"]["type"] == "BadRequestError"
90
+ if task == "plugin":
91
+ err_msg = "No IOProcessor plugin installed."
92
+ else:
93
err_msg = f"Unsupported task: {task!r}"
93
- assert response.json()["error"]["message"].startswith(err_msg)
94
+ assert response.json()["error"]["message"].startswith(err_msg)
tests/entrypoints/serve/disagg/test_generate_stream.py
-2
@@ -86,7 +86,6 @@ def _build_serving_tokens(engine: AsyncLLM, **kwargs) -> ServingTokens:
86
serving_render = OpenAIServingRender(
87
model_config=engine.model_config,
88
renderer=engine.renderer,
89
- io_processor=engine.io_processor,
89
model_registry=models.registry,
90
request_logger=None,
91
chat_template=None,
@@ -148,7 +147,6 @@ def _mock_engine() -> MagicMock:
147
engine.errored = False
148
engine.model_config = MockModelConfig()
149
engine.input_processor = MagicMock()
151
- engine.io_processor = MagicMock()
150
engine.renderer = _build_renderer(engine.model_config)
151
return engine
152
tests/entrypoints/serve/lora/test_serving_models.py
-1
@@ -34,7 +34,6 @@ async def _async_serving_models_init() -> OpenAIServingModels:
34
mock_model_config.max_model_len = 2048
35
mock_engine_client.model_config = mock_model_config
36
mock_engine_client.input_processor = MagicMock()
37
- mock_engine_client.io_processor = MagicMock()
37
mock_engine_client.renderer = MagicMock()
38
39
serving_models = OpenAIServingModels(
tests/v1/engine/test_async_llm.py
-1
@@ -514,7 +514,6 @@ async def test_header_dp_rank_argument():
514
serving_render = OpenAIServingRender(
515
model_config=engine.model_config,
516
renderer=engine.renderer,
517
- io_processor=engine.io_processor,
517
model_registry=models.registry,
518
request_logger=None,
519
chat_template=None,
vllm/engine/protocol.py
-2
@@ -14,7 +14,6 @@ from vllm.distributed.weight_transfer.base import (
14
from vllm.inputs import EngineInput, PromptType
15
from vllm.lora.request import LoRARequest
16
from vllm.outputs import PoolingRequestOutput, RequestOutput
17
-from vllm.plugins.io_processors import IOProcessor
17
from vllm.pooling_params import PoolingParams
18
from vllm.renderers import BaseRenderer
19
from vllm.sampling_params import SamplingParams
@@ -44,7 +43,6 @@ class EngineClient(ABC):
43
vllm_config: VllmConfig
44
model_config: ModelConfig
45
renderer: BaseRenderer
47
- io_processor: IOProcessor | None
46
input_processor: InputProcessor
47
48
@property
vllm/entrypoints/llm.py
+59
-118
@@ -49,9 +49,7 @@ from vllm.entrypoints.chat_utils import (
49
load_chat_template,
50
)
51
from vllm.entrypoints.pooling.io_processor_factories import init_pooling_io_processors
52
-from vllm.entrypoints.pooling.scoring.io_processor import (
53
- ScoringIOProcessor,
54
-)
52
+from vllm.entrypoints.pooling.scoring.io_processor import ScoringIOProcessor
53
from vllm.entrypoints.pooling.scoring.typing import ScoreInput
54
from vllm.entrypoints.pooling.typing import OfflineInputsContext, OfflineOutputsContext
55
from vllm.entrypoints.utils import log_non_default_args
@@ -398,12 +396,11 @@ class LLM:
396
self.runner_type = self.model_config.runner_type
397
self.renderer = self.llm_engine.renderer
398
self.chat_template = load_chat_template(chat_template)
401
- self.io_processor = self.llm_engine.io_processor
399
self.input_processor = self.llm_engine.input_processor
400
self.chat_template_config = ChatTemplateConfig(chat_template=self.chat_template)
401
self.pooling_io_processors = init_pooling_io_processors(
402
supported_tasks=supported_tasks,
406
- model_config=self.model_config,
403
+ vllm_config=self.llm_engine.vllm_config,
404
renderer=self.renderer,
405
chat_template_config=self.chat_template_config,
406
)
@@ -1081,118 +1078,55 @@ class LLM:
1078
pooled hidden states in the same order as the input prompts.
1079
"""
1080
1081
+ if isinstance(prompts, dict) and "data" in prompts and pooling_task != "plugin":
1082
+ raise ValueError(
1083
+ "The 'data' field is only supported for the 'plugin' pooling task."
1084
+ )
1085
self._verify_pooling_task(pooling_task)
1086
+ assert pooling_task is not None and pooling_task in self.pooling_io_processors
1087
1086
- if isinstance(prompts, dict) and "data" in prompts:
1087
- if self.io_processor is None:
1088
- raise ValueError(
1089
- "No IOProcessor plugin installed. Please refer "
1090
- "to the documentation and to the "
1091
- "'prithvi_geospatial_mae_io_processor' "
1092
- "offline inference example for more details."
1093
- )
1088
+ io_processor = self.pooling_io_processors[pooling_task]
1089
1095
- # Validate the request data is valid for the loaded plugin
1096
- prompt_data = prompts.get("data")
1097
- if prompt_data is None:
1098
- raise ValueError(
1099
- "The 'data' field of the prompt is expected to contain "
1100
- "the prompt data and it cannot be None. "
1101
- "Refer to the documentation of the IOProcessor "
1102
- "in use for more details."
1103
- )
1104
- validated_prompt = self.io_processor.parse_data(prompt_data)
1090
+ if pooling_params is None:
1091
+ pooling_params = PoolingParams()
1092
1106
- # obtain the actual model prompts from the pre-processor
1107
- prompts = self.io_processor.pre_process(prompt=validated_prompt)
1108
- prompts_seq = prompt_to_seq(prompts)
1093
+ ctx = OfflineInputsContext(
1094
+ prompts=prompts,
1095
+ pooling_params=pooling_params,
1096
+ tokenization_kwargs=tokenization_kwargs,
1097
+ )
1098
1110
- params_seq: Sequence[PoolingParams] = [
1111
- self.io_processor.merge_pooling_params(param)
1112
- for param in self._params_to_seq(
1113
- pooling_params,
1114
- len(prompts_seq),
1115
- )
1116
- ]
1117
- for p in params_seq:
1118
- if p.task is None:
1119
- p.task = "plugin"
1120
-
1121
- outputs = self._run_completion(
1122
- prompts=prompts_seq,
1123
- params=params_seq,
1124
- output_type=PoolingRequestOutput,
1125
- use_tqdm=use_tqdm,
1126
- lora_request=lora_request,
1127
- tokenization_kwargs=tokenization_kwargs,
1128
- )
1099
+ engine_inputs = io_processor.pre_process_offline(ctx)
1100
+ n_inputs = len(engine_inputs)
1101
+ assert ctx.pooling_params is not None
1102
1130
- # get the post-processed model outputs
1131
- assert self.io_processor is not None
1132
- processed_outputs = self.io_processor.post_process(outputs)
1103
+ params_seq = self._params_to_seq(ctx.pooling_params, n_inputs)
1104
1134
- return [
1135
- PoolingRequestOutput[Any](
1136
- request_id="",
1137
- outputs=processed_outputs,
1138
- num_cached_tokens=getattr(
1139
- processed_outputs, "num_cached_tokens", 0
1140
- ),
1141
- prompt_token_ids=[],
1142
- finished=True,
1143
- )
1144
- ]
1145
- else:
1146
- if pooling_params is None:
1147
- # Use default pooling params.
1148
- pooling_params = PoolingParams()
1149
-
1150
- prompts_seq = prompt_to_seq(prompts)
1151
- params_seq = self._params_to_seq(pooling_params, len(prompts_seq))
1152
-
1153
- for param in params_seq:
1154
- if param.task is None:
1155
- param.task = pooling_task
1156
- elif param.task != pooling_task:
1157
- msg = (
1158
- f"You cannot overwrite {param.task=!r} with {pooling_task=!r}!"
1159
- )
1160
- raise ValueError(msg)
1105
+ for param in params_seq:
1106
+ if param.task is None:
1107
+ param.task = pooling_task
1108
+ elif pooling_task == "plugin":
1109
+ # `plugin` task uses io_processor.parse_request to verify inputs.
1110
+ # We actually allow plugin to overwrite pooling_task.
1111
+ pass
1112
+ elif param.task != pooling_task:
1113
+ msg = f"You cannot overwrite {param.task=!r} with {pooling_task=!r}!"
1114
+ raise ValueError(msg)
1115
1162
- if pooling_task in self.pooling_io_processors:
1163
- io_processor = self.pooling_io_processors[pooling_task]
1164
- processor_inputs = io_processor.pre_process_offline(
1165
- ctx=OfflineInputsContext(
1166
- prompts=prompts_seq, tokenization_kwargs=tokenization_kwargs
1167
- )
1168
- )
1169
- seq_lora_requests = self._lora_request_to_seq(
1170
- lora_request, len(prompts_seq)
1171
- )
1172
- seq_priority = self._priority_to_seq(None, len(prompts))
1116
+ seq_lora_requests = self._lora_request_to_seq(lora_request, n_inputs)
1117
+ seq_priority = self._priority_to_seq(None, n_inputs)
1118
1174
- self._render_and_add_requests(
1175
- prompts=processor_inputs,
1176
- params=params_seq,
1177
- lora_requests=seq_lora_requests,
1178
- priorities=seq_priority,
1179
- )
1119
+ self._render_and_add_requests(
1120
+ prompts=engine_inputs,
1121
+ params=params_seq,
1122
+ lora_requests=seq_lora_requests,
1123
+ priorities=seq_priority,
1124
+ )
1125
1181
- outputs = self._run_engine(
1182
- use_tqdm=use_tqdm, output_type=PoolingRequestOutput
1183
- )
1184
- outputs = io_processor.post_process_offline(
1185
- ctx=OfflineOutputsContext(outputs=outputs)
1186
- )
1187
- else:
1188
- outputs = self._run_completion(
1189
- prompts=prompts_seq,
1190
- params=params_seq,
1191
- output_type=PoolingRequestOutput,
1192
- use_tqdm=use_tqdm,
1193
- lora_request=lora_request,
1194
- tokenization_kwargs=tokenization_kwargs,
1195
- )
1126
+ outputs = self._run_engine(use_tqdm=use_tqdm, output_type=PoolingRequestOutput)
1127
+ outputs = io_processor.post_process_offline(
1128
+ ctx=OfflineOutputsContext(outputs=outputs)
1129
+ )
1130
return outputs
1131
1132
def _verify_pooling_task(self, pooling_task: PoolingTask | None):
@@ -1254,6 +1188,14 @@ class LLM:
1188
pooling_task,
1189
)
1190
1191
+ if pooling_task == "plugin" and "plugin" not in self.pooling_io_processors:
1192
+ raise ValueError(
1193
+ "No IOProcessor plugin installed. Please refer "
1194
+ "to the documentation and to the "
1195
+ "'prithvi_geospatial_mae_io_processor' "
1196
+ "offline inference example for more details."
1197
+ )
1198
+
1199
def embed(
1200
self,
1201
prompts: PromptType | Sequence[PromptType],
@@ -1458,6 +1400,9 @@ class LLM:
1400
scoring_data = io_processor.valid_inputs(data_1, data_2)
1401
n_queries = len(scoring_data.data_1)
1402
1403
+ if pooling_params is None:
1404
+ pooling_params = PoolingParams()
1405
+
1406
ctx = OfflineInputsContext(
1407
prompts=scoring_data,
1408
pooling_params=pooling_params,
@@ -1466,15 +1411,11 @@ class LLM:
1411
n_queries=n_queries,
1412
)
1413
1469
- processor_inputs = io_processor.pre_process_offline(ctx)
1414
+ engine_inputs = io_processor.pre_process_offline(ctx)
1415
+ n_inputs = len(engine_inputs)
1416
1471
- seq_lora_requests = self._lora_request_to_seq(
1472
- lora_request, len(processor_inputs)
1473
- )
1474
-
1475
- if ctx.pooling_params is None:
1476
- ctx.pooling_params = PoolingParams()
1477
- params_seq = self._params_to_seq(ctx.pooling_params, len(processor_inputs))
1417
+ seq_lora_requests = self._lora_request_to_seq(lora_request, n_inputs)
1418
+ params_seq = self._params_to_seq(ctx.pooling_params, n_inputs)
1419
1420
for param in params_seq:
1421
if param.task is None:
@@ -1483,10 +1424,10 @@ class LLM:
1424
msg = f"You cannot overwrite {param.task=!r} with {pooling_task=!r}!"
1425
raise ValueError(msg)
1426
1486
- seq_priority = self._priority_to_seq(None, len(processor_inputs))
1427
+ seq_priority = self._priority_to_seq(None, n_inputs)
1428
1429
self._render_and_add_requests(
1489
- prompts=processor_inputs,
1430
+ prompts=engine_inputs,
1431
params=params_seq,
1432
lora_requests=seq_lora_requests,
1433
priorities=seq_priority,
@@ -1579,7 +1520,7 @@ class LLM:
1520
if isinstance(params, Sequence):
1521
if len(params) != num_requests:
1522
raise ValueError(
1582
- f"The lengths of prompts ({params}) "
1523
+ f"The lengths of prompts ({num_requests}) "
1524
f"and params ({len(params)}) must be the same."
1525
)
1526
vllm/entrypoints/openai/api_server.py
+1
-7
@@ -370,7 +370,6 @@ async def init_app_state(
370
state.openai_serving_render = OpenAIServingRender(
371
model_config=engine_client.model_config,
372
renderer=engine_client.renderer,
373
- io_processor=engine_client.io_processor,
373
model_registry=state.openai_serving_models.registry,
374
request_logger=request_logger,
375
chat_template=resolved_chat_template,
@@ -441,13 +440,12 @@ async def init_render_app_state(
440
441
Unlike :func:`init_app_state` this function does not require an
442
:class:`~vllm.engine.protocol.EngineClient`; it bootstraps the
444
- preprocessing pipeline (renderer, io_processor, input_processor)
443
+ preprocessing pipeline (renderer, input_processor)
444
directly from the :class:`~vllm.config.VllmConfig`.
445
"""
446
from vllm.entrypoints.chat_utils import load_chat_template
447
from vllm.entrypoints.openai.models.serving import OpenAIModelRegistry
448
from vllm.entrypoints.serve.render.serving import OpenAIServingRender
450
- from vllm.plugins.io_processors import get_io_processor
449
from vllm.renderers import renderer_from_config
450
451
served_model_names = args.served_model_name or [args.model]
@@ -465,15 +463,11 @@ async def init_render_app_state(
463
request_logger = None
464
465
renderer = renderer_from_config(vllm_config)
468
- io_processor = get_io_processor(
469
- vllm_config, renderer, vllm_config.model_config.io_processor_plugin
470
- )
466
resolved_chat_template = load_chat_template(args.chat_template)
467
468
state.openai_serving_render = OpenAIServingRender(
469
model_config=vllm_config.model_config,
470
renderer=renderer,
476
- io_processor=io_processor,
471
model_registry=model_registry,
472
request_logger=request_logger,
473
chat_template=resolved_chat_template,
vllm/entrypoints/openai/engine/serving.py
+5
-180
@@ -44,12 +44,6 @@ from vllm.entrypoints.openai.speech_to_text.protocol import (
44
TranscriptionResponse,
45
TranslationRequest,
46
)
47
-from vllm.entrypoints.pooling.pooling.protocol import (
48
- IOProcessorRequest,
49
- PoolingChatRequest,
50
- PoolingCompletionRequest,
51
- PoolingResponse,
52
-)
47
from vllm.entrypoints.serve.disagg.protocol import GenerateRequest, GenerateResponse
48
from vllm.entrypoints.serve.tokenize.protocol import (
49
DetokenizeRequest,
@@ -62,8 +56,7 @@ from vllm.inputs import EngineInput, PromptType
56
from vllm.logger import init_logger
57
from vllm.logprobs import Logprob, PromptLogprobs
58
from vllm.lora.request import LoRARequest
65
-from vllm.outputs import CompletionOutput, PoolingRequestOutput, RequestOutput
66
-from vllm.pooling_params import PoolingParams
59
+from vllm.outputs import CompletionOutput, RequestOutput
60
from vllm.renderers import ChatParams, TokenizeParams
61
from vllm.renderers.inputs.preprocess import (
62
extract_prompt_components,
@@ -78,10 +71,7 @@ from vllm.tracing import (
71
log_tracing_disabled_warning,
72
)
73
from vllm.utils import random_uuid
81
-from vllm.utils.async_utils import (
82
- collect_from_async_generator,
83
- merge_async_iterators,
84
-)
74
+from vllm.utils.async_utils import collect_from_async_generator
75
76
logger = init_logger(__name__)
77
@@ -101,17 +91,11 @@ class RendererChatRequest(RendererRequest, Protocol):
91
92
93
CompletionLikeRequest: TypeAlias = (
104
- CompletionRequest
105
- | TokenizeCompletionRequest
106
- | DetokenizeRequest
107
- | PoolingCompletionRequest
94
+ CompletionRequest | TokenizeCompletionRequest | DetokenizeRequest
95
)
96
97
ChatLikeRequest: TypeAlias = (
111
- ChatCompletionRequest
112
- | BatchChatCompletionRequest
113
- | TokenizeChatRequest
114
- | PoolingChatRequest
98
+ ChatCompletionRequest | BatchChatCompletionRequest | TokenizeChatRequest
99
)
100
101
SpeechToTextRequest: TypeAlias = TranscriptionRequest | TranslationRequest
@@ -121,7 +105,6 @@ AnyRequest: TypeAlias = (
105
| ChatLikeRequest
106
| SpeechToTextRequest
107
| ResponsesRequest
124
- | IOProcessorRequest
108
| GenerateRequest
109
)
110
@@ -130,7 +113,6 @@ AnyResponse: TypeAlias = (
113
| ChatCompletionResponse
114
| TranscriptionResponse
115
| TokenizeResponse
133
- | PoolingResponse
116
| GenerateResponse
117
)
118
@@ -146,12 +128,6 @@ class ServeContext(Generic[RequestT]):
128
created_time: int = field(default_factory=lambda: int(time.time()))
129
lora_request: LoRARequest | None = None
130
engine_inputs: list[EngineInput] | None = None
149
-
150
- result_generator: AsyncGenerator[tuple[int, PoolingRequestOutput], None] | None = (
151
- None
152
- )
153
- final_res_batch: list[PoolingRequestOutput] = field(default_factory=list)
154
-
131
model_config = ConfigDict(arbitrary_types_allowed=True)
132
133
@@ -171,7 +147,6 @@ class OpenAIServing:
147
super().__init__()
148
149
self.engine_client = engine_client
174
-
150
self.models = models
151
152
self.request_logger = request_logger
@@ -179,7 +154,6 @@ class OpenAIServing:
154
155
self.model_config = engine_client.model_config
156
self.renderer = engine_client.renderer
182
- self.io_processor = engine_client.io_processor
157
self.input_processor = engine_client.input_processor
158
159
async def beam_search(
@@ -381,155 +355,6 @@ class OpenAIServing:
355
prompt_logprobs=None,
356
)
357
384
- async def _preprocess(
385
- self,
386
- ctx: ServeContext,
387
- ) -> ErrorResponse | None:
388
- """
389
- Default preprocessing hook. Subclasses may override to prepare `ctx`.
390
- """
391
- return None
392
-
393
- def _build_response(
394
- self,
395
- ctx: ServeContext,
396
- ) -> AnyResponse | ErrorResponse:
397
- """
398
- Default response builder. Subclass may override this method
399
- to return the appropriate response object.
400
- """
401
- return self.create_error_response("unimplemented endpoint")
402
-
403
- async def handle(
404
- self,
405
- ctx: ServeContext,
406
- ) -> AnyResponse | ErrorResponse:
407
- async for response in self._pipeline(ctx):
408
- return response
409
-
410
- return self.create_error_response("No response yielded from pipeline")
411
-
412
- async def _pipeline(
413
- self,
414
- ctx: ServeContext,
415
- ) -> AsyncGenerator[AnyResponse | ErrorResponse, None]:
416
- """Execute the request processing pipeline yielding responses."""
417
- if error := await self._check_model(ctx.request):
418
- yield error
419
- if error := self._validate_request(ctx):
420
- yield error
421
-
422
- preprocess_ret = await self._preprocess(ctx)
423
- if isinstance(preprocess_ret, ErrorResponse):
424
- yield preprocess_ret
425
-
426
- generators_ret = await self._prepare_generators(ctx)
427
- if isinstance(generators_ret, ErrorResponse):
428
- yield generators_ret
429
-
430
- collect_ret = await self._collect_batch(ctx)
431
- if isinstance(collect_ret, ErrorResponse):
432
- yield collect_ret
433
-
434
- yield self._build_response(ctx)
435
-
436
- def _validate_request(self, ctx: ServeContext) -> ErrorResponse | None:
437
- truncate_prompt_tokens = getattr(ctx.request, "truncate_prompt_tokens", None)
438
-
439
- if (
440
- truncate_prompt_tokens is not None
441
- and truncate_prompt_tokens > self.model_config.max_model_len
442
- ):
443
- return self.create_error_response(
444
- "truncate_prompt_tokens value is "
445
- "greater than max_model_len."
446
- " Please request a smaller truncation size."
447
- )
448
- return None
449
-
450
- def _create_pooling_params(
451
- self,
452
- ctx: ServeContext,
453
- ) -> PoolingParams | ErrorResponse:
454
- if not hasattr(ctx.request, "to_pooling_params"):
455
- return self.create_error_response(
456
- "Request type does not support pooling parameters"
457
- )
458
-
459
- return ctx.request.to_pooling_params()
460
-
461
- async def _prepare_generators(
462
- self,
463
- ctx: ServeContext,
464
- ) -> ErrorResponse | None:
465
- """Schedule the request and get the result generator."""
466
- generators: list[AsyncGenerator[PoolingRequestOutput, None]] = []
467
-
468
- trace_headers = (
469
- None
470
- if ctx.raw_request is None
471
- else await self._get_trace_headers(ctx.raw_request.headers)
472
- )
473
-
474
- pooling_params = self._create_pooling_params(ctx)
475
- if isinstance(pooling_params, ErrorResponse):
476
- return pooling_params
477
-
478
- if ctx.engine_inputs is None:
479
- return self.create_error_response("Engine prompts not available")
480
-
481
- for i, engine_input in enumerate(ctx.engine_inputs):
482
- request_id_item = f"{ctx.request_id}-{i}"
483
-
484
- self._log_inputs(
485
- request_id_item,
486
- engine_input,
487
- params=pooling_params,
488
- lora_request=ctx.lora_request,
489
- )
490
-
491
- generator = self.engine_client.encode(
492
- engine_input,
493
- pooling_params,
494
- request_id_item,
495
- lora_request=ctx.lora_request,
496
- trace_headers=trace_headers,
497
- priority=getattr(ctx.request, "priority", 0),
498
- )
499
-
500
- generators.append(generator)
501
-
502
- ctx.result_generator = merge_async_iterators(*generators)
503
-
504
- return None
505
-
506
- async def _collect_batch(
507
- self,
508
- ctx: ServeContext,
509
- ) -> ErrorResponse | None:
510
- """Collect batch results from the result generator."""
511
- if ctx.engine_inputs is None:
512
- return self.create_error_response("Engine prompts not available")
513
-
514
- num_prompts = len(ctx.engine_inputs)
515
- final_res_batch: list[PoolingRequestOutput | None]
516
- final_res_batch = [None] * num_prompts
517
-
518
- if ctx.result_generator is None:
519
- return self.create_error_response("Result generator not available")
520
-
521
- async for i, res in ctx.result_generator:
522
- final_res_batch[i] = res
523
-
524
- if None in final_res_batch:
525
- return self.create_error_response(
526
- "Failed to generate results for all prompts"
527
- )
528
-
529
- ctx.final_res_batch = [res for res in final_res_batch if res is not None]
530
-
531
- return None
532
-
358
@staticmethod
359
def create_error_response(
360
message: str | Exception,
@@ -719,7 +544,7 @@ class OpenAIServing:
544
self,
545
request_id: str,
546
inputs: PromptType | EngineInput,
722
- params: SamplingParams | PoolingParams | BeamSearchParams | None,
547
+ params: SamplingParams | BeamSearchParams | None,
548
lora_request: LoRARequest | None,
549
) -> None:
550
if self.request_logger is None:
vllm/entrypoints/openai/models/serving.py
-1
@@ -112,7 +112,6 @@ class OpenAIServingModels:
112
113
self.model_config = self.engine_client.model_config
114
self.renderer = self.engine_client.renderer
115
- self.io_processor = self.engine_client.io_processor
115
self.input_processor = self.engine_client.input_processor
116
117
async def init_static_loras(self):
vllm/entrypoints/pooling/__init__.py
+2
-4
@@ -67,20 +67,18 @@ def init_pooling_state(
67
from vllm.entrypoints.chat_utils import load_chat_template
68
from vllm.entrypoints.pooling.classify.serving import ServingClassification
69
from vllm.entrypoints.pooling.embed.serving import ServingEmbedding
70
- from vllm.entrypoints.pooling.pooling.serving import OpenAIServingPooling
70
+ from vllm.entrypoints.pooling.pooling.serving import ServingPooling
71
from vllm.entrypoints.pooling.scoring.serving import ServingScores
72
from vllm.tasks import POOLING_TASKS
73
74
model_config = engine_client.model_config
75
-
75
resolved_chat_template = load_chat_template(args.chat_template)
76
77
state.serving_pooling = (
78
(
80
- OpenAIServingPooling(
79
+ ServingPooling(
80
engine_client,
81
state.openai_serving_models,
83
- state.openai_serving_render,
82
supported_tasks=supported_tasks,
83
request_logger=request_logger,
84
chat_template=resolved_chat_template,
vllm/entrypoints/pooling/base/io_processor.py
+30
-9
@@ -4,8 +4,8 @@
4
from collections.abc import Sequence
5
from typing import Any, Final
6
7
-from vllm import PoolingRequestOutput, PromptType
8
-from vllm.config import ModelConfig
7
+from vllm import PoolingParams, PoolingRequestOutput, PromptType
8
+from vllm.config import VllmConfig
9
from vllm.entrypoints.chat_utils import (
10
ChatCompletionMessageParam,
11
ChatTemplateConfig,
@@ -33,11 +33,12 @@ class PoolingIOProcessor:
33
34
def __init__(
35
self,
36
- model_config: ModelConfig,
36
+ vllm_config: VllmConfig,
37
renderer: BaseRenderer,
38
chat_template_config: ChatTemplateConfig,
39
):
40
- self.model_config = model_config
40
+ self.vllm_config = vllm_config
41
+ self.model_config = vllm_config.model_config
42
self.renderer = renderer
43
44
self.chat_template = chat_template_config.chat_template
@@ -48,12 +49,12 @@ class PoolingIOProcessor:
49
chat_template_config.trust_request_chat_template
50
)
51
51
- def create_pooling_params(self, request):
52
- return request.to_pooling_params()
53
-
52
#######################################
53
# online APIs
54
55
+ def create_pooling_params(self, request):
56
+ return request.to_pooling_params()
57
+
58
def pre_process_online(self, ctx: PoolingServeContext):
59
request = ctx.request
60
@@ -100,12 +101,16 @@ class PoolingIOProcessor:
101
# offline APIs
102
103
def pre_process_offline(self, ctx: OfflineInputsContext) -> Sequence[EngineInput]:
103
- assert not isinstance(ctx.prompts, ScoringData)
104
+ assert not isinstance(ctx.prompts, ScoringData) and not (
105
+ isinstance(ctx.prompts, dict) and "data" in ctx.prompts
106
+ )
107
+
108
+ prompts_seq = prompt_to_seq(ctx.prompts)
109
tok_params = self.renderer.default_cmpl_tok_params.with_kwargs(
110
**(ctx.tokenization_kwargs or {})
111
)
112
return self._preprocess_completion_offline(
108
- prompts=ctx.prompts, tok_params=tok_params
113
+ prompts=prompts_seq, tok_params=tok_params
114
)
115
116
async def pre_process_offline_async(self, ctx: OfflineInputsContext):
@@ -243,3 +248,19 @@ class PoolingIOProcessor:
248
"Refused request with untrusted chat template."
249
)
250
return None
251
+
252
+ def _params_to_seq(
253
+ self,
254
+ params: PoolingParams | Sequence[PoolingParams],
255
+ num_requests: int,
256
+ ) -> Sequence[PoolingParams]:
257
+ if isinstance(params, Sequence):
258
+ if len(params) != num_requests:
259
+ raise ValueError(
260
+ f"The lengths of prompts ({num_requests}) "
261
+ f"and params ({len(params)}) must be the same."
262
+ )
263
+
264
+ return params
265
+
266
+ return [params] * num_requests
vllm/entrypoints/pooling/base/serving.py
+47
-26
@@ -1,5 +1,7 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+from abc import ABC, abstractmethod
5
from collections.abc import AsyncGenerator, Mapping
6
from http import HTTPStatus
7
from typing import ClassVar
@@ -9,7 +11,7 @@ from fastapi.responses import Response
11
from starlette.datastructures import Headers
12
13
from vllm import PoolingParams, PoolingRequestOutput, envs
12
-from vllm.config import ModelConfig
14
+from vllm.config import VllmConfig
15
from vllm.engine.protocol import EngineClient
16
from vllm.entrypoints.chat_utils import (
17
ChatTemplateConfig,
@@ -35,7 +37,7 @@ from vllm.utils.async_utils import merge_async_iterators
37
from .io_processor import PoolingIOProcessor
38
39
38
-class PoolingServing:
40
+class PoolingServingBase(ABC):
41
request_id_prefix: ClassVar[str]
42
43
def __init__(
@@ -50,10 +52,11 @@ class PoolingServing:
52
return_tokens_as_token_ids: bool = False,
53
log_error_stack: bool = False,
54
):
53
- super().__init__()
55
self.engine_client = engine_client
56
self.models = models
57
self.model_config = models.model_config
58
+ self.renderer = models.renderer
59
+ self.vllm_config = engine_client.vllm_config
60
self.max_model_len = self.model_config.max_model_len
61
self.request_logger = request_logger
62
self.return_tokens_as_token_ids = return_tokens_as_token_ids
@@ -63,31 +66,14 @@ class PoolingServing:
66
chat_template_content_format=chat_template_content_format,
67
trust_request_chat_template=trust_request_chat_template,
68
)
66
- self.io_processor = self.init_io_processor(
67
- model_config=models.model_config,
68
- renderer=models.renderer,
69
- chat_template_config=self.chat_template_config,
70
- )
71
-
72
- def init_io_processor(
73
- self,
74
- model_config: ModelConfig,
75
- renderer: BaseRenderer,
76
- chat_template_config: ChatTemplateConfig,
77
- ) -> PoolingIOProcessor:
78
- raise NotImplementedError
69
70
+ @abstractmethod
71
async def __call__(
72
self,
73
request: AnyPoolingRequest,
74
raw_request: Request | None = None,
75
) -> Response:
85
- ctx = await self._init_ctx(request, raw_request)
86
- await self.io_processor.pre_process_online_async(ctx)
87
- await self._prepare_generators(ctx)
88
- await self._collect_batch(ctx)
89
- await self.io_processor.post_process_online_async(ctx)
90
- return await self._build_response(ctx)
76
+ raise NotImplementedError
77
78
async def _init_ctx(
79
self,
@@ -124,10 +110,8 @@ class PoolingServing:
110
else await self._get_trace_headers(ctx.raw_request.headers)
111
)
112
127
- if ctx.pooling_params is None:
128
- pooling_params = self.io_processor.create_pooling_params(ctx.request)
129
- else:
130
- pooling_params = ctx.pooling_params
113
+ assert ctx.pooling_params is not None
114
+ pooling_params = ctx.pooling_params
115
116
if isinstance(pooling_params, list):
117
for params in pooling_params:
@@ -190,6 +174,7 @@ class PoolingServing:
174
175
ctx.final_res_batch = [res for res in final_res_batch if res is not None]
176
177
+ @abstractmethod
178
async def _build_response(
179
self,
180
ctx: PoolingServeContext,
@@ -355,3 +340,39 @@ class PoolingServing:
340
params=params,
341
lora_request=lora_request,
342
)
343
+
344
+
345
+class PoolingServing(PoolingServingBase, ABC):
346
+ def __init__(self, *args, **kwargs):
347
+ super().__init__(*args, **kwargs)
348
+
349
+ self.io_processor = self.init_io_processor(
350
+ vllm_config=self.vllm_config,
351
+ renderer=self.renderer,
352
+ chat_template_config=self.chat_template_config,
353
+ )
354
+
355
+ @abstractmethod
356
+ def init_io_processor(
357
+ self,
358
+ vllm_config: VllmConfig,
359
+ renderer: BaseRenderer,
360
+ chat_template_config: ChatTemplateConfig,
361
+ ) -> PoolingIOProcessor:
362
+ raise NotImplementedError
363
+
364
+ async def __call__(
365
+ self,
366
+ request: AnyPoolingRequest,
367
+ raw_request: Request | None = None,
368
+ ) -> Response:
369
+ ctx = await self._init_ctx(request, raw_request)
370
+ await self.io_processor.pre_process_online_async(ctx)
371
+
372
+ if ctx.pooling_params is None:
373
+ ctx.pooling_params = self.io_processor.create_pooling_params(request)
374
+
375
+ await self._prepare_generators(ctx)
376
+ await self._collect_batch(ctx)
377
+ await self.io_processor.post_process_online_async(ctx)
378
+ return await self._build_response(ctx)
vllm/entrypoints/pooling/classify/io_processor.py
+5
-1
@@ -5,4 +5,8 @@ from vllm.entrypoints.pooling.base.io_processor import PoolingIOProcessor
5
6
7
class ClassifyIOProcessor(PoolingIOProcessor):
8
- name = "classification"
8
+ name = "classify"
9
+
10
+
11
+class TokenClassifyIOProcessor(PoolingIOProcessor):
12
+ name = "token_classify"
vllm/entrypoints/pooling/classify/serving.py
+2
-14
@@ -6,14 +6,11 @@ from typing import TypeAlias
6
import numpy as np
7
from fastapi.responses import JSONResponse
8
9
-from vllm.config import ModelConfig
10
-from vllm.entrypoints.chat_utils import ChatTemplateConfig
9
from vllm.entrypoints.openai.engine.protocol import UsageInfo
10
from vllm.entrypoints.pooling.base.serving import PoolingServing
11
from vllm.entrypoints.pooling.typing import PoolingServeContext
12
from vllm.logger import init_logger
13
from vllm.outputs import ClassificationOutput
16
-from vllm.renderers import BaseRenderer
14
15
from .io_processor import ClassifyIOProcessor
16
from .protocol import (
@@ -31,17 +28,8 @@ ClassificationServeContext: TypeAlias = PoolingServeContext[ClassificationReques
28
class ServingClassification(PoolingServing):
29
request_id_prefix = "classify"
30
34
- def init_io_processor(
35
- self,
36
- model_config: ModelConfig,
37
- renderer: BaseRenderer,
38
- chat_template_config: ChatTemplateConfig,
39
- ) -> ClassifyIOProcessor:
40
- return ClassifyIOProcessor(
41
- model_config=model_config,
42
- renderer=renderer,
43
- chat_template_config=chat_template_config,
44
- )
31
+ def init_io_processor(self, *args, **kwargs) -> ClassifyIOProcessor:
32
+ return ClassifyIOProcessor(*args, **kwargs)
33
34
async def _build_response(
35
self,
vllm/entrypoints/pooling/embed/io_processor.py
+5
-1
@@ -37,7 +37,7 @@ logger = init_logger(__name__)
37
38
39
class EmbedIOProcessor(PoolingIOProcessor):
40
- name = "embedding"
40
+ name = "embed"
41
42
def __init__(self, *args, **kwargs):
43
super().__init__(*args, **kwargs)
@@ -549,3 +549,7 @@ class EmbedIOProcessor(PoolingIOProcessor):
549
request = ctx.request
550
if request.truncate == "NONE" and request.max_tokens is not None:
551
self._check_cohere_max_tokens(ctx.final_res_batch, request.max_tokens)
552
+
553
+
554
+class TokenEmbedIOProcessor(PoolingIOProcessor):
555
+ name = "token_embed"
vllm/entrypoints/pooling/embed/serving.py
+10
-18
@@ -8,8 +8,6 @@ from typing import Literal, TypeAlias, cast
8
from fastapi.responses import JSONResponse, Response, StreamingResponse
9
from typing_extensions import assert_never
10
11
-from vllm.config import ModelConfig
12
-from vllm.entrypoints.chat_utils import ChatTemplateConfig
11
from vllm.entrypoints.openai.engine.protocol import UsageInfo
12
from vllm.entrypoints.pooling.base.serving import PoolingServing
13
from vllm.entrypoints.pooling.embed.io_processor import EmbedIOProcessor
@@ -33,12 +31,10 @@ from vllm.entrypoints.pooling.utils import (
31
)
32
from vllm.logger import init_logger
33
from vllm.outputs import PoolingRequestOutput
36
-from vllm.renderers import BaseRenderer
34
from vllm.utils.serial_utils import EmbedDType, Endianness
35
36
logger = init_logger(__name__)
37
41
-JSONResponseCLS = get_json_response_cls()
38
39
EmbeddingServeContext: TypeAlias = PoolingServeContext[EmbeddingRequest]
40
@@ -49,17 +45,13 @@ class ServingEmbedding(PoolingServing):
45
request_id_prefix = "embd"
46
io_processor: EmbedIOProcessor
47
52
- def init_io_processor(
53
- self,
54
- model_config: ModelConfig,
55
- renderer: BaseRenderer,
56
- chat_template_config: ChatTemplateConfig,
57
- ) -> EmbedIOProcessor:
58
- return EmbedIOProcessor(
59
- model_config=model_config,
60
- renderer=renderer,
61
- chat_template_config=chat_template_config,
62
- )
48
+ def __init__(self, *args, **kwargs):
49
+ super().__init__(*args, **kwargs)
50
+
51
+ self.json_response_cls = get_json_response_cls()
52
+
53
+ def init_io_processor(self, *args, **kwargs) -> EmbedIOProcessor:
54
+ return EmbedIOProcessor(*args, **kwargs)
55
56
async def _build_response(
57
self,
@@ -149,7 +141,7 @@ class ServingEmbedding(PoolingServing):
141
data=items,
142
usage=usage,
143
)
152
- return JSONResponseCLS(content=response.model_dump())
144
+ return self.json_response_cls(content=response.model_dump())
145
146
def _openai_bytes_response(
147
self,
@@ -190,8 +182,8 @@ class ServingEmbedding(PoolingServing):
182
media_type=response.media_type,
183
)
184
193
- @staticmethod
185
def _build_cohere_response_from_ctx(
186
+ self,
187
ctx: PoolingServeContext,
188
) -> JSONResponse:
189
request = ctx.request
@@ -218,4 +210,4 @@ class ServingEmbedding(PoolingServing):
210
),
211
),
212
)
221
- return JSONResponse(content=response.model_dump(exclude_none=True))
213
+ return self.json_response_cls(content=response.model_dump(exclude_none=True))
vllm/entrypoints/pooling/io_processor_factories.py
+41
-14
@@ -1,42 +1,69 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
-
5
-from vllm.config import ModelConfig
4
+from vllm.config import VllmConfig
5
from vllm.entrypoints.chat_utils import ChatTemplateConfig
7
-from vllm.entrypoints.pooling.base.io_processor import PoolingIOProcessor
8
-from vllm.entrypoints.pooling.scoring.io_processor import ScoringIOProcessors
9
-from vllm.entrypoints.pooling.utils import enable_scoring_api
6
+from vllm.plugins.io_processors import has_io_processor
7
from vllm.renderers import BaseRenderer
8
from vllm.tasks import SupportedTask
9
10
+from .base.io_processor import PoolingIOProcessor
11
+from .utils import enable_scoring_api
12
+
13
14
def init_pooling_io_processors(
15
supported_tasks: tuple[SupportedTask, ...],
16
- model_config: ModelConfig,
16
+ vllm_config: VllmConfig,
17
renderer: BaseRenderer,
18
chat_template_config: ChatTemplateConfig,
19
) -> dict[str, PoolingIOProcessor]:
20
- processors: list[tuple[str, type[PoolingIOProcessor]]] = []
20
+ model_config = vllm_config.model_config
21
+ processors: dict[str, type[PoolingIOProcessor]] = {}
22
+
23
if "classify" in supported_tasks:
22
- from vllm.entrypoints.pooling.classify.io_processor import ClassifyIOProcessor
24
+ from .classify.io_processor import ClassifyIOProcessor
25
+
26
+ processors["classify"] = ClassifyIOProcessor
27
+
28
+ if "token_classify" in supported_tasks:
29
+ from .classify.io_processor import TokenClassifyIOProcessor
30
+
31
+ processors["token_classify"] = TokenClassifyIOProcessor
32
24
- processors.append(("classify", ClassifyIOProcessor))
33
if "embed" in supported_tasks:
26
- from vllm.entrypoints.pooling.embed.io_processor import EmbedIOProcessor
34
+ from .embed.io_processor import EmbedIOProcessor
35
28
- processors.append(("embed", EmbedIOProcessor))
36
+ processors["embed"] = EmbedIOProcessor
37
+
38
+ if "token_embed" in supported_tasks:
39
+ from .embed.io_processor import TokenEmbedIOProcessor
40
+
41
+ processors["token_embed"] = TokenEmbedIOProcessor
42
+
43
+ if has_io_processor(
44
+ vllm_config,
45
+ model_config.io_processor_plugin,
46
+ ):
47
+ from .pooling.io_processor import PluginWithIOProcessorPlugins
48
+
49
+ processors["plugin"] = PluginWithIOProcessorPlugins
50
+ elif "plugin" in supported_tasks:
51
+ from .pooling.io_processor import PluginWithoutIOProcessorPlugins
52
+
53
+ processors["plugin"] = PluginWithoutIOProcessorPlugins
54
55
if enable_scoring_api(supported_tasks, model_config):
56
score_type = model_config.score_type
57
+ from .scoring.io_processor import ScoringIOProcessors
58
+
59
if score_type is not None and score_type in ScoringIOProcessors:
33
- processors.append((score_type, ScoringIOProcessors[score_type]))
60
+ processors[score_type] = ScoringIOProcessors[score_type]
61
62
return {
63
task: processor_cls(
37
- model_config=model_config,
64
+ vllm_config=vllm_config,
65
renderer=renderer,
66
chat_template_config=chat_template_config,
67
)
41
- for task, processor_cls in processors
68
+ for task, processor_cls in processors.items()
69
}
vllm/entrypoints/pooling/pooling/api_router.py
+4
-26
@@ -3,24 +3,17 @@
3
from http import HTTPStatus
4
5
from fastapi import APIRouter, Depends, Request
6
-from fastapi.responses import JSONResponse, StreamingResponse
7
-from typing_extensions import assert_never
6
7
from vllm.entrypoints.openai.engine.protocol import ErrorResponse
8
from vllm.entrypoints.openai.utils import validate_json_request
11
-from vllm.entrypoints.pooling.pooling.protocol import (
12
- IOProcessorResponse,
13
- PoolingBytesResponse,
14
- PoolingRequest,
15
- PoolingResponse,
16
-)
17
-from vllm.entrypoints.pooling.pooling.serving import OpenAIServingPooling
9
+from vllm.entrypoints.pooling.pooling.protocol import PoolingRequest
10
+from vllm.entrypoints.pooling.pooling.serving import ServingPooling
11
from vllm.entrypoints.utils import load_aware_call, with_cancellation
12
13
router = APIRouter()
14
15
23
-def pooling(request: Request) -> OpenAIServingPooling | None:
16
+def pooling(request: Request) -> ServingPooling | None:
17
return request.app.state.serving_pooling
18
19
@@ -39,19 +32,4 @@ async def create_pooling(request: PoolingRequest, raw_request: Request):
32
if handler is None:
33
raise NotImplementedError("The model does not support Pooling API")
34
42
- generator = await handler.create_pooling(request, raw_request)
43
-
44
- if isinstance(generator, ErrorResponse):
45
- return JSONResponse(
46
- content=generator.model_dump(), status_code=generator.error.code
47
- )
48
- elif isinstance(generator, (PoolingResponse, IOProcessorResponse)):
49
- return JSONResponse(content=generator.model_dump())
50
- elif isinstance(generator, PoolingBytesResponse):
51
- return StreamingResponse(
52
- content=generator.content,
53
- headers=generator.headers,
54
- media_type=generator.media_type,
55
- )
56
-
57
- assert_never(generator)
35
+ return await handler(request, raw_request)
vllm/entrypoints/pooling/pooling/io_processor.py
new
+156
@@ -0,0 +1,156 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+from collections.abc import Sequence
4
+from typing import Any
5
+
6
+from vllm import PoolingParams, PoolingRequestOutput
7
+from vllm.entrypoints.pooling.base.io_processor import PoolingIOProcessor
8
+from vllm.inputs import EngineInput
9
+from vllm.logger import init_logger
10
+from vllm.plugins.io_processors import get_io_processor
11
+from vllm.renderers.inputs.preprocess import parse_model_prompt, prompt_to_seq
12
+
13
+from ..typing import OfflineInputsContext, OfflineOutputsContext, PoolingServeContext
14
+from .protocol import IOProcessorRequest, IOProcessorResponse
15
+
16
+logger = init_logger(__name__)
17
+
18
+
19
+class PluginWithoutIOProcessorPlugins(PoolingIOProcessor):
20
+ name = "plugin"
21
+
22
+
23
+class PluginWithIOProcessorPlugins(PoolingIOProcessor):
24
+ """IO Processor plugins are a feature that allows pre- and post-processing
25
+ of the model input and output for pooling models."""
26
+
27
+ name = "plugin"
28
+
29
+ def __init__(self, *args, **kwargs):
30
+ super().__init__(*args, **kwargs)
31
+
32
+ io_processor = get_io_processor(
33
+ self.vllm_config,
34
+ self.renderer,
35
+ self.model_config.io_processor_plugin,
36
+ )
37
+
38
+ assert io_processor is not None
39
+ self.io_processor = io_processor
40
+
41
+ #######################################
42
+ # online APIs
43
+
44
+ def pre_process_online(self, ctx: PoolingServeContext):
45
+ assert isinstance(ctx.request, IOProcessorRequest)
46
+
47
+ validated_prompt = self.io_processor.parse_data(ctx.request.data)
48
+
49
+ raw_prompts = self.io_processor.pre_process(
50
+ prompt=validated_prompt, request_id=ctx.request_id
51
+ )
52
+
53
+ parsed_prompts = [
54
+ (
55
+ prompt
56
+ if isinstance(prompt, bytes)
57
+ else parse_model_prompt(self.model_config, prompt)
58
+ )
59
+ for prompt in prompt_to_seq(raw_prompts)
60
+ ]
61
+
62
+ tok_params = ctx.request.build_tok_params(self.model_config)
63
+
64
+ ctx.engine_inputs = self.renderer.render_cmpl(
65
+ parsed_prompts,
66
+ tok_params,
67
+ prompt_extras={
68
+ k: v
69
+ for k in ("mm_processor_kwargs", "cache_salt")
70
+ if (v := getattr(ctx.request, k, None)) is not None
71
+ },
72
+ )
73
+
74
+ pooling_params = self.io_processor.merge_pooling_params()
75
+ if pooling_params.task is None:
76
+ pooling_params.task = "plugin"
77
+ ctx.pooling_params = pooling_params
78
+
79
+ def post_process_online(
80
+ self,
81
+ ctx: PoolingServeContext,
82
+ ):
83
+ output = self.io_processor.post_process(
84
+ ctx.final_res_batch,
85
+ request_id=ctx.request_id,
86
+ )
87
+
88
+ if callable(
89
+ output_to_response := getattr(self.io_processor, "output_to_response", None)
90
+ ):
91
+ logger.warning_once(
92
+ "`IOProcessor.output_to_response` is deprecated. To ensure "
93
+ "consistency between offline and online APIs, "
94
+ "`IOProcessorResponse` will become a transparent wrapper "
95
+ "around output data from v0.19 onwards.",
96
+ )
97
+
98
+ if hasattr(output, "request_id") and output.request_id is None:
99
+ output.request_id = ctx.request_id # type: ignore
100
+
101
+ ctx.response = output_to_response(output) # type: ignore
102
+ else:
103
+ ctx.response = IOProcessorResponse(request_id=ctx.request_id, data=output)
104
+
105
+ #######################################
106
+ # offline APIs
107
+
108
+ def pre_process_offline(self, ctx: OfflineInputsContext) -> Sequence[EngineInput]:
109
+ assert isinstance(ctx.prompts, dict) and "data" in ctx.prompts
110
+ assert ctx.pooling_params is not None
111
+
112
+ # Validate the request data is valid for the loaded plugin
113
+ prompt_data = ctx.prompts.get("data")
114
+ if prompt_data is None:
115
+ raise ValueError(
116
+ "The 'data' field of the prompt is expected to contain "
117
+ "the prompt data and it cannot be None. "
118
+ "Refer to the documentation of the IOProcessor "
119
+ "in use for more details."
120
+ )
121
+ validated_prompt = self.io_processor.parse_data(prompt_data)
122
+
123
+ # obtain the actual model prompts from the pre-processor
124
+ prompts = self.io_processor.pre_process(prompt=validated_prompt)
125
+ prompts_seq = prompt_to_seq(prompts)
126
+
127
+ params_seq: list[PoolingParams] = [
128
+ self.io_processor.merge_pooling_params(param)
129
+ for param in self._params_to_seq(
130
+ ctx.pooling_params,
131
+ len(prompts_seq),
132
+ )
133
+ ]
134
+ for p in params_seq:
135
+ if p.task is None:
136
+ p.task = "plugin"
137
+
138
+ ctx.pooling_params = params_seq
139
+ ctx.prompts = prompts_seq
140
+ return super().pre_process_offline(ctx)
141
+
142
+ def post_process_offline(
143
+ self,
144
+ ctx: OfflineOutputsContext,
145
+ ) -> list[PoolingRequestOutput]:
146
+ processed_outputs = self.io_processor.post_process(ctx.outputs)
147
+
148
+ return [
149
+ PoolingRequestOutput[Any](
150
+ request_id="",
151
+ outputs=processed_outputs,
152
+ num_cached_tokens=getattr(processed_outputs, "num_cached_tokens", 0),
153
+ prompt_token_ids=[],
154
+ finished=True,
155
+ )
156
+ ]
vllm/entrypoints/pooling/pooling/serving.py
+101
-223
@@ -1,252 +1,157 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
-import asyncio
4
+
5
import json
6
-import time
7
-from collections.abc import AsyncGenerator, Callable, Sequence
6
+from collections.abc import Callable
7
from functools import partial
9
-from typing import Final, Literal, cast
8
+from typing import Literal, cast
9
10
from fastapi import Request
11
+from fastapi.responses import JSONResponse, Response, StreamingResponse
12
from typing_extensions import assert_never
13
14
-from vllm.engine.protocol import EngineClient
15
-from vllm.entrypoints.chat_utils import ChatTemplateContentFormatOption
16
-from vllm.entrypoints.logger import RequestLogger
17
-from vllm.entrypoints.openai.engine.protocol import ErrorResponse, UsageInfo
18
-from vllm.entrypoints.openai.engine.serving import OpenAIServing
19
-from vllm.entrypoints.openai.models.serving import OpenAIServingModels
14
+from vllm.entrypoints.openai.engine.protocol import UsageInfo
15
+from vllm.entrypoints.pooling.base.serving import PoolingServingBase
16
+from vllm.entrypoints.pooling.io_processor_factories import init_pooling_io_processors
17
from vllm.entrypoints.pooling.pooling.protocol import (
18
IOProcessorRequest,
22
- IOProcessorResponse,
19
PoolingBytesResponse,
24
- PoolingChatRequest,
25
- PoolingCompletionRequest,
20
PoolingRequest,
21
PoolingResponse,
22
PoolingResponseData,
23
)
24
+from vllm.entrypoints.pooling.typing import AnyPoolingRequest, PoolingServeContext
25
from vllm.entrypoints.pooling.utils import (
26
encode_pooling_bytes,
27
encode_pooling_output_base64,
28
encode_pooling_output_float,
29
+ get_json_response_cls,
30
)
35
-from vllm.entrypoints.serve.render.serving import OpenAIServingRender
36
-from vllm.inputs import EngineInput
31
from vllm.logger import init_logger
32
from vllm.outputs import PoolingRequestOutput
39
-from vllm.renderers.inputs.preprocess import prompt_to_seq
33
from vllm.tasks import SupportedTask
41
-from vllm.utils.async_utils import merge_async_iterators
42
-from vllm.utils.serial_utils import EmbedDType, EncodingFormat, Endianness
34
+from vllm.utils.serial_utils import EmbedDType, Endianness
35
36
logger = init_logger(__name__)
37
38
47
-class OpenAIServingPooling(OpenAIServing):
39
+class ServingPooling(PoolingServingBase):
40
+ request_id_prefix = "pooling"
41
+
42
def __init__(
43
self,
50
- engine_client: EngineClient,
51
- models: OpenAIServingModels,
52
- openai_serving_render: OpenAIServingRender,
44
+ *args,
45
supported_tasks: tuple[SupportedTask, ...],
54
- *,
55
- request_logger: RequestLogger | None,
56
- chat_template: str | None,
57
- chat_template_content_format: ChatTemplateContentFormatOption,
58
- trust_request_chat_template: bool = False,
59
- ) -> None:
60
- super().__init__(
61
- engine_client=engine_client,
62
- models=models,
63
- request_logger=request_logger,
64
- )
46
+ **kwargs,
47
+ ):
48
+ super().__init__(*args, **kwargs)
49
+
50
self.supported_tasks = supported_tasks
51
self.pooling_task = self.model_config.get_pooling_task(supported_tasks)
67
- self.openai_serving_render = openai_serving_render
68
- self.chat_template = chat_template
69
- self.chat_template_content_format: Final = chat_template_content_format
70
- self.trust_request_chat_template = trust_request_chat_template
52
+ self.io_processors = init_pooling_io_processors(
53
+ supported_tasks=supported_tasks,
54
+ vllm_config=self.vllm_config,
55
+ renderer=self.renderer,
56
+ chat_template_config=self.chat_template_config,
57
+ )
58
+ self.json_response_cls = get_json_response_cls()
59
72
- async def create_pooling(
60
+ async def __call__(
61
self,
74
- request: PoolingRequest,
62
+ request: AnyPoolingRequest,
63
raw_request: Request | None = None,
76
- ) -> PoolingResponse | IOProcessorResponse | PoolingBytesResponse | ErrorResponse:
77
- """
78
- See https://platform.openai.com/docs/api-reference/embeddings/create
79
- for the API specification. This API mimics the OpenAI Embedding API.
80
- """
81
- error_check_ret = await self._check_model(request)
82
- if error_check_ret is not None:
83
- return error_check_ret
64
+ ) -> Response:
65
+ assert isinstance(request, PoolingRequest)
66
+ pooling_task = self._verify_pooling_task(request)
67
85
- model_name = self.models.model_name()
68
+ io_processor = self.io_processors[pooling_task]
69
+ ctx = await self._init_ctx(request, raw_request)
70
87
- request_id = f"pool-{self._base_request_id(raw_request)}"
88
- created_time = int(time.time())
71
+ await io_processor.pre_process_online_async(ctx)
72
90
- lora_request = self._maybe_get_adapters(request)
73
+ if ctx.pooling_params is None:
74
+ ctx.pooling_params = io_processor.create_pooling_params(request)
75
+
76
+ await self._prepare_generators(ctx)
77
+ await self._collect_batch(ctx)
78
+
79
+ await io_processor.post_process_online_async(ctx)
80
+ return await self._build_response(ctx)
81
+
82
+ def _verify_pooling_task(self, request: PoolingRequest) -> str:
83
+ if getattr(request, "dimensions", None) is not None:
84
+ raise ValueError("dimensions is currently not supported")
85
86
if request.task is None:
87
request.task = self.pooling_task
88
95
- if getattr(request, "dimensions", None) is not None:
96
- return self.create_error_response("dimensions is currently not supported")
89
+ if isinstance(request, IOProcessorRequest):
90
+ request.task = "plugin"
91
+
92
+ assert request.task is not None
93
+ pooling_task = request.task
94
95
# plugin task uses io_processor.parse_request to verify inputs
99
- if request.task != "plugin" and request.task != self.pooling_task:
100
- if request.task not in self.supported_tasks:
96
+ if pooling_task != "plugin" and pooling_task != self.pooling_task:
97
+ if pooling_task not in self.io_processors:
98
raise ValueError(
102
- f"Unsupported task: {request.task!r} "
99
+ f"Unsupported task: {pooling_task!r} "
100
f"Supported tasks: {self.supported_tasks}"
101
)
102
else:
103
logger.warning_once(
104
"Pooling multitask support is deprecated and will be removed "
105
"in v0.20. When the default pooling task is not what you want, you "
109
- 'need to manually specify it via --pooler-config.task "%s". ',
110
- request.task,
111
- )
112
-
113
- engine_inputs: Sequence[EngineInput]
114
- if use_io_processor := isinstance(request, IOProcessorRequest):
115
- if self.io_processor is None:
116
- raise ValueError(
117
- "No IOProcessor plugin installed. Please refer "
118
- "to the documentation and to the "
119
- "'prithvi_geospatial_mae_io_processor' "
120
- "offline inference example for more details."
106
+ "need to manually specify it via --pooler-config.task %s. ",
107
+ pooling_task,
108
)
109
123
- validated_prompt = self.io_processor.parse_data(request.data)
124
-
125
- raw_prompts = await self.io_processor.pre_process_async(
126
- prompt=validated_prompt, request_id=request_id
127
- )
128
- engine_inputs = await self.openai_serving_render.preprocess_cmpl(
129
- request,
130
- prompt_to_seq(raw_prompts),
131
- )
132
- elif isinstance(request, PoolingChatRequest):
133
- error_check_ret = self.openai_serving_render.validate_chat_template(
134
- request_chat_template=request.chat_template,
135
- chat_template_kwargs=request.chat_template_kwargs,
136
- trust_request_chat_template=self.trust_request_chat_template,
137
- )
138
- if error_check_ret is not None:
139
- return error_check_ret
140
-
141
- _, engine_inputs = await self.openai_serving_render.preprocess_chat(
142
- request,
143
- request.messages,
144
- default_template=self.chat_template,
145
- default_template_content_format=self.chat_template_content_format,
146
- default_template_kwargs=None,
147
- )
148
- elif isinstance(request, PoolingCompletionRequest):
149
- engine_inputs = await self.openai_serving_render.preprocess_completion(
150
- request,
151
- prompt_input=request.input,
152
- prompt_embeds=None,
153
- )
154
- else:
155
- raise ValueError(f"Unsupported request of type {type(request)}")
156
-
157
- # Schedule the request and get the result generator.
158
- generators: list[AsyncGenerator[PoolingRequestOutput, None]] = []
159
- if use_io_processor:
160
- assert self.io_processor is not None
161
-
162
- pooling_params = self.io_processor.merge_pooling_params()
163
- if pooling_params.task is None:
164
- pooling_params.task = "plugin"
165
- else:
166
- pooling_params = request.to_pooling_params() # type: ignore
167
-
168
- for i, engine_input in enumerate(engine_inputs):
169
- request_id_item = f"{request_id}-{i}"
170
-
171
- self._log_inputs(
172
- request_id_item,
173
- engine_input,
174
- params=pooling_params,
175
- lora_request=lora_request,
110
+ if pooling_task == "plugin" and "plugin" not in self.io_processors:
111
+ raise ValueError(
112
+ "No IOProcessor plugin installed. Please refer "
113
+ "to the documentation and to the "
114
+ "'prithvi_geospatial_mae_io_processor' "
115
+ "offline inference example for more details."
116
)
117
178
- trace_headers = (
179
- None
180
- if raw_request is None
181
- else await self._get_trace_headers(raw_request.headers)
182
- )
183
-
184
- generator = self.engine_client.encode(
185
- engine_input,
186
- pooling_params,
187
- request_id_item,
188
- lora_request=lora_request,
189
- trace_headers=trace_headers,
190
- priority=request.priority,
191
- )
118
+ return pooling_task
119
193
- generators.append(generator)
120
+ async def _build_response(
121
+ self,
122
+ ctx: PoolingServeContext,
123
+ ) -> Response:
124
+ if ctx.response is not None:
125
+ # for IOProcessorResponse
126
+ return self.json_response_cls(content=ctx.response.model_dump())
127
195
- result_generator = merge_async_iterators(*generators)
128
+ encoding_format = ctx.request.encoding_format
129
+ embed_dtype = ctx.request.embed_dtype
130
+ endianness = ctx.request.endianness
131
197
- if use_io_processor:
198
- assert self.io_processor is not None
199
- output = await self.io_processor.post_process_async(
200
- result_generator,
201
- request_id=request_id,
132
+ if encoding_format == "float" or encoding_format == "base64":
133
+ return self.request_output_to_pooling_json_response(
134
+ ctx.final_res_batch,
135
+ ctx.request_id,
136
+ ctx.created_time,
137
+ ctx.model_name,
138
+ encoding_format,
139
+ embed_dtype,
140
+ endianness,
141
)
142
204
- if callable(
205
- output_to_response := getattr(
206
- self.io_processor, "output_to_response", None
207
- )
208
- ):
209
- logger.warning_once(
210
- "`IOProcessor.output_to_response` is deprecated. To ensure "
211
- "consistency between offline and online APIs, "
212
- "`IOProcessorResponse` will become a transparent wrapper "
213
- "around output data from v0.19 onwards.",
214
- )
215
-
216
- if hasattr(output, "request_id") and output.request_id is None:
217
- output.request_id = request_id # type: ignore
218
-
219
- return output_to_response(output) # type: ignore
220
-
221
- return IOProcessorResponse(request_id=request_id, data=output)
222
-
223
- assert isinstance(request, (PoolingCompletionRequest, PoolingChatRequest))
224
- num_prompts = len(engine_inputs)
225
-
226
- # Non-streaming response
227
- final_res_batch: list[PoolingRequestOutput | None]
228
- final_res_batch = [None] * num_prompts
229
- try:
230
- async for i, res in result_generator:
231
- final_res_batch[i] = res
232
-
233
- assert all(final_res is not None for final_res in final_res_batch)
234
-
235
- final_res_batch_checked = cast(list[PoolingRequestOutput], final_res_batch)
236
-
237
- response = self.request_output_to_pooling_response(
238
- final_res_batch_checked,
239
- request_id,
240
- created_time,
241
- model_name,
242
- request.encoding_format,
243
- request.embed_dtype,
244
- request.endianness,
143
+ if encoding_format == "bytes" or encoding_format == "bytes_only":
144
+ return self.request_output_to_pooling_bytes_response(
145
+ ctx.final_res_batch,
146
+ ctx.request_id,
147
+ ctx.created_time,
148
+ ctx.model_name,
149
+ encoding_format,
150
+ embed_dtype,
151
+ endianness,
152
)
246
- except asyncio.CancelledError:
247
- return self.create_error_response("Client disconnected")
153
249
- return response
154
+ assert_never(encoding_format)
155
156
def request_output_to_pooling_json_response(
157
self,
@@ -257,7 +162,7 @@ class OpenAIServingPooling(OpenAIServing):
162
encoding_format: Literal["float", "base64"],
163
embed_dtype: EmbedDType,
164
endianness: Endianness,
260
- ) -> PoolingResponse:
165
+ ) -> JSONResponse:
166
encode_fn = cast(
167
Callable[[PoolingRequestOutput], list[float] | str],
168
(
@@ -289,13 +194,14 @@ class OpenAIServingPooling(OpenAIServing):
194
total_tokens=num_prompt_tokens,
195
)
196
292
- return PoolingResponse(
197
+ response = PoolingResponse(
198
id=request_id,
199
created=created_time,
200
model=model_name,
201
data=items,
202
usage=usage,
203
)
204
+ return self.json_response_cls(content=response.model_dump())
205
206
def request_output_to_pooling_bytes_response(
207
self,
@@ -306,7 +212,7 @@ class OpenAIServingPooling(OpenAIServing):
212
encoding_format: Literal["bytes", "bytes_only"],
213
embed_dtype: EmbedDType,
214
endianness: Endianness,
309
- ) -> PoolingBytesResponse:
215
+ ) -> StreamingResponse:
216
content, items, usage = encode_pooling_bytes(
217
pooling_outputs=final_res_batch,
218
embed_dtype=embed_dtype,
@@ -329,38 +235,10 @@ class OpenAIServingPooling(OpenAIServing):
235
}
236
)
237
332
- return PoolingBytesResponse(content=content, headers=headers)
333
-
334
- def request_output_to_pooling_response(
335
- self,
336
- final_res_batch: list[PoolingRequestOutput],
337
- request_id: str,
338
- created_time: int,
339
- model_name: str,
340
- encoding_format: EncodingFormat,
341
- embed_dtype: EmbedDType,
342
- endianness: Endianness,
343
- ) -> PoolingResponse | PoolingBytesResponse:
344
- if encoding_format == "float" or encoding_format == "base64":
345
- return self.request_output_to_pooling_json_response(
346
- final_res_batch,
347
- request_id,
348
- created_time,
349
- model_name,
350
- encoding_format,
351
- embed_dtype,
352
- endianness,
353
- )
354
-
355
- if encoding_format == "bytes" or encoding_format == "bytes_only":
356
- return self.request_output_to_pooling_bytes_response(
357
- final_res_batch,
358
- request_id,
359
- created_time,
360
- model_name,
361
- encoding_format,
362
- embed_dtype,
363
- endianness,
364
- )
238
+ response = PoolingBytesResponse(content=content, headers=headers)
239
366
- assert_never(encoding_format)
240
+ return StreamingResponse(
241
+ content=response.content,
242
+ headers=response.headers,
243
+ media_type=response.media_type,
244
+ )
vllm/entrypoints/pooling/scoring/io_processor.py
+1
-1
@@ -278,7 +278,7 @@ class CrossEncoderIOProcessor(ScoringIOProcessor):
278
279
def pre_process_offline(self, ctx: OfflineInputsContext) -> Sequence[EngineInput]:
280
assert isinstance(ctx.prompts, ScoringData)
281
- assert not isinstance(ctx.pooling_params, list)
281
+ assert not isinstance(ctx.pooling_params, Sequence)
282
283
tok_params = self.renderer.default_cmpl_tok_params.with_kwargs(
284
**(ctx.tokenization_kwargs or {})
vllm/entrypoints/pooling/scoring/serving.py
+5
-12
@@ -4,15 +4,13 @@
4
from fastapi.responses import JSONResponse, Response
5
6
from vllm import PoolingParams
7
-from vllm.config import ModelConfig
7
+from vllm.config import VllmConfig
8
from vllm.engine.protocol import EngineClient
9
-from vllm.entrypoints.chat_utils import ChatTemplateConfig
9
from vllm.entrypoints.openai.engine.protocol import UsageInfo
10
from vllm.entrypoints.pooling.base.io_processor import PoolingIOProcessor
11
from vllm.entrypoints.pooling.base.serving import PoolingServing
12
from vllm.logger import init_logger
13
from vllm.outputs import PoolingRequestOutput, ScoringRequestOutput
15
-from vllm.renderers import BaseRenderer
14
from vllm.v1.pool.late_interaction import (
15
build_late_interaction_doc_params,
16
build_late_interaction_query_params,
@@ -52,22 +50,17 @@ class ServingScores(PoolingServing):
50
super().__init__(engine_client, *args, **kwargs)
51
52
def init_io_processor(
55
- self,
56
- model_config: ModelConfig,
57
- renderer: BaseRenderer,
58
- chat_template_config: ChatTemplateConfig,
53
+ self, vllm_config: VllmConfig, *args, **kwargs
54
) -> PoolingIOProcessor:
55
+ model_config = vllm_config.model_config
56
+
57
score_type: str = model_config.score_type
58
if self.enable_flash_late_interaction:
59
score_type = "flash-late-interaction"
60
61
assert score_type in ScoringIOProcessors
62
processor_cls = ScoringIOProcessors[score_type]
66
- return processor_cls(
67
- model_config=model_config,
68
- renderer=renderer,
69
- chat_template_config=chat_template_config,
70
- )
63
+ return processor_cls(vllm_config, *args, **kwargs)
64
65
async def __call__(self, *args, **kwargs) -> Response:
66
if not self.enable_flash_late_interaction:
vllm/entrypoints/pooling/typing.py
+6
-3
@@ -30,7 +30,7 @@ from vllm.entrypoints.pooling.pooling.protocol import (
30
)
31
from vllm.entrypoints.pooling.scoring.protocol import ScoringRequest, ScoringResponse
32
from vllm.entrypoints.pooling.scoring.typing import ScoringData
33
-from vllm.inputs import EngineInput
33
+from vllm.inputs import DataPrompt, EngineInput
34
from vllm.lora.request import LoRARequest
35
36
PoolingCompletionLikeRequest: TypeAlias = (
@@ -86,11 +86,14 @@ class PoolingServeContext(Generic[PoolingRequestT]):
86
## for bi-encoder & late-interaction
87
n_queries: int | None = None
88
89
+ ## for IOProcessorResponse
90
+ response: Any | None = None
91
+
92
93
@dataclass
94
class OfflineInputsContext:
92
- prompts: PromptType | Sequence[PromptType] | ScoringData
93
- pooling_params: PoolingParams | list[PoolingParams] | None = None
95
+ prompts: PromptType | Sequence[PromptType] | DataPrompt | ScoringData
96
+ pooling_params: PoolingParams | Sequence[PoolingParams]
97
tokenization_kwargs: dict[str, Any] | None = None
98
chat_template: str | None = None
99
vllm/entrypoints/sagemaker/api_router.py
+2
-2
@@ -14,7 +14,7 @@ from vllm.config import ModelConfig
14
from vllm.entrypoints.openai.engine.protocol import ErrorResponse
15
from vllm.entrypoints.openai.engine.serving import OpenAIServing
16
from vllm.entrypoints.openai.utils import validate_json_request
17
-from vllm.entrypoints.pooling.base.serving import PoolingServing
17
+from vllm.entrypoints.pooling.base.serving import PoolingServingBase
18
from vllm.entrypoints.pooling.utils import enable_scoring_api
19
from vllm.entrypoints.serve.instrumentator.basic import base
20
from vllm.entrypoints.serve.instrumentator.health import health
@@ -23,7 +23,7 @@ from vllm.tasks import POOLING_TASKS, SupportedTask
23
# TODO: RequestType = TypeForm[BaseModel] when recognized by type checkers
24
# (requires typing_extensions >= 4.13)
25
RequestType = Any
26
-GetHandlerFn = Callable[[Request], OpenAIServing | PoolingServing | None]
26
+GetHandlerFn = Callable[[Request], OpenAIServing | PoolingServingBase | None]
27
EndpointFn = Callable[[RequestType, Request], Awaitable[Any]]
28
29
vllm/entrypoints/serve/render/serving.py
-2
@@ -65,7 +65,6 @@ class OpenAIServingRender:
65
self,
66
model_config: ModelConfig,
67
renderer: BaseRenderer,
68
- io_processor: Any,
68
model_registry: OpenAIModelRegistry,
69
*,
70
request_logger: RequestLogger | None,
@@ -81,7 +80,6 @@ class OpenAIServingRender:
80
) -> None:
81
self.model_config = model_config
82
self.renderer = renderer
84
- self.io_processor = io_processor
83
self.model_registry = model_registry
84
self.request_logger = request_logger
85
self.chat_template = chat_template
vllm/plugins/io_processors/__init__.py
+17
@@ -12,6 +12,23 @@ from vllm.utils.import_utils import resolve_obj_by_qualname
12
logger = logging.getLogger(__name__)
13
14
15
+def has_io_processor(
16
+ vllm_config: VllmConfig,
17
+ plugin_from_init: str | None = None,
18
+):
19
+ if plugin_from_init:
20
+ model_plugin = plugin_from_init
21
+ else:
22
+ # A plugin can be specified via the model config
23
+ # Retrieve the model specific plugin if available
24
+ # This is using a custom field in the hf_config for the model
25
+ hf_config = vllm_config.model_config.hf_config.to_dict()
26
+ config_plugin = hf_config.get("io_processor_plugin")
27
+ model_plugin = config_plugin
28
+
29
+ return model_plugin is not None
30
+
31
+
32
def get_io_processor(
33
vllm_config: VllmConfig,
34
renderer: BaseRenderer,
vllm/v1/engine/async_llm.py
-6
@@ -26,7 +26,6 @@ from vllm.logger import init_logger
26
from vllm.lora.request import LoRARequest
27
from vllm.multimodal import MULTIMODAL_REGISTRY, MultiModalRegistry
28
from vllm.outputs import STREAM_FINISHED, PoolingRequestOutput, RequestOutput
29
-from vllm.plugins.io_processors import get_io_processor
29
from vllm.pooling_params import PoolingParams
30
from vllm.renderers import renderer_from_config
31
from vllm.renderers.inputs.preprocess import extract_prompt_components
@@ -133,11 +132,6 @@ class AsyncLLM(EngineClient):
132
)
133
134
self.renderer = renderer = renderer_from_config(self.vllm_config)
136
- self.io_processor = get_io_processor(
137
- self.vllm_config,
138
- self.renderer,
139
- self.model_config.io_processor_plugin,
140
- )
135
136
# Convert EngineInput --> EngineCoreRequest.
137
self.input_processor = InputProcessor(self.vllm_config, renderer)
vllm/v1/engine/llm_engine.py
-6
@@ -19,7 +19,6 @@ from vllm.logger import init_logger
19
from vllm.lora.request import LoRARequest
20
from vllm.multimodal import MULTIMODAL_REGISTRY, MultiModalRegistry
21
from vllm.outputs import PoolingRequestOutput, RequestOutput
22
-from vllm.plugins.io_processors import get_io_processor
22
from vllm.pooling_params import PoolingParams
23
from vllm.renderers import renderer_from_config
24
from vllm.renderers.inputs.preprocess import extract_prompt_components
@@ -90,11 +89,6 @@ class LLMEngine:
89
self.should_execute_dummy_batch = False
90
91
self.renderer = renderer = renderer_from_config(self.vllm_config)
93
- self.io_processor = get_io_processor(
94
- self.vllm_config,
95
- self.renderer,
96
- self.model_config.io_processor_plugin,
97
- )
92
93
# Convert EngineInput --> EngineCoreRequest.
94
self.input_processor = InputProcessor(self.vllm_config, renderer)