DeepInfer Explained

distributed/shm_broadcast.py: MessageQueue

AdvancedCode mapabout 9 min read + 4 min deep dives

Code version: DeepInfer @ 9bd9cf2 · 2026-10-21

Purpose#

shm_broadcast.py sends Python objects from one process to several processes on the same host, through a ring buffer in shared memory, with ZeroMQ sockets for set-up and for large objects.

Filedeepinfer/distributed/shm_broadcast.py, 461 lines of Python. "Derived from vLLM; modified by DeepSeek." (deepinfer/distributed/shm_broadcast.py:1-2). deepinfer/distributed/__init__.py is empty.
Main classesShmRingBuffer (deepinfer/distributed/shm_broadcast.py:33), Handle (deepinfer/distributed/shm_broadcast.py:152-159), MessageQueue (deepinfer/distributed/shm_broadcast.py:162).
Runs inThe CPU side of every rank of a CP group with more than one rank: the writer in the engine process (CP rank 0), a reader in each CP follower process. No device code.
Called byParallelProcessGroup.create_process_groups (set-up) and cp_broadcast_object (every message), both in parallel_state.py.
Callsmultiprocessing.shared_memory, pickle, zmq, torch.distributed.broadcast_object_list (one time, for the handle).
Not in this fileTensor data. Device tensors go through NCCL or HCCL broadcasts in communication_op.py. The pinned memory pool for images is a different module: shared_memory.py: the pinned memory pool for images.

Where it sits#

Figure: the ring buffer of one CP group with 4 ranks (panel 1), the life of one chunk (panel 2), and the path of one object (panel 3). Click to zoom.
Open full size ↗

Read panel 2 from left to right. The dashed arrow is the writer that comes back to the same chunk 10 messages later. Panel 3 is the path of every call from the CP driver to its followers.

Who calls it#

What it calls#

Key classes and functions#

NameLineWhat it does
ShmRingBuffer.__init__deepinfer/distributed/shm_broadcast.py:34-121Makes (writer) or opens (reader) the shared memory; the docstring describes the layout and the flag protocol.
handle, __reduce__deepinfer/distributed/shm_broadcast.py:123-127Pickles as (n_reader, max_chunk_bytes, max_chunks, name), so another process opens the same memory.
__del__deepinfer/distributed/shm_broadcast.py:129-135Closes; the creator also unlinks.
get_data, get_metadatadeepinfer/distributed/shm_broadcast.py:137-149Memory views of one chunk and of its flag row.
Handledeepinfer/distributed/shm_broadcast.py:152-159Dataclass that a reader needs: IP, local reader ranks, buffer handle, ports.
MessageQueue.__init__deepinfer/distributed/shm_broadcast.py:163-239Writer side: the ring buffer, an XPUB socket for local readers, another for remote readers.
create_from_handledeepinfer/distributed/shm_broadcast.py:244-284Reader side: opens the buffer and connects a SUB socket.
wait_until_readydeepinfer/distributed/shm_broadcast.py:286-306Handshake: the writer waits for all subscriptions, then sends b"READY".
_spin_waitdeepinfer/distributed/shm_broadcast.py:308-318One wait turn: yield the CPU, log now and then, raise on time-out.
acquire_write, acquire_readdeepinfer/distributed/shm_broadcast.py:320-387Context managers that wait for a usable chunk and update its flags.
enqueue, dequeuedeepinfer/distributed/shm_broadcast.py:389-422Pickle and write; read and unpickle. Large objects go over the socket.
broadcast_objectdeepinfer/distributed/shm_broadcast.py:424-429Writer enqueues, reader dequeues. DeepInfer uses its own copy of this logic in cp_broadcast_object; only the test calls this method.
create_from_process_groupdeepinfer/distributed/shm_broadcast.py:431-461Collective set-up over a process group: writer makes, readers open, all wait until ready.

Walk through the main methods#

