Communication#
For communication among stages in sglang-omni, ZMQ carries coordination and
serialized control metadata while sglang_omni.comm owns the data movement
contract. Stage code routes by stage name. The comm router chooses same-process
object passing, direct PyTorch CUDA IPC when same-placement processes use
compatible CUDA device ordinals, pooled CUDA IPC for other same-node GPU edges,
SHM for local CPU relay movement, or Mooncake for configured cross-node
movement.
The main implementation entry points are:
File |
Role |
|---|---|
|
Typed relay |
|
Locality and transport selection |
|
Stage-facing communication facade |
|
Payload and stream tensor packing/unpacking |
|
ZMQ sockets, msgpack serialization, stage/coordinator message routing |
|
Same-process Python object dispatch between colocated stages |
|
Backend interface and backend registry |
|
Sender-owned CUDA pool, slot allocation, copies, and completion |
|
Concrete relay backends |
|
Control-plane message types |
Transfer Model#
sequenceDiagram
participant A as Stage A
participant L as Local Dispatcher
participant R as Relay
participant Z as ZMQ Control Plane
participant B as Stage B
alt Same process
A->>L: send Python object
L->>B: receive Python object
else Direct PyTorch CUDA IPC
A->>Z: DataReadyMessage(header and CUDA handles)
Z->>B: receive handles and header
B->>B: import producer CUDA storage
Note over A,B: no relay ACK
else Relay-backed
A->>R: put tensor buffer
A->>Z: DataReadyMessage(data_ref)
Z->>B: receive DataReadyMessage
B->>R: get tensor buffer or blob
B->>Z: DataAckMessage
Z->>A: receive completion ACK
end
Path |
Transport |
Carries |
|---|---|---|
Coordination |
ZMQ |
|
Broadcast coordination |
ZMQ |
|
Same-process movement |
LOCAL_OBJECT |
Full |
Same-placement direct GPU movement |
PyTorch CUDA IPC |
CUDA storage handles plus ordinary payload or stream control metadata |
Same-node pooled GPU movement |
CUDA IPC relay |
Packed payload tensor buffers, CUDA stream chunks, and stream metadata tensors |
Local CPU relay movement |
SHM relay |
Full payload tensor buffers and stream chunks that are not CUDA-local |
Cross-node movement |
Mooncake relay |
Full payload tensor buffers and stream chunks over Mooncake-selected transport |
DataReadyMessage.data_ref carries either a direct PyTorch CUDA IPC envelope or
a typed relay DataRef. A direct envelope contains a pickled payload header or
stream metadata together with PyTorch CUDA storage handles. A relay DataRef
contains the object id, data kind, transport, layout, backend buffer reference,
tensor layout, and optional stream metadata. Backend-owned details from
RelayOperation.metadata live under DataRef.buffer.info.
Normal Payload Flow#
The coordinator submits the first StagePayload directly to the entry stage in a
SubmitMessage. After that, a stage-to-stage payload uses LOCAL_OBJECT, direct
PyTorch CUDA IPC, or a relay according to the edge and payload.
For direct CUDA IPC, Stage extracts CUDA tensor leaves, pickles the remaining
StagePayload as ordinary control metadata, and serializes each CUDA tensor with
PyTorch’s CUDA multiprocessing reducer. It then sends one DataReadyMessage
containing those storage handles. The receiver maps the producer allocations and
restores the payload without a relay buffer. This path has no relay
DataAckMessage. Its lifetime is carried by PyTorch’s CUDA IPC ownership
mechanism.
Ordinary direct-payload headers are ZMQ control metadata. Their size is not tied to the pooled CUDA relay’s slot size, and a large header is not split into relay slots or application messages.
For a relay-backed payload:
The sender asks
CommRouterfor the edge transport and callsCommEngine.send_payload(...).The send worker calls
stage_io.write_payload(), which recursively extracts tensors frompayload.data, replaces them with placeholders, pickles the tensor-freeStagePayload, and concatenates tensors into oneuint8buffer.The sender calls
relay.put_async()for that buffer and sends aDataReadyMessage(data_ref=...)containing aDataRefwith:buffer.info: backend-specific metadata fromRelayOperation.metadataheader: base64-encodedStagePayloadwithout tensorstensors: path, shape, dtype, offset, and byte size for each tensor
The receiver handles the message in
Stage._on_data_ready(), callsCommEngine.read_payload(), waits forrelay.get_async(), restores tensors, and passes the payload through the stage input handler.The receiver sends one
DataAckMessage. The sender then releases the operations retained for the logical envelope.If fan-in is complete, the stage enqueues an
IncomingMessageintoscheduler.inbox.
The relay payload format is intentionally backend-neutral. Backends only need to
move a flat tensor buffer and return metadata that another backend instance can
use for get_async().
LOCAL_OBJECT bypasses relay and the ZMQ DataReadyMessage: the sender calls the
process-local dispatcher, which invokes receive_local_payload() on the target
stage with the projected StagePayload object itself. This is a direct Python
reference transfer, not serialization. Receivers must treat the payload, nested
data containers, tensors, stream chunks, and metadata as read-only. The object
must also stay valid for the receiver’s scheduler queue lifetime. Senders and
projection functions must not mutate or recycle objects after dispatch.
Request and control objects should retain parameters needed downstream, but they should not retain consumed bulk media across later stage hops. The stage that turns raw media into canonical pipeline state owns releasing those references.
For full payloads, LOCAL_OBJECT is allowed for single-target same-process routes.
For fan-out, it is allowed only when each projected payload is a StagePayload
with its own data container, so downstream stages do not share mutable payload
state. Tensor leaves may still be shared intentionally and must be treated as
read-only.
Streaming Flow#
Streaming is used for producer-consumer edges such as thinker to talker hidden
states or talker to vocoder code tensors. The stage layer exposes one sending
helper, CommEngine.send_stream_chunk(), and the router chooses the transport.
For same-node GPU targets:
namespace-compatible processes on the same placement may send CUDA chunks as direct PyTorch CUDA IPC envelopes
direct stream metadata may contain CUDA tensors and ordinary inline values, but not CPU tensors, and the direct codec retains a separate 64 KiB inline metadata admission limit
other same-node GPU edges use the pooled CUDA IPC relay
a pooled stream
DataReadyMessagecarries aDataRefwithtransport="cuda_ipc"and achunk_id
For same-process stream targets:
the stage sends the chunk through
LocalStageDispatcher.send_stream_chunk()the receiver gets the original Python object and metadata by reference
the same read-only and lifetime caveats as payload LOCAL_OBJECT apply
For nonlocal stream targets:
the chunk is written with
write_tensor()tensor-valued metadata is extracted and written as separate
DataRefsthe control message is sent before waiting for pending put operations
the receiver reads the blob in
Stage._on_stream_chunk()and enqueues astream_chunkmessage intoscheduler.inbox
The control-before-wait ordering is important for NIXL and other credit-based backends. If the sender waited for completion before notifying the receiver, the receiver would never start the read that releases the sender’s credit.
Stream completion and stream errors are control-only messages sent with
send_stream_signal().
Relay Interface#
All backends implement Relay:
class Relay:
async def put_async(
self, tensor: torch.Tensor, request_id: str | None = None, dst_rank: int | None = None
) -> RelayOperation: ...
async def get_async(
self, metadata: Any, dest_tensor: torch.Tensor, request_id: str | None = None
) -> RelayOperation: ...
def cleanup(self, request_id: str) -> None: ...
def close(self) -> None: ...
put_async() returns a RelayOperation whose metadata is placed in the
control message. Both put and get operations expose
await wait_for_completion(timeout=...). Stages keep the operation alive until
the transfer is safe to release.
The CUDA IPC relay owns a bounded sender-side GPU pool. Its allocation granule
defaults to 64 KiB and is configurable with cuda_ipc_slot_size_kb. A tensor may
reserve several contiguous slots, but those slots remain one logical transfer.
The sender publishes one DataReadyMessage, the receiver copies from the
exported pool range, and one logical DataAckMessage releases the complete
range. The slots are allocator granularity, not application-level pagination.
Transport Selection#
There is no public backend selector. CommRouter derives the transport from
stage locality and placement:
Transport |
Selection rule |
|---|---|
|
Source and target stages share one OS process and the payload is eligible for direct local dispatch. |
Direct PyTorch CUDA IPC |
Source and target share one placement, are in separate processes, and the runtime can prove compatible process-local CUDA ordinals. The payload or stream chunk must also be direct-codec eligible. |
|
Same-node GPU edge that does not use direct PyTorch CUDA IPC. The pooled relay supports same-GPU and cross-GPU movement. |
|
Same-node host/CPU transfer where the selected edge is not GPU-to-GPU. |
|
Cross-node stage edges listed as remote. Mooncake owns protocol selection for those transfers. |
CommConfig can tune slot size, credits, and Mooncake connection options per
stage. It does not select a transport backend.
Each backend owns only transport mechanics. It does not route requests, perform fan-in, choose downstream stages, or interpret model payloads.
Resource Lifetime#
The stage layer follows a simple ownership rule:
sender writes data, sends
DataReadyMessage, then waits for the put operation when required by the backendreceiver allocates the destination buffer, waits for the get operation, restores the payload, and calls
relay.cleanup(request_id)a pooled CUDA IPC sender retains its complete slot range until the receiver’s one logical ACK marks every operation for the envelope complete
direct PyTorch CUDA IPC has no relay ACK and relies on PyTorch’s imported storage lifetime
LOCAL_OBJECT has no backend cleanup. Sender and receiver share Python object references, so correctness depends on read-only use until the receiver is done
aborts call
relay.cleanup(request_id)from the stage abort pathstage shutdown calls
relay.close()
Backend-specific cleanup is hidden behind that interface. For example, shm
unlinks blocks on receive, NIXL and Mooncake release memory-pool credits after
completion, and NCCL tears down the process group on close.