vLLM Explained

distributed/device_communicators/shm_broadcast.py: MessageQueue

AdvancedCode mapabout 8 min read + 4 min deep dives

Code version: vLLM @ 3e44c0d · 2026-10-22

Purpose#

vllm/distributed/device_communicators/shm_broadcast.py implements the MessageQueue: one process writes a Python object once, and every reader gets a copy. On one node the bytes go through a ring buffer in shared memory, with no lock and no copy per reader; too-large messages and readers on other nodes go through ZMQ.

Filevllm/distributed/device_communicators/shm_broadcast.py, 1,148 lines of Python.
Main classesMessageQueue (vllm/distributed/device_communicators/shm_broadcast.py:483), ShmRingBuffer (vllm/distributed/device_communicators/shm_broadcast.py:265), SpinCondition (vllm/distributed/device_communicators/shm_broadcast.py:116), Handle (vllm/distributed/device_communicators/shm_broadcast.py:465-474).
Runs inThe writer: the engine core process (executor broadcast) or a worker (its reply). The readers: the workers, or the engine core for replies. Main threads, plus a worker's WorkerAsyncOutputCopy thread, which enqueues replies.
Called byMultiprocExecutor and WorkerProc (vllm/v1/executor/multiproc_executor.py:165-172, vllm/v1/executor/multiproc_executor.py:615-645); GroupCoordinator broadcasters (vllm/distributed/parallel_state.py:562-567); the Ray executor (vllm/v1/executor/ray_executor_v2.py:330-338).
Callsmultiprocessing.shared_memory, pickle (protocol 5 with out-of-band buffers), pyzmq (XPUB/SUB, PUB/SUB, PAIR), torch.distributed to share the handle in a group.
Not in this fileThe RPC protocol on top: the message tuple (method, args, kwargs, output_rank) and the reply (status, value) are defined by the executor (v1/executor/multiproc_executor.py: MultiprocExecutor and WorkerProc). The zero-copy tensor arena is in shm_tensor_arena.py (opt-in, --enable-shm-tensor-arena).

The page Processes, ZMQ and shared memory introduces the queue; this page reads the file.

Where it sits#

Figure: the ring buffer of the executor's broadcast queue (panel 1), the bytes of one message (panel 2), and the flag states of one chunk (panel 3).
Open full size ↗

Who calls it#

What it calls#

Key classes and functions#

NameLineWhat it does
memory_fencevllm/distributed/device_communicators/shm_broadcast.py:83-100Lock acquire/release as a full memory barrier around flag writes.
SpinConditionvllm/distributed/device_communicators/shm_broadcast.py:116-224Reader wait: spin for 1 s after a read, then sleep on a ZMQ notify socket.
check_shm_free_spacevllm/distributed/device_communicators/shm_broadcast.py:230-262Raise if /dev/shm (or the cgroup) cannot hold the segment.
ShmRingBuffervllm/distributed/device_communicators/shm_broadcast.py:265-401The segment: data chunks, then flag bytes; create or attach by name.
_rebuild_tensor, _reduce_tensorvllm/distributed/device_communicators/shm_broadcast.py:404-462Pickle CPU tensors as out-of-band buffers instead of copying them into the stream.
Handlevllm/distributed/device_communicators/shm_broadcast.py:465-474Picklable description of a queue: buffer name, socket addresses, local reader ranks.
MessageQueue.__init__ / create_from_handlevllm/distributed/device_communicators/shm_broadcast.py:484-674Writer side and reader side of one queue.
wait_until_readyvllm/distributed/device_communicators/shm_broadcast.py:676-707Collective start-up: wait for every subscription, then send READY.
acquire_write / acquire_readvllm/distributed/device_communicators/shm_broadcast.py:716-898The lock-free protocol on one chunk.
enqueue / dequeue / recvvllm/distributed/device_communicators/shm_broadcast.py:900-1007Serialize, write or overflow; read and deserialize.
broadcast_objectvllm/distributed/device_communicators/shm_broadcast.py:1009-1013Writer enqueues, readers dequeue.
create_from_process_group, …_single_readervllm/distributed/device_communicators/shm_broadcast.py:1015-1148Build a queue for a torch process group and share the handle.