create_from_process_group: set-up over a group#

    @staticmethod
    def create_from_process_group(
        pg: ProcessGroup,
        max_chunk_bytes: int,
        max_chunks: int,
        writer_rank: int = 0,
    ) -> "MessageQueue":
        group_rank = dist.get_rank(pg)
        group_world_size = dist.get_world_size(pg)
        global_ranks = dist.get_process_group_ranks(pg)

        # assume the group does not cross nodes, so every reader is a local reader
        n_reader = group_world_size - 1
        local_reader_ranks = [i for i in range(group_world_size) if i != writer_rank]
        buffer_io: MessageQueue
        if group_rank == writer_rank:
            buffer_io = MessageQueue(
                n_reader=n_reader,
                n_local_reader=n_reader,
                local_reader_ranks=local_reader_ranks,
                max_chunk_bytes=max_chunk_bytes,
                max_chunks=max_chunks,
            )
            dist.broadcast_object_list([buffer_io.export_handle()], src=global_ranks[writer_rank], group=pg)
        else:
            recv = [None]
            dist.broadcast_object_list(recv, src=global_ranks[writer_rank], group=pg)
            handle = recv[0]  # type: ignore
            buffer_io = MessageQueue.create_from_handle(handle, group_rank)
        buffer_io.wait_until_ready()
        return buffer_io
  1. Line 442: the comment states the assumption. Every reader is a local (shared-memory) reader. So the group must be on one host.
  2. Lines 446-454: the writer (group rank 0 by default) makes the queue and broadcasts its Handle with broadcast_object_list. This is a Gloo collective on the CPU CP group.
  3. Lines 455-459: the readers get the handle and open the queue.
  4. Line 460: all ranks call wait_until_ready.

MessageQueue.__init__ and create_from_handle: the two sides#

        if n_local_reader > 0:
            # for local readers, we will:
            # 1. create a shared memory ring buffer to communicate small data
            # 2. create a publish-subscribe socket to communicate large data
            self.buffer = ShmRingBuffer(n_local_reader, max_chunk_bytes, max_chunks)

            # XPUB is very similar to PUB,
            # except that it can receive subscription messages
            # to confirm the number of subscribers
            self.local_socket = context.socket(XPUB)
            # set the verbose option so that we can receive every subscription
            # message. otherwise, we will only receive the first subscription
            # see http://api.zeromq.org/3-3:zmq-setsockopt for more details
            self.local_socket.setsockopt(XPUB_VERBOSE, True)
            local_subscribe_port = get_open_port()
            socket_addr = f"tcp://127.0.0.1:{local_subscribe_port}"
            logger.debug("Binding to %s", socket_addr)
            self.local_socket.bind(socket_addr)

            self.current_idx = 0

        else:
            self.buffer = None  # type: ignore
            local_subscribe_port = None
            self.local_socket = None
            self.current_idx = -1

The writer makes a ShmRingBuffer with one flag per local reader, and binds an XPUB socket on a free local port. XPUB_VERBOSE makes the socket pass every subscription message to the writer, so the writer can count the readers (the comment on lines 194-196). A reader opens the same buffer from the handle and connects a SUB socket that accepts all messages (deepinfer/distributed/shm_broadcast.py:252-264). The reader's position in local_reader_ranks is its flag number (deepinfer/distributed/shm_broadcast.py:256).

When a reader opens the buffer, the code patches multiprocessing.resource_tracker.register to do nothing. The comment explains: "Python incorrectly tracks shared memory even if it is not created by the process" (deepinfer/distributed/shm_broadcast.py:110-115).

acquire_write: wait for a free chunk#

    @contextmanager
    def acquire_write(self, timeout: float | None = None) -> Generator[memoryview, None, None]:
        assert self._is_writer, "Only writers can acquire write"
        start_time = time.monotonic()
        n_warning = 1
        while True:
            with self.buffer.get_metadata(self.current_idx) as metadata_buffer:
                read_count = sum(metadata_buffer[1:])
                written_flag = metadata_buffer[0]
                if written_flag and read_count != self.buffer.n_reader:
                    # 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
                    n_warning = self._spin_wait(start_time, n_warning, timeout)
                    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
                # mark the block as written
                metadata_buffer[0] = 1
                self.current_idx = (self.current_idx + 1) % self.buffer.max_chunks
                break
  1. Lines 326-335: look at the flag row of chunk current_idx. If it is written and not read by all readers, wait one turn and look again.
  2. Line 341: set written = 0. The chunk is now "not written" for all readers.
  3. Lines 343-344: give the chunk to the caller, which writes the data.
  4. Lines 351-355: set all reader flags to 0, then set written = 1. The comment on lines 347-350 says the order is important: with the other order, a reader could see "written and already read" for a short time.
  5. Line 356: the next write uses the next chunk, modulo 10.

