@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 6c117cff7d
Fork 0

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] Pass API server count to each process (#23717)

Signed-off-by: DarkLight1337 <tlleungac@connect.ust.hk>

Cyrus Leung committed Sep 20, 2025 at 01:15 UTC 6c117cff7d0110c74f97f68cda009595a2fdae5e
12 files changed +221 -51
benchmarks/kernels/benchmark_w8a8_block_fp8.py
+1 -1
@@ -11,13 +11,13 @@ from datetime import datetime
11 from typing import Any
12
13 import torch
14 -import triton
14 from tqdm import tqdm
15
16 from vllm.model_executor.layers.quantization.utils.fp8_utils import (
17 _w8a8_block_fp8_matmul,
18 )
19 from vllm.platforms import current_platform
20 +from vllm.triton_utils import triton
21 from vllm.utils import FlexibleArgumentParser
22
23 mp.set_start_method("spawn", force=True)
examples/others/tensorize_vllm_model.py
+1 -8
@@ -1,8 +1,6 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 -import argparse
5 -import dataclasses
4 import json
5 import logging
6 import os
@@ -327,12 +325,7 @@ def main():
325
326
327 if args.command == "serialize":
330 - eng_args_dict = {f.name: getattr(args, f.name) for f in
331 - dataclasses.fields(EngineArgs)}
332 -
333 - engine_args = EngineArgs.from_cli_args(
334 - argparse.Namespace(**eng_args_dict)
335 - )
328 + engine_args = EngineArgs.from_cli_args(args)
329
330 input_dir = tensorizer_dir.rstrip('/')
331 suffix = args.suffix if args.suffix else uuid.uuid4().hex
tests/entrypoints/test_api_server_process_manager.py
+1 -1
@@ -60,7 +60,7 @@ def test_api_server_process_manager_init(api_server_args, with_stats_update):
60 global WORKER_RUNTIME_SECONDS
61 WORKER_RUNTIME_SECONDS = 0.5
62
63 - # Copy the args to avoid mutating the
63 + # Copy the args to avoid mutating them
64 args = api_server_args.copy()
65
66 if not with_stats_update:
tests/v1/test_external_lb_dp.py
+48 -4
@@ -9,6 +9,7 @@ from contextlib import AsyncExitStack
9 import openai # use the official client for correctness check
10 import pytest
11 import pytest_asyncio
12 +import requests
13
14 from tests.utils import RemoteOpenAIServer
15 from vllm.platforms import current_platform
@@ -70,6 +71,8 @@ class ExternalLBServerManager:
71 sargs,
72 auto_port=False,
73 env_dict={
74 + "VLLM_SERVER_DEV_MODE":
75 + "1",
76 current_platform.device_control_env_var:
77 ",".join(
78 str(
@@ -127,11 +130,19 @@ def default_server_args():
130
131
132 @pytest.fixture(scope="module", params=[1, 4])
130 -def servers(request, default_server_args):
133 +def server_manager(request, default_server_args):
134 api_server_count = request.param
132 - with ExternalLBServerManager(MODEL_NAME, DP_SIZE, api_server_count,
133 - default_server_args) as server_list:
134 - yield server_list
135 + server_manager = ExternalLBServerManager(MODEL_NAME, DP_SIZE,
136 + api_server_count,
137 + default_server_args)
138 +
139 + with server_manager:
140 + yield server_manager
141 +
142 +
143 +@pytest.fixture
144 +def servers(server_manager):
145 + return server_manager.servers
146
147
148 @pytest_asyncio.fixture
@@ -144,6 +155,39 @@ async def clients(servers: list[tuple[RemoteOpenAIServer, list[str]]]):
155 ]
156
157
158 +def _get_parallel_config(server: RemoteOpenAIServer):
159 + response = requests.get(server.url_for("server_info?config_format=json"))
160 + response.raise_for_status()
161 +
162 + vllm_config = response.json()["vllm_config"]
163 + return vllm_config["parallel_config"]
164 +
165 +
166 +def test_external_lb_server_info(server_manager):
167 + servers = server_manager.servers
168 + api_server_count = server_manager.api_server_count
169 +
170 + for i, (server, _) in enumerate(servers):
171 + print(f"Testing {i=}")
172 +
173 + # Each request will hit one of the API servers
174 + # `n_reqs` is set so that there is a good chance each server
175 + # receives at least one request
176 + n_reqs = 2 * api_server_count * api_server_count
177 + parallel_configs = [
178 + _get_parallel_config(server) for _ in range(n_reqs)
179 + ]
180 + api_process_counts = [
181 + c["_api_process_count"] for c in parallel_configs
182 + ]
183 + api_process_ranks = [c["_api_process_rank"] for c in parallel_configs]
184 +
185 + assert all(c == api_server_count
186 + for c in api_process_counts), api_process_counts
187 + assert all(0 <= r < api_server_count
188 + for r in api_process_ranks), api_process_ranks
189 +
190 +
191 @pytest.mark.asyncio
192 @pytest.mark.parametrize(
193 "model_name",
tests/v1/test_hybrid_lb_dp.py
+49 -5
@@ -9,6 +9,7 @@ from contextlib import AsyncExitStack
9 import openai # use the official client for correctness check
10 import pytest
11 import pytest_asyncio
12 +import requests
13
14 from tests.utils import RemoteOpenAIServer
15 from tests.v1.test_utils import check_request_balancing
@@ -92,6 +93,8 @@ class HybridLBServerManager:
93 sargs,
94 auto_port=False,
95 env_dict={
96 + "VLLM_SERVER_DEV_MODE":
97 + "1",
98 current_platform.device_control_env_var:
99 ",".join(
100 str(
@@ -150,12 +153,20 @@ def default_server_args():
153
154
155 @pytest.fixture(scope="module", params=[1, 4])
153 -def servers(request, default_server_args):
156 +def server_manager(request, default_server_args):
157 api_server_count = request.param
155 - with HybridLBServerManager(MODEL_NAME, DP_SIZE, api_server_count,
156 - default_server_args, DP_SIZE_LOCAL,
157 - TP_SIZE) as server_list:
158 - yield server_list
158 + server_manager = HybridLBServerManager(MODEL_NAME, DP_SIZE,
159 + api_server_count,
160 + default_server_args, DP_SIZE_LOCAL,
161 + TP_SIZE)
162 +
163 + with server_manager:
164 + yield server_manager
165 +
166 +
167 +@pytest.fixture
168 +def servers(server_manager):
169 + return server_manager.servers
170
171
172 @pytest_asyncio.fixture
@@ -168,6 +179,39 @@ async def clients(servers: list[tuple[RemoteOpenAIServer, list[str]]]):
179 ]
180
181
182 +def _get_parallel_config(server: RemoteOpenAIServer):
183 + response = requests.get(server.url_for("server_info?config_format=json"))
184 + response.raise_for_status()
185 +
186 + vllm_config = response.json()["vllm_config"]
187 + return vllm_config["parallel_config"]
188 +
189 +
190 +def test_hybrid_dp_server_info(server_manager):
191 + servers = server_manager.servers
192 + api_server_count = server_manager.api_server_count
193 +
194 + for i, (server, _) in enumerate(servers):
195 + print(f"Testing {i=}")
196 +
197 + # Each request will hit one of the API servers
198 + # `n_reqs` is set so that there is a good chance each server
199 + # receives at least one request
200 + n_reqs = 2 * api_server_count * api_server_count
201 + parallel_configs = [
202 + _get_parallel_config(server) for _ in range(n_reqs)
203 + ]
204 + api_process_counts = [
205 + c["_api_process_count"] for c in parallel_configs
206 + ]
207 + api_process_ranks = [c["_api_process_rank"] for c in parallel_configs]
208 +
209 + assert all(c == api_server_count
210 + for c in api_process_counts), api_process_counts
211 + assert all(0 <= r < api_server_count
212 + for r in api_process_ranks), api_process_ranks
213 +
214 +
215 @pytest.mark.asyncio
216 @pytest.mark.parametrize(
217 "model_name",
tests/v1/test_internal_lb_dp.py
+49 -8
@@ -10,6 +10,7 @@ from typing import Optional, cast
10 import openai # use the official client for correctness check
11 import pytest
12 import pytest_asyncio
13 +import requests
14
15 from tests.utils import RemoteOpenAIServer
16 from tests.v1.test_utils import check_request_balancing
@@ -101,6 +102,8 @@ class MultinodeInternalLBServerManager:
102 sargs,
103 auto_port=False,
104 env_dict={
105 + "VLLM_SERVER_DEV_MODE":
106 + "1",
107 current_platform.device_control_env_var:
108 ",".join(
109 str(
@@ -214,7 +217,10 @@ class APIOnlyServerManager:
217 self.model_name,
218 api_server_args,
219 auto_port=False,
217 - env_dict={}) # No GPUs needed for API-only server
220 + env_dict={
221 + "VLLM_SERVER_DEV_MODE": "1",
222 + # No GPUs needed for API-only server
223 + })
224 server.__enter__()
225 print(f"API-only server started successfully with "
226 f"{self.api_server_count} API servers")
@@ -293,14 +299,21 @@ def default_server_args():
299
300
301 @pytest.fixture(scope="module", params=[1, 4])
296 -def servers(request, default_server_args):
302 +def server_manager(request, default_server_args):
303 api_server_count = request.param
298 - with MultinodeInternalLBServerManager(MODEL_NAME, DP_SIZE,
299 - api_server_count,
300 - default_server_args,
301 - DP_SIZE // NUM_NODES,
302 - TP_SIZE) as server_list:
303 - yield server_list
304 + server_manager = MultinodeInternalLBServerManager(MODEL_NAME, DP_SIZE,
305 + api_server_count,
306 + default_server_args,
307 + DP_SIZE // NUM_NODES,
308 + TP_SIZE)
309 +
310 + with server_manager:
311 + yield server_manager
312 +
313 +
314 +@pytest.fixture
315 +def servers(server_manager):
316 + return server_manager.servers
317
318
319 @pytest.fixture(scope="module", params=[1, 4])
@@ -331,6 +344,34 @@ async def api_only_client(api_only_servers: list[tuple[RemoteOpenAIServer,
344 yield client
345
346
347 +def _get_parallel_config(server: RemoteOpenAIServer):
348 + response = requests.get(server.url_for("server_info?config_format=json"))
349 + response.raise_for_status()
350 +
351 + vllm_config = response.json()["vllm_config"]
352 + return vllm_config["parallel_config"]
353 +
354 +
355 +def test_multinode_dp_server_info(server_manager):
356 + head_server = server_manager.servers[0][0]
357 + api_server_count = server_manager.api_server_count
358 +
359 + # Each request will hit one of the API servers
360 + # `n_reqs` is set so that there is a good chance each server
361 + # receives at least one request
362 + n_reqs = 2 * api_server_count * api_server_count
363 + parallel_configs = [
364 + _get_parallel_config(head_server) for _ in range(n_reqs)
365 + ]
366 + api_process_counts = [c["_api_process_count"] for c in parallel_configs]
367 + api_process_ranks = [c["_api_process_rank"] for c in parallel_configs]
368 +
369 + assert all(c == api_server_count
370 + for c in api_process_counts), api_process_counts
371 + assert all(0 <= r < api_server_count
372 + for r in api_process_ranks), api_process_ranks
373 +
374 +
375 @pytest.mark.asyncio
376 @pytest.mark.parametrize(
377 "model_name",
vllm/config/parallel.py
+25
@@ -193,6 +193,25 @@ class ParallelConfig:
193 not change by dcp, it simply reuse the GPUs of TP group, and tp_size
194 needs to be divisible by dcp_size."""
195
196 + _api_process_count: int = 1
197 + """
198 + The number of API processes initialized.
199 +
200 + Note:
201 + This is an internal config that is only valid for and
202 + should only be set by API server scale-out.
203 + """
204 +
205 + _api_process_rank: int = 0
206 + """
207 + The rank of this API process, or `-1` for engine core processes
208 + under API server scale-out.
209 +
210 + Note:
211 + This is an internal config that is only valid for and
212 + should only be set by API server scale-out.
213 + """
214 +
215 @property
216 def world_size_across_dp(self) -> int:
217 """world_size_across_dp is TPxPPxDP, it is the size of the world
@@ -428,6 +447,12 @@ class ParallelConfig:
447 if self.distributed_executor_backend is None and self.world_size == 1:
448 self.distributed_executor_backend = "uni"
449
450 + if not -1 <= self._api_process_rank < self._api_process_count:
451 + raise ValueError(
452 + "Invalid value of `_api_process_rank`. "
453 + f"Expected to be `-1` or `[0, {self._api_process_count})`, "
454 + f"but found: {self._api_process_rank}")
455 +
456 @property
457 def use_ray(self) -> bool:
458 return self.distributed_executor_backend == "ray" or (
vllm/engine/arg_utils.py
+8 -1
@@ -333,6 +333,8 @@ class EngineArgs:
333 enable_eplb: bool = ParallelConfig.enable_eplb
334 expert_placement_strategy: ExpertPlacementStrategy = \
335 ParallelConfig.expert_placement_strategy
336 + _api_process_count: int = ParallelConfig._api_process_count
337 + _api_process_rank: int = ParallelConfig._api_process_rank
338 num_redundant_experts: int = EPLBConfig.num_redundant_experts
339 eplb_window_size: int = EPLBConfig.window_size
340 eplb_step_interval: int = EPLBConfig.step_interval
@@ -952,7 +954,10 @@ class EngineArgs:
954 # Get the list of attributes of this dataclass.
955 attrs = [attr.name for attr in dataclasses.fields(cls)]
956 # Set the attributes from the parsed arguments.
955 - engine_args = cls(**{attr: getattr(args, attr) for attr in attrs})
957 + engine_args = cls(**{
958 + attr: getattr(args, attr)
959 + for attr in attrs if hasattr(args, attr)
960 + })
961 return engine_args
962
963 def create_model_config(self) -> ModelConfig:
@@ -1366,6 +1371,8 @@ class EngineArgs:
1371 worker_cls=self.worker_cls,
1372 worker_extension_cls=self.worker_extension_cls,
1373 decode_context_parallel_size=self.decode_context_parallel_size,
1374 + _api_process_count=self._api_process_count,
1375 + _api_process_rank=self._api_process_rank,
1376 )
1377
1378 speculative_config = self.create_speculative_config(
vllm/entrypoints/cli/serve.py
+6 -12
@@ -135,23 +135,20 @@ def run_headless(args: argparse.Namespace):
135 def run_multi_api_server(args: argparse.Namespace):
136
137 assert not args.headless
138 - num_api_servers = args.api_server_count
138 + num_api_servers: int = args.api_server_count
139 assert num_api_servers > 0
140
141 - orig_mm_processor_cache_gb = args.mm_processor_cache_gb
142 -
141 if num_api_servers > 1:
142 setup_multiprocess_prometheus()
143
146 - # Not compatible with API server scale-out
147 - args.mm_processor_cache_gb = 0
148 -
144 listen_address, sock = setup_server(args)
145
146 engine_args = vllm.AsyncEngineArgs.from_cli_args(args)
147 + engine_args._api_process_count = num_api_servers
148 + engine_args._api_process_rank = -1
149 +
150 usage_context = UsageContext.OPENAI_API_SERVER
151 vllm_config = engine_args.create_engine_config(usage_context=usage_context)
154 - model_config = vllm_config.model_config
152
153 if num_api_servers > 1:
154 if not envs.VLLM_USE_V1:
@@ -161,10 +158,6 @@ def run_multi_api_server(args: argparse.Namespace):
158 raise ValueError("VLLM_ALLOW_RUNTIME_LORA_UPDATING cannot be used "
159 "with api_server_count > 1")
160
164 - if model_config.is_multimodal_model and orig_mm_processor_cache_gb > 0:
165 - logger.warning("Multi-modal processor cache is disabled because "
166 - "it is not compatible with `api_server_count > 1`.")
167 -
161 executor_class = Executor.get_class(vllm_config)
162 log_stats = not engine_args.disable_log_stats
163
@@ -221,9 +214,10 @@ def run_api_server_worker_proc(listen_address,
214 client_config=None,
215 **uvicorn_kwargs) -> None:
216 """Entrypoint for individual API server worker processes."""
217 + client_config = client_config or {}
218 + server_index = client_config.get("client_index", 0)
219
220 # Set process title and add process-specific prefix to stdout and stderr.
226 - server_index = client_config.get("client_index", 0) if client_config else 0
221 set_process_title("APIServer", str(server_index))
222 decorate_logs()
223
vllm/entrypoints/openai/api_server.py
+29 -9
@@ -17,13 +17,14 @@ from argparse import Namespace
17 from collections.abc import AsyncGenerator, AsyncIterator, Awaitable
18 from contextlib import asynccontextmanager
19 from http import HTTPStatus
20 -from typing import Annotated, Any, Callable, Optional
20 +from typing import Annotated, Any, Callable, Literal, Optional
21
22 import prometheus_client
23 import pydantic
24 import regex as re
25 import uvloop
26 -from fastapi import APIRouter, Depends, FastAPI, Form, HTTPException, Request
26 +from fastapi import (APIRouter, Depends, FastAPI, Form, HTTPException, Query,
27 + Request)
28 from fastapi.exceptions import RequestValidationError
29 from fastapi.middleware.cors import CORSMiddleware
30 from fastapi.responses import JSONResponse, Response, StreamingResponse
@@ -166,6 +167,9 @@ async def build_async_engine_client(
167 # Context manager to handle engine_client lifecycle
168 # Ensures everything is shutdown and cleaned up on error/exit
169 engine_args = AsyncEngineArgs.from_cli_args(args)
170 + if client_config:
171 + engine_args._api_process_count = client_config.get("client_count", 1)
172 + engine_args._api_process_rank = client_config.get("client_index", 0)
173
174 if disable_frontend_multiprocessing is None:
175 disable_frontend_multiprocessing = bool(
@@ -209,8 +213,12 @@ async def build_async_engine_client_from_engine_args(
213
214 from vllm.v1.engine.async_llm import AsyncLLM
215 async_llm: Optional[AsyncLLM] = None
212 - client_count = client_config.pop("client_count") if client_config else 1
213 - client_index = client_config.pop("client_index") if client_config else 0
216 +
217 + # Don't mutate the input client_config
218 + client_config = dict(client_config) if client_config else {}
219 + client_count = client_config.pop("client_count", 1)
220 + client_index = client_config.pop("client_index", 0)
221 +
222 try:
223 async_llm = AsyncLLM.from_vllm_config(
224 vllm_config=vllm_config,
@@ -956,9 +964,22 @@ if envs.VLLM_SERVER_DEV_MODE:
964 logger.warning("SECURITY WARNING: Development endpoints are enabled! "
965 "This should NOT be used in production!")
966
967 + PydanticVllmConfig = pydantic.TypeAdapter(VllmConfig)
968 +
969 @router.get("/server_info")
960 - async def show_server_info(raw_request: Request):
961 - server_info = {"vllm_config": str(raw_request.app.state.vllm_config)}
970 + async def show_server_info(
971 + raw_request: Request,
972 + config_format: Annotated[Literal["text", "json"],
973 + Query()] = "text",
974 + ):
975 + vllm_config: VllmConfig = raw_request.app.state.vllm_config
976 + server_info = {
977 + "vllm_config":
978 + str(vllm_config)
979 + if config_format == "text" else PydanticVllmConfig.dump_python(
980 + vllm_config, mode="json", fallback=str)
981 + # fallback=str is needed to handle e.g. torch.dtype
982 + }
983 return JSONResponse(content=server_info)
984
985 @router.post("/reset_prefix_cache")
@@ -1856,8 +1877,6 @@ async def run_server_worker(listen_address,
1877 if args.tool_parser_plugin and len(args.tool_parser_plugin) > 3:
1878 ToolParserManager.import_tool_parser(args.tool_parser_plugin)
1879
1859 - server_index = client_config.get("client_index", 0) if client_config else 0
1860 -
1880 # Load logging config for uvicorn if specified
1881 log_config = load_log_config(args.log_config_file)
1882 if log_config is not None:
@@ -1873,7 +1892,8 @@ async def run_server_worker(listen_address,
1892 vllm_config = await engine_client.get_vllm_config()
1893 await init_app_state(engine_client, vllm_config, app.state, args)
1894
1876 - logger.info("Starting vLLM API server %d on %s", server_index,
1895 + logger.info("Starting vLLM API server %d on %s",
1896 + vllm_config.parallel_config._api_process_rank,
1897 listen_address)
1898 shutdown_task = await serve_http(
1899 app,
vllm/multimodal/cache.py
+2 -1
@@ -494,7 +494,8 @@ def _enable_processor_cache(
494
495 def _enable_ipc_cache(vllm_config: "VllmConfig") -> bool:
496 parallel_config = vllm_config.parallel_config
497 - supports_ipc_cache = (parallel_config.data_parallel_size == 1
497 + supports_ipc_cache = ((parallel_config._api_process_count == 1
498 + and parallel_config.data_parallel_size == 1)
499 or parallel_config.data_parallel_external_lb)
500
501 return supports_ipc_cache
vllm/v1/engine/core_client.py
+2 -1
@@ -437,7 +437,7 @@ class MPClient(EngineCoreClient):
437 self.engines_running = False
438
439 self.stats_update_address: Optional[str] = None
440 - if client_addresses is not None:
440 + if client_addresses:
441 # Engines are managed externally to this client.
442 input_address = client_addresses["input_address"]
443 output_address = client_addresses["output_address"]
@@ -774,6 +774,7 @@ class AsyncMPClient(MPClient):
774 client_addresses=client_addresses,
775 )
776
777 + self.client_count = client_count
778 self.client_index = client_index
779 self.outputs_queue = asyncio.Queue[Union[EngineCoreOutputs,
780 Exception]]()