A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 15, 17:15 UTC
[V1] AsyncLLM data parallel (#13923)
Signed-off-by: Nick Hill <nhill@redhat.com>
Nick Hill committed
Mar 27, 2025 at 16:14 UTC
15dac210f0e6b907f191911917238273042552ed
18 files changed
+726
-160
.buildkite/test-pipeline.yaml
+5
@@ -135,12 +135,14 @@ steps:
135
- examples/offline_inference/rlhf.py
136
- examples/offline_inference/rlhf_colocate.py
137
- tests/examples/offline_inference/data_parallel.py
138
+ - tests/v1/test_async_llm_dp.py
139
commands:
140
# test with tp=2 and external_dp=2
141
- VLLM_USE_V1=0 torchrun --nproc-per-node=4 distributed/test_torchrun_example.py
142
- torchrun --nproc-per-node=4 distributed/test_torchrun_example.py
143
# test with internal dp
144
- python3 ../examples/offline_inference/data_parallel.py
145
+ - TP_SIZE=2 DP_SIZE=2 pytest -v -s v1/test_async_llm_dp.py
146
- pytest -v -s distributed/test_utils.py
147
- pytest -v -s compile/test_basic_correctness.py
148
- pytest -v -s distributed/test_pynccl.py
@@ -514,7 +516,10 @@ steps:
516
- vllm/worker/worker.py
517
- vllm/worker/model_runner.py
518
- entrypoints/llm/test_collective_rpc.py
519
+ - tests/v1/test_async_llm_dp.py
520
+ - vllm/v1/engine/
521
commands:
522
+ - TP_SIZE=1 DP_SIZE=2 pytest -v -s v1/test_async_llm_dp.py
523
- VLLM_ENABLE_V1_MULTIPROCESSING=0 pytest -v -s entrypoints/llm/test_collective_rpc.py
524
- pytest -v -s ./compile/test_basic_correctness.py
525
- pytest -v -s ./compile/test_wrapper.py
examples/offline_inference/data_parallel.py
+15
-7
@@ -28,6 +28,7 @@ Multi-node:
28
--master-port=13345
29
"""
30
import os
31
+from time import sleep
32
33
from vllm import LLM, SamplingParams
34
from vllm.utils import get_open_port
@@ -36,14 +37,13 @@ from vllm.utils import get_open_port
37
def main(model, dp_size, local_dp_rank, global_dp_rank, dp_master_ip,
38
dp_master_port, GPUs_per_dp_rank):
39
os.environ["VLLM_DP_RANK"] = str(global_dp_rank)
40
+ os.environ["VLLM_DP_RANK_LOCAL"] = str(local_dp_rank)
41
os.environ["VLLM_DP_SIZE"] = str(dp_size)
42
os.environ["VLLM_DP_MASTER_IP"] = dp_master_ip
43
os.environ["VLLM_DP_MASTER_PORT"] = str(dp_master_port)
42
- # set devices for each dp_rank
43
- os.environ["CUDA_VISIBLE_DEVICES"] = ",".join(
44
- str(i)
45
- for i in range(local_dp_rank * GPUs_per_dp_rank, (local_dp_rank + 1) *
46
- GPUs_per_dp_rank))
44
+
45
+ # CUDA_VISIBLE_DEVICES for each DP rank is set automatically inside the
46
+ # engine processes.
47
48
# Sample prompts.
49
prompts = [
@@ -90,6 +90,9 @@ def main(model, dp_size, local_dp_rank, global_dp_rank, dp_master_ip,
90
print(f"DP rank {global_dp_rank}, Prompt: {prompt!r}, "
91
f"Generated text: {generated_text!r}")
92
93
+ # Give engines time to pause their processing loops before exiting.
94
+ sleep(1)
95
+
96
97
if __name__ == "__main__":
98
import argparse
@@ -152,8 +155,13 @@ if __name__ == "__main__":
155
procs.append(proc)
156
exit_code = 0
157
for proc in procs:
155
- proc.join()
156
- if proc.exitcode:
158
+ proc.join(timeout=300)
159
+ if proc.exitcode is None:
160
+ print(f"Killing process {proc.pid} that "
161
+ f"didn't stop within 5 minutes.")
162
+ proc.kill()
163
+ exit_code = 1
164
+ elif proc.exitcode:
165
exit_code = proc.exitcode
166
167
exit(exit_code)
tests/v1/engine/test_engine_core_client.py
+4
-4
@@ -167,11 +167,11 @@ def test_engine_core_client(monkeypatch: pytest.MonkeyPatch,
167
168
core_client: SyncMPClient = client
169
170
- result = core_client._call_utility("echo", "testarg")
170
+ result = core_client.call_utility("echo", "testarg")
171
assert result == "testarg"
172
173
with pytest.raises(Exception) as e_info:
174
- core_client._call_utility("echo", None, "help!")
174
+ core_client.call_utility("echo", None, "help!")
175
176
assert str(e_info.value) == "Call to echo method failed: help!"
177
@@ -238,10 +238,10 @@ async def test_engine_core_client_asyncio(monkeypatch: pytest.MonkeyPatch):
238
239
core_client: AsyncMPClient = client
240
241
- result = await core_client._call_utility_async("echo", "testarg")
241
+ result = await core_client.call_utility_async("echo", "testarg")
242
assert result == "testarg"
243
244
with pytest.raises(Exception) as e_info:
245
- await core_client._call_utility_async("echo", None, "help!")
245
+ await core_client.call_utility_async("echo", None, "help!")
246
247
assert str(e_info.value) == "Call to echo method failed: help!"
tests/v1/test_async_llm_dp.py
new
+109
@@ -0,0 +1,109 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+
3
+import asyncio
4
+import os
5
+from contextlib import ExitStack
6
+from typing import Optional
7
+
8
+import pytest
9
+
10
+from vllm import SamplingParams
11
+from vllm.engine.arg_utils import AsyncEngineArgs
12
+from vllm.inputs import PromptType
13
+from vllm.platforms import current_platform
14
+from vllm.sampling_params import RequestOutputKind
15
+from vllm.v1.engine.async_llm import AsyncLLM
16
+from vllm.v1.engine.core_client import DPAsyncMPClient
17
+
18
+engine_args = AsyncEngineArgs(
19
+ model="ibm-research/PowerMoE-3b",
20
+ enforce_eager=True,
21
+ disable_log_requests=True,
22
+ tensor_parallel_size=int(os.getenv("TP_SIZE", 1)),
23
+ data_parallel_size=int(os.getenv("DP_SIZE", 2)),
24
+)
25
+
26
+if not current_platform.supports_v1(engine_args.create_model_config()):
27
+ pytest.skip(reason="Requires V1-supporting platform.",
28
+ allow_module_level=True)
29
+
30
+
31
+async def generate(engine: AsyncLLM,
32
+ request_id: str,
33
+ prompt: PromptType,
34
+ output_kind: RequestOutputKind,
35
+ max_tokens: int,
36
+ prompt_logprobs: Optional[int] = None) -> tuple[int, str]:
37
+ # Ensure generate doesn't complete too fast for cancellation test.
38
+ await asyncio.sleep(0.2)
39
+
40
+ count = 0
41
+ sampling_params = SamplingParams(max_tokens=max_tokens,
42
+ ignore_eos=True,
43
+ output_kind=output_kind,
44
+ temperature=0,
45
+ prompt_logprobs=prompt_logprobs)
46
+ async for out in engine.generate(request_id=request_id,
47
+ prompt=prompt,
48
+ sampling_params=sampling_params):
49
+
50
+ num_tokens = len(out.outputs[0].token_ids)
51
+ if output_kind == RequestOutputKind.DELTA:
52
+ count += num_tokens
53
+ else:
54
+ count = num_tokens
55
+
56
+ await asyncio.sleep(0.)
57
+
58
+ return count, request_id
59
+
60
+
61
+@pytest.mark.parametrize(
62
+ "output_kind", [RequestOutputKind.DELTA, RequestOutputKind.FINAL_ONLY])
63
+@pytest.mark.asyncio
64
+async def test_load(output_kind: RequestOutputKind):
65
+
66
+ with ExitStack() as after:
67
+
68
+ prompt = "This is a test of data parallel"
69
+
70
+ engine = AsyncLLM.from_engine_args(engine_args)
71
+ after.callback(engine.shutdown)
72
+
73
+ NUM_REQUESTS = 100
74
+ NUM_EXPECTED_TOKENS = 10
75
+
76
+ request_ids = [f"request-{i}" for i in range(NUM_REQUESTS)]
77
+
78
+ # Create concurrent requests.
79
+ tasks = []
80
+ for request_id in request_ids:
81
+ tasks.append(
82
+ asyncio.create_task(
83
+ generate(engine, request_id, prompt, output_kind,
84
+ NUM_EXPECTED_TOKENS)))
85
+
86
+ # Confirm that we got all the EXPECTED tokens from the requests.
87
+ done, pending = await asyncio.wait(tasks,
88
+ return_when=asyncio.FIRST_EXCEPTION)
89
+ for task in pending:
90
+ task.cancel()
91
+ for task in done:
92
+ num_generated_tokens, request_id = await task
93
+ assert num_generated_tokens == NUM_EXPECTED_TOKENS, (
94
+ f"{request_id} generated {num_generated_tokens} but "
95
+ f"expected {NUM_EXPECTED_TOKENS}")
96
+
97
+ assert not engine.output_processor.has_unfinished_requests()
98
+
99
+ # testing internals here which may break
100
+ core_client: DPAsyncMPClient = engine.engine_core
101
+ # the engines only synchronize stopping every N steps so
102
+ # allow a small amount of time here.
103
+ for _ in range(10):
104
+ if core_client.num_engines_running == 0:
105
+ break
106
+ await asyncio.sleep(0.5)
107
+
108
+ assert core_client.num_engines_running == 0
109
+ assert not core_client.reqs_in_flight
vllm/config.py
+16
-5
@@ -40,7 +40,8 @@ from vllm.transformers_utils.config import (
40
from vllm.transformers_utils.s3_utils import S3Model
41
from vllm.transformers_utils.utils import is_s3, maybe_model_redirect
42
from vllm.utils import (GiB_bytes, LayerBlockType, cuda_device_count_stateless,
43
- get_cpu_memory, random_uuid, resolve_obj_by_qualname)
43
+ get_cpu_memory, get_open_port, random_uuid,
44
+ resolve_obj_by_qualname)
45
46
if TYPE_CHECKING:
47
from ray.util.placement_group import PlacementGroup
@@ -1389,6 +1390,8 @@ class ParallelConfig:
1390
tensor_parallel_size: int = 1 # Number of tensor parallel groups.
1391
data_parallel_size: int = 1 # Number of data parallel groups.
1392
data_parallel_rank: int = 0 # Rank of the data parallel group.
1393
+ # Local rank of the data parallel group, defaults to global rank.
1394
+ data_parallel_rank_local: Optional[int] = None
1395
# IP of the data parallel master.
1396
data_parallel_master_ip: str = "127.0.0.1"
1397
data_parallel_master_port: int = 29500 # Port of the data parallel master.
@@ -1493,10 +1496,18 @@ class ParallelConfig:
1496
self.world_size = self.pipeline_parallel_size * \
1497
self.tensor_parallel_size
1498
1496
- self.data_parallel_size = envs.VLLM_DP_SIZE
1497
- self.data_parallel_rank = envs.VLLM_DP_RANK
1498
- self.data_parallel_master_ip = envs.VLLM_DP_MASTER_IP
1499
- self.data_parallel_master_port = envs.VLLM_DP_MASTER_PORT
1499
+ if self.data_parallel_size > 1:
1500
+ # Data parallel was specified in the engine args.
1501
+ self.data_parallel_master_port = get_open_port()
1502
+ # TODO multi-node
1503
+ else:
1504
+ # Otherwise fall back to env vars (e.g. for offline SPMD case).
1505
+ self.data_parallel_size = envs.VLLM_DP_SIZE
1506
+ self.data_parallel_rank = envs.VLLM_DP_RANK
1507
+ self.data_parallel_rank_local = envs.VLLM_DP_RANK_LOCAL
1508
+ self.data_parallel_master_ip = envs.VLLM_DP_MASTER_IP
1509
+ self.data_parallel_master_port = envs.VLLM_DP_MASTER_PORT
1510
+
1511
self.world_size_across_dp = self.world_size * self.data_parallel_size
1512
1513
if self.distributed_executor_backend == "external_launcher":
vllm/distributed/utils.py
+12
@@ -15,6 +15,8 @@ import torch
15
from torch.distributed import ProcessGroup, TCPStore
16
from torch.distributed.distributed_c10d import (Backend, PrefixStore,
17
_get_default_timeout,
18
+ _shutdown_backend,
19
+ _unregister_process_group,
20
is_nccl_available)
21
from torch.distributed.rendezvous import rendezvous
22
@@ -333,3 +335,13 @@ def stateless_init_torch_distributed_process_group(
335
pg._register_backend(device, backend_type, backend_class)
336
337
return pg
338
+
339
+
340
+def stateless_destroy_torch_distributed_process_group(
341
+ pg: ProcessGroup) -> None:
342
+ """
343
+ Destroy ProcessGroup returned by
344
+ stateless_init_torch_distributed_process_group().
345
+ """
346
+ _shutdown_backend(pg)
347
+ _unregister_process_group(pg.group_name)
vllm/engine/arg_utils.py
+10
@@ -114,6 +114,7 @@ class EngineArgs:
114
# number of P/D disaggregation (or other disaggregation) workers
115
pipeline_parallel_size: int = 1
116
tensor_parallel_size: int = 1
117
+ data_parallel_size: int = 1
118
enable_expert_parallel: bool = False
119
max_parallel_loading_workers: Optional[int] = None
120
block_size: Optional[int] = None
@@ -442,6 +443,14 @@ class EngineArgs:
443
type=int,
444
default=EngineArgs.tensor_parallel_size,
445
help='Number of tensor parallel replicas.')
446
+ parser.add_argument('--data-parallel-size',
447
+ '-dp',
448
+ type=int,
449
+ default=EngineArgs.data_parallel_size,
450
+ help='Number of data parallel replicas. '
451
+ 'MoE layers will be sharded according to the '
452
+ 'product of the tensor-parallel-size and '
453
+ 'data-parallel-size.')
454
parser.add_argument(
455
'--enable-expert-parallel',
456
action='store_true',
@@ -1359,6 +1368,7 @@ class EngineArgs:
1368
parallel_config = ParallelConfig(
1369
pipeline_parallel_size=self.pipeline_parallel_size,
1370
tensor_parallel_size=self.tensor_parallel_size,
1371
+ data_parallel_size=self.data_parallel_size,
1372
enable_expert_parallel=self.enable_expert_parallel,
1373
max_parallel_loading_workers=self.max_parallel_loading_workers,
1374
disable_custom_all_reduce=self.disable_custom_all_reduce,
vllm/envs.py
+8
@@ -2,6 +2,7 @@
2
3
import hashlib
4
import os
5
+import sys
6
import tempfile
7
from typing import TYPE_CHECKING, Any, Callable, Optional
8
@@ -95,6 +96,7 @@ if TYPE_CHECKING:
96
VLLM_CUDART_SO_PATH: Optional[str] = None
97
VLLM_USE_HPU_CONTIGUOUS_CACHE_FETCH: bool = True
98
VLLM_DP_RANK: int = 0
99
+ VLLM_DP_RANK_LOCAL: int = -1
100
VLLM_DP_SIZE: int = 1
101
VLLM_DP_MASTER_IP: str = ""
102
VLLM_DP_MASTER_PORT: int = 0
@@ -625,6 +627,12 @@ environment_variables: dict[str, Callable[[], Any]] = {
627
"VLLM_DP_RANK":
628
lambda: int(os.getenv("VLLM_DP_RANK", "0")),
629
630
+ # Rank of the process in the data parallel setting.
631
+ # Defaults to VLLM_DP_RANK when not set.
632
+ "VLLM_DP_RANK_LOCAL":
633
+ lambda: int(
634
+ os.getenv("VLLM_DP_RANK_LOCAL", sys.modules[__name__].VLLM_DP_RANK)),
635
+
636
# World size of the data parallel setting
637
"VLLM_DP_SIZE":
638
lambda: int(os.getenv("VLLM_DP_SIZE", "1")),
vllm/utils.py
+9
-5
@@ -578,7 +578,7 @@ def get_open_port() -> int:
578
dp_port = envs.VLLM_DP_MASTER_PORT
579
while True:
580
port = _get_open_port()
581
- if port >= dp_port and port < dp_port + 10:
581
+ if dp_port <= port < dp_port + 10:
582
continue
583
return port
584
return _get_open_port()
@@ -2176,11 +2176,11 @@ def make_zmq_socket(
2176
if socket_type == zmq.constants.PULL:
2177
socket.setsockopt(zmq.constants.RCVHWM, 0)
2178
socket.setsockopt(zmq.constants.RCVBUF, buf_size)
2179
- socket.connect(path)
2179
+ socket.bind(path)
2180
elif socket_type == zmq.constants.PUSH:
2181
socket.setsockopt(zmq.constants.SNDHWM, 0)
2182
socket.setsockopt(zmq.constants.SNDBUF, buf_size)
2183
- socket.bind(path)
2183
+ socket.connect(path)
2184
else:
2185
raise ValueError(f"Unknown Socket Type: {socket_type}")
2186
@@ -2188,7 +2188,11 @@ def make_zmq_socket(
2188
2189
2190
@contextlib.contextmanager
2191
-def zmq_socket_ctx(path: str, socket_type: Any) -> Iterator[zmq.Socket]:
2191
+def zmq_socket_ctx(
2192
+ path: str,
2193
+ socket_type: Any,
2194
+ linger: int = 0,
2195
+) -> Iterator[zmq.Socket]:
2196
"""Context manager for a ZMQ socket"""
2197
2198
ctx = zmq.Context() # type: ignore[attr-defined]
@@ -2199,7 +2203,7 @@ def zmq_socket_ctx(path: str, socket_type: Any) -> Iterator[zmq.Socket]:
2203
logger.debug("Got Keyboard Interrupt.")
2204
2205
finally:
2202
- ctx.destroy(linger=0)
2206
+ ctx.destroy(linger=linger)
2207
2208
2209
def is_in_ray_actor():
vllm/v1/core/sched/scheduler.py
+15
-2
@@ -37,9 +37,10 @@ class Scheduler(SchedulerInterface):
37
cache_config: CacheConfig,
38
lora_config: Optional[LoRAConfig],
39
speculative_config: Optional[SpeculativeConfig],
40
- log_stats: bool,
40
structured_output_manager: StructuredOutputManager,
41
mm_registry: MultiModalRegistry = MULTIMODAL_REGISTRY,
42
+ include_finished_set: bool = False,
43
+ log_stats: bool = False,
44
) -> None:
45
self.scheduler_config = scheduler_config
46
self.cache_config = cache_config
@@ -48,6 +49,12 @@ class Scheduler(SchedulerInterface):
49
self.log_stats = log_stats
50
self.structured_output_manager = structured_output_manager
51
52
+ # include_finished_set controls whether a separate set of finished
53
+ # request ids should be included in the EngineCoreOutputs returned
54
+ # by update_from_outputs(). This is currently used in the multi-engine
55
+ # case to track request lifetimes efficiently.
56
+ self.include_finished_set = include_finished_set
57
+
58
# Scheduling constraints.
59
self.max_num_running_reqs = self.scheduler_config.max_num_seqs
60
self.max_num_scheduled_tokens = \
@@ -663,10 +670,16 @@ class Scheduler(SchedulerInterface):
670
new_running.append(request)
671
672
self.running = new_running
666
- return EngineCoreOutputs(
673
+ engine_core_outputs = EngineCoreOutputs(
674
outputs=outputs,
675
scheduler_stats=self.make_stats(),
676
)
677
+ if self.include_finished_set:
678
+ #TODO currently sending duplicates here, improve this
679
+ engine_core_outputs.finished_requests = (
680
+ scheduler_output.finished_req_ids | self.finished_req_ids)
681
+
682
+ return engine_core_outputs
683
684
def add_request(self, request: Request) -> None:
685
self.waiting.append(request)
vllm/v1/engine/__init__.py
+8
-1
@@ -128,12 +128,18 @@ class EngineCoreOutputs(
128
#NOTE(Nick): We could consider ways to make this more compact,
129
# e.g. columnwise layout
130
131
+ engine_index: int = 0
132
+
133
# [num_reqs]
134
outputs: list[EngineCoreOutput] = []
135
scheduler_stats: Optional[SchedulerStats] = None
136
timestamp: float = 0.0
137
138
utility_output: Optional[UtilityOutput] = None
139
+ finished_requests: Optional[set[str]] = None
140
+
141
+ # In DP case, used to signal that the engine is paused.
142
+ engine_paused: bool = False
143
144
def __post_init__(self):
145
if self.timestamp == 0.0:
@@ -147,4 +153,5 @@ class EngineCoreRequestType(enum.Enum):
153
"""
154
ADD = b'\x00'
155
ABORT = b'\x01'
150
- UTILITY = b'\x02'
156
+ START_DP = b'\x02'
157
+ UTILITY = b'\x03'
vllm/v1/engine/async_llm.py
+16
-7
@@ -66,11 +66,17 @@ class AsyncLLM(EngineClient):
66
67
self.log_requests = log_requests
68
self.log_stats = log_stats
69
- self.stat_loggers: list[StatLoggerBase] = []
69
+
70
+ # Set up stat loggers; independent set for each DP rank.
71
+ self.stat_loggers: list[list[StatLoggerBase]] = []
72
if self.log_stats:
71
- if logger.isEnabledFor(logging.INFO):
72
- self.stat_loggers.append(LoggingStatLogger())
73
- self.stat_loggers.append(PrometheusStatLogger(vllm_config))
73
+ for i in range(vllm_config.parallel_config.data_parallel_size):
74
+ loggers: list[StatLoggerBase] = []
75
+ if logger.isEnabledFor(logging.INFO):
76
+ loggers.append(LoggingStatLogger(engine_index=i))
77
+ loggers.append(
78
+ PrometheusStatLogger(vllm_config, engine_index=i))
79
+ self.stat_loggers.append(loggers)
80
81
# Tokenizer (+ ensure liveness if running in another process).
82
self.tokenizer = init_tokenizer_from_configs(
@@ -329,6 +335,7 @@ class AsyncLLM(EngineClient):
335
# TODO(rob): make into a coroutine and launch it in
336
# background thread once Prometheus overhead is non-trivial.
337
self._record_stats(
338
+ engine_index=outputs.engine_index,
339
scheduler_stats=outputs.scheduler_stats,
340
iteration_stats=iteration_stats,
341
)
@@ -350,12 +357,13 @@ class AsyncLLM(EngineClient):
357
self,
358
scheduler_stats: Optional[SchedulerStats],
359
iteration_stats: Optional[IterationStats],
360
+ engine_index: int = 0,
361
):
362
if not self.log_stats:
363
return
364
365
assert scheduler_stats is not None
358
- for stat_logger in self.stat_loggers:
366
+ for stat_logger in self.stat_loggers[engine_index]:
367
stat_logger.record(scheduler_stats=scheduler_stats,
368
iteration_stats=iteration_stats)
369
@@ -393,8 +401,9 @@ class AsyncLLM(EngineClient):
401
scheduler_outputs=None,
402
model_output=None,
403
) -> None:
396
- for stat_logger in self.stat_loggers:
397
- stat_logger.log()
404
+ for loggers in self.stat_loggers:
405
+ for stat_logger in loggers:
406
+ stat_logger.log()
407
408
async def check_health(self) -> None:
409
logger.debug("Called check_health.")
vllm/v1/engine/core.py
+183
-31
@@ -1,12 +1,13 @@
1
# SPDX-License-Identifier: Apache-2.0
2
-
2
+import os
3
import queue
4
import signal
5
+import sys
6
import threading
7
import time
8
from concurrent.futures import Future
9
from inspect import isclass, signature
9
-from multiprocessing.connection import Connection
10
+from logging import DEBUG
11
from typing import Any, Optional
12
13
import msgspec
@@ -14,7 +15,9 @@ import psutil
15
import zmq
16
import zmq.asyncio
17
17
-from vllm.config import VllmConfig
18
+from vllm.config import ParallelConfig, VllmConfig
19
+from vllm.distributed import stateless_destroy_torch_distributed_process_group
20
+from vllm.executor.multiproc_worker_utils import _add_prefix
21
from vllm.logger import init_logger
22
from vllm.lora.request import LoRARequest
23
from vllm.transformers_utils.config import (
@@ -91,6 +94,8 @@ class EngineCore:
94
cache_config=vllm_config.cache_config,
95
lora_config=vllm_config.lora_config,
96
speculative_config=vllm_config.speculative_config,
97
+ include_finished_set=vllm_config.parallel_config.data_parallel_size
98
+ > 1,
99
log_stats=self.log_stats,
100
structured_output_manager=self.structured_output_manager,
101
)
@@ -283,10 +288,10 @@ class EngineCoreProc(EngineCore):
288
self,
289
input_path: str,
290
output_path: str,
286
- ready_pipe: Connection,
291
vllm_config: VllmConfig,
292
executor_class: type[Executor],
293
log_stats: bool,
294
+ engine_index: int = 0,
295
):
296
super().__init__(vllm_config, executor_class, log_stats)
297
@@ -302,14 +307,20 @@ class EngineCoreProc(EngineCore):
307
args=(input_path, ),
308
daemon=True).start()
309
threading.Thread(target=self.process_output_socket,
305
- args=(output_path, ),
310
+ args=(output_path, engine_index),
311
daemon=True).start()
312
308
- # Send Readiness signal to EngineClient.
309
- ready_pipe.send({"status": "READY"})
313
+ self.global_unfinished_reqs = False
314
+
315
+ self.step_fn = (self.step if self.batch_queue is None else
316
+ self.step_with_batch_queue)
317
318
@staticmethod
312
- def run_engine_core(*args, **kwargs):
319
+ def run_engine_core(*args,
320
+ dp_rank: int = 0,
321
+ local_dp_rank: int = 0,
322
+ ready_pipe,
323
+ **kwargs):
324
"""Launch EngineCore busy loop in background process."""
325
326
# Signal handler used for graceful termination.
@@ -331,9 +342,21 @@ class EngineCoreProc(EngineCore):
342
signal.signal(signal.SIGINT, signal_handler)
343
344
parent_process = psutil.Process().parent()
334
- engine_core = None
345
+ engine_core: Optional[EngineCoreProc] = None
346
try:
336
- engine_core = EngineCoreProc(*args, **kwargs)
347
+ parallel_config: ParallelConfig = kwargs[
348
+ "vllm_config"].parallel_config
349
+ if parallel_config.data_parallel_size > 1:
350
+ # Set data parallel rank for this engine process.
351
+ parallel_config.data_parallel_rank = dp_rank
352
+ parallel_config.data_parallel_rank_local = local_dp_rank
353
+ engine_core = DPEngineCoreProc(*args, **kwargs)
354
+ else:
355
+ engine_core = EngineCoreProc(*args, **kwargs)
356
+
357
+ # Send Readiness signal to EngineClient.
358
+ ready_pipe.send({"status": "READY"})
359
+
360
engine_core.run_busy_loop()
361
362
except SystemExit:
@@ -351,28 +374,44 @@ class EngineCoreProc(EngineCore):
374
def run_busy_loop(self):
375
"""Core busy loop of the EngineCore."""
376
354
- step_fn = (self.step
355
- if self.batch_queue is None else self.step_with_batch_queue)
356
-
377
# Loop until process is sent a SIGINT or SIGTERM
378
while True:
379
# 1) Poll the input queue until there is work to do.
360
- while not self.scheduler.has_requests():
361
- logger.debug("EngineCore busy loop waiting.")
362
- req = self.input_queue.get()
363
- self._handle_client_request(*req)
364
-
365
- # 2) Handle any new client requests.
366
- while not self.input_queue.empty():
367
- req = self.input_queue.get_nowait()
368
- self._handle_client_request(*req)
369
-
370
- # 3) Step the engine core.
371
- outputs = step_fn()
372
-
373
- # 4) Put EngineCoreOutputs into the output queue.
374
- if outputs is not None:
375
- self.output_queue.put_nowait(outputs)
380
+ self._process_input_queue()
381
+ # 2) Step the engine core and return the outputs.
382
+ self._process_engine_step()
383
+
384
+ def _process_input_queue(self):
385
+ """Exits when an engine step needs to be performed."""
386
+
387
+ waited = False
388
+ while not self.global_unfinished_reqs and not (
389
+ self.scheduler.has_requests()):
390
+ if logger.isEnabledFor(DEBUG) and self.input_queue.empty():
391
+ logger.debug("EngineCore waiting for work.")
392
+ waited = True
393
+ req = self.input_queue.get()
394
+ self._handle_client_request(*req)
395
+
396
+ if waited:
397
+ logger.debug(
398
+ "EngineCore loop active - local unfinished: %s, finished: %s.",
399
+ self.scheduler.has_unfinished_requests(),
400
+ self.scheduler.has_finished_requests())
401
+
402
+ # Handle any more client requests.
403
+ while not self.input_queue.empty():
404
+ req = self.input_queue.get_nowait()
405
+ self._handle_client_request(*req)
406
+
407
+ def _process_engine_step(self):
408
+ """Called only when there are unfinished local requests."""
409
+
410
+ # Step the engine core.
411
+ outputs = self.step_fn()
412
+ # Put EngineCoreOutputs into the output queue.
413
+ if outputs is not None:
414
+ self.output_queue.put_nowait(outputs)
415
416
def _handle_client_request(self, request_type: EngineCoreRequestType,
417
request: Any) -> None:
@@ -382,6 +421,10 @@ class EngineCoreProc(EngineCore):
421
self.add_request(request)
422
elif request_type == EngineCoreRequestType.ABORT:
423
self.abort_requests(request)
424
+ elif request_type == EngineCoreRequestType.START_DP:
425
+ if not self.global_unfinished_reqs:
426
+ logger.debug("EngineCore starting idle loop.")
427
+ self.global_unfinished_reqs = True
428
elif request_type == EngineCoreRequestType.UTILITY:
429
call_id, method_name, args = request
430
output = UtilityOutput(call_id)
@@ -432,7 +475,7 @@ class EngineCoreProc(EngineCore):
475
# Push to input queue for core busy loop.
476
self.input_queue.put_nowait((request_type, request))
477
435
- def process_output_socket(self, output_path: str):
478
+ def process_output_socket(self, output_path: str, engine_index: int):
479
"""Output socket IO thread."""
480
481
# Msgpack serialization encoding.
@@ -443,5 +486,114 @@ class EngineCoreProc(EngineCore):
486
with zmq_socket_ctx(output_path, zmq.constants.PUSH) as socket:
487
while True:
488
outputs = self.output_queue.get()
489
+ outputs.engine_index = engine_index
490
encoder.encode_into(outputs, buffer)
447
- socket.send_multipart((buffer, ), copy=False)
491
+ socket.send(buffer, copy=False)
492
+
493
+
494
+ENGINE_PAUSED_OUTPUTS = EngineCoreOutputs(engine_paused=True)
495
+
496
+
497
+class DPEngineCoreProc(EngineCoreProc):
498
+ """ZMQ-wrapper for running EngineCore in background process
499
+ in a data parallel context."""
500
+
501
+ def __init__(
502
+ self,
503
+ input_path: str,
504
+ output_path: str,
505
+ vllm_config: VllmConfig,
506
+ executor_class: type[Executor],
507
+ log_stats: bool,
508
+ ):
509
+ # Add process-specific prefix to stdout and stderr before
510
+ # we initialize the engine.
511
+ from multiprocessing import current_process
512
+ process_name = current_process().name
513
+ pid = os.getpid()
514
+ _add_prefix(sys.stdout, process_name, pid)
515
+ _add_prefix(sys.stderr, process_name, pid)
516
+
517
+ dp_size = vllm_config.parallel_config.data_parallel_size
518
+ dp_rank = vllm_config.parallel_config.data_parallel_rank
519
+ local_dp_rank = vllm_config.parallel_config.data_parallel_rank_local
520
+
521
+ assert dp_size > 1
522
+ assert 0 <= local_dp_rank <= dp_rank < dp_size
523
+
524
+ from vllm.platforms import current_platform
525
+ if current_platform.is_cuda_alike():
526
+ from vllm.platforms.cuda import device_id_to_physical_device_id
527
+ tp_size = vllm_config.parallel_config.tensor_parallel_size
528
+ os.environ["CUDA_VISIBLE_DEVICES"] = ",".join(
529
+ str(device_id_to_physical_device_id(i))
530
+ for i in range(local_dp_rank * tp_size, (local_dp_rank + 1) *
531
+ tp_size))
532
+
533
+ self.dp_group = vllm_config.parallel_config.stateless_init_dp_group()
534
+
535
+ # Initialize the engine after setting up environment.
536
+ super().__init__(input_path, output_path, vllm_config, executor_class,
537
+ log_stats, dp_rank)
538
+
539
+ # Counts forward-passes of the model so that we can synchronize
540
+ # finished with DP peers every N steps.
541
+ self.counter = 0
542
+
543
+ def shutdown(self):
544
+ super().shutdown()
545
+ if dp_group := getattr(self, "dp_group", None):
546
+ stateless_destroy_torch_distributed_process_group(dp_group)
547
+
548
+ def run_busy_loop(self):
549
+ """Core busy loop of the EngineCore for data parallel case."""
550
+
551
+ # Loop until process is sent a SIGINT or SIGTERM
552
+ while True:
553
+ # 1) Poll the input queue until there is work to do.
554
+ self._process_input_queue()
555
+
556
+ local_unfinished_reqs = self.scheduler.has_unfinished_requests()
557
+
558
+ if local_unfinished_reqs:
559
+ # 2) Step the engine core.
560
+ self._process_engine_step()
561
+
562
+ # Check if we have now finished all requests.
563
+ local_unfinished_reqs = (
564
+ self.scheduler.has_unfinished_requests())
565
+ else:
566
+ if self.scheduler.has_finished_requests():
567
+ # There are no unfinished requests, but there are some
568
+ # finished requests remaining to be removed from the
569
+ # batch state. This engine step won't perform a forward
570
+ # pass but will flush the finished requests to ensure
571
+ # up-to-date state is returned in the engine outputs.
572
+ self._process_engine_step()
573
+
574
+ if not self.global_unfinished_reqs:
575
+ # All engines are idle.
576
+ continue
577
+
578
+ # There must be unfinished requests in DP peers, run a
579
+ # dummy forward pass.
580
+ self.execute_dummy_batch()
581
+
582
+ # 3) All-reduce operation to determine global unfinished reqs.
583
+ self.global_unfinished_reqs = self._has_global_unfinished_reqs(
584
+ local_unfinished_reqs)
585
+
586
+ if not self.global_unfinished_reqs:
587
+ # Notify client that we are pausing the loop.
588
+ self.output_queue.put_nowait(ENGINE_PAUSED_OUTPUTS)
589
+
590
+ def _has_global_unfinished_reqs(self, local_unfinished: bool) -> bool:
591
+
592
+ # Optimization - only perform finish-sync all-reduce every 16 steps.
593
+ self.counter += 1
594
+ if self.counter != 16:
595
+ return True
596
+ self.counter = 0
597
+
598
+ return ParallelConfig.has_unfinished_dp(self.dp_group,
599
+ local_unfinished)
vllm/v1/engine/core_client.py
+266
-68
@@ -8,10 +8,11 @@ import threading
8
import uuid
9
import weakref
10
from abc import ABC, abstractmethod
11
+from collections.abc import Awaitable, Sequence
12
from concurrent.futures import Future
12
-from dataclasses import dataclass
13
+from dataclasses import dataclass, field
14
from threading import Thread
14
-from typing import Any, Optional, Union
15
+from typing import Any, Callable, Optional, Union
16
17
import zmq
18
import zmq.asyncio
@@ -60,6 +61,9 @@ class EngineCoreClient(ABC):
61
"is not currently supported.")
62
63
if multiprocess_mode and asyncio_mode:
64
+ if vllm_config.parallel_config.data_parallel_size > 1:
65
+ return DPAsyncMPClient(vllm_config, executor_class, log_stats)
66
+
67
return AsyncMPClient(vllm_config, executor_class, log_stats)
68
69
if multiprocess_mode and not asyncio_mode:
@@ -207,28 +211,74 @@ class InprocClient(EngineCoreClient):
211
return self.engine_core.pin_lora(lora_id)
212
213
214
+class CoreEngine:
215
+ """One per data parallel rank."""
216
+
217
+ def __init__(
218
+ self,
219
+ vllm_config: VllmConfig,
220
+ executor_class: type[Executor],
221
+ log_stats: bool,
222
+ ctx: Union[zmq.Context, zmq.asyncio.Context],
223
+ output_path: str,
224
+ index: int = 0,
225
+ local_dp_rank: int = 0,
226
+ ):
227
+ # Paths and sockets for IPC.
228
+ input_path = get_open_zmq_ipc_path()
229
+ self.input_socket = make_zmq_socket(ctx, input_path,
230
+ zmq.constants.PUSH)
231
+ try:
232
+ # Start EngineCore in background process.
233
+ self.proc_handle = BackgroundProcHandle(
234
+ input_path=input_path,
235
+ output_path=output_path,
236
+ process_name=f"EngineCore_{index}",
237
+ target_fn=EngineCoreProc.run_engine_core,
238
+ process_kwargs={
239
+ "vllm_config": vllm_config,
240
+ "dp_rank": index,
241
+ "local_dp_rank": local_dp_rank,
242
+ "executor_class": executor_class,
243
+ "log_stats": log_stats,
244
+ })
245
+
246
+ self.num_reqs_in_flight = 0
247
+ finally:
248
+ if not hasattr(self, "num_reqs_in_flight"):
249
+ # Ensure socket is closed if process fails to start.
250
+ self.close()
251
+
252
+ def send_multipart(self, msg_parts: Sequence):
253
+ return self.input_socket.send_multipart(msg_parts, copy=False)
254
+
255
+ def close(self):
256
+ if proc_handle := getattr(self, "proc_handle", None):
257
+ proc_handle.shutdown()
258
+ if socket := getattr(self, "input_socket", None):
259
+ socket.close(linger=0)
260
+
261
+
262
@dataclass
263
class BackgroundResources:
264
"""Used as a finalizer for clean shutdown, avoiding
265
circular reference back to the client object."""
266
215
- ctx: zmq.Context
267
+ ctx: Union[zmq.Context]
268
+ core_engines: list[CoreEngine] = field(default_factory=list)
269
output_socket: Optional[Union[zmq.Socket, zmq.asyncio.Socket]] = None
217
- input_socket: Optional[Union[zmq.Socket, zmq.asyncio.Socket]] = None
218
- proc_handle: Optional[BackgroundProcHandle] = None
270
shutdown_path: Optional[str] = None
271
272
def __call__(self):
273
"""Clean up background resources."""
274
224
- if self.proc_handle is not None:
225
- self.proc_handle.shutdown()
275
+ for core_engine in self.core_engines:
276
+ core_engine.close()
277
+
278
# ZMQ context termination can hang if the sockets
279
# aren't explicitly closed first.
280
if self.output_socket is not None:
281
self.output_socket.close(linger=0)
230
- if self.input_socket is not None:
231
- self.input_socket.close(linger=0)
282
if self.shutdown_path is not None:
283
# We must ensure that the sync output socket is
284
# closed cleanly in its own thread.
@@ -284,7 +334,7 @@ class MPClient(EngineCoreClient):
334
self.decoder = MsgpackDecoder(EngineCoreOutputs)
335
336
# ZMQ setup.
287
- sync_ctx = zmq.Context()
337
+ sync_ctx = zmq.Context(io_threads=2)
338
self.ctx = zmq.asyncio.Context(sync_ctx) if asyncio_mode else sync_ctx
339
340
# This will ensure resources created so far are closed
@@ -293,28 +343,38 @@ class MPClient(EngineCoreClient):
343
self.resources = BackgroundResources(ctx=sync_ctx)
344
self._finalizer = weakref.finalize(self, self.resources)
345
296
- # Paths for IPC.
346
+ # Paths and sockets for IPC.
347
self.output_path = get_open_zmq_ipc_path()
298
- input_path = get_open_zmq_ipc_path()
348
300
- # Start EngineCore in background process.
301
- self.resources.proc_handle = BackgroundProcHandle(
302
- input_path=input_path,
303
- output_path=self.output_path,
304
- process_name="EngineCore",
305
- target_fn=EngineCoreProc.run_engine_core,
306
- process_kwargs={
307
- "vllm_config": vllm_config,
308
- "executor_class": executor_class,
309
- "log_stats": log_stats,
310
- })
311
-
312
- # Create input socket.
313
- self.resources.input_socket = make_zmq_socket(self.ctx, input_path,
314
- zmq.constants.PUSH)
315
- self.input_socket = self.resources.input_socket
349
+ new_core_engine = lambda index, local_dp_rank=None: CoreEngine(
350
+ vllm_config, executor_class, log_stats, self.ctx, self.output_path,
351
+ index, local_dp_rank)
352
+
353
+ # Start engine core process(es).
354
+ self._init_core_engines(vllm_config, new_core_engine,
355
+ self.resources.core_engines)
356
+
357
+ # Wait for engine core process(es) to start.
358
+ for engine in self.resources.core_engines:
359
+ engine.proc_handle.wait_for_startup()
360
+
361
self.utility_results: dict[int, AnyFuture] = {}
362
363
+ def _init_core_engines(
364
+ self,
365
+ vllm_config: VllmConfig,
366
+ new_core_engine: Callable[[int, Optional[int]], CoreEngine],
367
+ core_engines: list[CoreEngine],
368
+ ) -> None:
369
+
370
+ # Default case - single core engine.
371
+ dp_rank = vllm_config.parallel_config.data_parallel_rank
372
+ local_dp_rank = vllm_config.parallel_config.data_parallel_rank_local
373
+ core_engine = new_core_engine(
374
+ dp_rank, local_dp_rank if local_dp_rank is not None else dp_rank)
375
+ core_engines.append(core_engine)
376
+ self.core_engine = core_engine
377
+
378
def shutdown(self):
379
self._finalizer()
380
@@ -370,7 +430,7 @@ class SyncMPClient(MPClient):
430
# shutdown signal, exit thread.
431
break
432
373
- (frame, ) = out_socket.recv_multipart(copy=False)
433
+ frame = out_socket.recv(copy=False)
434
outputs = decoder.decode(frame.buffer)
435
if outputs.utility_output:
436
_process_utility_output(outputs.utility_output,
@@ -391,18 +451,15 @@ class SyncMPClient(MPClient):
451
def get_output(self) -> EngineCoreOutputs:
452
return self.outputs_queue.get()
453
394
- def _send_input(self, request_type: EngineCoreRequestType,
395
- request: Any) -> None:
396
-
454
+ def _send_input(self, request_type: EngineCoreRequestType, request: Any):
455
# (RequestType, SerializedRequest)
456
msg = (request_type.value, self.encoder.encode(request))
399
- self.input_socket.send_multipart(msg, copy=False)
457
+ self.core_engine.send_multipart(msg)
458
401
- def _call_utility(self, method: str, *args) -> Any:
459
+ def call_utility(self, method: str, *args) -> Any:
460
call_id = uuid.uuid1().int >> 64
461
future: Future[Any] = Future()
462
self.utility_results[call_id] = future
405
-
463
self._send_input(EngineCoreRequestType.UTILITY,
464
(call_id, method, args))
465
@@ -419,34 +476,34 @@ class SyncMPClient(MPClient):
476
self._send_input(EngineCoreRequestType.ABORT, request_ids)
477
478
def profile(self, is_start: bool = True) -> None:
422
- self._call_utility("profile", is_start)
479
+ self.call_utility("profile", is_start)
480
481
def reset_prefix_cache(self) -> None:
425
- self._call_utility("reset_prefix_cache")
482
+ self.call_utility("reset_prefix_cache")
483
484
def add_lora(self, lora_request: LoRARequest) -> bool:
428
- return self._call_utility("add_lora", lora_request)
485
+ return self.call_utility("add_lora", lora_request)
486
487
def remove_lora(self, lora_id: int) -> bool:
431
- return self._call_utility("remove_lora", lora_id)
488
+ return self.call_utility("remove_lora", lora_id)
489
490
def list_loras(self) -> set[int]:
434
- return self._call_utility("list_loras")
491
+ return self.call_utility("list_loras")
492
493
def pin_lora(self, lora_id: int) -> bool:
437
- return self._call_utility("pin_lora", lora_id)
494
+ return self.call_utility("pin_lora", lora_id)
495
496
def sleep(self, level: int = 1) -> None:
440
- self._call_utility("sleep", level)
497
+ self.call_utility("sleep", level)
498
499
def wake_up(self) -> None:
443
- self._call_utility("wake_up")
500
+ self.call_utility("wake_up")
501
502
def is_sleeping(self) -> bool:
446
- return self._call_utility("is_sleeping")
503
+ return self.call_utility("is_sleeping")
504
505
def execute_dummy_batch(self) -> None:
449
- self._call_utility("execute_dummy_batch")
506
+ self.call_utility("execute_dummy_batch")
507
508
509
class AsyncMPClient(MPClient):
@@ -464,13 +521,21 @@ class AsyncMPClient(MPClient):
521
self.outputs_queue: Optional[asyncio.Queue[EngineCoreOutputs]] = None
522
self.queue_task: Optional[asyncio.Task] = None
523
467
- async def _start_output_queue_task(self):
524
+ self.outputs_handler: Optional[Callable[
525
+ [AsyncMPClient, EngineCoreOutputs], Awaitable[None]]] = None
526
+
527
+ def _ensure_output_queue_task(self):
528
+ if self.outputs_queue is not None:
529
+ return
530
+
531
# Perform IO in separate task to parallelize as much as possible.
532
# Avoid task having direct reference back to the client.
533
self.outputs_queue = asyncio.Queue()
534
decoder = self.decoder
535
utility_results = self.utility_results
536
outputs_queue = self.outputs_queue
537
+ output_handler = self.outputs_handler
538
+ _self_ref = weakref.ref(self) if output_handler else None
539
output_path = self.output_path
540
output_socket = make_zmq_socket(self.ctx, output_path,
541
zmq.constants.PULL)
@@ -483,34 +548,52 @@ class AsyncMPClient(MPClient):
548
if outputs.utility_output:
549
_process_utility_output(outputs.utility_output,
550
utility_results)
486
- else:
551
+ continue
552
+
553
+ if output_handler is not None:
554
+ assert _self_ref is not None
555
+ _self = _self_ref()
556
+ if not _self:
557
+ # Client has been garbage collected, abort.
558
+ return
559
+ await output_handler(_self, outputs)
560
+
561
+ if outputs.outputs or outputs.scheduler_stats:
562
outputs_queue.put_nowait(outputs)
563
564
self.queue_task = asyncio.create_task(process_outputs_socket(),
565
name="EngineCoreOutputQueueTask")
566
567
async def get_output_async(self) -> EngineCoreOutputs:
493
- if self.outputs_queue is None:
494
- await self._start_output_queue_task()
495
- assert self.outputs_queue is not None
568
+ self._ensure_output_queue_task()
569
+ assert self.outputs_queue is not None
570
return await self.outputs_queue.get()
571
572
async def _send_input(self, request_type: EngineCoreRequestType,
573
request: Any) -> None:
574
+ await self.core_engine.send_multipart(
575
+ (request_type.value, self.encoder.encode(request)))
576
501
- msg = (request_type.value, self.encoder.encode(request))
502
- await self.input_socket.send_multipart(msg, copy=False)
577
+ self._ensure_output_queue_task()
578
504
- if self.outputs_queue is None:
505
- await self._start_output_queue_task()
579
+ async def call_utility_async(self, method: str, *args) -> Any:
580
+ return await self._call_utility_async(method,
581
+ *args,
582
+ engine=self.core_engine)
583
507
- async def _call_utility_async(self, method: str, *args) -> Any:
584
+ async def _call_utility_async(
585
+ self,
586
+ method: str,
587
+ *args,
588
+ engine: CoreEngine,
589
+ ) -> Any:
590
call_id = uuid.uuid1().int >> 64
591
future = asyncio.get_running_loop().create_future()
592
self.utility_results[call_id] = future
511
- await self._send_input(EngineCoreRequestType.UTILITY,
512
- (call_id, method, args))
513
-
593
+ message = (EngineCoreRequestType.UTILITY.value,
594
+ self.encoder.encode((call_id, method, args)))
595
+ await engine.send_multipart(message)
596
+ self._ensure_output_queue_task()
597
return await future
598
599
async def add_request_async(self, request: EngineCoreRequest) -> None:
@@ -524,31 +607,146 @@ class AsyncMPClient(MPClient):
607
await self._send_input(EngineCoreRequestType.ABORT, request_ids)
608
609
async def profile_async(self, is_start: bool = True) -> None:
527
- await self._call_utility_async("profile", is_start)
610
+ await self.call_utility_async("profile", is_start)
611
612
async def reset_prefix_cache_async(self) -> None:
530
- await self._call_utility_async("reset_prefix_cache")
613
+ await self.call_utility_async("reset_prefix_cache")
614
615
async def sleep_async(self, level: int = 1) -> None:
533
- await self._call_utility_async("sleep", level)
616
+ await self.call_utility_async("sleep", level)
617
618
async def wake_up_async(self) -> None:
536
- await self._call_utility_async("wake_up")
619
+ await self.call_utility_async("wake_up")
620
621
async def is_sleeping_async(self) -> bool:
539
- return await self._call_utility_async("is_sleeping")
622
+ return await self.call_utility_async("is_sleeping")
623
624
async def execute_dummy_batch_async(self) -> None:
542
- await self._call_utility_async("execute_dummy_batch")
625
+ await self.call_utility_async("execute_dummy_batch")
626
627
async def add_lora_async(self, lora_request: LoRARequest) -> bool:
545
- return await self._call_utility_async("add_lora", lora_request)
628
+ return await self.call_utility_async("add_lora", lora_request)
629
630
async def remove_lora_async(self, lora_id: int) -> bool:
548
- return await self._call_utility_async("remove_lora", lora_id)
631
+ return await self.call_utility_async("remove_lora", lora_id)
632
633
async def list_loras_async(self) -> set[int]:
551
- return await self._call_utility_async("list_loras")
634
+ return await self.call_utility_async("list_loras")
635
636
async def pin_lora_async(self, lora_id: int) -> bool:
554
- return await self._call_utility_async("pin_lora", lora_id)
637
+ return await self.call_utility_async("pin_lora", lora_id)
638
+
639
+
640
+class DPAsyncMPClient(AsyncMPClient):
641
+ """Asyncio-compatible client for multi-proc, multi-engine (data parallel)
642
+ EngineCore."""
643
+
644
+ def __init__(self, vllm_config: VllmConfig, executor_class: type[Executor],
645
+ log_stats: bool):
646
+ super().__init__(vllm_config, executor_class, log_stats)
647
+
648
+ assert len(self.core_engines) > 1
649
+
650
+ # Control message used for triggering dp idle mode loop.
651
+ self.start_dp_msg = (EngineCoreRequestType.START_DP.value,
652
+ self.encoder.encode(None))
653
+
654
+ self.num_engines_running = 0
655
+ self.reqs_in_flight: dict[str, CoreEngine] = {}
656
+
657
+ self.outputs_handler = DPAsyncMPClient.process_engine_outputs # type: ignore[assignment]
658
+
659
+ def _init_core_engines(
660
+ self,
661
+ vllm_config: VllmConfig,
662
+ new_core_engine: Callable[[int, Optional[int]], CoreEngine],
663
+ core_engines: list[CoreEngine],
664
+ ) -> None:
665
+
666
+ # Launch a core engine for each data parallel rank.
667
+ dp_size = vllm_config.parallel_config.data_parallel_size
668
+ for i in range(dp_size):
669
+ # Multi-node not yet supported so local_dp_rank == dp_rank.
670
+ core_engines.append(new_core_engine(i, i))
671
+
672
+ self.core_engines = core_engines
673
+
674
+ async def call_utility_async(self, method: str, *args) -> Any:
675
+ # Only the result from the first engine is returned.
676
+ return (await asyncio.gather(*[
677
+ self._call_utility_async(method, *args, engine=engine)
678
+ for engine in self.core_engines
679
+ ]))[0]
680
+
681
+ async def add_request_async(self, request: EngineCoreRequest) -> None:
682
+ # NOTE: text prompt is not needed in the core engine as it has been
683
+ # tokenized.
684
+ request.prompt = None
685
+
686
+ msg = (EngineCoreRequestType.ADD.value, self.encoder.encode(request))
687
+
688
+ chosen_engine = self.get_core_engine_for_request()
689
+ self.reqs_in_flight[request.request_id] = chosen_engine
690
+ chosen_engine.num_reqs_in_flight += 1
691
+ if self.num_engines_running >= len(self.core_engines):
692
+ await chosen_engine.send_multipart(msg)
693
+ else:
694
+ # Send request to chosen engine and dp start loop
695
+ # control message to all other engines.
696
+ self.num_engines_running += len(self.core_engines)
697
+ await asyncio.gather(*[
698
+ engine.send_multipart(msg if engine is
699
+ chosen_engine else self.start_dp_msg)
700
+ for engine in self.core_engines
701
+ ])
702
+
703
+ self._ensure_output_queue_task()
704
+
705
+ def get_core_engine_for_request(self) -> CoreEngine:
706
+ return min(self.core_engines, key=lambda e: e.num_reqs_in_flight)
707
+
708
+ @staticmethod
709
+ async def process_engine_outputs(self: "DPAsyncMPClient",
710
+ outputs: EngineCoreOutputs):
711
+ if self.reqs_in_flight:
712
+ for req_id in outputs.finished_requests or ():
713
+ if engine := self.reqs_in_flight.pop(req_id, None):
714
+ engine.num_reqs_in_flight -= 1
715
+
716
+ if outputs.engine_paused:
717
+ assert self.num_engines_running >= 1
718
+ self.num_engines_running -= 1
719
+ if not self.num_engines_running and self.reqs_in_flight:
720
+ # If there are requests in flight here, they must have
721
+ # been sent after the engines paused. We must make
722
+ # sure to start the other engines:
723
+ self.num_engines_running = len(self.core_engines)
724
+ coros = [
725
+ engine.send_multipart(self.start_dp_msg)
726
+ for engine in self.core_engines
727
+ if not engine.num_reqs_in_flight
728
+ ]
729
+ if coros:
730
+ await asyncio.gather(*coros)
731
+
732
+ async def abort_requests_async(self, request_ids: list[str]) -> None:
733
+ if not request_ids:
734
+ return
735
+
736
+ if len(request_ids) == 1:
737
+ # Fast-path common case.
738
+ if engine := self.reqs_in_flight.get(request_ids[0]):
739
+ await self._abort_requests(request_ids, engine)
740
+ return
741
+
742
+ by_engine: dict[CoreEngine, list[str]] = {}
743
+ for req_id in request_ids:
744
+ if engine := self.reqs_in_flight.get(req_id):
745
+ by_engine.setdefault(engine, []).append(req_id)
746
+ for engine, req_ids in by_engine.items():
747
+ await self._abort_requests(req_ids, engine)
748
+
749
+ async def _abort_requests(self, request_ids: list[str],
750
+ engine: CoreEngine) -> None:
751
+ await engine.send_multipart((EngineCoreRequestType.ABORT.value,
752
+ self.encoder.encode(request_ids)))
vllm/v1/engine/llm_engine.py
+12
-5
@@ -8,6 +8,7 @@ from typing_extensions import TypeVar
8
9
import vllm.envs as envs
10
from vllm.config import ParallelConfig, VllmConfig
11
+from vllm.distributed import stateless_destroy_torch_distributed_process_group
12
from vllm.engine.arg_utils import EngineArgs
13
from vllm.engine.metrics_types import StatLoggerBase
14
from vllm.inputs import INPUT_REGISTRY, InputRegistry, PromptType
@@ -60,11 +61,13 @@ class LLMEngine:
61
self.cache_config = vllm_config.cache_config
62
63
# important: init dp group before init the engine_core
63
- self.parallel_config = vllm_config.parallel_config
64
- self.dp_enabled = self.parallel_config.data_parallel_size > 1 # noqa
64
+ # In the decoupled engine case this is handled in EngineCoreProc.
65
+ parallel_config = vllm_config.parallel_config
66
+ if not multiprocess_mode and parallel_config.data_parallel_size > 1:
67
+ self.dp_group = parallel_config.stateless_init_dp_group()
68
+ else:
69
+ self.dp_group = None
70
self.should_execute_dummy_batch = False
66
- if self.dp_enabled:
67
- self.dp_group = self.parallel_config.stateless_init_dp_group()
71
72
# Tokenizer (+ ensure liveness if running in another process).
73
self.tokenizer = init_tokenizer_from_configs(
@@ -148,7 +151,7 @@ class LLMEngine:
151
152
def has_unfinished_requests(self) -> bool:
153
has_unfinished = self.output_processor.has_unfinished_requests()
151
- if not self.dp_enabled:
154
+ if self.dp_group is None:
155
return has_unfinished
156
return self.has_unfinished_requests_dp(has_unfinished)
157
@@ -280,3 +283,7 @@ class LLMEngine:
283
def pin_lora(self, lora_id: int) -> bool:
284
"""Prevent an adapter from being evicted."""
285
return self.engine_core.pin_lora(lora_id)
286
+
287
+ def __del__(self):
288
+ if dp_group := getattr(self, "dp_group", None):
289
+ stateless_destroy_torch_distributed_process_group(dp_group)
vllm/v1/executor/multiproc_executor.py
+21
-17
@@ -235,7 +235,10 @@ class WorkerProc:
235
worker_response_mq_handle = self.worker_response_mq.export_handle()
236
237
# Send Readiness signal to EngineCore process.
238
- with zmq_socket_ctx(ready_path, zmq.constants.PUSH) as ready_socket:
238
+ # Set linger here because we want to ensure the message has
239
+ # been sent before the context is closed.
240
+ with zmq_socket_ctx(ready_path, zmq.constants.PUSH,
241
+ linger=10000) as ready_socket:
242
payload = pickle.dumps(worker_response_mq_handle,
243
protocol=pickle.HIGHEST_PROTOCOL)
244
ready_socket.send_string(WorkerProc.READY_STR)
@@ -270,11 +273,13 @@ class WorkerProc:
273
proc = context.Process(target=WorkerProc.worker_main,
274
kwargs=process_kwargs,
275
daemon=True)
273
- proc.start()
276
275
- # Wait for startup
276
- worker_response_mq_handle = WorkerProc.wait_for_startup(
277
- proc, ready_path)
277
+ with zmq_socket_ctx(ready_path, zmq.constants.PULL) as ready_socket:
278
+ proc.start()
279
+
280
+ # Wait for startup
281
+ worker_response_mq_handle = WorkerProc.wait_for_startup(
282
+ proc, ready_socket)
283
284
worker_response_mq = MessageQueue.create_from_handle(
285
worker_response_mq_handle, 0)
@@ -337,23 +342,22 @@ class WorkerProc:
342
@staticmethod
343
def wait_for_startup(
344
proc: BaseProcess,
340
- ready_path: str,
345
+ ready_socket: zmq.Socket,
346
) -> Optional[Handle]:
347
"""Wait until the Worker is ready."""
343
- with zmq_socket_ctx(ready_path, zmq.constants.PULL) as socket:
348
345
- # Wait for Worker to send READY.
346
- while socket.poll(timeout=POLLING_TIMEOUT_MS) == 0:
347
- logger.debug("Waiting for WorkerProc to startup.")
349
+ # Wait for Worker to send READY.
350
+ while ready_socket.poll(timeout=POLLING_TIMEOUT_MS) == 0:
351
+ logger.debug("Waiting for WorkerProc to startup.")
352
349
- if not proc.is_alive():
350
- raise RuntimeError("WorkerProc failed to start.")
353
+ if not proc.is_alive():
354
+ raise RuntimeError("WorkerProc failed to start.")
355
352
- message = socket.recv_string()
353
- assert message == WorkerProc.READY_STR
354
- handle_frame = socket.recv(copy=False)
355
- handle = pickle.loads(handle_frame.buffer)
356
- return handle
356
+ message = ready_socket.recv_string()
357
+ assert message == WorkerProc.READY_STR
358
+ handle_frame = ready_socket.recv(copy=False)
359
+ handle = pickle.loads(handle_frame.buffer)
360
+ return handle
361
362
class ResponseStatus(Enum):
363
SUCCESS = auto()
vllm/v1/metrics/loggers.py
+10
-4
@@ -31,7 +31,8 @@ class StatLoggerBase(ABC):
31
32
class LoggingStatLogger(StatLoggerBase):
33
34
- def __init__(self):
34
+ def __init__(self, engine_index: int = 0):
35
+ self.engine_index = engine_index
36
self._reset(time.monotonic())
37
self.last_scheduler_stats = SchedulerStats()
38
# Prefix cache metrics. This cannot be reset.
@@ -78,11 +79,13 @@ class LoggingStatLogger(StatLoggerBase):
79
80
# Format and print output.
81
logger.info(
82
+ "Engine %03d: "
83
"Avg prompt throughput: %.1f tokens/s, "
84
"Avg generation throughput: %.1f tokens/s, "
85
"Running: %d reqs, Waiting: %d reqs, "
86
"GPU KV cache usage: %.1f%%, "
87
"Prefix cache hit rate: %.1f%%",
88
+ self.engine_index,
89
prompt_throughput,
90
generation_throughput,
91
scheduler_stats.num_running_reqs,
@@ -94,7 +97,7 @@ class LoggingStatLogger(StatLoggerBase):
97
98
class PrometheusStatLogger(StatLoggerBase):
99
97
- def __init__(self, vllm_config: VllmConfig):
100
+ def __init__(self, vllm_config: VllmConfig, engine_index: int = 0):
101
self._unregister_vllm_metrics()
102
103
# Use this flag to hide metrics that were deprecated in
@@ -102,8 +105,11 @@ class PrometheusStatLogger(StatLoggerBase):
105
self.show_hidden_metrics = \
106
vllm_config.observability_config.show_hidden_metrics
107
105
- labelnames = ["model_name"]
106
- labelvalues = [vllm_config.model_config.served_model_name]
108
+ labelnames = ["model_name", "engine"]
109
+ labelvalues = [
110
+ vllm_config.model_config.served_model_name,
111
+ str(engine_index)
112
+ ]
113
114
max_model_len = vllm_config.model_config.max_model_len
115
vllm/v1/utils.py
+7
-4
@@ -105,7 +105,7 @@ class BackgroundProcHandle:
105
process_kwargs: dict[Any, Any],
106
):
107
context = get_mp_context()
108
- reader, writer = context.Pipe(duplex=False)
108
+ self.reader, writer = context.Pipe(duplex=False)
109
110
assert ("ready_pipe" not in process_kwargs
111
and "input_path" not in process_kwargs
@@ -115,14 +115,17 @@ class BackgroundProcHandle:
115
process_kwargs["output_path"] = output_path
116
117
# Run busy loop in background process.
118
- self.proc = context.Process(target=target_fn, kwargs=process_kwargs)
118
+ self.proc = context.Process(target=target_fn,
119
+ kwargs=process_kwargs,
120
+ name=process_name)
121
self._finalizer = weakref.finalize(self, shutdown, self.proc,
122
input_path, output_path)
123
self.proc.start()
124
125
+ def wait_for_startup(self):
126
# Wait for startup.
124
- if reader.recv()["status"] != "READY":
125
- raise RuntimeError(f"{process_name} initialization failed. "
127
+ if self.reader.recv()["status"] != "READY":
128
+ raise RuntimeError(f"{self.proc.name} initialization failed. "
129
"See root cause above.")
130
131
def shutdown(self):