Walk through the main methods#

ShmRingBuffer: layout and the flag rules#

The docstring is the specification. Data chunks come first, then max_chunks × (1 + n_reader) flag bytes. The first flag of a chunk is "written"; then one "read" flag per reader (vllm/distributed/device_communicators/shm_broadcast.py:281-302). The four states:

        metadata memory layout: each byte is a flag, the first byte is the written
        flag, and the rest are reader flags. The flags are set to 0 by default.
        +--------------+--------------+--------------+-----+--------------+
        | written_flag | reader0_flag | reader1_flag | ... | readerN_flag |
        +--------------+--------------+--------------+-----+--------------+

        The state of metadata is as follows:

        (case 1) 0???...???: the block is not written yet, cannot read, can write
        (case 2) 1000...000: the block is just written, can read, cannot write
        (case 3) 1???...???: the block is written and read by some readers, can read if not read, cannot write
        (case 4) 1111...111: the block is written and read by all readers, cannot read, can write

        State transition for readers:

        When a reader finds a block that it can read (case 2 or 3), it can yield the block for caller to read.
        Only after the caller finishes reading the block, the reader can mark the block as read.
        Readers only mark the block as read (from 0 to 1), the writer marks the block as ready to read (from 1 to 0).

        State transition for writer:

        When the writer writes to a block (case 1 or 4), it first resets the written flag to 0, converting either case
        to case 1. Then it can yield the block for caller to write. After the caller finishes writing the block, the writer
        can reset the reader flags to 0, and mark the block as written (from 0 to 1).
        NOTE: the order is important here, first reset the reader flags (so that we are still in case 1), then mark the block as written. The state transition is atomic. If we do it in the reverse order, it will go through case 3 and then back to case 2, and readers might read the intermediate case 3, which is not correct.

The writer creates the segment and zeroes the flags; a reader attaches by name and patches out Python's resource tracker, so a reader process does not unlink a segment it did not create (vllm/distributed/device_communicators/shm_broadcast.py:332-365).

acquire_write: the writer's side of one chunk#

        while True:
            with self.buffer.get_metadata(self.current_idx) as metadata_buffer:

                def check():
                    memory_fence()
                    read_count = sum(metadata_buffer[1:])
                    written_flag = metadata_buffer[0]
                    return not (written_flag and read_count != self.buffer.n_reader)

                if SPINLOOP_EXT_ENABLED and not check():
                    spinloop(metadata_buffer, check, timeout=SPINLOOP_TIMEOUT_SECONDS)

                if not check():
                    # this block is written and not read by all readers
                    # for writers, `self.current_idx` is the next block to write
                    # if this block is not ready to write,
                    # we need to wait until it is read by all readers

                    # Release the processor to other threads
                    sched_yield()

                    # if we time out, raise an exception
                    elapsed = time.monotonic() - start_time
                    if timeout is not None and elapsed > timeout:
                        raise TimeoutError

                    # if we wait for a long time, log a message
                    if elapsed > VLLM_RINGBUFFER_WARNING_INTERVAL * n_warning:
                        logger.info(
                            LONG_WAIT_TIME_LOG_MSG, VLLM_RINGBUFFER_WARNING_INTERVAL
                        )
                        n_warning += 1

                    continue
                # found a block that is either
                # (1) not written
                # (2) read by all readers

                # mark the block as not written
                metadata_buffer[0] = 0
                # let caller write to the buffer
                with self.buffer.get_data(self.current_idx) as buf:
                    yield buf

                # caller has written to the buffer
                # NOTE: order is important here
                # first set the read flags to 0
                # then set the written flag to 1
                # otherwise, the readers may think they already read the block
                for i in range(1, self.buffer.n_reader + 1):
                    # set read flag to 0, meaning it is not read yet
                    metadata_buffer[i] = 0
                # Memory fence here ensures the order of the buffer and flag
                # writes. This guarantees that when `metadata_buffer[0] = 1` is
                # visible to readers, `buf` can be completely ready. Without
                # this, some CPU architectures with weak ordering may incur
                # memory inconsistency.
                memory_fence()
                # mark the block as written
                metadata_buffer[0] = 1
                # Memory fence ensures the write is visible to readers on other cores
                # before we proceed. Without this, readers may spin indefinitely
                # waiting for a write that's stuck in our CPU's store buffer.
                memory_fence()
                self.current_idx = (self.current_idx + 1) % self.buffer.max_chunks
                break
  1. Lines 724-728. A chunk is writable unless it is written and not yet read by all readers.
  2. Lines 733-754. Not writable: yield the CPU and retry. A timeout raises TimeoutError; every VLLM_RINGBUFFER_WARNING_INTERVAL seconds (60) the writer logs "No available shared memory broadcast block found in 60 seconds".
  3. Line 760. Clear "written" first, so no reader starts on a half-written chunk.
  4. Lines 770-784. After the caller wrote: reset all read flags, fence, set "written", fence. The comment explains why this order matters: the other order passes through the "read by some" state.
  5. Line 785. Advance current_idx around the ring.

