chore: import upstream snapshot with attribution
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

This commit is contained in:
wehub-resource-sync
2026-07-13 12:38:16 +08:00
commit 94057c3d3e
7152 changed files with 2120455 additions and 0 deletions
@@ -0,0 +1,2 @@
# SPDX-License-Identifier: Apache-2.0
"""Disaggregation support for diffusion pipelines."""
@@ -0,0 +1,28 @@
# SPDX-License-Identifier: Apache-2.0
"""Compatibility shim for disaggregated diffusion argument helpers."""
from __future__ import annotations
import argparse
from sglang.multimodal_gen.runtime.disaggregation.roles import RoleType
from sglang.multimodal_gen.runtime.server_args.disagg import DisaggServerArgsMixin
# Keep the historical disagg_args import path working.
DISAGG_RESULT_PORT_OFFSETS = DisaggServerArgsMixin.DISAGG_RESULT_PORT_OFFSETS
DisaggArgsMixin = DisaggServerArgsMixin
def add_disagg_cli_args(parser: argparse.ArgumentParser) -> None:
"""Register disaggregated-diffusion CLI args through ServerArgs."""
from sglang.multimodal_gen.runtime.server_args import ServerArgs
ServerArgs.add_disagg_cli_args(parser)
def convert_disagg_role_string(kwargs: dict) -> None:
"""Convert ``disagg_role`` from string to ``RoleType`` enum in-place."""
if "disagg_role" in kwargs and isinstance(kwargs["disagg_role"], str):
kwargs["disagg_role"] = RoleType.from_string(kwargs["disagg_role"])
@@ -0,0 +1,165 @@
# SPDX-License-Identifier: Apache-2.0
"""Dispatch policies for multi-instance disaggregated diffusion pipelines."""
import abc
import logging
import threading
logger = logging.getLogger(__name__)
class DispatchPolicy(abc.ABC):
def __init__(self, num_instances: int):
if num_instances < 1:
raise ValueError(f"num_instances must be >= 1, got {num_instances}")
self._num_instances = num_instances
@property
def num_instances(self) -> int:
return self._num_instances
@abc.abstractmethod
def select(self, active_counts: list[int] | None = None) -> int: ...
def select_with_capacity(self, free_slots: list[int]) -> int | None:
"""Select an instance that has free capacity, or None if all full."""
if not any(s > 0 for s in free_slots):
return None
return self.select(active_counts=None)
def record_completion(self, instance_id: int) -> None:
pass
class RoundRobin(DispatchPolicy):
def __init__(self, num_instances: int):
super().__init__(num_instances)
self._lock = threading.Lock()
self._next = 0
def select(self, active_counts: list[int] | None = None) -> int:
with self._lock:
chosen = self._next
self._next = (self._next + 1) % self._num_instances
return chosen
def select_with_capacity(self, free_slots: list[int]) -> int | None:
with self._lock:
for _ in range(self._num_instances):
idx = self._next
self._next = (self._next + 1) % self._num_instances
if free_slots[idx] > 0:
return idx
return None
class MaxFreeSlotsFirst(DispatchPolicy):
"""Dispatch to the instance with the most free slots."""
def __init__(self, num_instances: int, max_slots_per_instance: int = 1):
super().__init__(num_instances)
self._max_slots = max_slots_per_instance
self._lock = threading.Lock()
self._tiebreak = 0
def select(self, active_counts: list[int] | None = None) -> int:
with self._lock:
if active_counts is None or len(active_counts) != self._num_instances:
chosen = self._tiebreak % self._num_instances
self._tiebreak += 1
return chosen
best_id = 0
best_free = self._max_slots - active_counts[0]
for i in range(1, self._num_instances):
free = self._max_slots - active_counts[i]
if free > best_free:
best_free = free
best_id = i
elif free == best_free:
if i == (self._tiebreak % self._num_instances):
best_id = i
self._tiebreak += 1
if best_free <= 0:
logger.warning(
"All %d instances are at capacity (%d slots each), "
"dispatching to instance %d anyway",
self._num_instances,
self._max_slots,
best_id,
)
return best_id
def select_with_capacity(self, free_slots: list[int]) -> int | None:
with self._lock:
best_id = -1
best_free = 0
for i in range(self._num_instances):
if free_slots[i] > best_free:
best_free = free_slots[i]
best_id = i
elif free_slots[i] == best_free and best_free > 0:
if i == (self._tiebreak % self._num_instances):
best_id = i
self._tiebreak += 1
if best_id < 0:
return None
return best_id
class PoolDispatcher:
"""Wraps three independent dispatch policies for encoder/denoiser/decoder pools."""
def __init__(
self,
num_encoders: int,
num_denoisers: int,
num_decoders: int,
policy_name: str = "round_robin",
**kwargs,
):
self.encoder_policy = create_dispatch_policy(
policy_name, num_encoders, **kwargs
)
self.denoiser_policy = create_dispatch_policy(
policy_name, num_denoisers, **kwargs
)
self.decoder_policy = create_dispatch_policy(
policy_name, num_decoders, **kwargs
)
def select_encoder(self, active_counts: list[int] | None = None) -> int:
return self.encoder_policy.select(active_counts)
def select_denoiser(self, active_counts: list[int] | None = None) -> int:
return self.denoiser_policy.select(active_counts)
def select_decoder(self, active_counts: list[int] | None = None) -> int:
return self.decoder_policy.select(active_counts)
def select_encoder_with_capacity(self, free_slots: list[int]) -> int | None:
return self.encoder_policy.select_with_capacity(free_slots)
def select_denoiser_with_capacity(self, free_slots: list[int]) -> int | None:
return self.denoiser_policy.select_with_capacity(free_slots)
def select_decoder_with_capacity(self, free_slots: list[int]) -> int | None:
return self.decoder_policy.select_with_capacity(free_slots)
def create_dispatch_policy(name: str, num_instances: int, **kwargs) -> DispatchPolicy:
policies = {
"round_robin": RoundRobin,
"max_free_slots": MaxFreeSlotsFirst,
}
cls = policies.get(name)
if cls is None:
raise ValueError(
f"Unknown dispatch policy '{name}'. Available: {list(policies.keys())}"
)
return cls(num_instances=num_instances, **kwargs)
@@ -0,0 +1,133 @@
# SPDX-License-Identifier: Apache-2.0
"""Observability metrics for disaggregated diffusion pipelines."""
import threading
import time
from dataclasses import dataclass
@dataclass
class _RequestTiming:
start_time: float
stage_start: float = 0.0
@dataclass
class RoleStats:
role: str
requests_completed: int = 0
requests_failed: int = 0
requests_in_flight: int = 0
requests_timed_out: int = 0
queue_depth: int = 0
last_latency_s: float = 0.0
avg_latency_s: float = 0.0
max_latency_s: float = 0.0
throughput_rps: float = 0.0
uptime_s: float = 0.0
def to_dict(self) -> dict:
return {
"role": self.role,
"requests_completed": self.requests_completed,
"requests_failed": self.requests_failed,
"requests_in_flight": self.requests_in_flight,
"requests_timed_out": self.requests_timed_out,
"queue_depth": self.queue_depth,
"last_latency_s": round(self.last_latency_s, 4),
"avg_latency_s": round(self.avg_latency_s, 4),
"max_latency_s": round(self.max_latency_s, 4),
"throughput_rps": round(self.throughput_rps, 4),
"uptime_s": round(self.uptime_s, 1),
}
class DisaggMetrics:
"""Thread-safe metrics collector for a single disagg role."""
def __init__(self, role: str):
self._role = role
self._lock = threading.Lock()
self._start_time = time.monotonic()
self._completed = 0
self._failed = 0
self._timed_out = 0
self._in_flight: dict[str, _RequestTiming] = {}
self._last_latency = 0.0
self._max_latency = 0.0
self._total_latency = 0.0
self._completion_times: list[float] = []
self._throughput_window_s = 60.0
self._queue_depth = 0
@property
def role(self) -> str:
return self._role
def record_request_start(self, request_id: str) -> None:
with self._lock:
self._in_flight[request_id] = _RequestTiming(start_time=time.monotonic())
def record_request_complete(self, request_id: str) -> None:
now = time.monotonic()
with self._lock:
timing = self._in_flight.pop(request_id, None)
if timing is not None:
latency = now - timing.start_time
self._last_latency = latency
self._max_latency = max(self._max_latency, latency)
self._total_latency += latency
self._completed += 1
self._completion_times.append(now)
self._prune_completion_times(now)
def record_request_failed(self, request_id: str) -> None:
with self._lock:
self._in_flight.pop(request_id, None)
self._failed += 1
def record_request_timeout(self, request_id: str) -> None:
with self._lock:
self._in_flight.pop(request_id, None)
self._timed_out += 1
def update_queue_depth(self, depth: int) -> None:
with self._lock:
self._queue_depth = depth
def snapshot(self) -> RoleStats:
now = time.monotonic()
with self._lock:
self._prune_completion_times(now)
total = self._completed + self._failed
avg_latency = self._total_latency / total if total > 0 else 0.0
rps = (
len(self._completion_times) / self._throughput_window_s
if self._completion_times
else 0.0
)
return RoleStats(
role=self._role,
requests_completed=self._completed,
requests_failed=self._failed,
requests_in_flight=len(self._in_flight),
requests_timed_out=self._timed_out,
queue_depth=self._queue_depth,
last_latency_s=self._last_latency,
avg_latency_s=avg_latency,
max_latency_s=self._max_latency,
throughput_rps=rps,
uptime_s=now - self._start_time,
)
def _prune_completion_times(self, now: float) -> None:
cutoff = now - self._throughput_window_s
while self._completion_times and self._completion_times[0] < cutoff:
self._completion_times.pop(0)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,165 @@
# SPDX-License-Identifier: Apache-2.0
"""Request state machine for disaggregated diffusion pipelines."""
import enum
import logging
import threading
import time
from dataclasses import dataclass, field
logger = logging.getLogger(__name__)
class RequestState(enum.Enum):
"""Lifecycle states for a disagg pipeline request.
*_WAITING: request queued, awaiting a free buffer slot.
*_RUNNING: request dispatched to a specific instance.
"""
PENDING = "pending"
ENCODER_WAITING = "encoder_waiting"
ENCODER_RUNNING = "encoder_running"
ENCODER_DONE = "encoder_done"
DENOISING_WAITING = "denoising_waiting"
DENOISING_RUNNING = "denoising_running"
DENOISING_DONE = "denoising_done"
DECODER_WAITING = "decoder_waiting"
DECODER_RUNNING = "decoder_running"
DONE = "done"
FAILED = "failed"
TIMED_OUT = "timed_out"
_TERMINAL_STATES = {RequestState.DONE, RequestState.FAILED, RequestState.TIMED_OUT}
_ACTIVE_STATES = set(RequestState) - _TERMINAL_STATES
# Normal (non-failure) transitions. FAILED and TIMED_OUT are handled
# separately in transition() — any active state can reach them.
_VALID_TRANSITIONS: dict[RequestState, set[RequestState]] = {
RequestState.PENDING: {RequestState.ENCODER_WAITING, RequestState.ENCODER_RUNNING},
RequestState.ENCODER_WAITING: {RequestState.ENCODER_RUNNING},
RequestState.ENCODER_RUNNING: {RequestState.ENCODER_DONE},
RequestState.ENCODER_DONE: {
RequestState.DENOISING_WAITING,
RequestState.DENOISING_RUNNING,
},
RequestState.DENOISING_WAITING: {RequestState.DENOISING_RUNNING},
RequestState.DENOISING_RUNNING: {RequestState.DENOISING_DONE},
RequestState.DENOISING_DONE: {
RequestState.DECODER_WAITING,
RequestState.DECODER_RUNNING,
},
RequestState.DECODER_WAITING: {RequestState.DECODER_RUNNING},
RequestState.DECODER_RUNNING: {RequestState.DONE},
}
@dataclass
class RequestRecord:
request_id: str
state: RequestState = RequestState.PENDING
submit_time: float = field(default_factory=time.monotonic)
last_transition_time: float = field(default_factory=time.monotonic)
encoder_instance: int | None = None
denoiser_instance: int | None = None
decoder_instance: int | None = None
error: str | None = None
def elapsed_s(self) -> float:
return time.monotonic() - self.submit_time
def is_terminal(self) -> bool:
return self.state in _TERMINAL_STATES
class RequestTracker:
"""Thread-safe tracker for request state machines."""
def __init__(self):
self._lock = threading.Lock()
self._requests: dict[str, RequestRecord] = {}
def submit(self, request_id: str) -> RequestRecord:
with self._lock:
if request_id in self._requests:
raise ValueError(f"Duplicate request_id: {request_id}")
record = RequestRecord(request_id=request_id)
self._requests[request_id] = record
return record
def transition(
self,
request_id: str,
new_state: RequestState,
*,
error: str | None = None,
encoder_instance: int | None = None,
denoiser_instance: int | None = None,
decoder_instance: int | None = None,
) -> RequestRecord:
with self._lock:
record = self._requests.get(request_id)
if record is None:
raise ValueError(f"Unknown request_id: {request_id}")
old_state = record.state
if new_state in _TERMINAL_STATES and new_state != RequestState.DONE:
# FAILED / TIMED_OUT: allowed from any active state
if old_state not in _ACTIVE_STATES:
raise ValueError(
f"Cannot transition {request_id} from terminal state "
f"{old_state.value} to {new_state.value}"
)
elif new_state not in _VALID_TRANSITIONS.get(old_state, set()):
raise ValueError(
f"Invalid transition for {request_id}: "
f"{old_state.value} -> {new_state.value}"
)
record.state = new_state
record.last_transition_time = time.monotonic()
if error is not None:
record.error = error
if encoder_instance is not None:
record.encoder_instance = encoder_instance
if denoiser_instance is not None:
record.denoiser_instance = denoiser_instance
if decoder_instance is not None:
record.decoder_instance = decoder_instance
logger.debug(
"Request %s: %s -> %s", request_id, old_state.value, new_state.value
)
return record
def get(self, request_id: str) -> RequestRecord | None:
with self._lock:
return self._requests.get(request_id)
def remove(self, request_id: str) -> RequestRecord | None:
with self._lock:
return self._requests.pop(request_id, None)
def find_timed_out(self, timeout_s: float) -> list[str]:
now = time.monotonic()
with self._lock:
return [
r.request_id
for r in self._requests.values()
if r.state in _ACTIVE_STATES and (now - r.submit_time) > timeout_s
]
def snapshot(self) -> dict:
with self._lock:
state_counts = {}
for r in self._requests.values():
state_counts[r.state.value] = state_counts.get(r.state.value, 0) + 1
return {
"total": len(self._requests),
"active": sum(
1 for r in self._requests.values() if not r.is_terminal()
),
"by_state": state_counts,
}
@@ -0,0 +1,99 @@
# SPDX-License-Identifier: Apache-2.0
"""Role definitions for diffusion pipeline disaggregation."""
from enum import Enum
_ROLE_ALIASES = {"denoising": "denoiser"}
class RoleType(str, Enum):
MONOLITHIC = "monolithic"
ENCODER = "encoder"
DENOISER = "denoiser"
DECODER = "decoder"
SERVER = "server" # Head node (no GPU, routes requests)
@classmethod
def from_string(cls, value: str) -> "RoleType":
v = _ROLE_ALIASES.get(value.lower(), value.lower())
try:
return cls(v)
except ValueError:
raise ValueError(
f"Invalid role: {value}. Must be one of: {', '.join([r.value for r in cls])}"
) from None
@classmethod
def choices(cls) -> list[str]:
return [role.value for role in cls] + sorted(_ROLE_ALIASES)
def get_module_role(module_name: str) -> "RoleType | None":
"""Classify a module name to its primary role. Returns None for shared modules."""
encoder_prefixes = (
"text_encoder",
"tokenizer",
"image_encoder",
"image_processor",
"processor",
"connectors",
"vision_language_encoder",
)
if any(
module_name == p or module_name.startswith(p + "_") for p in encoder_prefixes
):
return RoleType.ENCODER
if module_name in {"hy3dshape_conditioner", "hy3dshape_image_processor"}:
return RoleType.ENCODER
denoising_prefixes = (
"transformer",
"unconditional_transformer",
"video_dit",
"audio_dit",
"dual_tower_bridge",
)
if any(
module_name == p or module_name.startswith(p + "_") for p in denoising_prefixes
):
return RoleType.DENOISER
if module_name == "hy3dshape_model":
return RoleType.DENOISER
decoder_prefixes = ("vae", "audio_vae", "video_vae", "vocoder")
if any(
module_name == p or module_name.startswith(p + "_") for p in decoder_prefixes
):
return RoleType.DECODER
if module_name == "hy3dshape_vae":
return RoleType.DECODER
return None
def filter_modules_for_role(
module_names: list[str],
role: "RoleType",
*,
extra_allowed_modules: set[str] | None = None,
) -> list[str]:
"""Filter module names to only those needed by the given role."""
if role in (RoleType.MONOLITHIC, RoleType.SERVER):
return module_names
extra_allowed_modules = extra_allowed_modules or set()
filtered = []
for name in module_names:
module_role = get_module_role(name)
if module_role is None:
filtered.append(name)
elif module_role == role:
filtered.append(name)
elif name in extra_allowed_modules:
filtered.append(name)
return filtered
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,2 @@
# SPDX-License-Identifier: Apache-2.0
"""Transport layer for disaggregated diffusion pipelines."""
@@ -0,0 +1,200 @@
# SPDX-License-Identifier: Apache-2.0
"""Buddy-system memory allocator for TransferTensorBuffer."""
from __future__ import annotations
import logging
import threading
from dataclasses import dataclass
from functools import lru_cache
logger = logging.getLogger(__name__)
@dataclass
class Block:
offset: int # byte offset from pool start
size: int
allocated: bool = False
request_id: str | None = None
class BuddyAllocator:
"""Power-of-2 buddy-system allocator for pinned memory."""
def __init__(self, pool_size: int, min_block_size: int = 1 << 20):
if min_block_size <= 0 or (min_block_size & (min_block_size - 1)) != 0:
raise ValueError(
f"min_block_size must be a power of 2, got {min_block_size}"
)
self._min_block_size = min_block_size
self._pool_size = self._next_power_of_2(max(pool_size, min_block_size))
self._lock = threading.Lock()
# Free lists indexed by order: order 0 = min_block_size, order 1 = 2*min_block_size, ...
self._max_order = self._size_to_order(self._pool_size)
self._free_lists: list[list[int]] = [[] for _ in range(self._max_order + 1)]
self._blocks: dict[int, Block] = {}
root = Block(offset=0, size=self._pool_size)
self._blocks[0] = root
self._free_lists[self._max_order].append(0)
self._allocated_bytes = 0
self._num_allocations = 0
@property
def pool_size(self) -> int:
return self._pool_size
def allocate(self, size: int, request_id: str | None = None) -> int | None:
"""Allocate a block of at least `size` bytes. Returns offset or None."""
if size <= 0:
raise ValueError(f"Allocation size must be positive, got {size}")
alloc_size = max(self._next_power_of_2(size), self._min_block_size)
target_order = self._size_to_order(alloc_size)
if target_order > self._max_order:
logger.warning(
"Requested size %d exceeds pool size %d", size, self._pool_size
)
return None
with self._lock:
return self._allocate_locked(target_order, request_id)
def free(self, offset: int) -> bool:
"""Free the block at the given offset and coalesce with buddy if possible."""
with self._lock:
return self._free_locked(offset)
def get_block_info(self, offset: int) -> Block | None:
with self._lock:
return self._blocks.get(offset)
def get_stats(self) -> dict:
with self._lock:
free_blocks_by_order = {}
for order, offsets in enumerate(self._free_lists):
if offsets:
block_size = self._min_block_size << order
free_blocks_by_order[block_size] = len(offsets)
return {
"pool_size": self._pool_size,
"min_block_size": self._min_block_size,
"allocated_bytes": self._allocated_bytes,
"free_bytes": self._pool_size - self._allocated_bytes,
"num_allocations": self._num_allocations,
"num_blocks": len(self._blocks),
"free_blocks_by_size": free_blocks_by_order,
}
def count_free_slots(self, slot_size: int) -> int:
"""Count how many allocations of the given size can fit."""
if slot_size <= 0:
return 0
alloc_size = max(self._next_power_of_2(slot_size), self._min_block_size)
with self._lock:
count = 0
for order in range(self._size_to_order(alloc_size), self._max_order + 1):
for _ in self._free_lists[order]:
block_size = self._min_block_size << order
count += block_size // alloc_size
return count
# --- Internal (caller must hold self._lock) ---
def _allocate_locked(self, target_order: int, request_id: str | None) -> int | None:
found_order = -1
for order in range(target_order, self._max_order + 1):
if self._free_lists[order]:
found_order = order
break
if found_order < 0:
return None
offset = self._free_lists[found_order].pop(0)
block = self._blocks[offset]
# Split down to target_order
while found_order > target_order:
found_order -= 1
buddy_size = self._min_block_size << found_order
buddy_offset = offset + buddy_size
buddy = Block(offset=buddy_offset, size=buddy_size)
self._blocks[buddy_offset] = buddy
self._free_lists[found_order].append(buddy_offset)
block.size = buddy_size
block.allocated = True
block.request_id = request_id
self._allocated_bytes += block.size
self._num_allocations += 1
return offset
def _free_locked(self, offset: int) -> bool:
block = self._blocks.get(offset)
if block is None or not block.allocated:
return False
block.allocated = False
block.request_id = None
self._allocated_bytes -= block.size
self._num_allocations -= 1
self._coalesce(block)
return True
def _coalesce(self, block: Block) -> None:
"""Recursively merge with buddy if both are free."""
while block.size < self._pool_size:
buddy_offset = block.offset ^ block.size
buddy = self._blocks.get(buddy_offset)
if buddy is None or buddy.allocated or buddy.size != block.size:
break
order = self._size_to_order(buddy.size)
self._free_lists[order].remove(buddy_offset)
if buddy_offset < block.offset:
del self._blocks[block.offset]
buddy.size *= 2
block = buddy
else:
del self._blocks[buddy_offset]
block.size *= 2
order = self._size_to_order(block.size)
self._free_lists[order].append(block.offset)
def _size_to_order(self, size: int) -> int:
order = 0
s = self._min_block_size
while s < size:
s <<= 1
order += 1
return order
@staticmethod
@lru_cache(maxsize=256)
def _next_power_of_2(n: int) -> int:
if n <= 0:
return 1
n -= 1
n |= n >> 1
n |= n >> 2
n |= n >> 4
n |= n >> 8
n |= n >> 16
n |= n >> 32
return n + 1
@@ -0,0 +1,272 @@
# SPDX-License-Identifier: Apache-2.0
"""TransferTensorBuffer: memory staging area for disaggregated tensor transfer."""
from __future__ import annotations
import logging
from dataclasses import dataclass, field
import torch
from sglang.multimodal_gen.runtime.disaggregation.transport.allocator import (
BuddyAllocator,
)
from sglang.multimodal_gen.runtime.disaggregation.transport.codec import (
str_to_dtype,
)
logger = logging.getLogger(__name__)
@dataclass
class SlotHandle:
request_id: str
offset: int # byte offset in the pool
size: int # allocated size in bytes
tensor_views: dict[str, torch.Tensor | list[torch.Tensor]] = field(
default_factory=dict
)
class TransferTensorBuffer:
"""Memory pool for staging tensor payloads between roles.
Wraps a contiguous block of memory (CPU pinned or GPU) with a BuddyAllocator.
"""
def __init__(
self,
pool_size: int,
min_block_size: int = 1 << 20,
role_name: str = "unknown",
device: str = "cpu",
):
self._role_name = role_name
self._device = device
self._allocator = BuddyAllocator(pool_size, min_block_size)
actual_size = self._allocator.pool_size
if device == "cpu":
self._pool = torch.empty(actual_size, dtype=torch.uint8, pin_memory=True)
else:
self._pool = torch.empty(actual_size, dtype=torch.uint8, device=device)
self._pool_ptr = self._pool.data_ptr()
pool_location = "pinned CPU" if device == "cpu" else f"GPU ({device})"
logger.info(
"TransferTensorBuffer[%s]: allocated %d MiB %s memory "
"(min_block=%d KiB)",
role_name,
actual_size >> 20,
pool_location,
min_block_size >> 10,
)
@property
def pool_size(self) -> int:
return self._allocator.pool_size
@property
def device(self) -> str:
return self._device
@property
def pool_data_ptr(self) -> int:
return self._pool_ptr
def allocate(self, size: int, request_id: str) -> SlotHandle | None:
"""Allocate a slot. Returns None if pool is full."""
offset = self._allocator.allocate(size, request_id=request_id)
if offset is None:
logger.warning(
"TransferTensorBuffer[%s]: allocation failed for %s (%d bytes). "
"Pool stats: %s",
self._role_name,
request_id,
size,
self._allocator.get_stats(),
)
return None
block = self._allocator.get_block_info(offset)
return SlotHandle(
request_id=request_id,
offset=offset,
size=block.size if block else size,
)
def free(self, handle: SlotHandle) -> bool:
return self._allocator.free(handle.offset)
def write_tensor(
self,
handle: SlotHandle,
name: str,
tensor: torch.Tensor,
byte_offset: int = 0,
stream: torch.Stream | None = None,
) -> int:
"""Copy a tensor into the pool slot. Returns bytes written."""
src_tensor = tensor.contiguous()
nbytes = src_tensor.numel() * src_tensor.element_size()
if byte_offset + nbytes > handle.size:
raise ValueError(
f"Write exceeds slot: offset={byte_offset}, nbytes={nbytes}, "
f"slot_size={handle.size}"
)
dst = self._pool[
handle.offset + byte_offset : handle.offset + byte_offset + nbytes
]
src_bytes = src_tensor.view(torch.uint8).reshape(-1)
if stream is not None:
with torch.get_device_module().stream(stream):
dst.copy_(src_bytes, non_blocking=True)
else:
dst.copy_(src_bytes, non_blocking=True)
return nbytes
def read_tensor(
self,
handle: SlotHandle,
shape: list[int],
dtype: torch.dtype,
byte_offset: int = 0,
device: torch.device | str = "cpu",
stream: torch.Stream | None = None,
) -> torch.Tensor:
"""Read a tensor from the pool slot. Returns a clone on target device."""
nbytes = 1
for s in shape:
nbytes *= s
nbytes *= torch.tensor([], dtype=dtype).element_size()
raw = self._pool[
handle.offset + byte_offset : handle.offset + byte_offset + nbytes
]
src = raw.view(dtype).reshape(shape)
pool_dev = str(self._pool.device)
target_dev = str(device)
same_device = pool_dev == target_dev
if same_device:
# Clone to decouple tensor lifetime from pool slot
if stream is not None:
with torch.get_device_module().stream(stream):
return src.clone()
return src.clone()
if stream is not None:
with torch.get_device_module().stream(stream):
return src.to(device, non_blocking=True)
return src.to(device, non_blocking=True)
def write_tensors_from_gpu(
self,
handle: SlotHandle,
tensors: dict[str, torch.Tensor | list[torch.Tensor] | None],
stream: torch.Stream | None = None,
) -> dict[str, list[dict]]:
"""Batch-write GPU tensors into a slot. Returns a manifest for later reads."""
manifest: dict[str, list[dict]] = {}
byte_offset = 0
# Ensure copy stream sees all prior compute kernels
if stream is not None:
stream.wait_stream(torch.get_device_module().current_stream())
for name, value in tensors.items():
if value is None:
continue
entries = []
if isinstance(value, torch.Tensor):
nbytes = self.write_tensor(handle, name, value, byte_offset, stream)
entries.append(
{
"offset": byte_offset,
"shape": list(value.shape),
"dtype": str(value.dtype).replace("torch.", ""),
}
)
byte_offset += nbytes
byte_offset = (byte_offset + 511) & ~511 # align to 512B
elif isinstance(value, list):
for i, t in enumerate(value):
if t is None:
continue
nbytes = self.write_tensor(
handle, f"{name}[{i}]", t, byte_offset, stream
)
entries.append(
{
"offset": byte_offset,
"shape": list(t.shape),
"dtype": str(t.dtype).replace("torch.", ""),
"list_index": i,
}
)
byte_offset += nbytes
byte_offset = (byte_offset + 511) & ~511
if entries:
manifest[name] = entries
return manifest
def read_tensors_from_manifest(
self,
handle: SlotHandle,
manifest: dict[str, list[dict]],
device: torch.device | str = "cpu",
stream: torch.Stream | None = None,
) -> dict[str, torch.Tensor | list[torch.Tensor]]:
"""Batch-read tensors from a slot using a manifest."""
result: dict[str, torch.Tensor | list[torch.Tensor]] = {}
for name, entries in manifest.items():
if not entries:
continue
has_list_index = any("list_index" in e for e in entries)
if has_list_index:
max_idx = max(e.get("list_index", 0) for e in entries) + 1
tensors = [None] * max_idx
for entry in entries:
t = self.read_tensor(
handle,
entry["shape"],
str_to_dtype(entry["dtype"]),
entry["offset"],
device,
stream,
)
tensors[entry["list_index"]] = t
result[name] = tensors
else:
entry = entries[0]
result[name] = self.read_tensor(
handle,
entry["shape"],
str_to_dtype(entry["dtype"]),
entry["offset"],
device,
stream,
)
return result
def free_slots_count(self, typical_request_size: int) -> int:
"""Estimate how many requests of typical size can still be buffered."""
return self._allocator.count_free_slots(typical_request_size)
def get_stats(self) -> dict:
alloc_stats = self._allocator.get_stats()
alloc_stats["role"] = self._role_name
return alloc_stats
@@ -0,0 +1,198 @@
# SPDX-License-Identifier: Apache-2.0
"""Zero-copy tensor codec for ZMQ multipart messages.
Frame 0: JSON metadata (tensor descriptors + scalar fields)
Frame 1-N: Raw tensor data buffers (one per tensor)
"""
import ctypes
import json
import logging
from dataclasses import dataclass
import torch
import zmq
logger = logging.getLogger(__name__)
_DTYPE_TO_STR = {
torch.float16: "float16",
torch.float32: "float32",
torch.float64: "float64",
torch.bfloat16: "bfloat16",
torch.int8: "int8",
torch.int16: "int16",
torch.int32: "int32",
torch.int64: "int64",
torch.uint8: "uint8",
torch.bool: "bool",
}
_STR_TO_DTYPE = {v: k for k, v in _DTYPE_TO_STR.items()}
def dtype_to_str(dtype: torch.dtype) -> str:
s = _DTYPE_TO_STR.get(dtype)
if s is None:
raise ValueError(f"Unsupported dtype: {dtype}")
return s
def str_to_dtype(s: str) -> torch.dtype:
d = _STR_TO_DTYPE.get(s)
if d is None:
raise ValueError(f"Unknown dtype string: {s}")
return d
class TensorWrapper:
"""Expose a CPU-contiguous tensor's data buffer for zero-copy ZMQ send."""
def __init__(self, tensor: torch.Tensor):
if tensor.is_cuda or tensor.is_npu:
tensor = tensor.cpu()
if not tensor.is_contiguous():
tensor = tensor.contiguous()
self.tensor = tensor
data_ptr = tensor.data_ptr()
total_bytes = tensor.numel() * tensor.element_size()
self._c_buf = (ctypes.c_char * total_bytes).from_address(data_ptr)
self._view = memoryview(self._c_buf)
@dataclass
class TensorDescriptor:
field_name: str
shape: list[int]
dtype: str
list_index: int = -1 # -1 means not part of a list
def to_dict(self) -> dict:
return {
"field_name": self.field_name,
"shape": self.shape,
"dtype": self.dtype,
"list_index": self.list_index,
}
@classmethod
def from_dict(cls, d: dict) -> "TensorDescriptor":
return cls(
field_name=d["field_name"],
shape=d["shape"],
dtype=d["dtype"],
list_index=d.get("list_index", -1),
)
def pack_tensors(
tensor_fields: dict[str, torch.Tensor | list[torch.Tensor] | None],
scalar_fields: dict | None = None,
) -> tuple[bytes, list[TensorWrapper]]:
"""Pack tensor fields into metadata + buffer list for send_multipart."""
descriptors = []
buffers = []
for field_name, value in tensor_fields.items():
if value is None:
continue
if isinstance(value, torch.Tensor):
wrapper = TensorWrapper(value)
descriptors.append(
TensorDescriptor(
field_name=field_name,
shape=list(value.shape),
dtype=dtype_to_str(value.dtype),
)
)
buffers.append(wrapper)
elif isinstance(value, list):
for i, t in enumerate(value):
if t is None:
continue
if not isinstance(t, torch.Tensor):
raise TypeError(
f"Expected Tensor in list for field '{field_name}', "
f"got {type(t)}"
)
wrapper = TensorWrapper(t)
descriptors.append(
TensorDescriptor(
field_name=field_name,
shape=list(t.shape),
dtype=dtype_to_str(t.dtype),
list_index=i,
)
)
buffers.append(wrapper)
metadata = {
"tensor_descriptors": [d.to_dict() for d in descriptors],
"scalar_fields": scalar_fields or {},
}
metadata_bytes = json.dumps(metadata, separators=(",", ":")).encode("utf-8")
return metadata_bytes, buffers
def send_tensors(
socket: zmq.Socket,
tensor_fields: dict[str, torch.Tensor | list[torch.Tensor] | None],
scalar_fields: dict | None = None,
flags: int = 0,
) -> None:
"""Send tensors over ZMQ using multipart with zero-copy."""
metadata_bytes, buffers = pack_tensors(tensor_fields, scalar_fields)
parts: list = [metadata_bytes]
parts.extend(w._view if isinstance(w, TensorWrapper) else w for w in buffers)
socket.send_multipart(parts, flags=flags, copy=True)
def unpack_tensors(
parts: list,
device: str | torch.device = "cpu",
) -> tuple[dict[str, torch.Tensor | list[torch.Tensor]], dict]:
"""Unpack multipart message frames into tensor fields and scalar fields."""
metadata_frame = parts[0]
metadata_bytes = (
bytes(metadata_frame.buffer)
if hasattr(metadata_frame, "buffer")
else bytes(metadata_frame)
)
metadata = json.loads(metadata_bytes)
descriptors = [
TensorDescriptor.from_dict(d) for d in metadata["tensor_descriptors"]
]
scalar_fields = metadata.get("scalar_fields", {})
if len(parts) - 1 != len(descriptors):
raise ValueError(
f"Expected {len(descriptors)} tensor frames, got {len(parts) - 1}"
)
tensor_fields: dict[str, torch.Tensor | list[torch.Tensor]] = {}
list_sizes: dict[str, int] = {}
for desc in descriptors:
if desc.list_index >= 0:
current_max = list_sizes.get(desc.field_name, 0)
list_sizes[desc.field_name] = max(current_max, desc.list_index + 1)
for field_name, size in list_sizes.items():
tensor_fields[field_name] = [None] * size
for i, desc in enumerate(descriptors):
frame = parts[i + 1]
buf = frame.buffer if hasattr(frame, "buffer") else bytes(frame)
dtype = str_to_dtype(desc.dtype)
# clone() to own the memory (decouple from ZMQ buffer lifetime)
tensor = torch.frombuffer(buf, dtype=dtype).reshape(desc.shape).clone()
if device != "cpu" and device != torch.device("cpu"):
tensor = tensor.to(device)
if desc.list_index >= 0:
tensor_fields[desc.field_name][desc.list_index] = tensor
else:
tensor_fields[desc.field_name] = tensor
return tensor_fields, scalar_fields
@@ -0,0 +1,126 @@
# SPDX-License-Identifier: Apache-2.0
"""Transfer engine abstraction for tensor transfer between role instances."""
import logging
from abc import ABC, abstractmethod
logger = logging.getLogger(__name__)
_MOONCAKE_AVAILABLE = None
def _check_mooncake() -> bool:
global _MOONCAKE_AVAILABLE
if _MOONCAKE_AVAILABLE is None:
try:
from sglang.srt.distributed.device_communicators.mooncake_transfer_engine import ( # noqa: F401
MooncakeTransferEngine as _MTE,
)
_MOONCAKE_AVAILABLE = True
except ImportError:
_MOONCAKE_AVAILABLE = False
return _MOONCAKE_AVAILABLE
class BaseTransferEngine(ABC):
"""Abstract transfer engine for data movement between roles."""
@property
def supports_gpu_direct(self) -> bool:
return False
@property
@abstractmethod
def session_id(self) -> str: ...
@abstractmethod
def register_buffer(self, ptr: int, length: int) -> None: ...
@abstractmethod
def deregister_buffer(self, ptr: int) -> None: ...
@abstractmethod
def transfer_sync(
self, dst_session_id: str, src_addr: int, dst_addr: int, length: int
) -> int:
"""Returns 0 on success, negative on failure."""
@abstractmethod
def batch_transfer_sync(
self,
dst_session_id: str,
src_addrs: list[int],
dst_addrs: list[int],
lengths: list[int],
) -> int: ...
class MooncakeDiffusionEngine(BaseTransferEngine):
"""Production engine backed by MooncakeTransferEngine (RDMA)."""
@property
def supports_gpu_direct(self) -> bool:
return True
def __init__(
self,
hostname: str,
gpu_id: int = 0,
ib_device: str | None = None,
):
from sglang.srt.distributed.device_communicators.mooncake_transfer_engine import (
MooncakeTransferEngine,
)
self._engine = MooncakeTransferEngine(
hostname=hostname,
gpu_id=gpu_id,
ib_device=ib_device,
)
logger.info(
"MooncakeDiffusionEngine initialized: session_id=%s",
self._engine.session_id,
)
@property
def session_id(self) -> str:
return self._engine.session_id
def register_buffer(self, ptr: int, length: int) -> None:
self._engine.register(ptr, length)
def deregister_buffer(self, ptr: int) -> None:
self._engine.deregister(ptr)
def transfer_sync(
self, dst_session_id: str, src_addr: int, dst_addr: int, length: int
) -> int:
return self._engine.transfer_sync(dst_session_id, src_addr, dst_addr, length)
def batch_transfer_sync(
self,
dst_session_id: str,
src_addrs: list[int],
dst_addrs: list[int],
lengths: list[int],
) -> int:
return self._engine.batch_transfer_sync(
dst_session_id, src_addrs, dst_addrs, lengths
)
def create_transfer_engine(
hostname: str = "127.0.0.1",
gpu_id: int = 0,
ib_device: str | None = None,
) -> BaseTransferEngine:
"""Factory: returns MooncakeDiffusionEngine if mooncake is available."""
if not _check_mooncake():
raise RuntimeError(
"Mooncake transfer engine is required for disaggregated diffusion "
"but is not installed. Please install mooncake first."
)
return MooncakeDiffusionEngine(
hostname=hostname, gpu_id=gpu_id, ib_device=ib_device
)
@@ -0,0 +1,391 @@
# 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")
@@ -0,0 +1,145 @@
# SPDX-License-Identifier: Apache-2.0
"""Transfer protocol messages for disaggregated diffusion.
All messages are sent as ZMQ multipart with a b"__transfer__" discriminator
in frame[0] and JSON payload in frame[1].
"""
import json
import logging
from dataclasses import asdict, dataclass, field
from typing import Any
logger = logging.getLogger(__name__)
TRANSFER_MAGIC = b"__transfer__"
class TransferMsgType:
# Instance → DiffusionServer
STAGED = "transfer_staged"
ALLOCATED = "transfer_allocated"
PUSHED = "transfer_pushed"
DONE = "transfer_done"
# DiffusionServer → Instance
ALLOC = "transfer_alloc"
PUSH = "transfer_push"
READY = "transfer_ready"
# Registration
REGISTER = "transfer_register"
REGISTER_ACK = "transfer_register_ack"
@dataclass
class TransferStagedMsg:
msg_type: str = TransferMsgType.STAGED
request_id: str = ""
data_size: int = 0
manifest: dict = None
session_id: str = ""
pool_ptr: int = 0
slot_offset: int = 0
def __post_init__(self):
if self.manifest is None:
self.manifest = {}
@dataclass
class TransferAllocMsg:
msg_type: str = TransferMsgType.ALLOC
request_id: str = ""
data_size: int = 0
source_role: str = ""
@dataclass
class TransferAllocatedMsg:
msg_type: str = TransferMsgType.ALLOCATED
request_id: str = ""
session_id: str = ""
pool_ptr: int = 0
slot_offset: int = 0
slot_size: int = 0
@dataclass
class TransferPushMsg:
msg_type: str = TransferMsgType.PUSH
request_id: str = ""
dest_session_id: str = ""
dest_addr: int = 0
transfer_size: int = 0
@dataclass
class TransferPushedMsg:
msg_type: str = TransferMsgType.PUSHED
request_id: str = ""
@dataclass
class TransferReadyMsg:
msg_type: str = TransferMsgType.READY
request_id: str = ""
manifest: dict = None
slot_offset: int = 0
scalar_fields: dict = None
def __post_init__(self):
if self.manifest is None:
self.manifest = {}
if self.scalar_fields is None:
self.scalar_fields = {}
@dataclass
class TransferDoneMsg:
msg_type: str = TransferMsgType.DONE
request_id: str = ""
error: str | None = None
@dataclass
class TransferRegisterMsg:
msg_type: str = TransferMsgType.REGISTER
role: str = ""
session_id: str = ""
pool_ptr: int = 0
pool_size: int = 0
# The instance's own work endpoint (e.g. tcp://host:port). Used by the
# DiffusionServer to key peer info by URL index (i.e. the same index used
# to build the PUSH work-socket list), so the control plane and the RDMA
# data plane cannot drift when instances register in a different order
# than --*-urls.
work_endpoint: str = ""
# Pre-allocated receive slots: [{"offset": int, "size": int, "slot_id": int, "addr": int}]
preallocated_slots: list = field(default_factory=list)
def encode_transfer_msg(msg: Any) -> list[bytes]:
"""Encode as [TRANSFER_MAGIC, json_payload_bytes]."""
if hasattr(msg, "__dataclass_fields__"):
d = asdict(msg)
elif isinstance(msg, dict):
d = msg
else:
raise TypeError(f"Cannot encode transfer message: {type(msg)}")
return [TRANSFER_MAGIC, json.dumps(d, separators=(",", ":")).encode("utf-8")]
def decode_transfer_msg(frames: list[bytes]) -> dict:
if len(frames) < 2 or frames[0] != TRANSFER_MAGIC:
raise ValueError(f"Not a transfer message: frame[0]={frames[0]!r}")
return json.loads(frames[1])
def is_transfer_message(frames: list) -> bool:
return len(frames) >= 2 and (
frames[0] == TRANSFER_MAGIC
or (isinstance(frames[0], memoryview) and bytes(frames[0]) == TRANSFER_MAGIC)
or (hasattr(frames[0], "bytes") and frames[0].bytes == TRANSFER_MAGIC)
)