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, 22:15 UTC
[V1][Core] Generic mechanism for handling engine utility (#13060)
Signed-off-by: Nick Hill <nhill@redhat.com>
Nick Hill committed
Feb 19, 2025 at 01:09 UTC
caf7ff4456d5c03b31cad89d02d03eb8d158e815
5 files changed
+197
-56
tests/lora/test_add_lora.py
+1
-1
@@ -41,7 +41,7 @@ def download_and_prepare_lora_module():
41
]
42
for tokenizer_file in tokenizer_files:
43
del_path = Path(LORA_MODULE_DOWNLOAD_PATH) / tokenizer_file
44
- del_path.unlink()
44
+ del_path.unlink(missing_ok=True)
45
46
47
@pytest.fixture(autouse=True)
tests/v1/engine/test_engine_core_client.py
+49
-8
@@ -3,7 +3,8 @@
3
import asyncio
4
import time
5
import uuid
6
-from typing import Dict, List
6
+from contextlib import ExitStack
7
+from typing import Dict, List, Optional
8
9
import pytest
10
from transformers import AutoTokenizer
@@ -14,7 +15,9 @@ from vllm.engine.arg_utils import EngineArgs
15
from vllm.platforms import current_platform
16
from vllm.usage.usage_lib import UsageContext
17
from vllm.v1.engine import EngineCoreRequest
17
-from vllm.v1.engine.core_client import EngineCoreClient
18
+from vllm.v1.engine.core import EngineCore
19
+from vllm.v1.engine.core_client import (AsyncMPClient, EngineCoreClient,
20
+ SyncMPClient)
21
from vllm.v1.executor.abstract import Executor
22
23
if not current_platform.is_cuda():
@@ -63,7 +66,7 @@ def loop_until_done(client: EngineCoreClient, outputs: Dict):
66
async def loop_until_done_async(client: EngineCoreClient, outputs: Dict):
67
68
while True:
66
- engine_core_outputs = await client.get_output_async().outputs
69
+ engine_core_outputs = (await client.get_output_async()).outputs
70
71
if len(engine_core_outputs) == 0:
72
break
@@ -78,6 +81,14 @@ async def loop_until_done_async(client: EngineCoreClient, outputs: Dict):
81
break
82
83
84
+# Dummy utility function to monkey-patch into engine core.
85
+def echo(self, msg: str, err_msg: Optional[str] = None) -> str:
86
+ print(f"echo util function called: {msg}, {err_msg}")
87
+ if err_msg is not None:
88
+ raise ValueError(err_msg)
89
+ return msg
90
+
91
+
92
@fork_new_process_for_each_test
93
@pytest.mark.parametrize("multiprocessing_mode", [True, False])
94
def test_engine_core_client(monkeypatch, multiprocessing_mode: bool):
@@ -85,7 +96,10 @@ def test_engine_core_client(monkeypatch, multiprocessing_mode: bool):
96
with monkeypatch.context() as m:
97
m.setenv("VLLM_USE_V1", "1")
98
88
- engine_args = EngineArgs(model=MODEL_NAME, compilation_config=3)
99
+ # Monkey-patch core engine utility function to test.
100
+ m.setattr(EngineCore, "echo", echo, raising=False)
101
+
102
+ engine_args = EngineArgs(model=MODEL_NAME, enforce_eager=True)
103
vllm_config = engine_args.create_engine_config(
104
UsageContext.UNKNOWN_CONTEXT)
105
executor_class = Executor.get_class(vllm_config)
@@ -147,15 +161,30 @@ def test_engine_core_client(monkeypatch, multiprocessing_mode: bool):
161
162
client.abort_requests([request.request_id])
163
164
+ if multiprocessing_mode:
165
+ """Utility method invocation"""
166
151
-@fork_new_process_for_each_test
152
-@pytest.mark.asyncio
167
+ core_client: SyncMPClient = client
168
+
169
+ result = core_client._call_utility("echo", "testarg")
170
+ assert result == "testarg"
171
+
172
+ with pytest.raises(Exception) as e_info:
173
+ core_client._call_utility("echo", None, "help!")
174
+
175
+ assert str(e_info.value) == "Call to echo method failed: help!"
176
+
177
+
178
+@pytest.mark.asyncio(loop_scope="function")
179
async def test_engine_core_client_asyncio(monkeypatch):
180
155
- with monkeypatch.context() as m:
181
+ with monkeypatch.context() as m, ExitStack() as after:
182
m.setenv("VLLM_USE_V1", "1")
183
158
- engine_args = EngineArgs(model=MODEL_NAME)
184
+ # Monkey-patch core engine utility function to test.
185
+ m.setattr(EngineCore, "echo", echo, raising=False)
186
+
187
+ engine_args = EngineArgs(model=MODEL_NAME, enforce_eager=True)
188
vllm_config = engine_args.create_engine_config(
189
usage_context=UsageContext.UNKNOWN_CONTEXT)
190
executor_class = Executor.get_class(vllm_config)
@@ -166,6 +195,7 @@ async def test_engine_core_client_asyncio(monkeypatch):
195
executor_class=executor_class,
196
log_stats=True,
197
)
198
+ after.callback(client.shutdown)
199
200
MAX_TOKENS = 20
201
params = SamplingParams(max_tokens=MAX_TOKENS)
@@ -204,3 +234,14 @@ async def test_engine_core_client_asyncio(monkeypatch):
234
else:
235
assert len(outputs[req_id]) == MAX_TOKENS, (
236
f"{len(outputs[req_id])=}, {MAX_TOKENS=}")
237
+ """Utility method invocation"""
238
+
239
+ core_client: AsyncMPClient = client
240
+
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!")
246
+
247
+ assert str(e_info.value) == "Call to echo method failed: help!"
vllm/v1/engine/__init__.py
+18
-6
@@ -2,7 +2,7 @@
2
3
import enum
4
import time
5
-from typing import List, Optional, Union
5
+from typing import Any, List, Optional, Union
6
7
import msgspec
8
@@ -106,6 +106,18 @@ class EngineCoreOutput(
106
return self.finish_reason is not None
107
108
109
+class UtilityOutput(
110
+ msgspec.Struct,
111
+ array_like=True, # type: ignore[call-arg]
112
+ gc=False): # type: ignore[call-arg]
113
+
114
+ call_id: int
115
+
116
+ # Non-None implies the call failed, result should be None.
117
+ failure_message: Optional[str] = None
118
+ result: Any = None
119
+
120
+
121
class EngineCoreOutputs(
122
msgspec.Struct,
123
array_like=True, # type: ignore[call-arg]
@@ -116,10 +128,12 @@ class EngineCoreOutputs(
128
# e.g. columnwise layout
129
130
# [num_reqs]
119
- outputs: List[EngineCoreOutput]
120
- scheduler_stats: Optional[SchedulerStats]
131
+ outputs: List[EngineCoreOutput] = []
132
+ scheduler_stats: Optional[SchedulerStats] = None
133
timestamp: float = 0.0
134
135
+ utility_output: Optional[UtilityOutput] = None
136
+
137
def __post_init__(self):
138
if self.timestamp == 0.0:
139
self.timestamp = time.monotonic()
@@ -132,6 +146,4 @@ class EngineCoreRequestType(enum.Enum):
146
"""
147
ADD = b'\x00'
148
ABORT = b'\x01'
135
- PROFILE = b'\x02'
136
- RESET_PREFIX_CACHE = b'\x03'
137
- ADD_LORA = b'\x04'
149
+ UTILITY = b'\x02'
vllm/v1/engine/core.py
+33
-16
@@ -5,9 +5,11 @@ import signal
5
import threading
6
import time
7
from concurrent.futures import Future
8
+from inspect import isclass, signature
9
from multiprocessing.connection import Connection
10
from typing import Any, List, Optional, Tuple, Type
11
12
+import msgspec
13
import psutil
14
import zmq
15
import zmq.asyncio
@@ -21,7 +23,7 @@ from vllm.utils import get_exception_traceback, zmq_socket_ctx
23
from vllm.v1.core.kv_cache_utils import get_kv_cache_configs
24
from vllm.v1.core.scheduler import Scheduler, SchedulerOutput
25
from vllm.v1.engine import (EngineCoreOutputs, EngineCoreRequest,
24
- EngineCoreRequestType)
26
+ EngineCoreRequestType, UtilityOutput)
27
from vllm.v1.engine.mm_input_cache import MMInputCacheServer
28
from vllm.v1.executor.abstract import Executor
29
from vllm.v1.outputs import ModelRunnerOutput
@@ -330,19 +332,39 @@ class EngineCoreProc(EngineCore):
332
self.add_request(request)
333
elif request_type == EngineCoreRequestType.ABORT:
334
self.abort_requests(request)
333
- elif request_type == EngineCoreRequestType.RESET_PREFIX_CACHE:
334
- self.reset_prefix_cache()
335
- elif request_type == EngineCoreRequestType.PROFILE:
336
- self.model_executor.profile(request)
337
- elif request_type == EngineCoreRequestType.ADD_LORA:
338
- self.model_executor.add_lora(request)
335
+ elif request_type == EngineCoreRequestType.UTILITY:
336
+ call_id, method_name, args = request
337
+ output = UtilityOutput(call_id)
338
+ try:
339
+ method = getattr(self, method_name)
340
+ output.result = method(
341
+ *self._convert_msgspec_args(method, args))
342
+ except BaseException as e:
343
+ logger.exception("Invocation of %s method failed", method_name)
344
+ output.failure_message = (f"Call to {method_name} method"
345
+ f" failed: {str(e)}")
346
+ self.output_queue.put_nowait(
347
+ EngineCoreOutputs(utility_output=output))
348
+
349
+ @staticmethod
350
+ def _convert_msgspec_args(method, args):
351
+ """If a provided arg type doesn't match corresponding target method
352
+ arg type, try converting to msgspec object."""
353
+ if not args:
354
+ return args
355
+ arg_types = signature(method).parameters.values()
356
+ assert len(args) <= len(arg_types)
357
+ return tuple(
358
+ msgspec.convert(v, type=p.annotation) if isclass(p.annotation)
359
+ and issubclass(p.annotation, msgspec.Struct)
360
+ and not isinstance(v, p.annotation) else v
361
+ for v, p in zip(args, arg_types))
362
363
def process_input_socket(self, input_path: str):
364
"""Input socket IO thread."""
365
366
# Msgpack serialization decoding.
367
add_request_decoder = MsgpackDecoder(EngineCoreRequest)
345
- add_lora_decoder = MsgpackDecoder(LoRARequest)
368
generic_decoder = MsgpackDecoder()
369
370
with zmq_socket_ctx(input_path, zmq.constants.PULL) as socket:
@@ -352,14 +374,9 @@ class EngineCoreProc(EngineCore):
374
request_type = EngineCoreRequestType(bytes(type_frame.buffer))
375
376
# Deserialize the request data.
355
- decoder = None
356
- if request_type == EngineCoreRequestType.ADD:
357
- decoder = add_request_decoder
358
- elif request_type == EngineCoreRequestType.ADD_LORA:
359
- decoder = add_lora_decoder
360
- else:
361
- decoder = generic_decoder
362
-
377
+ decoder = add_request_decoder if (
378
+ request_type
379
+ == EngineCoreRequestType.ADD) else generic_decoder
380
request = decoder.decode(data_frame.buffer)
381
382
# Push to input queue for core busy loop.
vllm/v1/engine/core_client.py
+96
-25
@@ -2,10 +2,14 @@
2
3
import asyncio
4
import os
5
+import queue
6
import signal
7
+import uuid
8
import weakref
9
from abc import ABC, abstractmethod
8
-from typing import Any, List, Optional, Type
10
+from concurrent.futures import Future
11
+from threading import Thread
12
+from typing import Any, Dict, List, Optional, Type, Union
13
14
import zmq
15
import zmq.asyncio
@@ -16,7 +20,7 @@ from vllm.lora.request import LoRARequest
20
from vllm.utils import (get_open_zmq_ipc_path, kill_process_tree,
21
make_zmq_socket)
22
from vllm.v1.engine import (EngineCoreOutputs, EngineCoreRequest,
19
- EngineCoreRequestType)
23
+ EngineCoreRequestType, UtilityOutput)
24
from vllm.v1.engine.core import EngineCore, EngineCoreProc
25
from vllm.v1.executor.abstract import Executor
26
from vllm.v1.serial_utils import MsgpackDecoder, MsgpackEncoder
@@ -24,6 +28,8 @@ from vllm.v1.utils import BackgroundProcHandle
28
29
logger = init_logger(__name__)
30
31
+AnyFuture = Union[asyncio.Future[Any], Future[Any]]
32
+
33
34
class EngineCoreClient(ABC):
35
"""
@@ -204,6 +210,8 @@ class MPClient(EngineCoreClient):
210
"log_stats": log_stats,
211
})
212
213
+ self.utility_results: Dict[int, AnyFuture] = {}
214
+
215
def shutdown(self):
216
"""Clean up background resources."""
217
if hasattr(self, "proc_handle"):
@@ -212,6 +220,16 @@ class MPClient(EngineCoreClient):
220
self._finalizer()
221
222
223
+def _process_utility_output(output: UtilityOutput,
224
+ utility_results: Dict[int, AnyFuture]):
225
+ """Set the result from a utility method in the waiting future"""
226
+ future = utility_results.pop(output.call_id)
227
+ if output.failure_message is not None:
228
+ future.set_exception(Exception(output.failure_message))
229
+ else:
230
+ future.set_result(output.result)
231
+
232
+
233
class SyncMPClient(MPClient):
234
"""Synchronous client for multi-proc EngineCore."""
235
@@ -224,10 +242,30 @@ class SyncMPClient(MPClient):
242
log_stats=log_stats,
243
)
244
227
- def get_output(self) -> EngineCoreOutputs:
245
+ self.outputs_queue: queue.Queue[EngineCoreOutputs] = queue.Queue()
246
229
- (frame, ) = self.output_socket.recv_multipart(copy=False)
230
- return self.decoder.decode(frame.buffer)
247
+ # Ensure that the outputs socket processing thread does not have
248
+ # a ref to the client which prevents gc.
249
+ output_socket = self.output_socket
250
+ decoder = self.decoder
251
+ utility_results = self.utility_results
252
+ outputs_queue = self.outputs_queue
253
+
254
+ def process_outputs_socket():
255
+ while True:
256
+ (frame, ) = output_socket.recv_multipart(copy=False)
257
+ outputs = decoder.decode(frame.buffer)
258
+ if outputs.utility_output:
259
+ _process_utility_output(outputs.utility_output,
260
+ utility_results)
261
+ else:
262
+ outputs_queue.put_nowait(outputs)
263
+
264
+ # Process outputs from engine in separate thread.
265
+ Thread(target=process_outputs_socket, daemon=True).start()
266
+
267
+ def get_output(self) -> EngineCoreOutputs:
268
+ return self.outputs_queue.get()
269
270
def _send_input(self, request_type: EngineCoreRequestType,
271
request: Any) -> None:
@@ -236,6 +274,16 @@ class SyncMPClient(MPClient):
274
msg = (request_type.value, self.encoder.encode(request))
275
self.input_socket.send_multipart(msg, copy=False)
276
277
+ def _call_utility(self, method: str, *args) -> Any:
278
+ call_id = uuid.uuid1().int >> 64
279
+ future: Future[Any] = Future()
280
+ self.utility_results[call_id] = future
281
+
282
+ self._send_input(EngineCoreRequestType.UTILITY,
283
+ (call_id, method, args))
284
+
285
+ return future.result()
286
+
287
def add_request(self, request: EngineCoreRequest) -> None:
288
# NOTE: text prompt is not needed in the core engine as it has been
289
# tokenized.
@@ -247,13 +295,13 @@ class SyncMPClient(MPClient):
295
self._send_input(EngineCoreRequestType.ABORT, request_ids)
296
297
def profile(self, is_start: bool = True) -> None:
250
- self._send_input(EngineCoreRequestType.PROFILE, is_start)
298
+ self._call_utility("profile", is_start)
299
300
def reset_prefix_cache(self) -> None:
253
- self._send_input(EngineCoreRequestType.RESET_PREFIX_CACHE, None)
301
+ self._call_utility("reset_prefix_cache")
302
303
def add_lora(self, lora_request: LoRARequest) -> None:
256
- self._send_input(EngineCoreRequestType.ADD_LORA, lora_request)
304
+ self._call_utility("add_lora", lora_request)
305
306
307
class AsyncMPClient(MPClient):
@@ -268,24 +316,35 @@ class AsyncMPClient(MPClient):
316
log_stats=log_stats,
317
)
318
271
- self.outputs_queue: Optional[asyncio.Queue[bytes]] = None
319
+ self.outputs_queue: Optional[asyncio.Queue[EngineCoreOutputs]] = None
320
self.queue_task: Optional[asyncio.Task] = None
321
322
+ async def _start_output_queue_task(self):
323
+ # Perform IO in separate task to parallelize as much as possible.
324
+ # Avoid task having direct reference back to the client.
325
+ self.outputs_queue = asyncio.Queue()
326
+ output_socket = self.output_socket
327
+ decoder = self.decoder
328
+ utility_results = self.utility_results
329
+ outputs_queue = self.outputs_queue
330
+
331
+ async def process_outputs_socket():
332
+ while True:
333
+ (frame, ) = await output_socket.recv_multipart(copy=False)
334
+ outputs: EngineCoreOutputs = decoder.decode(frame.buffer)
335
+ if outputs.utility_output:
336
+ _process_utility_output(outputs.utility_output,
337
+ utility_results)
338
+ else:
339
+ outputs_queue.put_nowait(outputs)
340
+
341
+ self.queue_task = asyncio.create_task(process_outputs_socket())
342
+
343
async def get_output_async(self) -> EngineCoreOutputs:
344
if self.outputs_queue is None:
276
- # Perform IO in separate task to parallelize as much as possible
277
- self.outputs_queue = asyncio.Queue()
278
-
279
- async def process_outputs_socket():
280
- assert self.outputs_queue is not None
281
- while True:
282
- (frame, ) = await self.output_socket.recv_multipart(
283
- copy=False)
284
- self.outputs_queue.put_nowait(frame.buffer)
285
-
286
- self.queue_task = asyncio.create_task(process_outputs_socket())
287
-
288
- return self.decoder.decode(await self.outputs_queue.get())
345
+ await self._start_output_queue_task()
346
+ assert self.outputs_queue is not None
347
+ return await self.outputs_queue.get()
348
349
async def _send_input(self, request_type: EngineCoreRequestType,
350
request: Any) -> None:
@@ -293,6 +352,18 @@ class AsyncMPClient(MPClient):
352
msg = (request_type.value, self.encoder.encode(request))
353
await self.input_socket.send_multipart(msg, copy=False)
354
355
+ if self.outputs_queue is None:
356
+ await self._start_output_queue_task()
357
+
358
+ async def _call_utility_async(self, method: str, *args) -> Any:
359
+ call_id = uuid.uuid1().int >> 64
360
+ future = asyncio.get_running_loop().create_future()
361
+ self.utility_results[call_id] = future
362
+ await self._send_input(EngineCoreRequestType.UTILITY,
363
+ (call_id, method, args))
364
+
365
+ return await future
366
+
367
async def add_request_async(self, request: EngineCoreRequest) -> None:
368
# NOTE: text prompt is not needed in the core engine as it has been
369
# tokenized.
@@ -304,10 +375,10 @@ class AsyncMPClient(MPClient):
375
await self._send_input(EngineCoreRequestType.ABORT, request_ids)
376
377
async def profile_async(self, is_start: bool = True) -> None:
307
- await self._send_input(EngineCoreRequestType.PROFILE, is_start)
378
+ await self._call_utility_async("profile", is_start)
379
380
async def reset_prefix_cache_async(self) -> None:
310
- await self._send_input(EngineCoreRequestType.RESET_PREFIX_CACHE, None)
381
+ await self._call_utility_async("reset_prefix_cache")
382
383
async def add_lora_async(self, lora_request: LoRARequest) -> None:
313
- await self._send_input(EngineCoreRequestType.ADD_LORA, lora_request)
384
+ await self._call_utility_async("add_lora", lora_request)