acquire_read: one reader#

    def acquire_read(
        self,
        timeout: float | None = None,
        indefinite: bool = False,
    ):
        assert self._is_local_reader, "Only readers can acquire read"
        read_timeout = self.ReadTimeoutWithWarnings(
            timeout=timeout, should_warn=not indefinite
        )
        with self.buffer.get_metadata(self.current_idx) as metadata_buffer:
            while True:
                if not self._wait_until_readable(metadata_buffer, read_timeout):
                    continue
                # found a block that is not read by this reader
                # let caller read from the buffer
                with self.buffer.get_data(self.current_idx) as buf:
                    try:
                        yield buf
                    finally:
                        # caller has read from the buffer; set the read flag.
                        metadata_buffer[self.local_reader_rank + 1] = 1
                        # Memory fence ensures the read flag is visible to the writer.
                        # Without this, writer may not see our read completion and
                        # could wait indefinitely for all readers to finish.
                        memory_fence()
                        next_idx = self.current_idx + 1
                        self.current_idx = next_idx % self.buffer.max_chunks
                        self._spin_condition.record_read()
                break
  1. Lines 876-882. Wait until the current chunk is written and not yet read by this reader. The wait uses SpinCondition: spin with sched_yield for 1 s after the last read, then sleep on the ZMQ notify socket for at most 5 s at a time before it checks the flags again (vllm/distributed/device_communicators/shm_broadcast.py:205-219, vllm/distributed/device_communicators/shm_broadcast.py:71, vllm/distributed/device_communicators/shm_broadcast.py:802-817).
  2. Lines 888-894. After the caller read, set this reader's flag and fence, so the writer sees it.
  3. Lines 895-897. Advance and record the read time (it controls the spin window).

