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
392 lines
12 KiB
Python
392 lines
12 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
"""Per-instance transfer manager for disaggregated diffusion roles."""
|
|
|
|
import logging
|
|
import threading
|
|
from dataclasses import dataclass, field
|
|
|
|
import torch
|
|
|
|
from sglang.multimodal_gen.runtime.disaggregation.transport.buffer import (
|
|
SlotHandle,
|
|
TransferTensorBuffer,
|
|
)
|
|
from sglang.multimodal_gen.runtime.disaggregation.transport.engine import (
|
|
BaseTransferEngine,
|
|
)
|
|
from sglang.multimodal_gen.runtime.platforms import current_platform
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@dataclass
|
|
class StagedTransfer:
|
|
request_id: str
|
|
slot: SlotHandle
|
|
manifest: dict
|
|
scalar_fields: dict = field(default_factory=dict)
|
|
|
|
|
|
@dataclass
|
|
class PendingReceive:
|
|
request_id: str
|
|
slot: SlotHandle
|
|
|
|
|
|
class DiffusionTransferManager:
|
|
"""Manages tensor transfers for a single role instance.
|
|
|
|
Owns a TransferTensorBuffer (memory pool) and a BaseTransferEngine (RDMA or mock).
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
engine: BaseTransferEngine,
|
|
buffer: TransferTensorBuffer,
|
|
):
|
|
self._engine = engine
|
|
self._buffer = buffer
|
|
self._lock = threading.Lock()
|
|
|
|
self._engine.register_buffer(self._buffer.pool_data_ptr, self._buffer.pool_size)
|
|
|
|
self._staged: dict[str, StagedTransfer] = {}
|
|
self._pending_receives: dict[str, PendingReceive] = {}
|
|
|
|
logger.info(
|
|
"DiffusionTransferManager initialized: session=%s, pool=%d bytes",
|
|
self._engine.session_id,
|
|
self._buffer.pool_size,
|
|
)
|
|
|
|
@property
|
|
def session_id(self) -> str:
|
|
return self._engine.session_id
|
|
|
|
@property
|
|
def pool_data_ptr(self) -> int:
|
|
return self._buffer.pool_data_ptr
|
|
|
|
@property
|
|
def pool_size(self) -> int:
|
|
return self._buffer.pool_size
|
|
|
|
def stage_tensors(
|
|
self,
|
|
request_id: str,
|
|
tensor_fields: dict[str, torch.Tensor | list[torch.Tensor] | None],
|
|
scalar_fields: dict | None = None,
|
|
stream: torch.Stream | None = None,
|
|
) -> StagedTransfer | None:
|
|
"""Stage GPU tensors into the local TransferBuffer. Returns None on allocation failure."""
|
|
total_size = 0
|
|
for name, t in tensor_fields.items():
|
|
if t is None:
|
|
continue
|
|
if isinstance(t, list):
|
|
for ti in t:
|
|
total_size += ti.nelement() * ti.element_size()
|
|
else:
|
|
total_size += t.nelement() * t.element_size()
|
|
|
|
if total_size == 0:
|
|
staged = StagedTransfer(
|
|
request_id=request_id,
|
|
slot=None,
|
|
manifest={},
|
|
scalar_fields=scalar_fields or {},
|
|
)
|
|
with self._lock:
|
|
self._staged[request_id] = staged
|
|
return staged
|
|
|
|
slot = self._buffer.allocate(total_size, request_id)
|
|
if slot is None:
|
|
logger.warning(
|
|
"TransferManager: failed to allocate %d bytes for %s",
|
|
total_size,
|
|
request_id,
|
|
)
|
|
return None
|
|
|
|
manifest = self._buffer.write_tensors_from_gpu(slot, tensor_fields, stream)
|
|
|
|
if stream is not None:
|
|
stream.synchronize()
|
|
elif torch.get_device_module().is_available():
|
|
torch.get_device_module().synchronize()
|
|
|
|
staged = StagedTransfer(
|
|
request_id=request_id,
|
|
slot=slot,
|
|
manifest=manifest,
|
|
scalar_fields=scalar_fields or {},
|
|
)
|
|
with self._lock:
|
|
self._staged[request_id] = staged
|
|
|
|
logger.debug(
|
|
"TransferManager: staged %s (%d bytes, offset=%d)",
|
|
request_id,
|
|
total_size,
|
|
slot.offset,
|
|
)
|
|
return staged
|
|
|
|
def stage_tensors_async(
|
|
self,
|
|
request_id: str,
|
|
tensor_fields: dict[str, torch.Tensor | list[torch.Tensor] | None],
|
|
scalar_fields: dict | None = None,
|
|
stream: torch.Stream | None = None,
|
|
) -> tuple[StagedTransfer | None, torch.Event | None]:
|
|
"""Stage GPU tensors, returning a CUDA event instead of blocking.
|
|
|
|
Caller MUST wait on the event before reading buffer data.
|
|
"""
|
|
total_size = 0
|
|
for name, t in tensor_fields.items():
|
|
if t is None:
|
|
continue
|
|
if isinstance(t, list):
|
|
for ti in t:
|
|
total_size += ti.nelement() * ti.element_size()
|
|
else:
|
|
total_size += t.nelement() * t.element_size()
|
|
|
|
if total_size == 0:
|
|
staged = StagedTransfer(
|
|
request_id=request_id,
|
|
slot=None,
|
|
manifest={},
|
|
scalar_fields=scalar_fields or {},
|
|
)
|
|
with self._lock:
|
|
self._staged[request_id] = staged
|
|
return staged, None
|
|
|
|
slot = self._buffer.allocate(total_size, request_id)
|
|
if slot is None:
|
|
logger.warning(
|
|
"TransferManager: failed to allocate %d bytes for %s",
|
|
total_size,
|
|
request_id,
|
|
)
|
|
return None, None
|
|
|
|
manifest = self._buffer.write_tensors_from_gpu(slot, tensor_fields, stream)
|
|
|
|
d2h_event = None
|
|
if stream is not None:
|
|
d2h_event = torch.get_device_module().Event()
|
|
d2h_event.record(stream)
|
|
elif torch.get_device_module().is_available():
|
|
d2h_event = torch.get_device_module().Event()
|
|
d2h_event.record(torch.get_device_module().current_stream())
|
|
|
|
staged = StagedTransfer(
|
|
request_id=request_id,
|
|
slot=slot,
|
|
manifest=manifest,
|
|
scalar_fields=scalar_fields or {},
|
|
)
|
|
with self._lock:
|
|
self._staged[request_id] = staged
|
|
|
|
logger.debug(
|
|
"TransferManager: staged_async %s (%d bytes, offset=%d)",
|
|
request_id,
|
|
total_size,
|
|
slot.offset,
|
|
)
|
|
return staged, d2h_event
|
|
|
|
def load_tensors_async(
|
|
self,
|
|
request_id: str,
|
|
manifest: dict,
|
|
device: torch.device | str = current_platform.device_type,
|
|
stream: torch.Stream | None = None,
|
|
) -> tuple[
|
|
dict[str, torch.Tensor | list[torch.Tensor]],
|
|
torch.get_device_module().Event | None,
|
|
]:
|
|
"""Load tensors from receive slot to GPU, returning a CUDA event.
|
|
|
|
Caller MUST wait on the event before using the returned tensors.
|
|
"""
|
|
with self._lock:
|
|
pending = self._pending_receives.get(request_id)
|
|
|
|
if pending is None:
|
|
raise ValueError(
|
|
f"TransferManager: no pending receive slot for {request_id}"
|
|
)
|
|
|
|
tensors = self._buffer.read_tensors_from_manifest(
|
|
pending.slot, manifest, device=device, stream=stream
|
|
)
|
|
|
|
load_event = None
|
|
if stream is not None:
|
|
load_event = torch.get_device_module().Event()
|
|
load_event.record(stream)
|
|
elif torch.get_device_module().is_available():
|
|
load_event = torch.get_device_module().Event()
|
|
load_event.record(torch.get_device_module().current_stream())
|
|
|
|
logger.debug(
|
|
"TransferManager: loaded_async %d tensor fields for %s to %s",
|
|
len(tensors),
|
|
request_id,
|
|
device,
|
|
)
|
|
return tensors, load_event
|
|
|
|
def push_to_peer(
|
|
self,
|
|
request_id: str,
|
|
dest_session_id: str,
|
|
dest_addr: int,
|
|
transfer_size: int,
|
|
) -> bool:
|
|
"""Push staged data to a remote peer's buffer via RDMA. Returns True on success."""
|
|
with self._lock:
|
|
staged = self._staged.get(request_id)
|
|
|
|
if staged is None:
|
|
logger.error("TransferManager: no staged transfer for %s", request_id)
|
|
return False
|
|
|
|
if staged.slot is None:
|
|
return True
|
|
|
|
src_addr = self._buffer.pool_data_ptr + staged.slot.offset
|
|
ret = self._engine.transfer_sync(
|
|
dest_session_id, src_addr, dest_addr, transfer_size
|
|
)
|
|
|
|
if ret == 0:
|
|
logger.debug(
|
|
"TransferManager: pushed %s (%d bytes) to %s",
|
|
request_id,
|
|
transfer_size,
|
|
dest_session_id,
|
|
)
|
|
else:
|
|
logger.error(
|
|
"TransferManager: RDMA push failed for %s (ret=%d)",
|
|
request_id,
|
|
ret,
|
|
)
|
|
|
|
return ret == 0
|
|
|
|
def free_staged(self, request_id: str) -> None:
|
|
with self._lock:
|
|
staged = self._staged.pop(request_id, None)
|
|
|
|
if staged and staged.slot is not None:
|
|
self._buffer.free(staged.slot)
|
|
logger.debug("TransferManager: freed staged slot for %s", request_id)
|
|
|
|
def allocate_receive_slot(
|
|
self, request_id: str, size: int
|
|
) -> PendingReceive | None:
|
|
"""Allocate a local buffer slot to receive incoming data."""
|
|
slot = self._buffer.allocate(size, request_id)
|
|
if slot is None:
|
|
logger.warning(
|
|
"TransferManager: failed to allocate receive slot (%d bytes) for %s",
|
|
size,
|
|
request_id,
|
|
)
|
|
return None
|
|
|
|
pending = PendingReceive(request_id=request_id, slot=slot)
|
|
with self._lock:
|
|
self._pending_receives[request_id] = pending
|
|
|
|
logger.debug(
|
|
"TransferManager: allocated receive slot for %s (offset=%d, size=%d)",
|
|
request_id,
|
|
slot.offset,
|
|
slot.size,
|
|
)
|
|
return pending
|
|
|
|
def load_tensors(
|
|
self,
|
|
request_id: str,
|
|
manifest: dict,
|
|
device: torch.device | str = current_platform.device_type,
|
|
stream: torch.Stream | None = None,
|
|
) -> dict[str, torch.Tensor | list[torch.Tensor]]:
|
|
"""Load tensors from a receive slot into GPU memory."""
|
|
with self._lock:
|
|
pending = self._pending_receives.get(request_id)
|
|
|
|
if pending is None:
|
|
raise ValueError(
|
|
f"TransferManager: no pending receive slot for {request_id}"
|
|
)
|
|
|
|
tensors = self._buffer.read_tensors_from_manifest(
|
|
pending.slot, manifest, device=device, stream=stream
|
|
)
|
|
|
|
if stream is not None:
|
|
stream.synchronize()
|
|
elif torch.get_device_module().is_available():
|
|
torch.get_device_module().synchronize()
|
|
|
|
logger.debug(
|
|
"TransferManager: loaded %d tensor fields for %s to %s",
|
|
len(tensors),
|
|
request_id,
|
|
device,
|
|
)
|
|
return tensors
|
|
|
|
def register_prealloc_as_receive(
|
|
self, request_id: str, slot: "SlotHandle"
|
|
) -> "PendingReceive":
|
|
"""Register a pre-allocated slot as a pending receive (fast path)."""
|
|
pending = PendingReceive(request_id=request_id, slot=slot)
|
|
with self._lock:
|
|
self._pending_receives[request_id] = pending
|
|
return pending
|
|
|
|
def free_receive_slot(self, request_id: str) -> None:
|
|
with self._lock:
|
|
pending = self._pending_receives.pop(request_id, None)
|
|
|
|
if pending:
|
|
self._buffer.free(pending.slot)
|
|
logger.debug("TransferManager: freed receive slot for %s", request_id)
|
|
|
|
def get_receive_slot_addr(self, request_id: str) -> int | None:
|
|
with self._lock:
|
|
pending = self._pending_receives.get(request_id)
|
|
if pending is None:
|
|
return None
|
|
return self._buffer.pool_data_ptr + pending.slot.offset
|
|
|
|
def get_receive_slot_offset(self, request_id: str) -> int | None:
|
|
with self._lock:
|
|
pending = self._pending_receives.get(request_id)
|
|
if pending is None:
|
|
return None
|
|
return pending.slot.offset
|
|
|
|
def get_staged_info(self, request_id: str) -> StagedTransfer | None:
|
|
with self._lock:
|
|
return self._staged.get(request_id)
|
|
|
|
def free_slots_count(self, typical_size: int = 64 * 1024 * 1024) -> int:
|
|
return self._buffer.free_slots_count(typical_size)
|
|
|
|
def cleanup(self) -> None:
|
|
self._engine.deregister_buffer(self._buffer.pool_data_ptr)
|
|
logger.info("DiffusionTransferManager cleaned up")
|