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
789 lines
26 KiB
Python
789 lines
26 KiB
Python
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
|
|
|
|
import dataclasses
|
|
import multiprocessing as mp
|
|
import os
|
|
import signal
|
|
import sys
|
|
import threading
|
|
import time
|
|
|
|
import psutil
|
|
import uvicorn
|
|
|
|
from sglang.multimodal_gen.runtime.disaggregation.orchestrator import (
|
|
DiffusionServer,
|
|
)
|
|
from sglang.multimodal_gen.runtime.disaggregation.roles import RoleType
|
|
from sglang.multimodal_gen.runtime.entrypoints.http_server import create_app
|
|
from sglang.multimodal_gen.runtime.entrypoints.utils import ShutdownReq
|
|
from sglang.multimodal_gen.runtime.managers.gpu_worker import run_scheduler_process
|
|
from sglang.multimodal_gen.runtime.scheduler_client import SchedulerClient
|
|
from sglang.multimodal_gen.runtime.server_args import (
|
|
ServerArgs,
|
|
prepare_server_args,
|
|
set_global_server_args,
|
|
)
|
|
from sglang.multimodal_gen.runtime.utils.common import is_port_available
|
|
from sglang.multimodal_gen.runtime.utils.logging_utils import configure_logger, logger
|
|
from sglang.multimodal_gen.runtime.utils.trace_wrapper import init_diffusion_tracing
|
|
from sglang.multimodal_gen.utils import kill_itself_when_parent_died
|
|
|
|
_SCHEDULER_SHUTDOWN_TIMEOUT_MS = 5000
|
|
_WORKER_JOIN_TIMEOUT_S = 10
|
|
_WORKER_TERMINATE_TIMEOUT_S = 1
|
|
_WORKER_KILL_TIMEOUT_S = 1
|
|
|
|
|
|
def _find_available_port(
|
|
start: int = 10000, avoid: set[int] | None = None, max_attempts: int = 100
|
|
) -> int:
|
|
"""Find an available port starting from *start*, skipping ports in *avoid*."""
|
|
if avoid is None:
|
|
avoid = set()
|
|
port = max(1024, min(start, 65535))
|
|
for _ in range(max_attempts):
|
|
if port not in avoid and is_port_available(port):
|
|
return port
|
|
port += 1
|
|
if port > 65535:
|
|
port = 1024
|
|
raise RuntimeError(
|
|
f"No available port found after {max_attempts} attempts (start={start})"
|
|
)
|
|
|
|
|
|
def kill_process_tree(parent_pid, include_parent: bool = True, skip_pid: int = None):
|
|
"""Kill the process and all its child processes."""
|
|
# Remove sigchld handler to avoid spammy logs.
|
|
if threading.current_thread() is threading.main_thread():
|
|
signal.signal(signal.SIGCHLD, signal.SIG_DFL)
|
|
|
|
if parent_pid is None:
|
|
parent_pid = os.getpid()
|
|
include_parent = False
|
|
|
|
try:
|
|
itself = psutil.Process(parent_pid)
|
|
except psutil.NoSuchProcess:
|
|
return
|
|
|
|
children = itself.children(recursive=True)
|
|
for child in children:
|
|
if child.pid == skip_pid:
|
|
continue
|
|
try:
|
|
child.kill()
|
|
except psutil.NoSuchProcess:
|
|
pass
|
|
|
|
if include_parent:
|
|
try:
|
|
if parent_pid == os.getpid():
|
|
itself.kill()
|
|
sys.exit(0)
|
|
|
|
itself.kill()
|
|
|
|
# Sometime processes cannot be killed with SIGKILL (e.g, PID=1 launched by kubernetes),
|
|
# so we send an additional signal to kill them.
|
|
itself.send_signal(signal.SIGQUIT)
|
|
except psutil.NoSuchProcess:
|
|
pass
|
|
|
|
|
|
def _process_names(processes) -> str:
|
|
return ", ".join(getattr(p, "name", repr(p)) for p in processes)
|
|
|
|
|
|
def _join_processes_with_deadline(processes, timeout_s: float) -> None:
|
|
deadline = time.monotonic() + timeout_s
|
|
for process in processes:
|
|
remaining_s = max(0.0, deadline - time.monotonic())
|
|
process.join(timeout=remaining_s)
|
|
|
|
|
|
def _terminate_alive_processes(processes, timeout_s: float) -> list:
|
|
alive = [p for p in processes if p.is_alive()]
|
|
if not alive:
|
|
return []
|
|
|
|
logger.warning(
|
|
"Worker process(es) did not exit in time; terminating: %s",
|
|
_process_names(alive),
|
|
)
|
|
for process in alive:
|
|
process.terminate()
|
|
_join_processes_with_deadline(alive, timeout_s)
|
|
return [p for p in alive if p.is_alive()]
|
|
|
|
|
|
def _kill_alive_processes(processes, timeout_s: float) -> None:
|
|
alive = [p for p in processes if p.is_alive()]
|
|
if not alive:
|
|
return
|
|
|
|
logger.warning(
|
|
"Worker process(es) did not terminate in time; killing: %s",
|
|
_process_names(alive),
|
|
)
|
|
for process in alive:
|
|
process.kill()
|
|
_join_processes_with_deadline(alive, timeout_s)
|
|
|
|
|
|
def _run_http_server_process(server_args: ServerArgs) -> None:
|
|
kill_itself_when_parent_died()
|
|
launch_http_server_only(server_args)
|
|
|
|
|
|
def _request_monolithic_scheduler_shutdown(server_args: ServerArgs) -> None:
|
|
if server_args.disagg_role != RoleType.MONOLITHIC:
|
|
return
|
|
|
|
client = SchedulerClient()
|
|
try:
|
|
client.initialize(server_args)
|
|
client.forward(ShutdownReq(), timeout_ms=_SCHEDULER_SHUTDOWN_TIMEOUT_MS)
|
|
except Exception as e:
|
|
logger.warning("Failed to request graceful scheduler shutdown: %s", e)
|
|
finally:
|
|
client.close()
|
|
|
|
|
|
def shutdown_scheduler_processes(
|
|
server_args: ServerArgs | None,
|
|
processes: list,
|
|
*,
|
|
request_shutdown: bool = True,
|
|
) -> None:
|
|
if not processes:
|
|
return
|
|
|
|
if request_shutdown and server_args is not None:
|
|
_request_monolithic_scheduler_shutdown(server_args)
|
|
|
|
_join_processes_with_deadline(processes, _WORKER_JOIN_TIMEOUT_S)
|
|
alive = _terminate_alive_processes(processes, _WORKER_TERMINATE_TIMEOUT_S)
|
|
_kill_alive_processes(alive, _WORKER_KILL_TIMEOUT_S)
|
|
|
|
|
|
def launch_server(server_args: ServerArgs, launch_http_server: bool = True):
|
|
"""
|
|
Args:
|
|
launch_http_server: False for offline local mode
|
|
"""
|
|
configure_logger(server_args)
|
|
|
|
# Start a new server with multiple worker processes
|
|
logger.info("Starting server...")
|
|
|
|
num_gpus = server_args.num_gpus
|
|
processes = []
|
|
|
|
# Pipes for master to talk to slaves
|
|
task_pipes_to_slaves_w = []
|
|
task_pipes_to_slaves_r = []
|
|
for _ in range(num_gpus - 1):
|
|
r, w = mp.Pipe(duplex=False)
|
|
task_pipes_to_slaves_r.append(r)
|
|
task_pipes_to_slaves_w.append(w)
|
|
|
|
# Pipes for slaves to talk to master
|
|
result_pipes_from_slaves_w = []
|
|
result_pipes_from_slaves_r = []
|
|
for _ in range(num_gpus - 1):
|
|
r, w = mp.Pipe(duplex=False)
|
|
result_pipes_from_slaves_r.append(r)
|
|
result_pipes_from_slaves_w.append(w)
|
|
|
|
# Launch all worker processes
|
|
master_port = server_args.master_port
|
|
scheduler_pipe_readers = []
|
|
scheduler_pipe_writers = []
|
|
|
|
for i in range(num_gpus):
|
|
reader, writer = mp.Pipe(duplex=False)
|
|
scheduler_pipe_writers.append(writer)
|
|
if i == 0: # Master worker
|
|
process = mp.Process(
|
|
target=run_scheduler_process,
|
|
args=(
|
|
i, # local_rank
|
|
i, # rank
|
|
master_port,
|
|
server_args,
|
|
writer,
|
|
None, # No task pipe to read from master
|
|
None, # No result pipe to write to master
|
|
task_pipes_to_slaves_w,
|
|
result_pipes_from_slaves_r,
|
|
),
|
|
name=f"sglang-diffusionWorker-{i}",
|
|
daemon=True,
|
|
)
|
|
else: # Slave workers
|
|
process = mp.Process(
|
|
target=run_scheduler_process,
|
|
args=(
|
|
i, # local_rank
|
|
i, # rank
|
|
master_port,
|
|
server_args,
|
|
writer,
|
|
None, # No task pipe to read from master
|
|
None, # No result pipe to write to master
|
|
task_pipes_to_slaves_r[i - 1],
|
|
result_pipes_from_slaves_w[i - 1],
|
|
),
|
|
name=f"sglang-diffusionWorker-{i}",
|
|
daemon=True,
|
|
)
|
|
scheduler_pipe_readers.append(reader)
|
|
process.start()
|
|
processes.append(process)
|
|
|
|
# Wait for all workers to be ready
|
|
scheduler_infos = []
|
|
for writer in scheduler_pipe_writers:
|
|
writer.close()
|
|
|
|
# Close unused pipe ends in parent process
|
|
for p in task_pipes_to_slaves_w:
|
|
p.close()
|
|
for p in task_pipes_to_slaves_r:
|
|
p.close()
|
|
for p in result_pipes_from_slaves_w:
|
|
p.close()
|
|
for p in result_pipes_from_slaves_r:
|
|
p.close()
|
|
|
|
for i, reader in enumerate(scheduler_pipe_readers):
|
|
try:
|
|
data = reader.recv()
|
|
except EOFError:
|
|
logger.error(
|
|
f"Rank {i} scheduler is dead. Please check if there are relevant logs."
|
|
)
|
|
processes[i].join()
|
|
logger.error(f"Exit code: {processes[i].exitcode}")
|
|
raise
|
|
|
|
if data["status"] != "ready":
|
|
raise RuntimeError(
|
|
"Initialization failed. Please see the error messages above."
|
|
)
|
|
scheduler_infos.append(data)
|
|
reader.close()
|
|
|
|
logger.debug("All workers are ready")
|
|
|
|
if launch_http_server:
|
|
if server_args.pipeline_config.task_type.is_action_gen():
|
|
logger.info(
|
|
"VLA pipeline ready: model=%s; per-request details are "
|
|
"debug-only (use --log-level debug).",
|
|
server_args.model_id or server_args.model_path,
|
|
)
|
|
logger.info("Starting FastAPI server.")
|
|
if server_args.webui:
|
|
logger.info("Launch FastAPI server in another process because of webui.")
|
|
http_server_process = mp.Process(
|
|
target=_run_http_server_process,
|
|
args=(server_args,),
|
|
name="sglang-diffusion-webui",
|
|
daemon=True,
|
|
)
|
|
http_server_process.start()
|
|
else:
|
|
try:
|
|
launch_http_server_only(server_args)
|
|
finally:
|
|
shutdown_scheduler_processes(server_args, processes)
|
|
|
|
return processes
|
|
|
|
|
|
def launch_pool_disagg_server(
|
|
server_args: ServerArgs,
|
|
encoder_gpus: list[list[int]],
|
|
denoiser_gpus: list[list[int]],
|
|
decoder_gpus: list[list[int]],
|
|
launch_http_server: bool = True,
|
|
):
|
|
"""Launch a pool-based disaggregated server with N:M:K independent role instances.
|
|
|
|
DiffusionServer orchestrates the full pipeline, dispatching at every
|
|
role transition (Encoder → Denoiser → Decoder).
|
|
|
|
Args:
|
|
server_args: Base server configuration
|
|
encoder_gpus: List of GPU ID lists, one per encoder instance.
|
|
e.g., [[0], [2]] for 2 encoder instances on GPUs 0 and 2.
|
|
denoiser_gpus: List of GPU ID lists, one per denoiser instance.
|
|
e.g., [[1], [3]] for 2 denoiser instances.
|
|
decoder_gpus: List of GPU ID lists, one per decoder instance.
|
|
e.g., [[0], [2]] for 2 decoder instances (can share with encoder).
|
|
launch_http_server: Whether to launch the HTTP server.
|
|
|
|
Example:
|
|
launch_pool_disagg_server(server_args,
|
|
encoder_gpus=[[0], [2]],
|
|
denoiser_gpus=[[1], [3]],
|
|
decoder_gpus=[[0], [2]],
|
|
)
|
|
"""
|
|
configure_logger(server_args)
|
|
|
|
num_encoders = len(encoder_gpus)
|
|
num_denoisers = len(denoiser_gpus)
|
|
num_decoders = len(decoder_gpus)
|
|
logger.info(
|
|
"Starting pool disagg server: %d encoder(s), %d denoiser(s), %d decoder(s)...",
|
|
num_encoders,
|
|
num_denoisers,
|
|
num_decoders,
|
|
)
|
|
|
|
host = server_args.host or "127.0.0.1"
|
|
|
|
def find_port(start):
|
|
return _find_available_port(start)
|
|
|
|
# Allocate endpoints
|
|
port_cursor = server_args.scheduler_port + 3000
|
|
|
|
# Per-instance work endpoints (instance binds PULL, DS connects PUSH)
|
|
encoder_work_endpoints = []
|
|
for i in range(num_encoders):
|
|
p = find_port(port_cursor)
|
|
encoder_work_endpoints.append(f"tcp://{host}:{p}")
|
|
port_cursor = p + 1
|
|
|
|
denoiser_work_endpoints = []
|
|
for i in range(num_denoisers):
|
|
p = find_port(port_cursor)
|
|
denoiser_work_endpoints.append(f"tcp://{host}:{p}")
|
|
port_cursor = p + 1
|
|
|
|
decoder_work_endpoints = []
|
|
for i in range(num_decoders):
|
|
p = find_port(port_cursor)
|
|
decoder_work_endpoints.append(f"tcp://{host}:{p}")
|
|
port_cursor = p + 1
|
|
|
|
# Per-role-type result endpoints (DS binds PULL, instances connect PUSH)
|
|
# Use deterministic convention: scheduler_port + {1,2,3}
|
|
base_port = server_args.scheduler_port
|
|
encoder_result_ep = f"tcp://{host}:{base_port + 1}"
|
|
denoiser_result_ep = f"tcp://{host}:{base_port + 2}"
|
|
decoder_result_ep = f"tcp://{host}:{base_port + 3}"
|
|
|
|
logger.info(
|
|
"Pool endpoints allocated: %d work + 3 result endpoints",
|
|
num_encoders + num_denoisers + num_decoders,
|
|
)
|
|
|
|
# Launch all role instances
|
|
all_processes = []
|
|
|
|
role_configs = [
|
|
(RoleType.ENCODER, encoder_gpus, encoder_work_endpoints, encoder_result_ep),
|
|
(
|
|
RoleType.DENOISER,
|
|
denoiser_gpus,
|
|
denoiser_work_endpoints,
|
|
denoiser_result_ep,
|
|
),
|
|
(RoleType.DECODER, decoder_gpus, decoder_work_endpoints, decoder_result_ep),
|
|
]
|
|
|
|
for role_type, gpu_lists, work_eps, result_ep in role_configs:
|
|
for inst_idx, gpu_ids in enumerate(gpu_lists):
|
|
num_role_gpus = len(gpu_ids)
|
|
|
|
# Per-role parallelism: use explicit overrides if set, else None (auto-derive)
|
|
role_par = server_args.get_role_parallelism(role_type)
|
|
|
|
role_overrides = {
|
|
"disagg_role": role_type,
|
|
"disagg_mode": True,
|
|
"pool_work_endpoint": work_eps[inst_idx],
|
|
"pool_result_endpoint": result_ep,
|
|
"num_gpus": num_role_gpus,
|
|
"warmup": role_type == RoleType.ENCODER,
|
|
"server_warmup": False,
|
|
"scheduler_port": find_port(port_cursor),
|
|
"master_port": find_port(port_cursor + 100),
|
|
# Per-role parallelism (None = auto-derive from num_gpus)
|
|
"tp_size": role_par["tp_size"],
|
|
"sp_degree": role_par["sp_degree"],
|
|
"ulysses_degree": role_par["ulysses_degree"],
|
|
"ring_degree": role_par["ring_degree"],
|
|
}
|
|
port_cursor = role_overrides["master_port"] + 100
|
|
|
|
base_dict = {
|
|
f.name: getattr(server_args, f.name)
|
|
for f in dataclasses.fields(server_args)
|
|
}
|
|
base_dict.update(role_overrides)
|
|
base_dict.pop("pipeline_config", None)
|
|
role_args = ServerArgs.from_kwargs(**base_dict)
|
|
|
|
pool_ctx = mp.get_context("spawn")
|
|
inst_readers = []
|
|
|
|
# Spawn all ranks first — NCCL init blocks until all ranks connect
|
|
for rank_idx in range(num_role_gpus):
|
|
reader, writer = pool_ctx.Pipe(duplex=False)
|
|
gpu_id = gpu_ids[rank_idx]
|
|
|
|
process = pool_ctx.Process(
|
|
target=_run_disagg_role_process,
|
|
args=(gpu_id, rank_idx, rank_idx, role_args, writer, [], []),
|
|
name=f"sglang-pool-{role_type.value}-{inst_idx}-r{rank_idx}",
|
|
daemon=True,
|
|
)
|
|
process.start()
|
|
all_processes.append(process)
|
|
inst_readers.append(reader)
|
|
|
|
# Wait for all ranks to be ready (after all are spawned)
|
|
for rank_idx, reader in enumerate(inst_readers):
|
|
try:
|
|
data = reader.recv()
|
|
except EOFError:
|
|
logger.error(
|
|
"Pool %s[%d] rank %d is dead.",
|
|
role_type.value,
|
|
inst_idx,
|
|
rank_idx,
|
|
)
|
|
raise
|
|
if data.get("status") != "ready":
|
|
raise RuntimeError(
|
|
f"Pool {role_type.value}[{inst_idx}] rank {rank_idx} "
|
|
"failed to initialize."
|
|
)
|
|
reader.close()
|
|
|
|
logger.info(
|
|
"Pool %s[%d] ready on GPU(s) %s (work=%s)",
|
|
role_type.value.upper(),
|
|
inst_idx,
|
|
gpu_ids,
|
|
work_eps[inst_idx],
|
|
)
|
|
|
|
logger.info("All pool role instances ready")
|
|
|
|
# Start DiffusionServer
|
|
frontend_endpoint = f"tcp://{host}:{server_args.scheduler_port}"
|
|
|
|
diffusion_server = DiffusionServer(
|
|
frontend_endpoint=frontend_endpoint,
|
|
encoder_work_endpoints=encoder_work_endpoints,
|
|
denoiser_work_endpoints=denoiser_work_endpoints,
|
|
decoder_work_endpoints=decoder_work_endpoints,
|
|
encoder_result_endpoint=encoder_result_ep,
|
|
denoiser_result_endpoint=denoiser_result_ep,
|
|
decoder_result_endpoint=decoder_result_ep,
|
|
dispatch_policy_name=server_args.disagg_dispatch_policy,
|
|
timeout_s=float(server_args.disagg_timeout),
|
|
)
|
|
diffusion_server.start()
|
|
|
|
if not diffusion_server.wait_ready(timeout=30.0):
|
|
raise RuntimeError("DiffusionServer failed to bind sockets within 30 seconds")
|
|
|
|
if launch_http_server:
|
|
logger.info(
|
|
"Starting FastAPI server (connected to DiffusionServer at port %d).",
|
|
server_args.scheduler_port,
|
|
)
|
|
try:
|
|
launch_http_server_only(server_args)
|
|
finally:
|
|
diffusion_server.stop()
|
|
shutdown_scheduler_processes(
|
|
server_args, all_processes, request_shutdown=False
|
|
)
|
|
|
|
return all_processes
|
|
|
|
|
|
def _run_disagg_role_process(
|
|
gpu_id: int,
|
|
_local_rank: int,
|
|
rank: int,
|
|
server_args: ServerArgs,
|
|
pipe_writer: mp.connection.Connection,
|
|
task_pipes: list,
|
|
result_pipes: list,
|
|
):
|
|
"""Entry point for a disagg role process.
|
|
|
|
Uses the physical GPU index (gpu_id) as local_rank so that
|
|
torch.cuda.set_device(local_rank) selects the correct GPU.
|
|
This avoids relying on CUDA_VISIBLE_DEVICES remapping, which
|
|
may not work if CUDA was pre-initialized in the parent process.
|
|
"""
|
|
run_scheduler_process(
|
|
local_rank=gpu_id,
|
|
rank=rank,
|
|
master_port=server_args.master_port,
|
|
server_args=server_args,
|
|
pipe_writer=pipe_writer,
|
|
task_pipe_r=None,
|
|
result_pipe_w=None,
|
|
task_pipes_to_slaves=task_pipes,
|
|
result_pipes_from_slaves=result_pipes,
|
|
)
|
|
|
|
|
|
def launch_http_server_only(server_args):
|
|
init_diffusion_tracing(server_args, "DiffHTTPServer")
|
|
|
|
# set for endpoints to access global_server_args
|
|
set_global_server_args(server_args)
|
|
app = create_app(server_args)
|
|
uvicorn.run(
|
|
app,
|
|
use_colors=True,
|
|
log_level=server_args.log_level,
|
|
host=server_args.host,
|
|
port=server_args.port,
|
|
reload=False,
|
|
ws_per_message_deflate=False,
|
|
)
|
|
|
|
|
|
def parse_url_string(url_str: str) -> list[str]:
|
|
"""Parse a semicolon-separated URL string into a list.
|
|
|
|
Example: "tcp://10.0.0.1:35000;tcp://10.0.0.2:35000" -> ["tcp://...", "tcp://..."]
|
|
"""
|
|
return [u.strip() for u in url_str.split(";") if u.strip()]
|
|
|
|
|
|
def launch_disagg_server(server_args: ServerArgs):
|
|
"""Launch DiffusionServer head node + HTTP server (--disagg-role server).
|
|
|
|
No GPU workers are spawned. Connects to remote role instances
|
|
specified by --encoder-urls, --denoiser-urls, --decoder-urls.
|
|
|
|
Result endpoints use deterministic convention:
|
|
encoder result: scheduler_port + 1
|
|
denoiser result: scheduler_port + 2
|
|
decoder result: scheduler_port + 3
|
|
"""
|
|
configure_logger(server_args)
|
|
|
|
for name, val in [
|
|
("--encoder-urls", server_args.encoder_urls),
|
|
("--denoiser-urls", server_args.denoiser_urls),
|
|
("--decoder-urls", server_args.decoder_urls),
|
|
]:
|
|
if val is None:
|
|
raise ValueError(f"{name} is required for --disagg-role server")
|
|
|
|
host = server_args.host or "127.0.0.1"
|
|
base_port = server_args.scheduler_port
|
|
|
|
encoder_work_endpoints = parse_url_string(server_args.encoder_urls)
|
|
denoiser_work_endpoints = parse_url_string(server_args.denoiser_urls)
|
|
decoder_work_endpoints = parse_url_string(server_args.decoder_urls)
|
|
|
|
encoder_result_ep = f"tcp://{host}:{base_port + 1}"
|
|
denoiser_result_ep = f"tcp://{host}:{base_port + 2}"
|
|
decoder_result_ep = f"tcp://{host}:{base_port + 3}"
|
|
|
|
frontend_endpoint = f"tcp://{host}:{base_port}"
|
|
|
|
logger.info(
|
|
"Starting DiffusionServer: %d encoder(s), %d denoiser(s), %d decoder(s)",
|
|
len(encoder_work_endpoints),
|
|
len(denoiser_work_endpoints),
|
|
len(decoder_work_endpoints),
|
|
)
|
|
logger.info(" Frontend: %s", frontend_endpoint)
|
|
logger.info(" Encoder work endpoints: %s", encoder_work_endpoints)
|
|
logger.info(" Denoiser work endpoints: %s", denoiser_work_endpoints)
|
|
logger.info(" Decoder work endpoints: %s", decoder_work_endpoints)
|
|
logger.info(
|
|
" Result endpoints: encoder=%s, denoiser=%s, decoder=%s",
|
|
encoder_result_ep,
|
|
denoiser_result_ep,
|
|
decoder_result_ep,
|
|
)
|
|
|
|
diffusion_server = DiffusionServer(
|
|
frontend_endpoint=frontend_endpoint,
|
|
encoder_work_endpoints=encoder_work_endpoints,
|
|
denoiser_work_endpoints=denoiser_work_endpoints,
|
|
decoder_work_endpoints=decoder_work_endpoints,
|
|
encoder_result_endpoint=encoder_result_ep,
|
|
denoiser_result_endpoint=denoiser_result_ep,
|
|
decoder_result_endpoint=decoder_result_ep,
|
|
dispatch_policy_name=server_args.disagg_dispatch_policy,
|
|
timeout_s=float(server_args.disagg_timeout),
|
|
)
|
|
diffusion_server.start()
|
|
|
|
if not diffusion_server.wait_ready(timeout=30.0):
|
|
raise RuntimeError("DiffusionServer failed to bind sockets within 30 seconds")
|
|
|
|
logger.info(
|
|
"Starting HTTP server (connected to DiffusionServer at port %d).",
|
|
base_port,
|
|
)
|
|
try:
|
|
launch_http_server_only(server_args)
|
|
finally:
|
|
diffusion_server.stop()
|
|
|
|
|
|
def launch_disagg_role(server_args: ServerArgs):
|
|
"""Launch a standalone disaggregated role instance (--disagg-role encoder/denoising/decoder).
|
|
|
|
The instance:
|
|
1. Binds its work PULL socket on tcp://0.0.0.0:{scheduler_port}
|
|
2. Connects its result PUSH socket to the DiffusionServer head node
|
|
(derived from --disagg-server-addr + role offset)
|
|
3. Spawns GPU worker processes for the assigned role.
|
|
"""
|
|
configure_logger(server_args)
|
|
|
|
role_type = server_args.disagg_role
|
|
if server_args.disagg_server_addr is None:
|
|
raise ValueError(
|
|
"--disagg-server-addr is required for --disagg-role " f"{role_type.value}"
|
|
)
|
|
|
|
# Derive endpoints
|
|
work_endpoint = server_args.derive_pool_work_endpoint()
|
|
result_endpoint = server_args.derive_pool_result_endpoint()
|
|
|
|
logger.info(
|
|
"Starting disagg role: %s, num_gpus=%d",
|
|
role_type.value,
|
|
server_args.num_gpus,
|
|
)
|
|
logger.info(" Work endpoint (bind): %s", work_endpoint)
|
|
logger.info(" Result endpoint (connect): %s", result_endpoint)
|
|
logger.info(
|
|
" P2P: hostname=%s, ib_device=%s, pool_size=%d",
|
|
server_args.disagg_p2p_hostname,
|
|
server_args.disagg_ib_device,
|
|
server_args.disagg_transfer_pool_size,
|
|
)
|
|
|
|
# Build role-specific ServerArgs
|
|
# Use a different port for the scheduler's internal ROUTER socket to avoid
|
|
# conflicting with the pool work PULL socket (both bind on scheduler_port).
|
|
internal_scheduler_port = _find_available_port(
|
|
start=server_args.scheduler_port + 100, avoid={server_args.scheduler_port}
|
|
)
|
|
|
|
role_par = server_args.get_role_parallelism(role_type)
|
|
role_overrides = {
|
|
"disagg_role": role_type,
|
|
"disagg_mode": True,
|
|
"pool_work_endpoint": work_endpoint,
|
|
"pool_result_endpoint": result_endpoint,
|
|
"warmup": role_type == RoleType.ENCODER,
|
|
"server_warmup": False,
|
|
"scheduler_port": internal_scheduler_port,
|
|
# Per-role parallelism (None = auto-derive from num_gpus)
|
|
"tp_size": role_par["tp_size"],
|
|
"sp_degree": role_par["sp_degree"],
|
|
"ulysses_degree": role_par["ulysses_degree"],
|
|
"ring_degree": role_par["ring_degree"],
|
|
}
|
|
|
|
base_dict = {
|
|
f.name: getattr(server_args, f.name) for f in dataclasses.fields(server_args)
|
|
}
|
|
base_dict.update(role_overrides)
|
|
base_dict.pop("pipeline_config", None)
|
|
role_args = ServerArgs.from_kwargs(**base_dict)
|
|
|
|
# Spawn GPU worker processes
|
|
# NOTE: All ranks must be spawned before waiting for ready signals,
|
|
# because NCCL init_process_group blocks until all ranks connect.
|
|
num_gpus = server_args.num_gpus
|
|
base_gpu_id = server_args.base_gpu_id
|
|
pool_ctx = mp.get_context("spawn")
|
|
processes = []
|
|
readers = []
|
|
|
|
for rank_idx in range(num_gpus):
|
|
reader, writer = pool_ctx.Pipe(duplex=False)
|
|
gpu_id = base_gpu_id + rank_idx
|
|
|
|
process = pool_ctx.Process(
|
|
target=_run_disagg_role_process,
|
|
args=(gpu_id, rank_idx, rank_idx, role_args, writer, [], []),
|
|
name=f"sglang-{role_type.value}-r{rank_idx}",
|
|
daemon=True,
|
|
)
|
|
process.start()
|
|
processes.append(process)
|
|
readers.append(reader)
|
|
|
|
# Wait for all ranks to be ready (after all are spawned)
|
|
for rank_idx, reader in enumerate(readers):
|
|
try:
|
|
data = reader.recv()
|
|
except EOFError:
|
|
logger.error(
|
|
"Role %s rank %d is dead.",
|
|
role_type.value,
|
|
rank_idx,
|
|
)
|
|
raise
|
|
if data.get("status") != "ready":
|
|
raise RuntimeError(
|
|
f"Role {role_type.value} rank {rank_idx} failed to initialize."
|
|
)
|
|
reader.close()
|
|
|
|
logger.info(
|
|
"Role %s ready (%d GPU(s), work=%s)",
|
|
role_type.value.upper(),
|
|
num_gpus,
|
|
work_endpoint,
|
|
)
|
|
|
|
# Block until interrupted
|
|
try:
|
|
for p in processes:
|
|
p.join()
|
|
except KeyboardInterrupt:
|
|
logger.info("Role %s shutting down.", role_type.value)
|
|
finally:
|
|
shutdown_scheduler_processes(role_args, processes, request_shutdown=False)
|
|
|
|
|
|
def dispatch_launch(server_args: ServerArgs):
|
|
"""Route to the correct launch function based on --disagg-role."""
|
|
role = server_args.disagg_role
|
|
if role == RoleType.MONOLITHIC:
|
|
launch_server(server_args)
|
|
elif role == RoleType.SERVER:
|
|
launch_disagg_server(server_args)
|
|
elif role in (RoleType.ENCODER, RoleType.DENOISER, RoleType.DECODER):
|
|
launch_disagg_role(server_args)
|
|
else:
|
|
raise ValueError(f"Unknown disagg_role: {role}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
server_args = prepare_server_args(sys.argv[1:])
|
|
|
|
try:
|
|
dispatch_launch(server_args)
|
|
finally:
|
|
kill_process_tree(os.getpid(), include_parent=False)
|