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, 13:30 UTC
[BugFix] Avoid race conditions in zero-copy tensor transmission (#17203)
Signed-off-by: Nick Hill <nhill@redhat.com>
Nick Hill committed
Apr 25, 2025 at 23:00 UTC
b07bf83c7d610ee88176b70eb58f66627b4576a2
3 files changed
+77
-12
tests/v1/test_serial_utils.py
+3
@@ -32,6 +32,7 @@ class MyType:
32
large_f_contig_tensor: torch.Tensor
33
small_non_contig_tensor: torch.Tensor
34
large_non_contig_tensor: torch.Tensor
35
+ empty_tensor: torch.Tensor
36
37
38
def test_encode_decode():
@@ -58,6 +59,7 @@ def test_encode_decode():
59
large_f_contig_tensor=torch.rand(1024, 4).t(),
60
small_non_contig_tensor=torch.rand(2, 4)[:, 1:3],
61
large_non_contig_tensor=torch.rand(1024, 512)[:, 10:20],
62
+ empty_tensor=torch.empty(0),
63
)
64
65
encoder = MsgpackEncoder(size_threshold=256)
@@ -193,3 +195,4 @@ def assert_equal(obj1: MyType, obj2: MyType):
195
obj2.small_non_contig_tensor)
196
assert torch.equal(obj1.large_non_contig_tensor,
197
obj2.large_non_contig_tensor)
198
+ assert torch.equal(obj1.empty_tensor, obj2.empty_tensor)
vllm/v1/engine/core.py
+22
-3
@@ -5,6 +5,7 @@ import signal
5
import sys
6
import threading
7
import time
8
+from collections import deque
9
from concurrent.futures import Future
10
from inspect import isclass, signature
11
from logging import DEBUG
@@ -527,8 +528,12 @@ class EngineCoreProc(EngineCore):
528
529
# Msgpack serialization encoding.
530
encoder = MsgpackEncoder()
530
- # Reuse send buffer.
531
- buffer = bytearray()
531
+ # Send buffers to reuse.
532
+ reuse_buffers: list[bytearray] = []
533
+ # Keep references to outputs and buffers until zmq is finished
534
+ # with them (outputs may contain tensors/np arrays whose
535
+ # backing buffers were extracted for zero-copy send).
536
+ pending = deque[tuple[zmq.MessageTracker, Any, bytearray]]()
537
538
# We must set linger to ensure the ENGINE_CORE_DEAD
539
# message is sent prior to closing the socket.
@@ -541,8 +546,22 @@ class EngineCoreProc(EngineCore):
546
break
547
assert not isinstance(outputs, bytes)
548
outputs.engine_index = engine_index
549
+
550
+ # Reclaim buffers that zmq is finished with.
551
+ while pending and pending[-1][0].done:
552
+ reuse_buffers.append(pending.pop()[2])
553
+
554
+ buffer = reuse_buffers.pop() if reuse_buffers else bytearray()
555
buffers = encoder.encode_into(outputs, buffer)
545
- socket.send_multipart(buffers, copy=False)
556
+ tracker = socket.send_multipart(buffers,
557
+ copy=False,
558
+ track=True)
559
+ if not tracker.done:
560
+ ref = outputs if len(buffers) > 1 else None
561
+ pending.appendleft((tracker, ref, buffer))
562
+ elif len(reuse_buffers) < 2:
563
+ # Keep at most 2 buffers to reuse.
564
+ reuse_buffers.append(buffer)
565
566
567
class DPEngineCoreProc(EngineCoreProc):
vllm/v1/engine/core_client.py
+52
-9
@@ -1,9 +1,11 @@
1
# SPDX-License-Identifier: Apache-2.0
2
import asyncio
3
+import contextlib
4
import queue
5
import uuid
6
import weakref
7
from abc import ABC, abstractmethod
8
+from collections import deque
9
from collections.abc import Awaitable, Sequence
10
from concurrent.futures import Future
11
from dataclasses import dataclass, field
@@ -396,6 +398,12 @@ class MPClient(EngineCoreClient):
398
self._wait_for_engine_startup()
399
400
self.utility_results: dict[int, AnyFuture] = {}
401
+
402
+ # Request objects which may contain pytorch-allocated tensors
403
+ # that we need to keep references to until zmq is done with the
404
+ # underlying data.
405
+ self.pending_messages = deque[tuple[zmq.MessageTracker, Any]]()
406
+
407
success = True
408
finally:
409
if not success:
@@ -459,6 +467,14 @@ class MPClient(EngineCoreClient):
467
if self.resources.engine_dead:
468
raise EngineDeadError()
469
470
+ def add_pending_message(self, tracker: zmq.MessageTracker, msg: Any):
471
+ if not tracker.done:
472
+ self.pending_messages.appendleft((tracker, msg))
473
+
474
+ def free_pending_messages(self):
475
+ while self.pending_messages and self.pending_messages[-1][0].done:
476
+ self.pending_messages.pop()
477
+
478
479
def _process_utility_output(output: UtilityOutput,
480
utility_results: dict[int, AnyFuture]):
@@ -544,10 +560,18 @@ class SyncMPClient(MPClient):
560
561
def _send_input(self, request_type: EngineCoreRequestType, request: Any):
562
self.ensure_alive()
563
+ self.free_pending_messages()
564
# (Identity, RequestType, SerializedRequest)
565
msg = (self.core_engine.identity, request_type.value,
566
*self.encoder.encode(request))
550
- self.input_socket.send_multipart(msg, copy=False)
567
+
568
+ if len(msg) <= 3:
569
+ # No auxiliary buffers => no tensor backing buffers in request.
570
+ self.input_socket.send_multipart(msg, copy=False)
571
+ return
572
+
573
+ tracker = self.input_socket.send_multipart(msg, copy=False, track=True)
574
+ self.add_pending_message(tracker, request)
575
576
def call_utility(self, method: str, *args) -> Any:
577
call_id = uuid.uuid1().int >> 64
@@ -698,19 +722,38 @@ class AsyncMPClient(MPClient):
722
def _send_input(self,
723
request_type: EngineCoreRequestType,
724
request: Any,
701
- engine: Optional[CoreEngine] = None) -> Awaitable[None]:
725
+ engine: Optional[CoreEngine] = None) -> Awaitable[Any]:
726
self.ensure_alive()
727
if engine is None:
728
engine = self.core_engine
729
730
message = (request_type.value, *self.encoder.encode(request))
707
- return self._send_input_message(message, engine)
708
-
709
- def _send_input_message(self, message: tuple[bytestr, ...],
710
- engine: CoreEngine) -> Awaitable[None]:
731
+ return self._send_input_message(message, engine, request)
732
+
733
+ def _send_input_message(self, message: tuple[bytestr,
734
+ ...], engine: CoreEngine,
735
+ objects: Any) -> Awaitable[Any]:
736
+ """
737
+ objects is a reference to retain until zmq is finished with the
738
+ buffers, in case they were extracted from tensors in the request.
739
+ """
740
self.ensure_alive()
712
- message = (engine.identity, ) + message
713
- return self.input_socket.send_multipart(message, copy=False)
741
+ self.free_pending_messages()
742
+
743
+ msg = (engine.identity, ) + message
744
+ if not objects or len(msg) <= 3:
745
+ # No auxiliary buffers => no tensor backing buffers in request.
746
+ return self.input_socket.send_multipart(msg, copy=False)
747
+
748
+ future: asyncio.Future[zmq.MessageTracker]
749
+ future = self.input_socket.send_multipart(msg, copy=False, track=True)
750
+
751
+ def add_pending(f: asyncio.Future[zmq.MessageTracker]):
752
+ with contextlib.suppress(BaseException):
753
+ self.add_pending_message(f.result(), objects)
754
+
755
+ future.add_done_callback(add_pending)
756
+ return future
757
758
async def call_utility_async(self, method: str, *args) -> Any:
759
return await self._call_utility_async(method,
@@ -724,7 +767,7 @@ class AsyncMPClient(MPClient):
767
self.utility_results[call_id] = future
768
message = (EngineCoreRequestType.UTILITY.value, *self.encoder.encode(
769
(call_id, method, args)))
727
- await self._send_input_message(message, engine)
770
+ await self._send_input_message(message, engine, args)
771
self._ensure_output_queue_task()
772
return await future
773