enqueue: serialize, then write or overflow#

    def enqueue(self, obj, timeout: float | None = None):
        """Write to message queue with optional timeout (in seconds)"""
        assert self._is_writer, "Only writers can enqueue"
        all_buffers: list[SizedBuffer] = [b""]
        total_bytes = 6  # 2 bytes for oob buffer count, 4 for main buffer size

        def oob_callback(buf: PickleBuffer) -> bool:
            raw_buf = buf.raw()
            if len(raw_buf) < 1024 * 1024:
                # In-line buffers smaller than 1MiB.
                return True
            all_buffers.append(raw_buf)
            nonlocal total_bytes
            total_bytes += len(raw_buf) + 4
            return False

        # CPU tensors are routed through `_reduce_tensor` so that their
        # bytes are emitted as out-of-band buffers instead of being
        # copied into the pickle stream by torch's default reducer.
        # Start from `copyreg.dispatch_table` to preserve globally
        # registered reducers (e.g. `re.Pattern`); the per-pickler
        # dispatch table would otherwise shadow them.
        dispatch_table = dict(copyreg.dispatch_table)
        dispatch_table[torch.Tensor] = _reduce_tensor
        arena = self.tensor_arena
        with io.BytesIO() as bio:
            pickler: pickle.Pickler
            if arena is not None:
                from vllm.distributed.device_communicators.shm_tensor_arena import (
                    _ArenaPickler,
                )

                pickler = _ArenaPickler(bio, arena, buffer_callback=oob_callback)
            else:
                pickler = pickle.Pickler(
                    bio,
                    protocol=pickle.HIGHEST_PROTOCOL,
                    buffer_callback=oob_callback,
                )
            pickler.dispatch_table = dispatch_table
            pickler.dump(obj)
            all_buffers[0] = bio.getvalue()
        if self.n_local_reader > 0:
            if total_bytes + len(all_buffers[0]) >= self.buffer.max_chunk_bytes:
                with self.acquire_write(timeout) as buf:
                    buf[0] = 1  # overflow
                self.local_socket.send_multipart(all_buffers, copy=False)
            else:
                # Byte 0: 0
                # Bytes 1-2: Count of buffers
                # Then each buffer follows, preceded by 4 bytes containing its length:
                # [4 byte int L][L bytes of buffer content] ...
                with self.acquire_write(timeout) as buf:
                    buf[0] = 0  # not overflow
                    offset = 3
                    buf[1:offset] = to_bytes_big(len(all_buffers), 2)  # oob buf count
                    for buffer in all_buffers:
                        buf_len = len(buffer)
                        # prepend each buffer with 4 bytes containing its size.
                        buf_offset = offset + 4
                        buf[offset:buf_offset] = to_bytes_big(buf_len, 4)
                        buf[buf_offset : (offset := buf_offset + buf_len)] = buffer

            self._spin_condition.notify()

        if self.n_remote_reader > 0:
            self.remote_socket.send_multipart(all_buffers, copy=False)
  1. Lines 906-914. Out-of-band buffers of 1 MiB or more stay outside the pickle stream; smaller ones go inline.
  2. Lines 922-923. CPU tensors are reduced by _reduce_tensor, so their bytes become such buffers instead of a copy inside the pickle (vllm/distributed/device_communicators/shm_broadcast.py:433-462).
  3. Lines 943-946. If the total does not fit in one chunk, the chunk carries only the overflow flag, and all buffers go over the local XPUB socket.
  4. Lines 952-961. Otherwise the chunk holds: byte 0 = 0, bytes 1-2 = buffer count (big-endian), then each buffer with a 4-byte length.
  5. Line 963. Wake sleeping readers.
  6. Lines 965-966. Remote readers always get the message over the TCP XPUB socket.

dequeue reverses the steps. It copies each out-of-band buffer into a new bytearray, because the ring slot is reused after the read; on overflow it receives from the SUB socket after it releases the chunk (vllm/distributed/device_communicators/shm_broadcast.py:968-998).

create_from_process_group: one queue for a torch group#

        from vllm.distributed.parallel_state import in_the_same_node_as

        status = in_the_same_node_as(pg, source_rank=writer_rank)
        if group_rank == writer_rank:
            if external_writer_handle is not None:
                buffer_io = MessageQueue.create_from_handle(
                    external_writer_handle, group_rank
                )
            else:
                same_node_ranks = [i for i, s in enumerate(status) if s]
                n_reader = group_world_size - 1
                n_local_reader = len(same_node_ranks) - 1
                local_reader_ranks = [i for i in same_node_ranks if i != writer_rank]
                buffer_io = MessageQueue(
                    n_reader=n_reader,
                    n_local_reader=n_local_reader,
                    local_reader_ranks=local_reader_ranks,
                    max_chunk_bytes=max_chunk_bytes,
                    max_chunks=max_chunks,
                )
            handle = buffer_io.export_handle()
            if isinstance(pg, ProcessGroup):
                dist.broadcast_object_list(
                    [handle], src=global_ranks[writer_rank], group=pg
                )
            else:
                pg.broadcast_obj(handle, writer_rank)
        else:
            if isinstance(pg, ProcessGroup):
                recv = [None]
                dist.broadcast_object_list(
                    recv, src=global_ranks[writer_rank], group=pg
                )
                handle = recv[0]  # type: ignore
            else:
                handle = pg.broadcast_obj(None, writer_rank)
            buffer_io = MessageQueue.create_from_handle(handle, group_rank)
        if blocking:
            buffer_io.wait_until_ready()
        return buffer_io

