Files
wehub-resource-sync 94057c3d3e
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:38:16 +08:00

1077 lines
38 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""Central request router for disaggregated diffusion pipelines."""
import json
import logging
import pickle
import threading
import time
from collections import deque
from dataclasses import dataclass
import zmq
from sglang.multimodal_gen.runtime.disaggregation.dispatch_policy import (
PoolDispatcher,
)
from sglang.multimodal_gen.runtime.disaggregation.request_state import (
RequestState,
RequestTracker,
)
from sglang.multimodal_gen.runtime.disaggregation.roles import RoleType
from sglang.multimodal_gen.runtime.disaggregation.transport.codec import (
unpack_tensors,
)
from sglang.multimodal_gen.runtime.disaggregation.transport.protocol import (
TransferAllocMsg,
TransferMsgType,
TransferPushMsg,
TransferReadyMsg,
decode_transfer_msg,
encode_transfer_msg,
is_transfer_message,
)
from sglang.multimodal_gen.runtime.utils.common import get_zmq_socket
logger = logging.getLogger(__name__)
@dataclass
class _EncoderTTAEntry:
request_id: str
client_identity: bytes
payload: bytes
@dataclass
class _TransferRequestState:
sender_session_id: str = ""
sender_pool_ptr: int = 0
sender_slot_offset: int = 0
data_size: int = 0
manifest: dict = None
scalar_fields: dict = None
receiver_session_id: str = ""
receiver_pool_ptr: int = 0
receiver_slot_offset: int = 0
sender_instance: int = -1
receiver_instance: int = -1
prealloc_slot_id: int | None = None
def __post_init__(self):
if self.manifest is None:
self.manifest = {}
if self.scalar_fields is None:
self.scalar_fields = {}
@dataclass
class _RoleTTAEntry:
request_id: str
transfer_state: _TransferRequestState | None = None
class DiffusionServer:
"""Global pipeline orchestrator for N:M:K disaggregated diffusion.
Capacity-aware dispatch with FreeBufferSlots per instance and TTA queues.
"""
def __init__(
self,
frontend_endpoint: str,
encoder_work_endpoints: list[str],
denoiser_work_endpoints: list[str],
decoder_work_endpoints: list[str],
encoder_result_endpoint: str,
denoiser_result_endpoint: str,
decoder_result_endpoint: str,
dispatch_policy_name: str = "round_robin",
timeout_s: float = 600.0,
encoder_capacity: int = 4,
denoiser_capacity: int = 2,
decoder_capacity: int = 4,
p2p_mode: bool = True,
):
self._frontend_endpoint = frontend_endpoint
self._encoder_work_endpoints = encoder_work_endpoints
self._denoiser_work_endpoints = denoiser_work_endpoints
self._decoder_work_endpoints = decoder_work_endpoints
self._encoder_result_endpoint = encoder_result_endpoint
self._denoiser_result_endpoint = denoiser_result_endpoint
self._decoder_result_endpoint = decoder_result_endpoint
self._num_encoders = len(encoder_work_endpoints)
self._num_denoisers = len(denoiser_work_endpoints)
self._num_decoders = len(decoder_work_endpoints)
self._timeout_s = timeout_s
self._tracker = RequestTracker()
self._dispatcher = PoolDispatcher(
num_encoders=self._num_encoders,
num_denoisers=self._num_denoisers,
num_decoders=self._num_decoders,
policy_name=dispatch_policy_name,
)
self._context = zmq.Context(io_threads=2)
self._running = False
self._ready = threading.Event()
self._thread: threading.Thread | None = None
self._pending: dict[str, bytes] = {} # request_id -> client ZMQ identity
self._lock = threading.Lock()
# FreeBufferSlots per instance
self._encoder_free_slots = [encoder_capacity] * self._num_encoders
self._denoiser_free_slots = [denoiser_capacity] * self._num_denoisers
self._decoder_free_slots = [decoder_capacity] * self._num_decoders
# TTA queues per role type
self._encoder_tta: deque[_EncoderTTAEntry] = deque()
self._denoiser_tta: deque[_RoleTTAEntry] = deque()
self._decoder_tta: deque[_RoleTTAEntry] = deque()
self._transfer_mode = p2p_mode
self._transfer_state: dict[str, _TransferRequestState] = {}
# Per-instance registration: instance_idx -> {session_id, pool_ptr, pool_size}
# Keyed by the same index used to build the PUSH work-socket list
# (i.e. the index into --encoder/denoiser/decoder-urls). The index is
# resolved from the registering instance's work_endpoint so the control
# plane (work PUSH) and the data plane (RDMA session_id / pool_ptr /
# preallocated slots) stay consistent regardless of startup order.
self._encoder_peers: dict[int, dict] = {}
self._denoiser_peers: dict[int, dict] = {}
self._decoder_peers: dict[int, dict] = {}
# work_endpoint -> index lookup tables, built from the --*-urls args
self._encoder_endpoint_to_idx = {
ep: i for i, ep in enumerate(encoder_work_endpoints)
}
self._denoiser_endpoint_to_idx = {
ep: i for i, ep in enumerate(denoiser_work_endpoints)
}
self._decoder_endpoint_to_idx = {
ep: i for i, ep in enumerate(decoder_work_endpoints)
}
@property
def tracker(self) -> RequestTracker:
return self._tracker
@property
def dispatcher(self) -> PoolDispatcher:
return self._dispatcher
def start(self) -> None:
if self._running:
return
self._running = True
self._thread = threading.Thread(
target=self._event_loop,
name="DiffusionServer",
daemon=True,
)
self._thread.start()
logger.info(
"DiffusionServer started: frontend=%s, "
"%d encoder(s), %d denoiser(s), %d decoder(s), policy=%s, "
"capacity=(%d/%d/%d)",
self._frontend_endpoint,
self._num_encoders,
self._num_denoisers,
self._num_decoders,
type(self._dispatcher.encoder_policy).__name__,
self._encoder_free_slots[0] if self._encoder_free_slots else 0,
self._denoiser_free_slots[0] if self._denoiser_free_slots else 0,
self._decoder_free_slots[0] if self._decoder_free_slots else 0,
)
def wait_ready(self, timeout: float = 30.0) -> bool:
"""Block until the event loop has bound all sockets, or *timeout* elapses."""
return self._ready.wait(timeout=timeout)
def stop(self) -> None:
self._running = False
if self._thread is not None:
self._thread.join(timeout=5.0)
self._thread = None
def _event_loop(self) -> None:
frontend, _ = get_zmq_socket(
self._context, zmq.ROUTER, self._frontend_endpoint, bind=True
)
encoder_pushes: list[zmq.Socket] = []
for i, ep in enumerate(self._encoder_work_endpoints):
sock, _ = get_zmq_socket(self._context, zmq.PUSH, ep, bind=False)
encoder_pushes.append(sock)
denoiser_pushes: list[zmq.Socket] = []
for i, ep in enumerate(self._denoiser_work_endpoints):
sock, _ = get_zmq_socket(self._context, zmq.PUSH, ep, bind=False)
denoiser_pushes.append(sock)
decoder_pushes: list[zmq.Socket] = []
for i, ep in enumerate(self._decoder_work_endpoints):
sock, _ = get_zmq_socket(self._context, zmq.PUSH, ep, bind=False)
decoder_pushes.append(sock)
encoder_result_pull, _ = get_zmq_socket(
self._context, zmq.PULL, self._encoder_result_endpoint, bind=True
)
denoiser_result_pull, _ = get_zmq_socket(
self._context, zmq.PULL, self._denoiser_result_endpoint, bind=True
)
decoder_result_pull, _ = get_zmq_socket(
self._context, zmq.PULL, self._decoder_result_endpoint, bind=True
)
poller = zmq.Poller()
poller.register(frontend, zmq.POLLIN)
poller.register(encoder_result_pull, zmq.POLLIN)
poller.register(denoiser_result_pull, zmq.POLLIN)
poller.register(decoder_result_pull, zmq.POLLIN)
self._encoder_pushes = encoder_pushes
self._denoiser_pushes = denoiser_pushes
self._decoder_pushes = decoder_pushes
self._frontend = frontend
self._ready.set()
all_sockets = (
[frontend, encoder_result_pull, denoiser_result_pull, decoder_result_pull]
+ encoder_pushes
+ denoiser_pushes
+ decoder_pushes
)
try:
while self._running:
events = dict(poller.poll(timeout=10))
self._handle_timeouts()
if frontend in events:
self._handle_client_request(frontend)
if encoder_result_pull in events:
self._handle_role_result(encoder_result_pull, RoleType.ENCODER)
if denoiser_result_pull in events:
self._handle_role_result(denoiser_result_pull, RoleType.DENOISER)
if decoder_result_pull in events:
self._handle_role_result(decoder_result_pull, RoleType.DECODER)
self._drain_all_queues()
except Exception:
logger.exception("DiffusionServer event loop error")
finally:
for sock in all_sockets:
sock.close()
self._context.destroy(linger=0)
def _handle_role_result(self, result_pull: zmq.Socket, role: RoleType) -> None:
try:
frames = result_pull.recv_multipart(zmq.NOBLOCK, copy=True)
except zmq.Again:
return
if is_transfer_message(frames):
self._handle_transfer_result(frames, role)
return
if role == RoleType.DECODER:
self._handle_decoder_result_frames(frames)
else:
# Non-transfer frames from encoder/denoiser are error results
# sent via send_tensors (e.g., _disagg_error).
self._handle_role_error_frames(frames, role)
def _handle_role_error_frames(self, frames: list, role: RoleType) -> None:
"""Handle non-transfer error results from encoder/denoiser roles."""
try:
tensor_fields, scalar_fields = unpack_tensors(frames, device="cpu")
except Exception as e:
logger.warning(
"DiffusionServer: failed to unpack non-transfer frames from %s: %s",
role.value,
e,
)
return
request_id = scalar_fields.get("request_id")
disagg_error = scalar_fields.get("_disagg_error")
if request_id and disagg_error:
logger.error(
"DiffusionServer: %s error for %s: %s",
role.value,
request_id,
disagg_error,
)
self._complete_with_error(request_id, f"{role.value} error: {disagg_error}")
elif request_id:
logger.warning(
"DiffusionServer: non-transfer frames from %s for %s without error",
role.value,
request_id,
)
else:
logger.warning(
"DiffusionServer: non-transfer frames from %s without request_id",
role.value,
)
def _handle_client_request(self, frontend: zmq.Socket) -> None:
try:
parts = frontend.recv_multipart(zmq.NOBLOCK)
except zmq.Again:
return
if len(parts) < 3:
return
client_identity = parts[0]
payload = parts[-1]
try:
reqs = pickle.loads(payload)
except (pickle.UnpicklingError, EOFError):
logger.warning("DiffusionServer: failed to deserialize request")
return
if not isinstance(reqs, list):
reqs = [reqs]
req = reqs[0]
if isinstance(req, dict) or not hasattr(req, "request_id"):
# Send empty reply so REQ socket doesn't hang
try:
frontend.send_multipart(
[client_identity, b"", pickle.dumps({"status": "ignored"})],
zmq.NOBLOCK,
)
except zmq.Again:
pass
return
request_id = getattr(req, "request_id", None)
if request_id is None:
request_id = f"ds-{time.monotonic()}"
try:
self._tracker.submit(request_id)
except ValueError:
logger.warning("DiffusionServer: duplicate request_id %s", request_id)
return
with self._lock:
self._pending[request_id] = client_identity
try:
self._tracker.transition(request_id, RequestState.ENCODER_WAITING)
except ValueError:
pass
self._encoder_tta.append(
_EncoderTTAEntry(
request_id=request_id,
client_identity=client_identity,
payload=payload,
)
)
logger.debug(
"DiffusionServer: queued %s to encoder_tta",
request_id,
)
def _handle_decoder_result_frames(self, frames: list) -> None:
from sglang.multimodal_gen.runtime.pipelines_core.schedule_batch import (
OutputBatch,
)
request_id = self._extract_request_id(frames)
if request_id is None:
logger.warning("DiffusionServer: decoder result missing request_id")
return
logger.debug("DiffusionServer: decoder result %s", request_id)
record = self._tracker.get(request_id)
if record and record.decoder_instance is not None:
self._decoder_free_slots[record.decoder_instance] += 1
tensor_fields, scalar_fields = unpack_tensors(frames, device="cpu")
output_batch = OutputBatch(
output=tensor_fields.get("output"),
audio=tensor_fields.get("audio"),
audio_sample_rate=scalar_fields.get("audio_sample_rate"),
error=scalar_fields.get("error"),
)
try:
if output_batch.error:
self._tracker.transition(
request_id, RequestState.FAILED, error=output_batch.error
)
else:
self._tracker.transition(request_id, RequestState.DONE)
except ValueError:
pass
with self._lock:
client_identity = self._pending.pop(request_id, None)
if client_identity is None:
logger.warning(
"DiffusionServer: no pending client for decoder result %s",
request_id,
)
self._tracker.remove(request_id)
return
try:
self._frontend.send_multipart(
[client_identity, b"", pickle.dumps(output_batch)]
)
except zmq.ZMQError as e:
logger.error(
"DiffusionServer: failed to send result for %s: %s",
request_id,
e,
)
logger.debug("DiffusionServer: returned result for %s", request_id)
self._transfer_state.pop(request_id, None)
self._tracker.remove(request_id)
def _dispatch_to_encoder(
self, request_id: str, payload: bytes, encoder_idx: int
) -> None:
self._encoder_free_slots[encoder_idx] -= 1
try:
self._tracker.transition(
request_id,
RequestState.ENCODER_RUNNING,
encoder_instance=encoder_idx,
)
except ValueError:
pass
self._encoder_pushes[encoder_idx].send_multipart(
[request_id.encode("utf-8"), payload]
)
logger.debug(
"DiffusionServer: dispatched %s to encoder[%d] (free=%d)",
request_id,
encoder_idx,
self._encoder_free_slots[encoder_idx],
)
def _drain_all_queues(self) -> None:
self._drain_encoder_tta()
self._drain_denoiser_tta()
self._drain_decoder_tta()
def _drain_encoder_tta(self) -> None:
while self._encoder_tta:
idx = self._dispatcher.select_encoder_with_capacity(
self._encoder_free_slots
)
if idx is None:
break
entry = self._encoder_tta.popleft()
self._dispatch_to_encoder(entry.request_id, entry.payload, idx)
def _drain_denoiser_tta(self) -> None:
while self._denoiser_tta:
idx = self._dispatcher.select_denoiser_with_capacity(
self._denoiser_free_slots
)
if idx is None:
break
entry = self._denoiser_tta.popleft()
self._transfer_dispatch_to_denoiser(
entry.request_id, entry.transfer_state, idx
)
def _drain_decoder_tta(self) -> None:
while self._decoder_tta:
idx = self._dispatcher.select_decoder_with_capacity(
self._decoder_free_slots
)
if idx is None:
break
entry = self._decoder_tta.popleft()
self._transfer_dispatch_to_decoder(
entry.request_id, entry.transfer_state, idx
)
def _extract_request_id(self, frames: list) -> str | None:
try:
metadata = json.loads(frames[0])
return metadata.get("scalar_fields", {}).get("request_id")
except (json.JSONDecodeError, IndexError, TypeError):
return None
def _complete_with_error(self, request_id: str, error_msg: str) -> None:
from sglang.multimodal_gen.runtime.pipelines_core.schedule_batch import (
OutputBatch,
)
logger.error("DiffusionServer: %s%s", request_id, error_msg)
try:
self._tracker.transition(request_id, RequestState.FAILED, error=error_msg)
except ValueError:
pass
with self._lock:
client_identity = self._pending.pop(request_id, None)
if client_identity is None:
self._tracker.remove(request_id)
return
error_batch = OutputBatch(error=error_msg)
try:
self._frontend.send_multipart(
[client_identity, b"", pickle.dumps(error_batch)]
)
except zmq.ZMQError as e:
logger.error(
"DiffusionServer: failed to send error for %s: %s",
request_id,
e,
)
self._tracker.remove(request_id)
def _handle_timeouts(self) -> None:
timed_out = self._tracker.find_timed_out(self._timeout_s)
for request_id in timed_out:
# Free the slot for the timed-out request
record = self._tracker.get(request_id)
if record:
self._free_slot_for_record(record)
self._complete_with_error(
request_id,
f"DiffusionServer timeout: request {request_id} "
f"not completed within {self._timeout_s}s",
)
if timed_out:
timed_set = set(timed_out)
self._encoder_tta = deque(
e for e in self._encoder_tta if e.request_id not in timed_set
)
self._denoiser_tta = deque(
e for e in self._denoiser_tta if e.request_id not in timed_set
)
self._decoder_tta = deque(
e for e in self._decoder_tta if e.request_id not in timed_set
)
def _free_slot_for_record(self, record) -> None:
if (
record.state in (RequestState.ENCODER_RUNNING, RequestState.ENCODER_DONE)
and record.encoder_instance is not None
):
self._encoder_free_slots[record.encoder_instance] += 1
if (
record.state
in (RequestState.DENOISING_RUNNING, RequestState.DENOISING_DONE)
and record.denoiser_instance is not None
):
self._denoiser_free_slots[record.denoiser_instance] += 1
if (
record.state == RequestState.DECODER_RUNNING
and record.decoder_instance is not None
):
self._decoder_free_slots[record.decoder_instance] += 1
def _handle_transfer_result(self, frames: list, role: RoleType) -> None:
try:
msg = decode_transfer_msg(frames)
except (ValueError, Exception) as e:
logger.error("DiffusionServer: failed to decode transfer message: %s", e)
return
msg_type = msg.get("msg_type")
if msg_type == TransferMsgType.REGISTER:
self._handle_transfer_register(msg)
elif msg_type == TransferMsgType.STAGED:
self._handle_transfer_staged(msg)
elif msg_type == TransferMsgType.ALLOCATED:
self._handle_transfer_allocated(msg)
elif msg_type == TransferMsgType.PUSHED:
self._handle_transfer_pushed(msg)
elif msg_type == TransferMsgType.DONE:
self._handle_transfer_done(msg, role)
else:
logger.warning("DiffusionServer: unknown transfer msg_type=%s", msg_type)
def _handle_transfer_register(self, msg: dict) -> None:
try:
role = RoleType.from_string(msg.get("role", ""))
except ValueError:
logger.warning(
"DiffusionServer transfer: unknown role in register: %s",
msg.get("role"),
)
return
work_endpoint = msg.get("work_endpoint", "")
if role == RoleType.ENCODER:
endpoint_to_idx = self._encoder_endpoint_to_idx
peers = self._encoder_peers
elif role == RoleType.DENOISER:
endpoint_to_idx = self._denoiser_endpoint_to_idx
peers = self._denoiser_peers
elif role == RoleType.DECODER:
endpoint_to_idx = self._decoder_endpoint_to_idx
peers = self._decoder_peers
else:
logger.warning(
"DiffusionServer transfer: unsupported role in register: %s", role
)
return
idx = endpoint_to_idx.get(work_endpoint)
if idx is None:
# Fail loudly: without a URL match, the control plane (work PUSH)
# and data plane (RDMA dest) would drift silently.
logger.error(
"DiffusionServer transfer: register for role=%s with unknown "
"work_endpoint=%r (known=%s); dropping registration",
role.value,
work_endpoint,
list(endpoint_to_idx.keys()),
)
return
info = {
"session_id": msg.get("session_id", ""),
"pool_ptr": msg.get("pool_ptr", 0),
"pool_size": msg.get("pool_size", 0),
"work_endpoint": work_endpoint,
}
prealloc = msg.get("preallocated_slots", [])
info["free_preallocated_slots"] = list(prealloc)
peers[idx] = info
logger.info(
"DiffusionServer transfer: registered %s[%d] work_endpoint=%s "
"session=%s pool_ptr=%#x prealloc=%d",
role,
idx,
work_endpoint,
info["session_id"],
info["pool_ptr"],
len(prealloc),
)
def _handle_transfer_staged(self, msg: dict) -> None:
request_id = msg["request_id"]
logger.debug("DiffusionServer transfer: encoder staged %s", request_id)
record = self._tracker.get(request_id)
encoder_idx = record.encoder_instance if record else 0
p2p = _TransferRequestState(
sender_session_id=msg.get("session_id", ""),
sender_pool_ptr=msg.get("pool_ptr", 0),
sender_slot_offset=msg.get("slot_offset", 0),
data_size=msg.get("data_size", 0),
manifest=msg.get("manifest", {}),
scalar_fields=msg.get("scalar_fields", {}),
sender_instance=encoder_idx,
)
self._transfer_state[request_id] = p2p
# Encoder slot freed later in _handle_transfer_pushed after RDMA completes
try:
self._tracker.transition(request_id, RequestState.ENCODER_DONE)
except ValueError:
pass
try:
self._tracker.transition(request_id, RequestState.DENOISING_WAITING)
except ValueError:
pass
self._denoiser_tta.append(
_RoleTTAEntry(request_id=request_id, transfer_state=p2p)
)
def _try_fast_path_push(
self,
request_id: str,
p2p: _TransferRequestState,
receiver_peer_info: dict,
sender_pushes: list,
receiver_role_label: str,
receiver_idx: int,
) -> bool:
"""Try to dispatch via a pre-allocated receive slot (fast path).
If the receiver already registered a free prealloc slot large enough
for this transfer, claim it and send a ``TransferPushMsg`` directly
to the sender so RDMA can start immediately. Returns True when the
fast path is used; False when the caller must fall back to the
round-trip alloc path.
"""
free_slots = receiver_peer_info.get("free_preallocated_slots", [])
if not (free_slots and free_slots[0].get("size", 0) >= p2p.data_size):
return False
slot_info = free_slots.pop(0)
p2p.receiver_session_id = receiver_peer_info.get("session_id", "")
p2p.receiver_pool_ptr = receiver_peer_info.get("pool_ptr", 0)
p2p.receiver_slot_offset = slot_info["offset"]
p2p.prealloc_slot_id = slot_info.get("slot_id")
push_msg = TransferPushMsg(
request_id=request_id,
dest_session_id=p2p.receiver_session_id,
dest_addr=slot_info["addr"],
transfer_size=p2p.data_size,
)
sender_pushes[p2p.sender_instance].send_multipart(encode_transfer_msg(push_msg))
logger.debug(
"DiffusionServer transfer: fast-path push to %s[%d] for %s "
"(prealloc slot %s, %d bytes)",
receiver_role_label,
receiver_idx,
request_id,
slot_info.get("slot_id"),
p2p.data_size,
)
return True
def _send_slow_path_alloc(
self,
request_id: str,
p2p: _TransferRequestState,
receiver_pushes: list,
receiver_idx: int,
source_role: str,
) -> None:
"""Ask the receiver to allocate a slot (slow path).
Used when the receiver has no free prealloc slot large enough. The
receiver will respond with ``transfer_allocated``; see
:meth:`_handle_transfer_allocated`.
"""
alloc_msg = TransferAllocMsg(
request_id=request_id,
data_size=p2p.data_size,
source_role=source_role,
)
receiver_pushes[receiver_idx].send_multipart(encode_transfer_msg(alloc_msg))
def _transfer_dispatch_to_denoiser(
self, request_id: str, p2p: _TransferRequestState, denoiser_idx: int
) -> None:
self._denoiser_free_slots[denoiser_idx] -= 1
p2p.receiver_instance = denoiser_idx
try:
self._tracker.transition(
request_id,
RequestState.DENOISING_RUNNING,
denoiser_instance=denoiser_idx,
)
except ValueError:
pass
peer_info = self._denoiser_peers.get(denoiser_idx, {})
if not self._try_fast_path_push(
request_id=request_id,
p2p=p2p,
receiver_peer_info=peer_info,
sender_pushes=self._encoder_pushes,
receiver_role_label="denoiser",
receiver_idx=denoiser_idx,
):
self._send_slow_path_alloc(
request_id=request_id,
p2p=p2p,
receiver_pushes=self._denoiser_pushes,
receiver_idx=denoiser_idx,
source_role="encoder",
)
def _handle_transfer_allocated(self, msg: dict) -> None:
request_id = msg["request_id"]
p2p = self._transfer_state.get(request_id)
if p2p is None:
logger.warning(
"DiffusionServer transfer: no state for allocated %s", request_id
)
return
p2p.receiver_session_id = msg.get("session_id", "")
p2p.receiver_pool_ptr = msg.get("pool_ptr", 0)
p2p.receiver_slot_offset = msg.get("slot_offset", 0)
dest_addr = p2p.receiver_pool_ptr + p2p.receiver_slot_offset
push_msg = TransferPushMsg(
request_id=request_id,
dest_session_id=p2p.receiver_session_id,
dest_addr=dest_addr,
transfer_size=p2p.data_size,
)
sender_idx = p2p.sender_instance
record = self._tracker.get(request_id)
if record and record.state in (
RequestState.DECODER_RUNNING,
RequestState.DECODER_WAITING,
):
self._denoiser_pushes[sender_idx].send_multipart(
encode_transfer_msg(push_msg)
)
else:
self._encoder_pushes[sender_idx].send_multipart(
encode_transfer_msg(push_msg)
)
def _handle_transfer_pushed(self, msg: dict) -> None:
request_id = msg["request_id"]
logger.debug("DiffusionServer transfer: pushed %s", request_id)
p2p = self._transfer_state.get(request_id)
if p2p is None:
logger.warning(
"DiffusionServer transfer: no state for pushed %s", request_id
)
return
# Use record state (not sender_idx) to determine sender role,
# because encoder and denoiser can share the same instance index.
record = self._tracker.get(request_id)
if record and record.state in (
RequestState.DENOISING_RUNNING,
RequestState.DENOISING_WAITING,
RequestState.DENOISING_DONE,
):
if record.encoder_instance is not None:
self._encoder_free_slots[record.encoder_instance] += 1
elif record and record.state in (
RequestState.DECODER_RUNNING,
RequestState.DECODER_WAITING,
):
if record.denoiser_instance is not None:
self._denoiser_free_slots[record.denoiser_instance] += 1
scalar_fields = dict(p2p.scalar_fields) if p2p.scalar_fields else {}
if p2p.prealloc_slot_id is not None:
scalar_fields["_prealloc_slot_id"] = p2p.prealloc_slot_id
ready_msg = TransferReadyMsg(
request_id=request_id,
manifest=p2p.manifest,
slot_offset=p2p.receiver_slot_offset,
scalar_fields=scalar_fields,
)
receiver_idx = p2p.receiver_instance
record = self._tracker.get(request_id)
if record and record.state in (
RequestState.DENOISING_RUNNING,
RequestState.DENOISING_WAITING,
):
self._denoiser_pushes[receiver_idx].send_multipart(
encode_transfer_msg(ready_msg)
)
elif record and record.state in (
RequestState.DECODER_RUNNING,
RequestState.DECODER_WAITING,
):
self._decoder_pushes[receiver_idx].send_multipart(
encode_transfer_msg(ready_msg)
)
logger.debug(
"DiffusionServer transfer: notified receiver for %s (data ready)",
request_id,
)
def _recycle_prealloc_slot(
self, p2p: _TransferRequestState, role: RoleType
) -> None:
if p2p is None or p2p.prealloc_slot_id is None:
return
receiver_idx = p2p.receiver_instance
if role == RoleType.DENOISER:
peer_info = self._denoiser_peers.get(receiver_idx, {})
elif role == RoleType.DECODER:
peer_info = self._decoder_peers.get(receiver_idx, {})
else:
return
free_list = peer_info.get("free_preallocated_slots", [])
free_list.append(
{
"offset": p2p.receiver_slot_offset,
"size": p2p.data_size,
"slot_id": p2p.prealloc_slot_id,
"addr": p2p.receiver_pool_ptr + p2p.receiver_slot_offset,
}
)
p2p.prealloc_slot_id = None
def _handle_transfer_done(self, msg: dict, role: RoleType) -> None:
request_id = msg.get("request_id", "")
logger.debug(
"DiffusionServer transfer: done %s role=%s",
request_id,
role.value,
)
error = msg.get("error")
p2p = self._transfer_state.get(request_id)
if role == RoleType.DENOISER:
record = self._tracker.get(request_id)
if p2p is not None:
self._recycle_prealloc_slot(p2p, RoleType.DENOISER)
if error:
if record and record.denoiser_instance is not None:
self._denoiser_free_slots[record.denoiser_instance] += 1
self._complete_with_error(request_id, f"Denoiser error: {error}")
return
try:
self._tracker.transition(request_id, RequestState.DENOISING_DONE)
except ValueError:
pass
if p2p is not None and msg.get("staged_for_decoder"):
# Denoiser slot freed later in _handle_transfer_pushed
p2p.sender_session_id = msg.get("session_id", "")
p2p.sender_pool_ptr = msg.get("pool_ptr", 0)
p2p.sender_slot_offset = msg.get("slot_offset", 0)
p2p.data_size = msg.get("data_size", 0)
p2p.manifest = msg.get("manifest", {})
p2p.scalar_fields = msg.get("scalar_fields", {})
p2p.sender_instance = record.denoiser_instance if record else 0
try:
self._tracker.transition(request_id, RequestState.DECODER_WAITING)
except ValueError:
pass
self._decoder_tta.append(
_RoleTTAEntry(request_id=request_id, transfer_state=p2p)
)
else:
if record and record.denoiser_instance is not None:
self._denoiser_free_slots[record.denoiser_instance] += 1
elif role == RoleType.DECODER:
if p2p is not None:
self._recycle_prealloc_slot(p2p, RoleType.DECODER)
record = self._tracker.get(request_id)
if record and record.decoder_instance is not None:
self._decoder_free_slots[record.decoder_instance] += 1
if error:
self._complete_with_error(request_id, f"Decoder error: {error}")
else:
try:
self._tracker.transition(request_id, RequestState.DONE)
except ValueError:
pass
self._transfer_return_to_client_from_msg(request_id, msg)
self._transfer_state.pop(request_id, None)
def _transfer_dispatch_to_decoder(
self, request_id: str, p2p: _TransferRequestState, decoder_idx: int
) -> None:
self._decoder_free_slots[decoder_idx] -= 1
p2p.receiver_instance = decoder_idx
try:
self._tracker.transition(
request_id,
RequestState.DECODER_RUNNING,
decoder_instance=decoder_idx,
)
except ValueError:
pass
peer_info = self._decoder_peers.get(decoder_idx, {})
if not self._try_fast_path_push(
request_id=request_id,
p2p=p2p,
receiver_peer_info=peer_info,
sender_pushes=self._denoiser_pushes,
receiver_role_label="decoder",
receiver_idx=decoder_idx,
):
self._send_slow_path_alloc(
request_id=request_id,
p2p=p2p,
receiver_pushes=self._decoder_pushes,
receiver_idx=decoder_idx,
source_role="denoiser",
)
def _transfer_return_to_client_from_msg(self, request_id: str, msg: dict) -> None:
from sglang.multimodal_gen.runtime.pipelines_core.schedule_batch import (
OutputBatch,
)
with self._lock:
client_identity = self._pending.pop(request_id, None)
if client_identity is None:
self._tracker.remove(request_id)
return
output_batch = OutputBatch(error=msg.get("error"))
try:
self._frontend.send_multipart(
[client_identity, b"", pickle.dumps(output_batch)]
)
except zmq.ZMQError as e:
logger.error(
"DiffusionServer transfer: failed to send result for %s: %s",
request_id,
e,
)
self._tracker.remove(request_id)
def get_stats(self) -> dict:
with self._lock:
pending_count = len(self._pending)
return {
"role": "diffusion_server",
"transfer_mode": self._transfer_mode,
"num_encoders": self._num_encoders,
"num_denoisers": self._num_denoisers,
"num_decoders": self._num_decoders,
"pending_requests": pending_count,
"dispatch_policy": type(self._dispatcher.encoder_policy).__name__,
"encoder_free_slots": list(self._encoder_free_slots),
"denoiser_free_slots": list(self._denoiser_free_slots),
"decoder_free_slots": list(self._decoder_free_slots),
"encoder_tta_depth": len(self._encoder_tta),
"denoiser_tta_depth": len(self._denoiser_tta),
"decoder_tta_depth": len(self._decoder_tta),
"transfer_active_transfers": len(self._transfer_state),
"encoder_peers": len(self._encoder_peers),
"denoiser_peers": len(self._denoiser_peers),
"decoder_peers": len(self._decoder_peers),
"tracker": self._tracker.snapshot(),
}