acquire_read: wait for a new chunk#

    @contextmanager
    def acquire_read(self, timeout: float | None = None) -> Generator[memoryview, None, None]:
        assert self._is_local_reader, "Only readers can acquire read"
        start_time = time.monotonic()
        n_warning = 1
        while True:
            with self.buffer.get_metadata(self.current_idx) as metadata_buffer:
                read_flag = metadata_buffer[self.local_reader_rank + 1]
                written_flag = metadata_buffer[0]
                if not written_flag or read_flag:
                    # this block is either
                    # (1) not written
                    # (2) already read by this reader

                    # for readers, `self.current_idx` is the next block to read
                    # if this block is not ready,
                    # we need to wait until it is written
                    n_warning = self._spin_wait(start_time, n_warning, 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:
                    yield buf

                # caller has read from the buffer
                # set the read flag
                metadata_buffer[self.local_reader_rank + 1] = 1
                self.current_idx = (self.current_idx + 1) % self.buffer.max_chunks
                break

Each reader keeps its own current_idx. It waits until its chunk is written and its own flag is 0. After the caller has read the data, it sets its own flag to 1 and moves to the next chunk. Readers never touch the written flag.

enqueue and dequeue: the object path#

    def enqueue(self, obj: Any, timeout: float | None = None) -> None:
        """Write to message queue with optional timeout (in seconds)"""
        assert self._is_writer, "Only writers can enqueue"
        serialized_obj = pickle.dumps(obj, protocol=pickle.HIGHEST_PROTOCOL)
        if self.n_local_reader > 0:
            # first byte of the chunk is the overflow flag; the payload is stored
            # inline if it fits, otherwise it is sent through the local socket
            overflow = len(serialized_obj) >= self.buffer.max_chunk_bytes
            with self.acquire_write(timeout) as buf:
                buf[0] = overflow
                if not overflow:
                    buf[1 : len(serialized_obj) + 1] = serialized_obj
            if overflow:
                self.local_socket.send(serialized_obj)
        if self.n_remote_reader > 0:
            self.remote_socket.send(serialized_obj)

    def dequeue(self, timeout: float | None = None) -> Any:
        """Read from message queue with optional timeout (in seconds)"""
        if self._is_local_reader:
            with self.acquire_read(timeout) as buf:
                overflow = buf[0] == 1
                if not overflow:
                    # no need to know the size of serialized object
                    # pickle format contains the size information internally
                    # see https://docs.python.org/3/library/pickle.html
                    obj = pickle.loads(buf[1:])
            if overflow:
                obj = pickle.loads(self.local_socket.recv())
        elif self._is_remote_reader:
            obj = pickle.loads(self.remote_socket.recv())
        else:
            raise RuntimeError("Only readers can dequeue")
        return obj
  1. Line 392: pickle the object with the highest protocol.
  2. Line 396: the object "overflows" if its pickle has max_chunk_bytes bytes or more (1 MiB in the CP queue).
  3. Lines 397-400: byte 0 of the chunk is the overflow flag. A small object goes into bytes 1 and up.
  4. Lines 401-402: a large object goes over the local XPUB socket. The chunk still goes through the ring, as a ticket.
  5. Lines 410-417: the reader reads byte 0. For a small object, pickle.loads(buf[1:]) reads the object; pickle knows its own length (comment, lines 412-414). For a large object, the reader reads one message from its SUB socket.

wait_until_ready and _spin_wait#

    def wait_until_ready(self) -> None:
        """This is a collective operation. All processes (including the
        readers and the writer) should call this function.
        """
        if self._is_writer:
            # wait for subscription messages from all readers, then send a
            # message to confirm the publish channel is working
            for n_reader, socket in (
                (self.n_local_reader, self.local_socket),
                (self.n_remote_reader, self.remote_socket),
            ):
                for _ in range(n_reader):
                    socket.recv()
                if n_reader > 0:
                    socket.send(b"READY")
        elif self._is_local_reader:
            # wait for the writer to send a message
            assert self.local_socket.recv() == b"READY"
        elif self._is_remote_reader:
            # wait for the writer to send a message
            assert self.remote_socket.recv() == b"READY"

    @staticmethod
    def _spin_wait(start_time: float, n_warning: int, timeout: float | None = None) -> int:
        """自旋等待一轮:让出 CPU、按间隔打 debug 日志、超时抛 TimeoutError。返回更新后的 n_warning。"""
        os.sched_yield()
        elapsed = time.monotonic() - start_time
        if elapsed > envs.DEEPINFER_RINGBUFFER_WARNING_INTERVAL * n_warning:
            logger.debug("No available shared memory block after %.1f seconds", elapsed)
            n_warning += 1
        if timeout is not None and elapsed > timeout:
            raise TimeoutError
        return n_warning

Translated comments (Chinese → English)

  • 310 Spin-wait for one turn: give up the CPU, write a debug log at intervals, raise TimeoutError on time-out. Returns the updated n_warning.

The handshake (lines 290-306) makes sure that every SUB socket is connected before the first large object goes out. A ZeroMQ publisher drops messages for subscribers that are not connected yet (general ZeroMQ behaviour). The wait loop (lines 308-318) calls os.sched_yield() on each turn. It does not sleep.

Deep dive#

Deep dive

Data flow: what goes through the queue in one step

With CP above 1, each prefill step sends these objects from the driver, in this order (the first three come from run_workers, _prepare_input_driver and prepare_sampler_inputs):

#ObjectTypical sizeSource
1("execute_model_step", {"is_prompt": True, "is_placeholder": False})tens of bytesdeepinfer/worker/base.py:300
2list of (key, value): the input metadata, with a TensorMetadata(dtype, size) for each tensora few KiBdeepinfer/model_executor/parallel_utils/communication_op.py:64-72
3the sampler metadata dict, the same waysmalldeepinfer/worker/model_runner.py:252-259

The tensor data does not go through the queue. After each metadata list, cp_broadcast_multi_tensor sends the tensors with one device broadcast per tensor (deepinfer/model_executor/parallel_utils/communication_op.py:36-43). So the 1 MiB chunk is large enough for normal steps, and the overflow path is rare. (Sizes in the table are estimates, not measured.)

Types. Any picklable object can go. The input metadata dict contains a class object ("type": type(input_metadata)), which pickle stores by name (deepinfer/worker/model_runner.py:204).

Deep dive

Processes, threads and CPU use

Deep dive

Worked example: chunk numbers over 12 messages

CP group of 4: writer W, readers R0, R1, R2, 10 chunks. W sends messages m0 to m11.

  1. m0 to m9 go into chunks 0 to 9. Each chunk becomes 1 0 0 0.
  2. R0 and R2 are fast. They read m0 to m9; the rows become 1 1 0 1. R1 is slow and has read only m0 to m3: chunks 0-3 are 1 1 1 1.
  3. m10 goes to chunk 0 (10 % 10). Chunk 0 is 1 1 1 1, so W can write at once.
  4. m11 goes to chunk 1, also free.
  5. m14 would go to chunk 4. Chunk 4 is 1 1 0 1, so W waits until R1 reads m4.

So the writer can be at most 10 messages ahead of the slowest reader. In DeepInfer, each step sends 3 messages (the method call, the input metadata and the sampler metadata), so the driver CPU can be about 3 steps ahead before it waits.

Deep dive

Error paths

SituationResultWhere
enqueue on a reader, acquire_read on the writerAssertionErrordeepinfer/distributed/shm_broadcast.py:322, deepinfer/distributed/shm_broadcast.py:361, deepinfer/distributed/shm_broadcast.py:391
dequeue on the writerRuntimeError("Only readers can dequeue")deepinfer/distributed/shm_broadcast.py:420-421
Time-out passed and reachedTimeoutError. DeepInfer passes no time-out, so it waits for ever.deepinfer/distributed/shm_broadcast.py:316-317
A reader on another hostThe buffer does not exist there; FileNotFoundError is suppressed, and the object has no shared_memorydeepinfer/distributed/shm_broadcast.py:117-121
Buffer size differs from the handleAssertionErrordeepinfer/distributed/shm_broadcast.py:116
Handshake message is not b"READY"AssertionErrordeepinfer/distributed/shm_broadcast.py:303, deepinfer/distributed/shm_broadcast.py:306
Buffer already unlinked at __del__suppresseddeepinfer/distributed/shm_broadcast.py:133-135

Settings. The only environment variable in this file is DEEPINFER_RINGBUFFER_WARNING_INTERVAL (default 60, deepinfer/environ.py:184). The chunk size and count come from the caller: 1 MiB and 10 for the CP queue (deepinfer/model_executor/parallel_utils/parallel_state.py:124). The defaults of the class are 10 MiB and 10 (deepinfer/distributed/shm_broadcast.py:168-169).

Deep dive

Remote readers: in the code, not used by DeepInfer

The class can also serve readers on other hosts. For them, the writer binds a second XPUB socket on tcp://*:<port>, and enqueue sends every object over it (deepinfer/distributed/shm_broadcast.py:211-220, deepinfer/distributed/shm_broadcast.py:403-404). The writer's IP comes from get_ip(), which opens a UDP socket toward 8.8.8.8 to learn the local address; no packet is needed (deepinfer/utils.py:40-43). IPv6 addresses set the IPV6 socket option.

create_from_process_group always sets n_local_reader = n_reader (deepinfer/distributed/shm_broadcast.py:443-453), so in DeepInfer the remote path stays unused.

Check yourself#

  1. 1
    Which process writes to the CP queue, and which processes read from it?
    easy
    Show answer

    CP rank 0 (the engine process with the driver worker) writes. Each CP follower reads. The writer is writer_rank = 0 of the CPU CP group (deepinfer/distributed/shm_broadcast.py:436, deepinfer/model_executor/parallel_utils/parallel_state.py:124).

  2. 2
    Does the queue carry the tensor data of the input metadata?
    easy
    Show answer

    No. It carries the keys, the small values and a TensorMetadata(dtype, size) for each tensor. The tensors go with device broadcasts (deepinfer/model_executor/parallel_utils/communication_op.py:64-81).

  3. 3
    A pickled object has exactly 1,048,576 bytes (1 MiB). Which path does it take in the CP queue?
    medium
    Show answer

    The overflow path. overflow = len(serialized_obj) >= max_chunk_bytes (deepinfer/distributed/shm_broadcast.py:396), and 1 MiB is not smaller than 1 MiB. Byte 0 of the chunk becomes 1, and the bytes go over the local ZeroMQ socket. (The chunk also needs byte 0 for the flag, so an inline object can have at most max_chunk_bytes − 1 bytes.)

  4. 4
    Why does acquire_write reset the reader flags before it sets written = 1, and not after?
    hard
    Show answer

    With the other order, the row would go from 0 1 1 1 to 1 1 1 1 and 1 0 1 1 before it reaches 1 0 0 0. These intermediate rows say "written and already read" for a message that nobody has read. The docstring calls this intermediate case 3 "not correct" (deepinfer/distributed/shm_broadcast.py:80-85, deepinfer/distributed/shm_broadcast.py:347-355). The reset-first order keeps the row in a valid state at every moment. (Our reading of acquire_read: a reader that sees its own flag still at 1 does not skip the chunk. It waits one more turn, because it moves to the next chunk only after it reads, lines 365-377.)

  5. 5
    A follower is idle for an hour between two requests. What does its CPU do, and what appears in the logs?
    hard
    Show answer

    It spins in acquire_read: each turn reads the flag row and calls os.sched_yield(), with no sleep, so one core stays busy (deepinfer/distributed/shm_broadcast.py:364-377). Every 60 s it writes a debug line "No available shared memory block after ... seconds" (deepinfer/distributed/shm_broadcast.py:312-315). Note: with LLM.generate, the driver keeps calling step() while there is work. When it stops, the follower waits.