The writer counts the ranks on its own node with in_the_same_node_as; those become local readers on the ring, and the others remote readers on TCP. It broadcasts the handle over the gloo group; each reader attaches with it, and wait_until_ready runs the start-up handshake.

Deep dive#

The sections below give the full detail. Open them one at a time, or use Open all deep dives at the top of the page.

Deep dive

Sizes of every queue in a TP = 2 server

QueueMade atReadersChunks × sizeSegment (derived)
executor broadcast (rpc_broadcast_mq)vllm/v1/executor/multiproc_executor.py:165-1712 local10 × 16 MiB160 MiB + 30 flag bytes
worker 0 reply (worker_response_mq)vllm/v1/executor/multiproc_executor.py:6251 local (the engine core)10 × 24 MiB (the defaults, vllm/distributed/device_communicators/shm_broadcast.py:489-492)240 MiB + 20 bytes
worker 1 replysame1 local10 × 24 MiB240 MiB + 20 bytes
TP group broadcaster (tp:0)vllm/distributed/parallel_state.py:564-5671 local (rank 1)6 × 4 MiB24 MiB + 12 bytes

Each segment size is (max_chunk_bytes + 1 + n_reader) × max_chunks (vllm/distributed/device_communicators/shm_broadcast.py:326-328), and check_shm_free_space raises before it creates one that does not fit (vllm/distributed/device_communicators/shm_broadcast.py:248-262). The comment on the 24 MiB default says it is "large enough to accommodate grammar bitmask tensors for large batches (1024 requests)" (vllm/distributed/device_communicators/shm_broadcast.py:489-490). So a TP = 2 engine asks for about 664 MiB of /dev/shm in total (derived). Docker's default /dev/shm is far smaller; the error text suggests --shm-size or --ipc=host.

Deep dive

Worked example: one execute_model call with TP = 2

  1. The executor calls enqueue(("execute_model", (scheduler_output,), {}, 0)). For a decode step of 64 requests the pickle is small (no tensor of 1 MiB or more), so it fits into one 16 MiB chunk (illustrative size).
  2. acquire_write finds chunk i in state 111 (read by both workers in an earlier step), clears the written flag, and the payload is copied in: [0][count=1][len][pickle bytes].
  3. Flags become 100; the writer sends one byte on the notify PUB socket and moves to chunk i + 1.
  4. Both workers spin in dequeue(indefinite=True). They read within the 1-second spin window, so they see the flag without the socket. Each unpickles its own copy and sets its read flag: 110, then 111.
  5. Worker 0 (the output rank) later replies on its own MessageQueue(1, 1) with (SUCCESS, None); worker 1 replies nothing (vllm/v1/executor/multiproc_executor.py:1070-1071).

A multimodal request with a 30 MiB image tensor would not fit: the chunk gets the overflow flag, and the pickle plus the image buffer travel over the XPUB socket as separate ZMQ frames, without a copy into the pickle stream (derived from vllm/distributed/device_communicators/shm_broadcast.py:906-946).

Deep dive

Processes, threads and waiting

