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 14, 05:30 UTC
[Frontend][4/N] Improve all pooling task | Add plugin pooling task (#26973)
Signed-off-by: wang.yuqi <noooop@126.com> Signed-off-by: Christian Pinto <christian.pinto@ibm.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Christian Pinto <christian.pinto@ibm.com>
wang.yuqi committed
Oct 23, 2025 at 22:46 UTC
3fa2c12185da7117a63132be7520a43bcf9a4896
16 files changed
+102
-54
docs/design/io_processor_plugins.md
+14
-4
@@ -13,7 +13,6 @@ IOProcessorInput = TypeVar("IOProcessorInput")
13
IOProcessorOutput = TypeVar("IOProcessorOutput")
14
15
class IOProcessor(ABC, Generic[IOProcessorInput, IOProcessorOutput]):
16
-
16
def __init__(self, vllm_config: VllmConfig):
17
self.vllm_config = vllm_config
18
@@ -49,13 +48,24 @@ class IOProcessor(ABC, Generic[IOProcessorInput, IOProcessorOutput]):
48
request_id: str | None = None,
49
**kwargs,
50
) -> IOProcessorOutput:
52
- collected_output = [item async for i, item in model_output]
51
+ # We cannot guarantee outputs are returned in the same order they were
52
+ # fed to vLLM.
53
+ # Let's sort them by id before post_processing
54
+ sorted_output = sorted(
55
+ [(i, item) async for i, item in model_output], key=lambda output: output[0]
56
+ )
57
+ collected_output = [output[1] for output in sorted_output]
58
return self.post_process(collected_output, request_id, **kwargs)
59
60
@abstractmethod
61
def parse_request(self, request: Any) -> IOProcessorInput:
62
raise NotImplementedError
63
64
+ def validate_or_generate_params(
65
+ self, params: SamplingParams | PoolingParams | None = None
66
+ ) -> SamplingParams | PoolingParams:
67
+ return params or PoolingParams()
68
+
69
@abstractmethod
70
def output_to_response(
71
self, plugin_output: IOProcessorOutput
@@ -66,10 +76,10 @@ class IOProcessor(ABC, Generic[IOProcessorInput, IOProcessorOutput]):
76
The `parse_request` method is used for validating the user prompt and converting it into the input expected by the `pre_process`/`pre_process_async` methods.
77
The `pre_process*` methods take the validated plugin input to generate vLLM's model prompts for regular inference.
78
The `post_process*` methods take `PoolingRequestOutput` objects as input and generate a custom plugin output.
69
-
79
+The `validate_or_generate_params` method is used for validating with the plugin any `SamplingParameters`/`PoolingParameters` received with the user request, or to generate new ones if none are specified. The function always returns the validated/generated parameters.
80
The `output_to_response` method is used only for online serving and converts the plugin output to the `IOProcessorResponse` type that is then returned by the API Server. The implementation of the `/pooling` serving endpoint is available here [vllm/entrypoints/openai/serving_pooling.py](../../vllm/entrypoints/openai/serving_pooling.py).
81
72
-An example implementation of a plugin that enables generating geotiff images with the PrithviGeospatialMAE model is available [here](https://github.com/christian-pinto/prithvi_io_processor_plugin). Please, also refer to our online ([examples/online_serving/prithvi_geospatial_mae.py](../../examples/online_serving/prithvi_geospatial_mae.py)) and offline ([examples/offline_inference/prithvi_geospatial_mae_io_processor.py](../../examples/offline_inference/prithvi_geospatial_mae_io_processor.py)) inference examples.
82
+An example implementation of a plugin that enables generating geotiff images with the PrithviGeospatialMAE model is available [here](https://github.com/IBM/terratorch/tree/main/terratorch/vllm/plugins/segmentation). Please, also refer to our online ([examples/online_serving/prithvi_geospatial_mae.py](../../examples/online_serving/prithvi_geospatial_mae.py)) and offline ([examples/offline_inference/prithvi_geospatial_mae_io_processor.py](../../examples/offline_inference/prithvi_geospatial_mae_io_processor.py)) inference examples.
83
84
## Using an IO Processor plugin
85
examples/offline_inference/prithvi_geospatial_mae.py
+1
-1
@@ -64,7 +64,7 @@ class PrithviMAE:
64
}
65
66
prompt = {"prompt_token_ids": [1], "multi_modal_data": mm_data}
67
- outputs = self.model.encode(prompt, use_tqdm=False)
67
+ outputs = self.model.encode(prompt, pooling_task="plugin", use_tqdm=False)
68
69
return outputs[0].outputs.data
70
examples/offline_inference/prithvi_geospatial_mae_io_processor.py
+5
-9
@@ -6,14 +6,14 @@ import os
6
import torch
7
8
from vllm import LLM
9
-from vllm.pooling_params import PoolingParams
9
10
# This example shows how to perform an offline inference that generates
11
# multimodal data. In this specific case this example will take a geotiff
12
# image as input, process it using the multimodal data processor, and
13
# perform inference.
15
-# Requirement - install plugin at:
16
-# https://github.com/christian-pinto/prithvi_io_processor_plugin
14
+# Requirements:
15
+# - install TerraTorch v1.1 (or later):
16
+# pip install terratorch>=v1.1
17
18
19
def main():
@@ -36,16 +36,12 @@ def main():
36
# to avoid the model going OOM.
37
# The maximum number depends on the available GPU memory
38
max_num_seqs=32,
39
- io_processor_plugin="prithvi_to_tiff",
39
+ io_processor_plugin="terratorch_segmentation",
40
model_impl="terratorch",
41
enable_mm_embeds=True,
42
)
43
44
- pooling_params = PoolingParams(task="token_classify", activation=False)
45
- pooler_output = llm.encode(
46
- img_prompt,
47
- pooling_params=pooling_params,
48
- )
44
+ pooler_output = llm.encode(img_prompt, pooling_task="plugin")
45
output = pooler_output[0].outputs
46
47
print(output)
examples/online_serving/prithvi_geospatial_mae.py
+3
-4
@@ -11,14 +11,14 @@ import requests
11
# image as input, process it using the multimodal data processor, and
12
# perform inference.
13
# Requirements :
14
-# - install plugin at:
15
-# https://github.com/christian-pinto/prithvi_io_processor_plugin
14
+# - install TerraTorch v1.1 (or later):
15
+# pip install terratorch>=v1.1
16
# - start vllm in serving mode with the below args
17
# --model='christian-pinto/Prithvi-EO-2.0-300M-TL-VLLM'
18
# --model-impl terratorch
19
# --task embed --trust-remote-code
20
# --skip-tokenizer-init --enforce-eager
21
-# --io-processor-plugin prithvi_to_tiff
21
+# --io-processor-plugin terratorch_segmentation
22
# --enable-mm-embeds
23
24
@@ -35,7 +35,6 @@ def main():
35
},
36
"priority": 0,
37
"model": "christian-pinto/Prithvi-EO-2.0-300M-TL-VLLM",
38
- "softmax": False,
38
}
39
40
ret = requests.post(server_endpoint, json=request_payload_url)
tests/models/multimodal/pooling/test_prithvi_mae.py
+1
-1
@@ -40,7 +40,7 @@ def _run_test(
40
max_num_seqs=32,
41
default_torch_num_threads=1,
42
) as vllm_model:
43
- vllm_model.llm.encode(prompt, pooling_task="token_classify")
43
+ vllm_model.llm.encode(prompt, pooling_task="plugin")
44
45
46
MODELS = ["mgazz/Prithvi-EO-2.0-300M-TL-Sen1Floods11"]
tests/plugins/prithvi_io_processor_plugin/prithvi_io_processor/prithvi_processor.py
+2
-2
@@ -368,9 +368,9 @@ class PrithviMultimodalDataProcessor(IOProcessor):
368
out_format = "b64_json"
369
370
for output in model_output:
371
- y_hat = output.outputs.data.argmax(dim=1)
371
+ y_hat = output.outputs.data.argmax(dim=0)
372
pred = torch.nn.functional.interpolate(
373
- y_hat.unsqueeze(1).float(),
373
+ y_hat[None, None, ...].float(),
374
size=self.img_size,
375
mode="nearest",
376
)
tests/plugins_tests/test_io_processor_plugins.py
+1
-6
@@ -9,7 +9,6 @@ from tests.utils import RemoteOpenAIServer
9
from vllm.config import VllmConfig
10
from vllm.entrypoints.openai.protocol import IOProcessorResponse
11
from vllm.plugins.io_processors import get_io_processor
12
-from vllm.pooling_params import PoolingParams
12
13
MODEL_NAME = "ibm-nasa-geospatial/Prithvi-EO-2.0-300M-TL-Sen1Floods11"
14
@@ -94,8 +93,6 @@ def test_prithvi_mae_plugin_offline(vllm_runner, model_name: str):
93
out_data_format="b64_json",
94
)
95
97
- pooling_params = PoolingParams(activation=False)
98
-
96
with vllm_runner(
97
model_name,
98
runner="pooling",
@@ -109,9 +106,7 @@ def test_prithvi_mae_plugin_offline(vllm_runner, model_name: str):
106
model_impl="terratorch",
107
io_processor_plugin="prithvi_to_tiff",
108
) as llm_runner:
112
- pooler_output = llm_runner.get_llm().encode(
113
- img_prompt, pooling_params=pooling_params, pooling_task="token_classify"
114
- )
109
+ pooler_output = llm_runner.get_llm().encode(img_prompt, pooling_task="plugin")
110
output = pooler_output[0].outputs
111
112
# verify the output is formatted as expected for this plugin
vllm/entrypoints/llm.py
+28
-13
@@ -1024,19 +1024,6 @@ class LLM:
1024
"pooling model."
1025
)
1026
1027
- if pooling_task not in self.supported_tasks:
1028
- raise ValueError(f"pooling_task must be one of {self.supported_tasks}.")
1029
-
1030
- if pooling_params is None:
1031
- # Use default pooling params.
1032
- pooling_params = PoolingParams()
1033
-
1034
- for param in as_iter(pooling_params):
1035
- param.verify(pooling_task, model_config)
1036
- # for backwards compatibility
1037
- if truncate_prompt_tokens is not None:
1038
- param.truncate_prompt_tokens = truncate_prompt_tokens
1039
-
1027
io_processor_prompt = False
1028
if isinstance(prompts, dict) and "data" in prompts:
1029
io_processor_prompt = True
@@ -1054,6 +1041,34 @@ class LLM:
1041
# obtain the actual model prompts from the pre-processor
1042
prompts = self.io_processor.pre_process(prompt=validated_prompt)
1043
1044
+ if io_processor_prompt:
1045
+ assert self.io_processor is not None
1046
+ if is_list_of(pooling_params, PoolingParams):
1047
+ validated_pooling_params: list[PoolingParams] = []
1048
+ for param in as_iter(pooling_params):
1049
+ validated_pooling_params.append(
1050
+ self.io_processor.validate_or_generate_params(param)
1051
+ )
1052
+ pooling_params = validated_pooling_params
1053
+ else:
1054
+ assert not isinstance(pooling_params, Sequence)
1055
+ pooling_params = self.io_processor.validate_or_generate_params(
1056
+ pooling_params
1057
+ )
1058
+ else:
1059
+ if pooling_params is None:
1060
+ # Use default pooling params.
1061
+ pooling_params = PoolingParams()
1062
+
1063
+ if pooling_task not in self.supported_tasks:
1064
+ raise ValueError(f"pooling_task must be one of {self.supported_tasks}.")
1065
+
1066
+ for param in as_iter(pooling_params):
1067
+ param.verify(pooling_task, model_config)
1068
+ # for backwards compatibility
1069
+ if truncate_prompt_tokens is not None:
1070
+ param.truncate_prompt_tokens = truncate_prompt_tokens
1071
+
1072
self._validate_and_add_requests(
1073
prompts=prompts,
1074
params=pooling_params,
vllm/entrypoints/openai/api_server.py
+6
-1
@@ -1748,7 +1748,12 @@ async def init_app_state(
1748
log_error_stack=args.log_error_stack,
1749
)
1750
)
1751
- if ("token_embed" in supported_tasks or "token_classify" in supported_tasks)
1751
+ if (
1752
+ any(
1753
+ task in supported_tasks
1754
+ for task in ["token_embed", "token_classify", "plugin"]
1755
+ )
1756
+ )
1757
else None
1758
)
1759
state.openai_serving_embedding = (
vllm/entrypoints/openai/protocol.py
+1
-6
@@ -1707,11 +1707,6 @@ class IOProcessorRequest(OpenAIBaseModel, Generic[T]):
1707
if the served model does not use priority scheduling.
1708
"""
1709
data: T
1710
- """
1711
- When using plugins IOProcessor plugins, the actual input is processed
1712
- by the plugin itself. Hence, we use a generic type for the request data
1713
- """
1714
- activation: bool = False
1710
1711
encoding_format: EncodingFormat = "float"
1712
embed_dtype: EmbedDType = Field(
@@ -1732,7 +1727,7 @@ class IOProcessorRequest(OpenAIBaseModel, Generic[T]):
1727
)
1728
1729
def to_pooling_params(self):
1735
- return PoolingParams(task="token_classify", activation=self.activation)
1730
+ return PoolingParams()
1731
1732
1733
class IOProcessorResponse(OpenAIBaseModel, Generic[T]):
vllm/entrypoints/openai/serving_pooling.py
+11
-2
@@ -32,7 +32,7 @@ from vllm.entrypoints.renderer import RenderConfig
32
from vllm.entrypoints.utils import _validate_truncation_size
33
from vllm.logger import init_logger
34
from vllm.outputs import PoolingRequestOutput
35
-from vllm.tasks import SupportedTask
35
+from vllm.tasks import PoolingTask, SupportedTask
36
from vllm.utils.async_utils import merge_async_iterators
37
from vllm.utils.serial_utils import (
38
EmbedDType,
@@ -161,12 +161,21 @@ class OpenAIServingPooling(OpenAIServing):
161
# Schedule the request and get the result generator.
162
generators: list[AsyncGenerator[PoolingRequestOutput, None]] = []
163
try:
164
- pooling_params = request.to_pooling_params()
164
+ if is_io_processor_request:
165
+ assert self.io_processor is not None and isinstance(
166
+ request, IOProcessorRequest
167
+ )
168
+ pooling_params = self.io_processor.validate_or_generate_params()
169
+ else:
170
+ pooling_params = request.to_pooling_params()
171
172
+ pooling_task: PoolingTask
173
if "token_embed" in self.supported_tasks:
174
pooling_task = "token_embed"
175
elif "token_classify" in self.supported_tasks:
176
pooling_task = "token_classify"
177
+ elif "plugin" in self.supported_tasks:
178
+ pooling_task = "plugin"
179
else:
180
return self.create_error_response(
181
f"pooling_task must be one of {self.supported_tasks}."
vllm/model_executor/layers/pooler.py
+12
@@ -414,6 +414,18 @@ class Pooler(nn.Module, ABC):
414
raise NotImplementedError
415
416
417
+class DummyPooler(Pooler):
418
+ def get_supported_tasks(self) -> Set[PoolingTask]:
419
+ return {"plugin", "score"}
420
+
421
+ def forward(
422
+ self,
423
+ hidden_states: list[torch.Tensor] | torch.Tensor,
424
+ pooling_metadata: PoolingMetadata,
425
+ ) -> PoolerOutput:
426
+ return hidden_states
427
+
428
+
429
class PoolerHead(nn.Module):
430
def __init__(self, activation: PoolerActivation) -> None:
431
super().__init__()
vllm/model_executor/models/terratorch.py
+2
-4
@@ -34,7 +34,7 @@ from transformers import BatchFeature
34
from vllm.config import VllmConfig
35
from vllm.config.multimodal import BaseDummyOptions
36
from vllm.logger import init_logger
37
-from vllm.model_executor.layers.pooler import DispatchPooler, Pooler
37
+from vllm.model_executor.layers.pooler import DispatchPooler, DummyPooler
38
from vllm.model_executor.model_loader.weight_utils import default_weight_loader
39
from vllm.model_executor.models.utils import AutoWeightsLoader
40
from vllm.multimodal import MULTIMODAL_REGISTRY
@@ -249,9 +249,7 @@ class Terratorch(nn.Module, IsAttentionFree, SupportsMultiModal):
249
pooler_config = vllm_config.model_config.pooler_config
250
assert pooler_config is not None
251
252
- self.pooler = DispatchPooler(
253
- {"token_classify": Pooler.for_token_classify(pooler_config)}
254
- )
252
+ self.pooler = DispatchPooler({"plugin": DummyPooler()})
253
254
def get_input_embeddings(
255
self,
vllm/plugins/io_processors/interface.py
+7
@@ -9,6 +9,8 @@ from vllm.config import VllmConfig
9
from vllm.entrypoints.openai.protocol import IOProcessorResponse
10
from vllm.inputs.data import PromptType
11
from vllm.outputs import PoolingRequestOutput
12
+from vllm.pooling_params import PoolingParams
13
+from vllm.sampling_params import SamplingParams
14
15
IOProcessorInput = TypeVar("IOProcessorInput")
16
IOProcessorOutput = TypeVar("IOProcessorOutput")
@@ -63,6 +65,11 @@ class IOProcessor(ABC, Generic[IOProcessorInput, IOProcessorOutput]):
65
def parse_request(self, request: Any) -> IOProcessorInput:
66
raise NotImplementedError
67
68
+ def validate_or_generate_params(
69
+ self, params: SamplingParams | PoolingParams | None = None
70
+ ) -> SamplingParams | PoolingParams:
71
+ return params or PoolingParams()
72
+
73
@abstractmethod
74
def output_to_response(
75
self, plugin_output: IOProcessorOutput
vllm/pooling_params.py
+5
@@ -84,6 +84,11 @@ class PoolingParams(
84
msg = f"You cannot overwrite {self.task=!r} with {task=!r}!"
85
raise ValueError(msg)
86
87
+ # plugin task uses io_processor.parse_request to verify inputs,
88
+ # skipping PoolingParams verify
89
+ if self.task == "plugin":
90
+ return
91
+
92
# NOTE: Task validation needs to done against the model instance,
93
# which is not available in model config. So, it's not included
94
# in this method
vllm/tasks.py
+3
-1
@@ -5,7 +5,9 @@ from typing import Literal, get_args
5
GenerationTask = Literal["generate", "transcription"]
6
GENERATION_TASKS = get_args(GenerationTask)
7
8
-PoolingTask = Literal["embed", "classify", "score", "token_embed", "token_classify"]
8
+PoolingTask = Literal[
9
+ "embed", "classify", "score", "token_embed", "token_classify", "plugin"
10
+]
11
POOLING_TASKS = get_args(PoolingTask)
12
13
SupportedTask = Literal[GenerationTask, PoolingTask]