Rate this Page

Source code for torchrl.modules.inference_server._mp

# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
from __future__ import annotations

import multiprocessing as mp

from torchrl.modules.inference_server._queue_transport import (
    _QueueInferenceClient,
    QueueBasedTransport,
)


[docs] class MPTransport(QueueBasedTransport): """Cross-process transport using :mod:`multiprocessing` queues. Response routing uses per-actor queues (one per :meth:`client` call) so that no ``mp.Queue`` object is ever serialised through another queue. Clients must be created with :meth:`client` **before** spawning child processes. Args: ctx: a multiprocessing context (e.g. ``mp.get_context("spawn")``). Defaults to ``mp.get_context("spawn")``. use_manager (bool, optional): if ``True``, back the request and response queues with a multiprocessing manager. This is useful when clients are forwarded through another spawned process. Defaults to ``False``. Example: >>> import multiprocessing as mp >>> transport = MPTransport() >>> client = transport.client() # creates response queue >>> p = mp.Process(target=actor_fn, args=(client,)) >>> p.start() # queue inherited """ def __init__( self, ctx: mp.context.BaseContext | None = None, *, use_manager: bool = False ): super().__init__() self._ctx = ctx if ctx is not None else mp.get_context("spawn") self._use_manager = bool(use_manager) self._manager = self._ctx.Manager() if use_manager else None if self._manager is None: self._request_queue: mp.Queue = self._ctx.Queue() self._response_queues: dict[int, mp.Queue] = {} else: self._request_queue = self._manager.Queue() self._response_queues = self._manager.dict() # Server-liveness flag baked into every client. It starts set # (optimistically alive) and is cleared by a process-backed server # owner when the server process exits, so blocked clients raise # MailboxPeerClosedError instead of waiting forever on a reply that # will never come. peer_alive = self._ctx.Event() peer_alive.set() self._set_peer_alive(peer_alive) def _make_response_queue(self) -> mp.Queue: if self._manager is not None: return self._manager.Queue() if self._use_manager: # The manager handle is dropped by __getstate__: an unpickled copy # cannot create manager-backed response queues, and falling back to # a raw mp.Queue would fail later (and confusingly) when registered # in the manager-backed response-queue dict. raise RuntimeError( "Cannot create a client from an unpickled manager-backed " "MPTransport: the manager handle is not serialized. Clients of " "a manager-backed MPTransport must be created with " "transport.client() in the owning process before " "pickling/sending the transport." ) return self._ctx.Queue() def __getstate__(self) -> dict: state = super().__getstate__() state["_manager"] = None return state
[docs] def close(self) -> None: """Release transport resources. Shuts down the multiprocessing manager backing the request/response queues when the transport was built with ``use_manager=True`` (a no-op otherwise). The process that owns the transport must call this once the server and all clients are done with it: :class:`~torchrl.modules.inference_server.ProcessInferenceServer` does not close the transport on ``shutdown()``. """ if self._manager is not None: self._manager.shutdown()
[docs] def client(self) -> _QueueInferenceClient: """Create an actor-side client with a dedicated response queue. Must be called in the parent process **before** spawning children. In particular, a manager-backed transport (``use_manager=True``) loses its manager handle when pickled, so calling this on an unpickled copy raises a :class:`RuntimeError`. Returns: A :class:`_QueueInferenceClient` that can be passed to a child process as an argument to :class:`multiprocessing.Process`. """ return super().client()