SideWhoHow it waits
writer, ring fullengine core main thread (or a worker's copy thread)busy loop with sched_yield; INFO every 60 s; TimeoutError only with a timeout (vllm/distributed/device_communicators/shm_broadcast.py:733-754)
reader, idleworker main thread in dequeue(indefinite=True)spin 1 s, then poll on the notify SUB socket (CONFLATE, so only one pending ping), rechecking every 5 s; no warnings (vllm/distributed/device_communicators/shm_broadcast.py:141-168, vllm/distributed/device_communicators/shm_broadcast.py:788-827)
reader, shutdownanother thread of the reader's processshutdown() sends on an in-process PAIR socket; the reader raises RuntimeError("cancelled") (vllm/distributed/device_communicators/shm_broadcast.py:709-714, vllm/distributed/device_communicators/shm_broadcast.py:861-862)

The writer's notify socket has a send high-water mark of 1: under load, extra pings are dropped silently, and the reader's SUB socket keeps only the last one (vllm/distributed/device_communicators/shm_broadcast.py:169-176). VLLM_USE_SPINLOOP_EXT=1 replaces the Python spin with an optional C extension, when it can be imported (vllm/distributed/device_communicators/shm_broadcast.py:48-58).

Deep dive

Error paths: what raises, what only logs

SituationResultWhere
not enough /dev/shmRuntimeError "Insufficient space in /dev/shm … Increase /dev/shm (e.g. --shm-size or --ipc=host)"vllm/distributed/device_communicators/shm_broadcast.py:248-258
attach on another nodeFileNotFoundError suppressed; the object is unused therevllm/distributed/device_communicators/shm_broadcast.py:361-365
write timeoutTimeoutErrorvllm/distributed/device_communicators/shm_broadcast.py:743-745
read timeout (or ZMQ poll timeout on overflow)TimeoutErrorvllm/distributed/device_communicators/shm_broadcast.py:814-816, vllm/distributed/device_communicators/shm_broadcast.py:1001-1007
long wait without a timeoutINFO "No available shared memory broadcast block found in 60 seconds. This typically happens when some processes are hanging or doing some time-consuming work …"vllm/distributed/device_communicators/shm_broadcast.py:107-113
a non-reader calls dequeueRuntimeError("Only readers can dequeue")vllm/distributed/device_communicators/shm_broadcast.py:996-998
reader cancelled during shutdownRuntimeError("cancelled")vllm/distributed/device_communicators/shm_broadcast.py:861-862
Deep dive

Environment variables

VariableDefaultRead atEffect
VLLM_MQ_MAX_CHUNK_BYTES_MB16 (vllm/envs.py:1856-1858)vllm/v1/executor/multiproc_executor.py:152Chunk size of the executor's broadcast queue only; replies and group broadcasters use their own sizes.
VLLM_RINGBUFFER_WARNING_INTERVAL60 (vllm/envs.py:745-747)vllm/distributed/device_communicators/shm_broadcast.py:67 (at import)Seconds between the "No available shared memory broadcast block" messages.
VLLM_USE_SPINLOOP_EXT0 (vllm/envs.py:2260)vllm/distributed/device_communicators/shm_broadcast.py:49-56 (at import)Use the optional vllm.spinloop extension for the flag waits.
VLLM_HOST_IP""through get_ip() (vllm/distributed/device_communicators/shm_broadcast.py:553-554)The address that remote readers connect to, when the caller gives no connect_ip.

Check yourself#

  1. 1
    In which order does the writer update the flags after it writes a chunk, and why?
    easy
    Show answer

    It resets every read flag first, then sets "written". The other order would pass through "written and read by some", and a reader could skip the new message (vllm/distributed/device_communicators/shm_broadcast.py:764-780).

  2. 2
    A message pickles to 3 MiB and has one 20 MiB tensor buffer. How does it travel through the executor's queue?
    medium
    Show answer

    The 20 MiB buffer is out of band (≥ 1 MiB). The total, 23 MiB plus framing, does not fit in a 16 MiB chunk, so the chunk only carries the overflow flag and both buffers go over the local XPUB socket (vllm/distributed/device_communicators/shm_broadcast.py:906-946).

  3. 3
    Why does an idle worker not print the "No available shared memory broadcast block" message every 60 seconds?
    medium
    Show answer

    Workers call dequeue(indefinite=True) (vllm/v1/executor/multiproc_executor.py:1046), and acquire_read turns warnings off when indefinite is true (vllm/distributed/device_communicators/shm_broadcast.py:876-878).

  4. 4
    How much /dev/shm do the queues of one TP = 4 engine core ask for?
    hard
    Show answer

    Broadcast: 10 × (16 MiB + 5) ≈ 160 MiB. Four reply queues of 10 × (24 MiB + 2) ≈ 240 MiB each = 960 MiB. The TP group broadcaster: 6 × (4 MiB + 4) ≈ 24 MiB. Total about 1,144 MiB (derived from vllm/distributed/device_communicators/shm_broadcast.py:326-328, vllm/distributed/device_communicators/shm_broadcast.py:489-492, vllm/v1/executor/multiproc_executor.py:152, vllm/distributed/parallel_state.py:564-567).