[Frontend] Consolidate scale out entrypoints (#44512)

Signed-off-by: wang.yuqi <yuqi.wang@daocloud.io>

wang.yuqi committed Jun 29, 2026 at 18:18 UTC 3483240b7ea3d4372b6c79369ea36617f8b1fbb2
45 files changed +422 -289
.buildkite/test-amd.yaml
+4
@@ -845,10 +845,12 @@ steps:
845 source_file_dependencies:
846 - vllm/
847 - tests/entrypoints/serve
848 + - tests/entrypoints/scale_out
849 commands:
850 - export VLLM_WORKER_MULTIPROC_METHOD=spawn
851 - pytest -v -s entrypoints/serve --ignore=entrypoints/serve/dev/rpc
852 - PYTHONPATH=/vllm-workspace pytest -v -s entrypoints/serve/dev/rpc
853 + - pytest -v -s entrypoints/scale_out
854
855 - label: Entrypoints Integration (API Server OpenAI - Part 1) # TBD
856 timeout_in_minutes: 180
@@ -2559,10 +2561,12 @@ steps:
2561 source_file_dependencies:
2562 - vllm/
2563 - tests/entrypoints/serve
2564 + - tests/entrypoints/scale_out
2565 commands:
2566 - export VLLM_WORKER_MULTIPROC_METHOD=spawn
2567 - pytest -v -s entrypoints/serve --ignore=entrypoints/serve/dev/rpc
2568 - PYTHONPATH=/vllm-workspace pytest -v -s entrypoints/serve/dev/rpc
2569 + - pytest -v -s entrypoints/scale_out
2570
2571 - label: Entrypoints Integration (API Server OpenAI - Part 1) # TBD
2572 timeout_in_minutes: 180
.buildkite/test_areas/entrypoints.yaml
+2
@@ -40,10 +40,12 @@ steps:
40 source_file_dependencies:
41 - vllm/
42 - tests/entrypoints/serve
43 + - tests/entrypoints/scale_out
44 commands:
45 - export VLLM_WORKER_MULTIPROC_METHOD=spawn
46 - pytest -v -s entrypoints/serve --ignore=entrypoints/serve/dev/rpc
47 - PYTHONPATH=/vllm-workspace pytest -v -s entrypoints/serve/dev/rpc
48 + - pytest -v -s entrypoints/scale_out
49 mirror:
50 amd:
51 device: mi325_1
.buildkite/test_areas/rust_frontend.yaml
+2 -2
@@ -46,7 +46,7 @@ steps:
46 - vllm/v1/engine/
47 - tests/utils.py
48 # - tests/entrypoints/serve/dev/rpc/test_collective_rpc.py
49 - - tests/entrypoints/serve/disagg/test_serving_tokens.py
49 + - tests/entrypoints/scale_out/token_in_token_out/test_serving_tokens.py
50 - tests/entrypoints/serve/instrumentator/test_basic.py
51 - tests/entrypoints/serve/instrumentator/test_metrics.py
52 # - tests/entrypoints/serve/dev/test_sleep.py
@@ -55,7 +55,7 @@ steps:
55 - export VLLM_WORKER_MULTIPROC_METHOD=spawn
56 # - pytest -v -s entrypoints/serve/dev/rpc/test_collective_rpc.py
57 - pytest -v -s entrypoints/serve/instrumentator/test_basic.py -k "not show_version and not server_load"
58 - - pytest -v -s entrypoints/serve/disagg/test_serving_tokens.py -k "not stream and not lora and not test_generate_logprobs and not stop_string_workflow"
58 + - pytest -v -s entrypoints/scale_out/token_in_token_out/test_serving_tokens.py -k "not stream and not lora and not test_generate_logprobs and not stop_string_workflow"
59 - pytest -v -s entrypoints/serve/instrumentator/test_metrics.py -k "text and not show and not run_batch and not test_metrics_counts and not test_metrics_exist"
60 # - pytest -v -s entrypoints/serve/dev/test_sleep.py
61
docs/examples/README.md
+2 -1
@@ -13,5 +13,6 @@ vLLM's examples are organized into the following categories:
13 - **[`rl/`](../../examples/rl)** – Reinforcement learning examples.
14 - **[`deployment/`](../../examples/deployment)** – Examples for deploying vLLM in production.
15 - **[`ray_serving/`](../../examples/ray_serving)** – Scalable serving using Ray.
16 -- **[`disaggregated/`](../../examples/disaggregated)** – Examples for disaggregated serving (separate prefill and decode), including various kv cache connectors (LMCache, Mooncake, FlexKV, P2P NCCL) and failure recovery.
16 +- **[`disaggregated/`](../../examples/disaggregated)** – Examples for Disaggregated P/D (Prefill/Decoding) inference, including various kv cache connectors (LMCache, Mooncake, FlexKV, P2P NCCL) and failure recovery.
17 +- **[`scale_out/`](../../examples/scale_out)** – Examples for Token In <> Token Out API Server.
18 - **[`observability/`](../../examples/observability)** – Metrics, logging, tracing (OpenTelemetry), and dashboards (Grafana, Perses).
docs/serving/online_serving/README.md
+2 -2
@@ -119,9 +119,9 @@ For further details on profiling vLLM, please refer to [this page](../../contrib
119 - `/ping` - SageMaker health check
120 - `/invocations` - SageMaker-compatible endpoint (routes to the same inference functions as `/v1` endpoints)
121
122 -## Disaggregated Everything
122 +## Scale-Out APIs
123
124 -### Tokens IN <> Tokens OUT
124 +### Tokens IN <> Tokens OUT APIs
125
126 - `/inference/v1/generate` - Generate completions
127 - `/abort_requests` - Abort in-flight requests (only when `--tokens-only` is also set)
examples/__init__.py renamed
examples/scale_out/__init__.py renamed
examples/scale_out/example_mm_serve.py renamed
examples/scale_out/token_generation_client.py renamed
tests/entrypoints/openai/chat_completion/test_chat_error.py
+2 -10
@@ -17,10 +17,9 @@ from vllm.entrypoints.openai.chat_completion.serving import OpenAIServingChat
17 from vllm.entrypoints.openai.engine.protocol import GenerationError
18 from vllm.entrypoints.openai.models.protocol import BaseModelPath
19 from vllm.entrypoints.openai.models.serving import OpenAIServingModels
20 -from vllm.entrypoints.serve.render.serving import ServingRender
20 +from vllm.entrypoints.scale_out.render.serving import ServingRender
21 from vllm.outputs import CompletionOutput, RequestOutput
22 from vllm.renderers.hf import HfRenderer
23 -from vllm.renderers.online_derenderer import OnlineDerenderer
23 from vllm.renderers.online_renderer import OnlineRenderer
24 from vllm.tokenizers.registry import cached_tokenizer_from_config
25 from vllm.v1.engine.async_llm import AsyncLLM
@@ -206,15 +205,8 @@ def _build_serving_render(engine: AsyncLLM) -> ServingRender:
205 chat_template=None,
206 chat_template_content_format="auto",
207 )
209 - online_derenderer = OnlineDerenderer(
210 - model_config=engine.model_config,
211 - renderer=engine.renderer,
212 - request_logger=None,
213 - chat_template=None,
214 - chat_template_content_format="auto",
215 - )
208
217 - serving_render = ServingRender(models, online_renderer, online_derenderer)
209 + serving_render = ServingRender(models, online_renderer)
210
211 async def _fake_preprocess_chat(*args, **kwargs):
212 # return conversation, engine_inputs
tests/entrypoints/openai/completion/test_completion_error.py
+2 -10
@@ -14,10 +14,9 @@ from vllm.entrypoints.openai.completion.serving import OpenAIServingCompletion
14 from vllm.entrypoints.openai.engine.protocol import GenerationError
15 from vllm.entrypoints.openai.models.protocol import BaseModelPath
16 from vllm.entrypoints.openai.models.serving import OpenAIServingModels
17 -from vllm.entrypoints.serve.render.serving import ServingRender
17 +from vllm.entrypoints.scale_out.render.serving import ServingRender
18 from vllm.outputs import CompletionOutput, RequestOutput
19 from vllm.renderers.hf import HfRenderer
20 -from vllm.renderers.online_derenderer import OnlineDerenderer
20 from vllm.renderers.online_renderer import OnlineRenderer
21 from vllm.tokenizers.registry import cached_tokenizer_from_config
22 from vllm.v1.engine.async_llm import AsyncLLM
@@ -191,15 +190,8 @@ def _build_serving_render(engine: AsyncLLM) -> ServingRender:
190 chat_template=None,
191 chat_template_content_format="auto",
192 )
194 - online_derenderer = OnlineDerenderer(
195 - model_config=engine.model_config,
196 - renderer=engine.renderer,
197 - request_logger=None,
198 - chat_template=None,
199 - chat_template_content_format="auto",
200 - )
193
202 - serving_render = ServingRender(models, online_renderer, online_derenderer)
194 + serving_render = ServingRender(models, online_renderer)
195
196 async def _fake_preprocess_chat(*args, **kwargs):
197 # return conversation, engine_inputs
tests/entrypoints/openai/test_render_token_offsets.py
+2 -2
@@ -3,7 +3,7 @@
3 """Unit tests for the token-offsets request/response protocol wiring:
4 the request flag flowing into ``TokenizeParams`` and the ``GenerateRequest``
5 serialization boundary. End-to-end behavior is covered by
6 -``tests/entrypoints/serve/render/test_render.py``; plain Pydantic field
6 +``tests/entrypoints/scale_out/render/test_render.py``; plain Pydantic field
7 storage is not retested here.
8 """
9
@@ -12,7 +12,7 @@ from unittest.mock import Mock
12 from vllm.config import ModelConfig
13 from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionRequest
14 from vllm.entrypoints.openai.completion.protocol import CompletionRequest
15 -from vllm.entrypoints.serve.disagg.protocol import GenerateRequest
15 +from vllm.entrypoints.scale_out.token_in_token_out.protocol import GenerateRequest
16 from vllm.sampling_params import SamplingParams
17
18
tests/entrypoints/scale_out/__init__.py renamed
tests/entrypoints/scale_out/derender/__init__.py
tests/entrypoints/scale_out/derender/test_derender.py renamed
tests/entrypoints/scale_out/render/__init__.py
tests/entrypoints/scale_out/render/test_launch_render.py renamed
tests/entrypoints/scale_out/render/test_render.py renamed
tests/entrypoints/scale_out/render/test_render_multimodal.py renamed
tests/entrypoints/scale_out/token_in_token_out/__init__.py
tests/entrypoints/scale_out/token_in_token_out/test_generate_stream.py renamed
+2 -2
@@ -12,11 +12,11 @@ from vllm.config.multimodal import MultiModalConfig
12 from vllm.entrypoints.openai.engine.protocol import StreamOptions
13 from vllm.entrypoints.openai.models.protocol import BaseModelPath
14 from vllm.entrypoints.openai.models.serving import OpenAIServingModels
15 -from vllm.entrypoints.serve.disagg.protocol import (
15 +from vllm.entrypoints.scale_out.token_in_token_out.protocol import (
16 GenerateRequest,
17 GenerateResponse,
18 )
19 -from vllm.entrypoints.serve.disagg.serving import ServingTokens
19 +from vllm.entrypoints.scale_out.token_in_token_out.serving import ServingTokens
20 from vllm.logprobs import Logprob
21 from vllm.outputs import CompletionOutput, RequestOutput
22 from vllm.renderers import renderer_from_config
tests/entrypoints/scale_out/token_in_token_out/test_mm_serde.py renamed
+6 -3
@@ -1,14 +1,17 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 -"""Roundtrip tests for multimodal serde used by the disagg generate endpoint."""
3 +"""
4 +Roundtrip tests for multimodal serde used by the
5 +token_in_token_out generate endpoint.
6 +"""
7
8 import torch
9
7 -from vllm.entrypoints.serve.disagg.mm_serde import (
10 +from vllm.entrypoints.scale_out.token_in_token_out.mm_serde import (
11 decode_mm_kwargs_item,
12 encode_mm_kwargs_item,
13 )
11 -from vllm.entrypoints.serve.disagg.protocol import (
14 +from vllm.entrypoints.scale_out.token_in_token_out.protocol import (
15 MultiModalFeatures,
16 PlaceholderRangeInfo,
17 )
tests/entrypoints/scale_out/token_in_token_out/test_protocol.py renamed
+2 -2
@@ -1,6 +1,6 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 -"""Unit tests for the disagg request/response protocol.
3 +"""Unit tests for the token_in_token_out request/response protocol.
4
5 These tests intentionally avoid spinning up a server — they exercise the
6 pydantic validators on ``GenerateRequest`` directly so they run fast and
@@ -9,7 +9,7 @@ fail loudly if the validator semantics ever drift.
9
10 import json
11
12 -from vllm.entrypoints.serve.disagg.protocol import GenerateRequest
12 +from vllm.entrypoints.scale_out.token_in_token_out.protocol import GenerateRequest
13 from vllm.sampling_params import SamplingParams
14
15
tests/entrypoints/scale_out/token_in_token_out/test_return_routed_experts.py renamed
tests/entrypoints/scale_out/token_in_token_out/test_serving_multimodal_tokens.py renamed
tests/entrypoints/scale_out/token_in_token_out/test_serving_tokens.py renamed
tests/entrypoints/scale_out/token_in_token_out/test_tokens_logprobs.py renamed
+1 -1
@@ -1,7 +1,7 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 -from vllm.entrypoints.serve.disagg.serving import ServingTokens
4 +from vllm.entrypoints.scale_out.token_in_token_out.serving import ServingTokens
5 from vllm.logprobs import Logprob
6
7
tests/renderers/test_token_offsets.py
+1 -1
@@ -5,7 +5,7 @@
5 These exercise ``_tokenize_prompt`` (offset extraction + capability/MM
6 gating) and the ``_tokenize_prompt -> _process_tokens -> TokensInput``
7 forwarding chain. Endpoint-level coverage lives in
8 -``tests/entrypoints/serve/render/test_render.py``.
8 +``tests/entrypoints/scale_out/render/test_render.py``.
9 """
10
11 import pytest
vllm/entrypoints/generate/api_router.py
-15
@@ -66,7 +66,6 @@ async def init_generate_state(
66 from vllm.entrypoints.openai.chat_completion.serving import OpenAIServingChat
67 from vllm.entrypoints.openai.completion.serving import OpenAIServingCompletion
68 from vllm.entrypoints.openai.responses.serving import OpenAIServingResponses
69 - from vllm.entrypoints.serve.disagg.serving import ServingTokens
69 from vllm.entrypoints.serve.utils.fingerprint import set_default_fingerprint_mode
70
71 # Applied before any serving class is constructed so that each one picks
@@ -175,20 +174,6 @@ async def init_generate_state(
174 if "generate" in supported_tasks
175 else None
176 )
178 - state.serving_tokens = (
179 - ServingTokens(
180 - engine_client,
181 - state.openai_serving_models,
182 - state.online_renderer,
183 - request_logger=request_logger,
184 - return_tokens_as_token_ids=args.return_tokens_as_token_ids,
185 - enable_prompt_tokens_details=args.enable_prompt_tokens_details,
186 - enable_log_outputs=args.enable_log_outputs,
187 - force_no_detokenize=args.tokens_only,
188 - )
189 - if "generate" in supported_tasks
190 - else None
191 - )
177
178 from .generative_scoring.serving import ServingGenerativeScoring
179
vllm/entrypoints/openai/api_server.py
+10 -23
@@ -32,7 +32,6 @@ from vllm.entrypoints.openai.engine.protocol import GenerationError
32 from vllm.entrypoints.openai.models.protocol import BaseModelPath
33 from vllm.entrypoints.openai.models.serving import OpenAIServingModels
34 from vllm.entrypoints.serve.elastic_ep.middleware import ScalingMiddleware
35 -from vllm.entrypoints.serve.render.serving import ServingRender
35 from vllm.entrypoints.serve.sagemaker.api_router import sagemaker_standards_bootstrap
36 from vllm.entrypoints.serve.tokenize.serving import ServingTokenization
37 from vllm.entrypoints.serve.utils.api_utils import (
@@ -208,12 +207,6 @@ def build_app(
207
208 register_generate_api_routers(app)
209
211 - from vllm.entrypoints.serve.disagg.api_router import (
212 - attach_router as attach_disagg_router,
213 - )
214 -
215 - attach_disagg_router(app)
216 -
210 from vllm.entrypoints.serve.elastic_ep.api_router import (
211 attach_router as elastic_ep_attach_router,
212 )
@@ -221,11 +214,9 @@ def build_app(
214 elastic_ep_attach_router(app)
215
216 if "generate" in supported_tasks or "render" in supported_tasks:
224 - from vllm.entrypoints.serve.render.api_router import (
225 - attach_router as attach_render_router,
226 - )
217 + from vllm.entrypoints.scale_out.factories import register_scale_out_api_routers
218
228 - attach_render_router(app)
219 + register_scale_out_api_routers(app, supported_tasks)
220
221 if "transcription" in supported_tasks or "realtime" in supported_tasks:
222 from vllm.entrypoints.speech_to_text.factories import (
@@ -401,12 +392,6 @@ async def init_app_state(
392 default_chat_template_kwargs=args.default_chat_template_kwargs,
393 trust_request_chat_template=args.trust_request_chat_template,
394 )
404 - state.serving_render = ServingRender(
405 - state.openai_serving_models,
406 - state.online_renderer,
407 - state.online_derenderer,
408 - request_logger=request_logger,
409 - )
395
396 if "generate" in supported_tasks:
397 from vllm.entrypoints.generate.api_router import init_generate_state
@@ -415,6 +400,10 @@ async def init_app_state(
400 engine_client, state, args, request_logger, supported_tasks
401 )
402
403 + from vllm.entrypoints.scale_out.factories import init_scale_out_state
404 +
405 + init_scale_out_state(state, args, engine_client, request_logger)
406 +
407 if "transcription" in supported_tasks or "realtime" in supported_tasks:
408 from vllm.entrypoints.speech_to_text.factories import init_speech_to_text_state
409
@@ -505,12 +494,10 @@ async def init_render_app_state(
494 default_chat_template_kwargs=args.default_chat_template_kwargs,
495 trust_request_chat_template=args.trust_request_chat_template,
496 )
508 - state.serving_render = ServingRender(
509 - model_registry,
510 - state.online_renderer,
511 - state.online_derenderer,
512 - request_logger=request_logger,
513 - )
497 +
498 + from vllm.entrypoints.scale_out.factories import init_render_state
499 +
500 + init_render_state(state, request_logger)
501
502 state.vllm_config = vllm_config
503 # Disable stats logging — there is no engine to poll.
vllm/entrypoints/scale_out/__init__.py
vllm/entrypoints/scale_out/derender/__init__.py
vllm/entrypoints/scale_out/derender/api_router.py new
+74
@@ -0,0 +1,74 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +from http import HTTPStatus
4 +
5 +from fastapi import APIRouter, Depends, Request
6 +from fastapi.responses import JSONResponse
7 +
8 +from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionResponse
9 +from vllm.entrypoints.openai.completion.protocol import CompletionResponse
10 +from vllm.entrypoints.openai.engine.protocol import ErrorResponse
11 +from vllm.entrypoints.serve.utils.api_utils import validate_json_request
12 +from vllm.logger import init_logger
13 +
14 +from ..token_in_token_out.protocol import (
15 + DerenderChatRequest,
16 + DerenderCompletionRequest,
17 +)
18 +from .serving import ServingDerender
19 +
20 +logger = init_logger(__name__)
21 +
22 +router = APIRouter()
23 +
24 +
25 +def derender(request: Request) -> ServingDerender | None:
26 + return getattr(request.app.state, "serving_derender", None)
27 +
28 +
29 +@router.post(
30 + "/v1/chat/completions/derender",
31 + dependencies=[Depends(validate_json_request)],
32 + response_model=ChatCompletionResponse,
33 + responses={
34 + HTTPStatus.BAD_REQUEST.value: {"model": ErrorResponse},
35 + HTTPStatus.NOT_FOUND.value: {"model": ErrorResponse},
36 + HTTPStatus.INTERNAL_SERVER_ERROR.value: {"model": ErrorResponse},
37 + },
38 +)
39 +async def derender_chat_completion(request: DerenderChatRequest, raw_request: Request):
40 + handler = derender(raw_request)
41 + if handler is None:
42 + raise NotImplementedError(
43 + "The model does not support Chat Completions Derender API"
44 + )
45 +
46 + result = await handler.derender_chat_response(request)
47 +
48 + if isinstance(result, ErrorResponse):
49 + return JSONResponse(content=result.model_dump(), status_code=result.error.code)
50 +
51 + return JSONResponse(content=result.model_dump())
52 +
53 +
54 +@router.post(
55 + "/v1/completions/derender",
56 + dependencies=[Depends(validate_json_request)],
57 + response_model=CompletionResponse,
58 + responses={
59 + HTTPStatus.BAD_REQUEST.value: {"model": ErrorResponse},
60 + HTTPStatus.NOT_FOUND.value: {"model": ErrorResponse},
61 + HTTPStatus.INTERNAL_SERVER_ERROR.value: {"model": ErrorResponse},
62 + },
63 +)
64 +async def derender_completion(request: DerenderCompletionRequest, raw_request: Request):
65 + handler = derender(raw_request)
66 + if handler is None:
67 + raise NotImplementedError("The model does not support Completions Derender API")
68 +
69 + result = await handler.derender_completion_response(request)
70 +
71 + if isinstance(result, ErrorResponse):
72 + return JSONResponse(content=result.model_dump(), status_code=result.error.code)
73 +
74 + return JSONResponse(content=result.model_dump())
vllm/entrypoints/scale_out/derender/serving.py new
+202
@@ -0,0 +1,202 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +import time
4 +from typing import cast
5 +
6 +from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionResponse
7 +from vllm.entrypoints.openai.completion.protocol import CompletionResponse
8 +from vllm.entrypoints.openai.engine.protocol import (
9 + ErrorResponse,
10 + UsageInfo,
11 +)
12 +from vllm.entrypoints.openai.models.serving import (
13 + OpenAIModelRegistry,
14 + OpenAIServingModels,
15 +)
16 +from vllm.entrypoints.serve.engine.serving import BaseServing
17 +from vllm.entrypoints.serve.utils.request_logger import RequestLogger
18 +from vllm.inputs import (
19 + EngineInput,
20 + MultiModalHashes,
21 + MultiModalInput,
22 + MultiModalPlaceholders,
23 +)
24 +from vllm.logger import init_logger
25 +from vllm.renderers.online_derenderer import OnlineDerenderer
26 +
27 +from ..token_in_token_out.mm_serde import encode_mm_kwargs_item
28 +from ..token_in_token_out.protocol import (
29 + DerenderChatRequest,
30 + DerenderCompletionRequest,
31 + MultiModalFeatures,
32 + PlaceholderRangeInfo,
33 +)
34 +
35 +logger = init_logger(__name__)
36 +
37 +
38 +class ServingDerender(BaseServing):
39 + def __init__(
40 + self,
41 + models: OpenAIServingModels | OpenAIModelRegistry,
42 + online_derenderer: "OnlineDerenderer",
43 + *,
44 + request_logger: RequestLogger | None = None,
45 + ) -> None:
46 + super().__init__(
47 + models=models,
48 + model_config=models.model_config,
49 + request_logger=request_logger,
50 + )
51 +
52 + self.online_derenderer = online_derenderer
53 +
54 + async def derender_chat_response(
55 + self,
56 + request: DerenderChatRequest,
57 + ) -> ChatCompletionResponse | ErrorResponse:
58 + """Postprocess a GenerateResponse into a ChatCompletionResponse.
59 +
60 + Non-streaming only: expects the complete GenerateResponse with all
61 + token IDs present. Uses ``parser.parse()`` for one-shot extraction.
62 +
63 + When ``request.chat_request`` is provided, the parser splits the
64 + output into (reasoning, content, tool_calls). Otherwise falls
65 + back to plain detokenization.
66 + """
67 + error_check_ret = await self._check_model(request)
68 + if error_check_ret is not None:
69 + return error_check_ret
70 +
71 + try:
72 + choices = await self.online_derenderer.derender_chat(
73 + request.generate_response, request.chat_request
74 + )
75 + except ValueError as exc:
76 + return self.create_error_response(str(exc))
77 +
78 + prompt_tokens = (
79 + request.prompt_tokens if request.prompt_tokens is not None else 0
80 + )
81 + gen = request.generate_response
82 + completion_tokens = sum(len(ch.token_ids) for ch in gen.choices if ch.token_ids)
83 + usage = UsageInfo(
84 + prompt_tokens=prompt_tokens,
85 + completion_tokens=completion_tokens,
86 + total_tokens=prompt_tokens + completion_tokens,
87 + )
88 +
89 + logger.debug(
90 + "derender_chat request_id=%s model=%s choices=%d completion_tokens=%d",
91 + gen.request_id,
92 + request.model,
93 + len(choices),
94 + completion_tokens,
95 + )
96 + return ChatCompletionResponse(
97 + id=gen.request_id,
98 + model=request.model,
99 + created=int(time.time()),
100 + choices=choices,
101 + usage=usage,
102 + prompt_logprobs=gen.prompt_logprobs,
103 + kv_transfer_params=gen.kv_transfer_params,
104 + )
105 +
106 + async def derender_completion_response(
107 + self,
108 + request: DerenderCompletionRequest,
109 + ) -> CompletionResponse | ErrorResponse:
110 + """Postprocess a list of GenerateResponses into a CompletionResponse.
111 +
112 + Non-streaming only. Mirrors the multi-prompt completions case: one
113 + GenerateResponse per prompt, parallel to the list[GenerateRequest]
114 + from /v1/completions/render.
115 + """
116 + error_check_ret = await self._check_model(request)
117 + if error_check_ret is not None:
118 + return error_check_ret
119 +
120 + (
121 + choices,
122 + total_prompt_tokens,
123 + total_completion_tokens,
124 + ) = await self.online_derenderer.derender_completion(
125 + request.generate_responses, request.prompt_tokens
126 + )
127 +
128 + if not request.generate_responses:
129 + return self.create_error_response("generate_responses must not be empty")
130 +
131 + first = request.generate_responses[0]
132 + kv_params = first.kv_transfer_params
133 + if any(
134 + r.kv_transfer_params != kv_params for r in request.generate_responses[1:]
135 + ):
136 + logger.warning(
137 + "derender_completion: kv_transfer_params differ across responses; "
138 + "setting to None on the aggregated response"
139 + )
140 + kv_params = None
141 +
142 + usage = UsageInfo(
143 + prompt_tokens=total_prompt_tokens,
144 + completion_tokens=total_completion_tokens,
145 + total_tokens=total_prompt_tokens + total_completion_tokens,
146 + )
147 +
148 + logger.debug(
149 + "derender_completion request_id=%s model=%s choices=%d"
150 + " completion_tokens=%d",
151 + first.request_id,
152 + request.model,
153 + len(choices),
154 + total_completion_tokens,
155 + )
156 + return CompletionResponse(
157 + id=first.request_id,
158 + model=request.model,
159 + created=int(time.time()),
160 + choices=choices,
161 + usage=usage,
162 + kv_transfer_params=kv_params,
163 + )
164 +
165 + @staticmethod
166 + def _extract_mm_features(
167 + engine_input: EngineInput,
168 + ) -> MultiModalFeatures | None:
169 + """Extract multimodal metadata from a rendered engine prompt.
170 +
171 + Returns ``None`` for text-only prompts.
172 + """
173 + if engine_input.get("type") != "multimodal":
174 + return None
175 +
176 + # At this point engine_input is a MultiModalInput TypedDict.
177 + mm_engine_input = cast(MultiModalInput, engine_input)
178 + mm_hashes: MultiModalHashes = mm_engine_input["mm_hashes"]
179 + raw_placeholders: MultiModalPlaceholders = mm_engine_input["mm_placeholders"]
180 +
181 + mm_placeholders = {
182 + modality: [
183 + PlaceholderRangeInfo(offset=p.offset, length=p.length) for p in ranges
184 + ]
185 + for modality, ranges in raw_placeholders.items()
186 + }
187 +
188 + # Serialize tensor data per modality.
189 + kwargs_data: dict[str, list[str | None]] | None = None
190 + if raw_mm_kwargs := mm_engine_input.get("mm_kwargs"):
191 + kwargs_data = {}
192 + for modality, items in raw_mm_kwargs.items():
193 + kwargs_data[modality] = [
194 + encode_mm_kwargs_item(item) if item is not None else None
195 + for item in items
196 + ]
197 +
198 + return MultiModalFeatures(
199 + mm_hashes=mm_hashes,
200 + mm_placeholders=mm_placeholders,
201 + kwargs_data=kwargs_data,
202 + )
vllm/entrypoints/scale_out/factories.py new
+78
@@ -0,0 +1,78 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +from argparse import Namespace
4 +from typing import TYPE_CHECKING
5 +
6 +from fastapi import FastAPI
7 +
8 +from vllm.engine.protocol import EngineClient
9 +from vllm.tasks import SupportedTask
10 +
11 +if TYPE_CHECKING:
12 + from starlette.datastructures import State
13 +
14 + from vllm.entrypoints.serve.utils.request_logger import RequestLogger
15 +else:
16 + RequestLogger = object
17 +
18 +
19 +def init_render_state(
20 + state: "State",
21 + request_logger: RequestLogger | None,
22 +):
23 + from .derender.serving import ServingDerender
24 + from .render.serving import ServingRender
25 +
26 + state.serving_render = ServingRender(
27 + state.openai_serving_models,
28 + state.online_renderer,
29 + request_logger=request_logger,
30 + )
31 +
32 + state.serving_derender = ServingDerender(
33 + state.openai_serving_models,
34 + state.online_derenderer,
35 + request_logger=request_logger,
36 + )
37 +
38 +
39 +def init_scale_out_state(
40 + state: "State",
41 + args: "Namespace",
42 + engine_client: "EngineClient",
43 + request_logger: RequestLogger | None,
44 +):
45 + init_render_state(state, request_logger)
46 +
47 + from vllm.entrypoints.scale_out.token_in_token_out.serving import ServingTokens
48 +
49 + state.serving_tokens = ServingTokens(
50 + engine_client,
51 + state.openai_serving_models,
52 + state.online_renderer,
53 + request_logger=request_logger,
54 + return_tokens_as_token_ids=args.return_tokens_as_token_ids,
55 + enable_prompt_tokens_details=args.enable_prompt_tokens_details,
56 + enable_log_outputs=args.enable_log_outputs,
57 + force_no_detokenize=args.tokens_only,
58 + )
59 +
60 +
61 +def register_scale_out_api_routers(
62 + app: FastAPI,
63 + supported_tasks: tuple["SupportedTask", ...],
64 +):
65 + from .render.api_router import router as render_render
66 +
67 + app.include_router(render_render)
68 +
69 + from .derender.api_router import router as derender_render
70 +
71 + app.include_router(derender_render)
72 +
73 + if "generate" in supported_tasks:
74 + from .token_in_token_out.api_router import (
75 + attach_router as attach_disagg_router,
76 + )
77 +
78 + attach_disagg_router(app)
vllm/entrypoints/scale_out/render/__init__.py renamed
vllm/entrypoints/scale_out/render/api_router.py renamed
+6 -67
@@ -2,27 +2,18 @@
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 from http import HTTPStatus
4
5 -from fastapi import APIRouter, Depends, FastAPI, Request
5 +from fastapi import APIRouter, Depends, Request
6 from fastapi.responses import JSONResponse
7
8 -from vllm.entrypoints.openai.chat_completion.protocol import (
9 - ChatCompletionRequest,
10 - ChatCompletionResponse,
11 -)
12 -from vllm.entrypoints.openai.completion.protocol import (
13 - CompletionRequest,
14 - CompletionResponse,
15 -)
8 +from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionRequest
9 +from vllm.entrypoints.openai.completion.protocol import CompletionRequest
10 from vllm.entrypoints.openai.engine.protocol import ErrorResponse
17 -from vllm.entrypoints.serve.disagg.protocol import (
18 - DerenderChatRequest,
19 - DerenderCompletionRequest,
20 - GenerateRequest,
21 -)
22 -from vllm.entrypoints.serve.render.serving import ServingRender
11 from vllm.entrypoints.serve.utils.api_utils import validate_json_request
12 from vllm.logger import init_logger
13
14 +from ..token_in_token_out.protocol import GenerateRequest
15 +from .serving import ServingRender
16 +
17 logger = init_logger(__name__)
18
19 router = APIRouter()
@@ -79,55 +70,3 @@ async def render_completion(request: CompletionRequest, raw_request: Request):
70 return JSONResponse(content=result.model_dump(), status_code=result.error.code)
71
72 return JSONResponse(content=[item.model_dump() for item in result])
82 -
83 -
84 -@router.post(
85 - "/v1/chat/completions/derender",
86 - dependencies=[Depends(validate_json_request)],
87 - response_model=ChatCompletionResponse,
88 - responses={
89 - HTTPStatus.BAD_REQUEST.value: {"model": ErrorResponse},
90 - HTTPStatus.NOT_FOUND.value: {"model": ErrorResponse},
91 - HTTPStatus.INTERNAL_SERVER_ERROR.value: {"model": ErrorResponse},
92 - },
93 -)
94 -async def derender_chat_completion(request: DerenderChatRequest, raw_request: Request):
95 - handler = render(raw_request)
96 - if handler is None:
97 - raise NotImplementedError(
98 - "The model does not support Chat Completions Derender API"
99 - )
100 -
101 - result = await handler.derender_chat_response(request)
102 -
103 - if isinstance(result, ErrorResponse):
104 - return JSONResponse(content=result.model_dump(), status_code=result.error.code)
105 -
106 - return JSONResponse(content=result.model_dump())
107 -
108 -
109 -@router.post(
110 - "/v1/completions/derender",
111 - dependencies=[Depends(validate_json_request)],
112 - response_model=CompletionResponse,
113 - responses={
114 - HTTPStatus.BAD_REQUEST.value: {"model": ErrorResponse},
115 - HTTPStatus.NOT_FOUND.value: {"model": ErrorResponse},
116 - HTTPStatus.INTERNAL_SERVER_ERROR.value: {"model": ErrorResponse},
117 - },
118 -)
119 -async def derender_completion(request: DerenderCompletionRequest, raw_request: Request):
120 - handler = render(raw_request)
121 - if handler is None:
122 - raise NotImplementedError("The model does not support Completions Derender API")
123 -
124 - result = await handler.derender_completion_response(request)
125 -
126 - if isinstance(result, ErrorResponse):
127 - return JSONResponse(content=result.model_dump(), status_code=result.error.code)
128 -
129 - return JSONResponse(content=result.model_dump())
130 -
131 -
132 -def attach_router(app: FastAPI) -> None:
133 - app.include_router(router)
vllm/entrypoints/scale_out/render/serving.py renamed
+5 -131
@@ -1,28 +1,16 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 -import time
3 from typing import cast
4
6 -from vllm.entrypoints.openai.chat_completion.protocol import (
7 - ChatCompletionRequest,
8 - ChatCompletionResponse,
9 -)
10 -from vllm.entrypoints.openai.completion.protocol import (
11 - CompletionRequest,
12 - CompletionResponse,
13 -)
14 -from vllm.entrypoints.openai.engine.protocol import (
15 - ErrorResponse,
16 - UsageInfo,
17 -)
5 +from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionRequest
6 +from vllm.entrypoints.openai.completion.protocol import CompletionRequest
7 +from vllm.entrypoints.openai.engine.protocol import ErrorResponse
8 from vllm.entrypoints.openai.models.serving import (
9 OpenAIModelRegistry,
10 OpenAIServingModels,
11 )
22 -from vllm.entrypoints.serve.disagg.mm_serde import encode_mm_kwargs_item
23 -from vllm.entrypoints.serve.disagg.protocol import (
24 - DerenderChatRequest,
25 - DerenderCompletionRequest,
12 +from vllm.entrypoints.scale_out.token_in_token_out.mm_serde import encode_mm_kwargs_item
13 +from vllm.entrypoints.scale_out.token_in_token_out.protocol import (
14 GenerateRequest,
15 MultiModalFeatures,
16 PlaceholderRangeInfo,
@@ -41,7 +29,6 @@ from vllm.renderers.inputs.preprocess import (
29 extract_prompt_components,
30 extract_prompt_len,
31 )
44 -from vllm.renderers.online_derenderer import OnlineDerenderer
32 from vllm.renderers.online_renderer import OnlineRenderer
33 from vllm.utils import random_uuid
34
@@ -53,7 +40,6 @@ class ServingRender(BaseServing):
40 self,
41 models: OpenAIServingModels | OpenAIModelRegistry,
42 online_renderer: "OnlineRenderer",
56 - online_derenderer: "OnlineDerenderer",
43 *,
44 request_logger: RequestLogger | None = None,
45 ) -> None:
@@ -64,7 +50,6 @@ class ServingRender(BaseServing):
50 )
51
52 self.online_renderer = online_renderer
67 - self.online_derenderer = online_derenderer
53
54 self.default_sampling_params = (
55 online_renderer.model_config.get_diff_sampling_param()
@@ -223,117 +208,6 @@ class ServingRender(BaseServing):
208
209 return generate_requests
210
226 - async def derender_chat_response(
227 - self,
228 - request: DerenderChatRequest,
229 - ) -> ChatCompletionResponse | ErrorResponse:
230 - """Postprocess a GenerateResponse into a ChatCompletionResponse.
231 -
232 - Non-streaming only: expects the complete GenerateResponse with all
233 - token IDs present. Uses ``parser.parse()`` for one-shot extraction.
234 -
235 - When ``request.chat_request`` is provided, the parser splits the
236 - output into (reasoning, content, tool_calls). Otherwise falls
237 - back to plain detokenization.
238 - """
239 - error_check_ret = await self._check_model(request)
240 - if error_check_ret is not None:
241 - return error_check_ret
242 -
243 - try:
244 - choices = await self.online_derenderer.derender_chat(
245 - request.generate_response, request.chat_request
246 - )
247 - except ValueError as exc:
248 - return self.create_error_response(str(exc))
249 -
250 - prompt_tokens = (
251 - request.prompt_tokens if request.prompt_tokens is not None else 0
252 - )
253 - gen = request.generate_response
254 - completion_tokens = sum(len(ch.token_ids) for ch in gen.choices if ch.token_ids)
255 - usage = UsageInfo(
256 - prompt_tokens=prompt_tokens,
257 - completion_tokens=completion_tokens,
258 - total_tokens=prompt_tokens + completion_tokens,
259 - )
260 -
261 - logger.debug(
262 - "derender_chat request_id=%s model=%s choices=%d completion_tokens=%d",
263 - gen.request_id,
264 - request.model,
265 - len(choices),
266 - completion_tokens,
267 - )
268 - return ChatCompletionResponse(
269 - id=gen.request_id,
270 - model=request.model,
271 - created=int(time.time()),
272 - choices=choices,
273 - usage=usage,
274 - prompt_logprobs=gen.prompt_logprobs,
275 - kv_transfer_params=gen.kv_transfer_params,
276 - )
277 -
278 - async def derender_completion_response(
279 - self,
280 - request: DerenderCompletionRequest,
281 - ) -> CompletionResponse | ErrorResponse:
282 - """Postprocess a list of GenerateResponses into a CompletionResponse.
283 -
284 - Non-streaming only. Mirrors the multi-prompt completions case: one
285 - GenerateResponse per prompt, parallel to the list[GenerateRequest]
286 - from /v1/completions/render.
287 - """
288 - error_check_ret = await self._check_model(request)
289 - if error_check_ret is not None:
290 - return error_check_ret
291 -
292 - (
293 - choices,
294 - total_prompt_tokens,
295 - total_completion_tokens,
296 - ) = await self.online_derenderer.derender_completion(
297 - request.generate_responses, request.prompt_tokens
298 - )
299 -
300 - if not request.generate_responses:
301 - return self.create_error_response("generate_responses must not be empty")
302 -
303 - first = request.generate_responses[0]
304 - kv_params = first.kv_transfer_params
305 - if any(
306 - r.kv_transfer_params != kv_params for r in request.generate_responses[1:]
307 - ):
308 - logger.warning(
309 - "derender_completion: kv_transfer_params differ across responses; "
310 - "setting to None on the aggregated response"
311 - )
312 - kv_params = None
313 -
314 - usage = UsageInfo(
315 - prompt_tokens=total_prompt_tokens,
316 - completion_tokens=total_completion_tokens,
317 - total_tokens=total_prompt_tokens + total_completion_tokens,
318 - )
319 -
320 - logger.debug(
321 - "derender_completion request_id=%s model=%s choices=%d"
322 - " completion_tokens=%d",
323 - first.request_id,
324 - request.model,
325 - len(choices),
326 - total_completion_tokens,
327 - )
328 - return CompletionResponse(
329 - id=first.request_id,
330 - model=request.model,
331 - created=int(time.time()),
332 - choices=choices,
333 - usage=usage,
334 - kv_transfer_params=kv_params,
335 - )
336 -
211 @staticmethod
212 def _extract_mm_features(
213 engine_input: EngineInput,
vllm/entrypoints/scale_out/token_in_token_out/__init__.py
vllm/entrypoints/scale_out/token_in_token_out/api_router.py renamed
+6 -7
@@ -11,13 +11,6 @@ from fastapi.responses import JSONResponse, StreamingResponse
11
12 from vllm.engine.protocol import EngineClient
13 from vllm.entrypoints.openai.engine.protocol import ErrorResponse
14 -from vllm.entrypoints.serve.disagg.protocol import (
15 - GenerateRequest,
16 - GenerateResponse,
17 -)
18 -from vllm.entrypoints.serve.disagg.serving import (
19 - ServingTokens,
20 -)
14 from vllm.entrypoints.serve.tokenize.serving import ServingTokenization
15 from vllm.entrypoints.serve.utils.api_utils import (
16 load_aware_call,
@@ -26,6 +19,12 @@ from vllm.entrypoints.serve.utils.api_utils import (
19 )
20 from vllm.logger import init_logger
21
22 +from .protocol import (
23 + GenerateRequest,
24 + GenerateResponse,
25 +)
26 +from .serving import ServingTokens
27 +
28 logger = init_logger(__name__)
29
30
vllm/entrypoints/scale_out/token_in_token_out/mm_serde.py renamed
vllm/entrypoints/scale_out/token_in_token_out/protocol.py renamed
vllm/entrypoints/scale_out/token_in_token_out/serving.py renamed
+9 -8
@@ -28,14 +28,6 @@ from vllm.entrypoints.openai.engine.protocol import (
28 )
29 from vllm.entrypoints.openai.engine.serving import OpenAIServing, clamp_prompt_logprobs
30 from vllm.entrypoints.openai.models.serving import OpenAIServingModels
31 -from vllm.entrypoints.serve.disagg.mm_serde import decode_mm_kwargs_item
32 -from vllm.entrypoints.serve.disagg.protocol import (
33 - GenerateRequest,
34 - GenerateResponse,
35 - GenerateResponseChoice,
36 - GenerateResponseStreamChoice,
37 - GenerateStreamResponse,
38 -)
31 from vllm.entrypoints.serve.utils.api_utils import get_max_tokens, should_include_usage
32 from vllm.entrypoints.serve.utils.request_logger import RequestLogger
33 from vllm.inputs import EngineInput, mm_input
@@ -51,6 +43,15 @@ from vllm.renderers.online_renderer import OnlineRenderer
43 from vllm.sampling_params import RequestOutputKind, SamplingParams
44 from vllm.utils.collection_utils import as_list
45
46 +from .mm_serde import decode_mm_kwargs_item
47 +from .protocol import (
48 + GenerateRequest,
49 + GenerateResponse,
50 + GenerateResponseChoice,
51 + GenerateResponseStreamChoice,
52 + GenerateStreamResponse,
53 +)
54 +
55 logger = init_logger(__name__)
56
57
vllm/entrypoints/serve/engine/typing.py
+1 -1
@@ -15,7 +15,7 @@ from vllm.entrypoints.openai.completion.protocol import (
15 CompletionResponse,
16 )
17 from vllm.entrypoints.openai.responses.protocol import ResponsesRequest
18 -from vllm.entrypoints.serve.disagg.protocol import (
18 +from vllm.entrypoints.scale_out.token_in_token_out.protocol import (
19 DerenderChatRequest,
20 DerenderCompletionRequest,
21 GenerateRequest,
vllm/renderers/online_derenderer.py
+1 -1
@@ -16,7 +16,7 @@ from vllm.entrypoints.openai.completion.protocol import (
16 )
17 from vllm.entrypoints.openai.engine.protocol import ToolCall
18 from vllm.entrypoints.openai.engine.serving import resolve_token_id_placeholder
19 -from vllm.entrypoints.serve.disagg.protocol import GenerateResponse
19 +from vllm.entrypoints.scale_out.token_in_token_out.protocol import GenerateResponse
20 from vllm.entrypoints.serve.utils.request_logger import RequestLogger
21 from vllm.logger import init_logger
22 from vllm.parser import Parser, ParserManager