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

1053 lines
40 KiB
Python

# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
# SPDX-License-Identifier: Apache-2.0
import gc
import logging
import multiprocessing as mp
import os
import tempfile
import time
from contextlib import ExitStack
from dataclasses import dataclass, field
from typing import Any, Callable, List, Union
import numpy as np
import torch
from setproctitle import setproctitle
from sglang.multimodal_gen import envs
from sglang.multimodal_gen.runtime.distributed import (
get_sp_group,
get_tp_rank,
get_tp_world_size,
maybe_init_distributed_environment_and_model_parallel,
model_parallel_is_initialized,
)
from sglang.multimodal_gen.runtime.distributed.parallel_state import (
get_cfg_group,
get_classifier_free_guidance_rank,
get_classifier_free_guidance_world_size,
get_ring_parallel_rank,
get_ring_parallel_world_size,
get_tp_group,
get_ulysses_parallel_rank,
get_ulysses_parallel_world_size,
)
from sglang.multimodal_gen.runtime.entrypoints.utils import (
materialize_output_sample,
post_process_sample,
save_outputs,
)
from sglang.multimodal_gen.runtime.managers.memory_managers.layerwise_offload import (
configure_layerwise_offload_modules,
)
from sglang.multimodal_gen.runtime.managers.memory_managers.memory_occupation_controller import (
MemoryOccupationController,
)
from sglang.multimodal_gen.runtime.pipelines_core import (
ComposedPipelineBase,
LoRAPipeline,
Req,
build_pipeline,
)
from sglang.multimodal_gen.runtime.pipelines_core.schedule_batch import OutputBatch
from sglang.multimodal_gen.runtime.platforms import current_platform
from sglang.multimodal_gen.runtime.post_training.gpu_worker_post_training_mixin import (
GPUWorkerPostTrainingMixin,
)
from sglang.multimodal_gen.runtime.realtime.session import (
RealtimeSessionCache,
)
from sglang.multimodal_gen.runtime.server_args import PortArgs, ServerArgs
from sglang.multimodal_gen.runtime.utils.common import set_cuda_arch, set_musa_arch
from sglang.multimodal_gen.runtime.utils.logging_utils import (
configure_logger,
globally_suppress_loggers,
init_logger,
)
from sglang.multimodal_gen.runtime.utils.perf_logger import (
PerformanceLogger,
capture_memory_snapshot,
)
from sglang.multimodal_gen.runtime.utils.realtime_video import (
RAW_RGB_CONTENT_TYPE,
build_raw_rgb_frame_batches,
)
from sglang.multimodal_gen.runtime.utils.trace_wrapper import (
DiffStage,
init_diffusion_tracing,
trace_slice,
)
from sglang.multimodal_gen.utils import kill_itself_when_parent_died
from sglang.srt.utils.network import NetworkAddress
logger = init_logger(__name__)
OFFLOAD_DISABLE_RECOMMENDATION_ORDER = (
"vae",
"image_encoder",
"text_encoder",
"text_encoder_2",
"transformer",
)
@dataclass
class _ExpandedOutputParts:
tensor_outputs: list[torch.Tensor] = field(default_factory=list)
list_outputs: list[Any] = field(default_factory=list)
tensor_audio: list[torch.Tensor] = field(default_factory=list)
trajectory_latents: list[torch.Tensor] = field(default_factory=list)
noise_preds: list[torch.Tensor] = field(default_factory=list)
output_file_paths: list[str] = field(default_factory=list)
metrics_list: list[Any] = field(default_factory=list)
trajectory_decoded_parts: list[list[torch.Tensor]] | None = None
class GPUWorker(GPUWorkerPostTrainingMixin):
"""
A worker that executes the model on a single GPU.
"""
def __init__(
self,
local_rank: int,
rank: int,
master_port: int,
server_args: ServerArgs,
):
self.local_rank = local_rank
self.rank = rank
self.master_port = master_port
# FIXME: should we use tcp as distribute init method?
self.server_args = server_args
self.pipeline: ComposedPipelineBase = None
self.init_device_and_model()
self.sp_group = get_sp_group()
self.sp_cpu_group = self.sp_group.cpu_group
self.tp_group = get_tp_group()
self.tp_cpu_group = self.tp_group.cpu_group
self.cfg_group = get_cfg_group()
self.cfg_cpu_group = self.cfg_group.cpu_group
self._realtime_sessions = RealtimeSessionCache(max_sessions=1)
self.memory_occupation: MemoryOccupationController | None = None
def release_realtime_session(self, session_id: str) -> OutputBatch:
"""release the session of a realtime connection"""
if not session_id:
return OutputBatch(
output={
"released": False,
"session_id": session_id,
"reason": "empty_session_id",
}
)
released = self._realtime_sessions.release(session_id)
if released:
if torch.cuda.is_initialized():
torch.cuda.empty_cache()
return OutputBatch(output={"released": released, "session_id": session_id})
def _configure_persistent_torch_compile_cache(self) -> None:
"""Persist torch.compile's Inductor/Triton cache across restarts"""
compile_cache_root = os.path.join(
envs.SGLANG_DIFFUSION_CACHE_ROOT, "torch_compile_cache"
)
tmp_root = tempfile.gettempdir()
for env_name, sub in (
("TORCHINDUCTOR_CACHE_DIR", "inductor"),
("TRITON_CACHE_DIR", "triton"),
):
current = os.environ.get(env_name)
if current and not current.startswith(tmp_root):
# Respect an explicit, non-ephemeral user-provided cache dir.
continue
cache_path = os.path.join(compile_cache_root, sub)
try:
os.makedirs(cache_path, exist_ok=True)
except OSError as e:
logger.warning(
"Could not create torch.compile cache dir %s: %s", cache_path, e
)
continue
os.environ[env_name] = cache_path
logger.info(
"torch.compile cache: TORCHINDUCTOR_CACHE_DIR=%s TRITON_CACHE_DIR=%s",
os.environ.get("TORCHINDUCTOR_CACHE_DIR"),
os.environ.get("TRITON_CACHE_DIR"),
)
def is_sleeping(self) -> bool:
return self.memory_occupation.is_sleeping() if self.memory_occupation else False
def _get_memory_occupation(self) -> MemoryOccupationController:
if self.memory_occupation is None:
self.memory_occupation = MemoryOccupationController(
pipeline=self.pipeline,
rank=self.rank,
use_fsdp_inference=self.server_args.use_fsdp_inference,
)
return self.memory_occupation
def init_device_and_model(self) -> None:
"""Initialize the device and load the model."""
torch.get_device_module().set_device(self.local_rank)
# Set environment variables for distributed initialization
os.environ["MASTER_ADDR"] = "localhost"
os.environ["MASTER_PORT"] = str(self.master_port)
os.environ["LOCAL_RANK"] = str(self.local_rank)
os.environ["RANK"] = str(self.rank)
os.environ["WORLD_SIZE"] = str(self.server_args.num_gpus)
self._configure_persistent_torch_compile_cache()
# initialize the distributed environment
maybe_init_distributed_environment_and_model_parallel(
tp_size=self.server_args.tp_size,
cfg_degree=self.server_args.cfg_parallel_degree or 1,
ulysses_degree=self.server_args.ulysses_degree,
ring_degree=self.server_args.ring_degree,
sp_size=self.server_args.sp_degree,
dp_size=self.server_args.dp_size,
distributed_init_method=NetworkAddress(
"127.0.0.1", self.master_port
).to_tcp(),
dist_timeout=self.server_args.dist_timeout,
)
# set proc title
if model_parallel_is_initialized():
suffix = ""
if get_tp_world_size() != 1:
tp_rank = get_tp_rank()
suffix += f"_TP{tp_rank}"
if get_ulysses_parallel_world_size() != 1:
u_rank = get_ulysses_parallel_rank()
suffix += f"_U{u_rank}"
if get_ring_parallel_world_size() != 1:
r_rank = get_ring_parallel_rank()
suffix += f"_R{r_rank}"
if get_classifier_free_guidance_world_size() != 1:
c_rank = get_classifier_free_guidance_rank()
suffix += f"_C{c_rank}"
setproctitle(f"sgl_diffusion::scheduler{suffix}")
else:
setproctitle(f"sgl_diffusion::scheduler_{self.local_rank}")
self.pipeline = build_pipeline(self.server_args)
# apply layerwise offload after lora is applied while building LoRAPipeline
# otherwise empty offloaded weights could fail lora converting
if self.server_args.layerwise_offload_components:
configure_layerwise_offload_modules(
self.pipeline.modules,
self.server_args,
component_names=self.server_args.layerwise_offload_components,
warn_missing=(
self.server_args.is_arg_explicitly_set(
"layerwise_offload_components"
)
or self.server_args.is_arg_explicitly_set("dit_layerwise_offload")
),
)
logger.info(
f"Worker {self.rank}: Initialized device, model, and distributed environment."
)
def do_mem_analysis(self, output_batch: OutputBatch):
final_snapshot = capture_memory_snapshot()
if output_batch.metrics:
output_batch.metrics.record_memory_snapshot("mem_analysis", final_snapshot)
# for details on max_memory_reserved: https://docs.pytorch.org/docs/stable/generated/torch.cuda.memory.max_memory_reserved.html
peak_reserved_bytes = torch.get_device_module().max_memory_reserved()
peak_allocated_bytes = torch.get_device_module().max_memory_allocated()
output_batch.peak_memory_mb = peak_reserved_bytes / (1024**2)
peak_reserved_gb = peak_reserved_bytes / (1024**3)
peak_allocated_gb = peak_allocated_bytes / (1024**3)
remaining_gpu_mem_gb = (
current_platform.get_device_total_memory() / (1024**3) - peak_reserved_gb
)
can_stay_resident = self.get_can_stay_resident_components(remaining_gpu_mem_gb)
suggested_args_str = self._format_offload_disable_suggestions(can_stay_resident)
pool_overhead_gb = peak_reserved_gb - peak_allocated_gb
logger.debug(
f"Peak GPU memory: {peak_reserved_gb:.2f} GB, "
f"Peak allocated: {peak_allocated_gb:.2f} GB, "
f"Memory pool overhead: {pool_overhead_gb:.2f} GB ({pool_overhead_gb / peak_reserved_gb * 100:.1f}%), "
f"Remaining GPU memory at peak: {remaining_gpu_mem_gb:.2f} GB. "
f"Components that could stay resident (based on the last request workload): {can_stay_resident}. "
f"Related offload server args to disable: {suggested_args_str}"
)
def _format_offload_disable_suggestions(self, components: List[str]) -> str:
component_set = set(components)
suggestions = []
seen_args = set()
for component in OFFLOAD_DISABLE_RECOMMENDATION_ORDER:
if component not in component_set:
continue
arg = None
if component == "vae":
arg = "--vae-cpu-offload"
elif component == "image_encoder":
arg = "--image-encoder-cpu-offload"
elif component in ("text_encoder", "text_encoder_2"):
arg = "--text-encoder-cpu-offload"
elif component == "transformer":
if self.server_args.is_dit_layerwise_offload_selected:
arg = "--dit-layerwise-offload"
elif self.server_args.dit_cpu_offload:
arg = "--dit-cpu-offload"
if arg is not None and arg not in seen_args:
suggestions.append(arg)
seen_args.add(arg)
return ", ".join(suggestions) if suggestions else "None"
def execute_forward(
self, batch: List[Req], return_req: bool = False
) -> OutputBatch | Req:
"""
Execute a forward pass.
Args:
batch: List of requests to process.
return_req: If True, return the raw Req instead of OutputBatch.
Used by disaggregated pipelines to access intermediate tensors.
"""
assert self.pipeline is not None
if len(batch) > 1:
if return_req:
raise ValueError(
"Grouped execute_forward does not support return_req=True"
)
# grouped reqs currently come only from expanded num_outputs_per_prompt
self._validate_group_forward_reqs(batch)
return self._execute_forward_batch(batch)
req = batch[0]
return self._execute_forward_common(
req,
forward_fn=lambda: self.pipeline.forward(req, self.server_args),
log_reqs=[req],
return_req=return_req,
save_output_paths=lambda output_batch: self._save_output_paths(
req, output_batch
),
error_context=f"request {req.request_id}",
)
def _execute_forward_batch(self, batch: list[Req]) -> OutputBatch | Req:
"""Execute expanded multi-output requests as one grouped forward."""
# TODO: support early return or mix-stage execution for reqs in a group
assert self.pipeline is not None
req = batch[0]
return self._execute_forward_common(
req,
forward_fn=lambda: self._forward_group(batch),
log_reqs=batch,
return_req=False,
save_output_paths=lambda output_batch: self._save_group_output_paths(
batch, output_batch
),
error_context=f"grouped request {req.request_id}",
)
def _execute_forward_common(
self,
req: Req,
*,
forward_fn: Callable[[], Req | OutputBatch],
log_reqs: list[Req],
return_req: bool,
save_output_paths: Callable[[OutputBatch], None],
error_context: str,
) -> OutputBatch | Req:
"""
Args:
forward_fn: the actual forward function for reqs
"""
output_batch = None
try:
if self.rank == 0 and not current_platform.is_cpu():
torch.get_device_module().reset_peak_memory_stats()
start_time = time.monotonic()
self._realtime_sessions.attach(req)
# capture memory baseline for each req in grouped forward on rank-0
request_metrics = [
item.metrics for item in log_reqs if item.metrics is not None
]
if self.rank == 0 and request_metrics and not current_platform.is_cpu():
baseline_snapshot = capture_memory_snapshot()
for metrics in request_metrics:
metrics.record_memory_snapshot("before_forward", baseline_snapshot)
for item in log_reqs:
item.log(server_args=self.server_args)
with ExitStack() as stack:
for item in log_reqs:
stack.enter_context(
trace_slice(item.trace_ctx, DiffStage.GPU_FORWARD)
)
result = forward_fn()
# disagg roles return raw Req so callers can keep and transfer intermediate tensors
# before converting it to OutputBatch
if return_req and isinstance(result, Req):
return result
output_batch = self._to_output_batch(result)
self._record_output_peak_memory(output_batch)
output_metrics = self._iter_output_metrics(output_batch)
if self.rank == 0 and output_metrics and not current_platform.is_cpu():
peak_snapshot = capture_memory_snapshot()
for metrics in output_metrics:
metrics.record_memory_snapshot("after_forward", peak_snapshot)
if (
self.rank == 0
and not req.suppress_logs
and not current_platform.is_cpu()
and logger.isEnabledFor(logging.DEBUG)
):
self.do_mem_analysis(output_batch)
duration_ms = (time.monotonic() - start_time) * 1000
for metrics in output_metrics:
metrics.total_duration_ms = duration_ms
self._materialize_output_transport(output_batch, req, save_output_paths)
if (
torch.cuda.is_initialized()
and output_batch.output is None
and not req.return_raw_frames
):
torch.cuda.empty_cache()
if req.perf_dump_path is not None or envs.SGLANG_DIFFUSION_STAGE_LOGGING:
if not req.is_warmup:
PerformanceLogger.log_request_summary(metrics=output_batch.metrics)
# dump per-request perf report to the server-mode file path.
if (
req.perf_dump_path is not None
and not req.is_warmup
and output_batch.metrics is not None
):
PerformanceLogger.dump_benchmark_report(
file_path=req.perf_dump_path,
metrics=output_batch.metrics,
meta={"model": self.server_args.model_path},
tag="server_perf_dump",
)
except Exception as e:
logger.error(
f"Error executing {error_context}: {e}",
exc_info=True,
)
if isinstance(e, _oom_exceptions()):
logger.warning(OOM_MSG)
if output_batch is None:
output_batch = OutputBatch()
output_batch.error = f"Error executing {error_context}: {e}"
self._record_output_peak_memory(output_batch)
# clean cache if OOM
if torch.cuda.is_initialized():
torch.cuda.empty_cache()
return output_batch
def _materialize_output_transport(
self,
output_batch: OutputBatch,
req: Req,
save_output_paths: Callable[[OutputBatch], None],
) -> None:
if req.return_raw_frames:
self._materialize_raw_frame_transport(output_batch, req)
elif req.save_output and req.return_file_paths_only:
self._materialize_file_path_transport(output_batch, save_output_paths)
elif getattr(req, "return_frames", False):
self._materialize_frame_outputs_for_return(output_batch, req)
def _materialize_raw_frame_transport(
self, output_batch: OutputBatch, req: Req
) -> None:
if self.rank != 0:
return
if output_batch.output is not None:
output_batch.raw_frame_content_type = RAW_RGB_CONTENT_TYPE
(
output_batch.raw_frame_batches,
output_batch.raw_frame_metadata,
) = build_raw_rgb_frame_batches(
output_batch.output,
req,
output_batch,
post_process_sample,
)
output_batch.output = None
output_batch.audio = None
output_batch.audio_sample_rate = None
def _materialize_file_path_transport(
self,
output_batch: OutputBatch,
save_output_paths: Callable[[OutputBatch], None],
) -> None:
if self.rank == 0:
save_output_paths(output_batch)
output_batch.output = None
output_batch.audio = None
output_batch.audio_sample_rate = None
def _materialize_frame_outputs_for_return(
self, output_batch: OutputBatch, req: Req
) -> None:
"""materialize the output from tensor to numpy frames for faster serialization"""
if (
self.rank != 0
or output_batch.output is None
or not getattr(req, "return_frames", False)
):
return
if (
os.environ.get("SGLANG_DIFFUSION_SYNC_STAGE_PROFILING", "0") == "1"
and torch.cuda.is_initialized()
):
torch.cuda.synchronize()
start_time = time.perf_counter()
output_batch.output = [
self._materialize_frame_output(output, output_batch, req)
for output in output_batch.output
]
if output_batch.metrics is not None:
if (
os.environ.get("SGLANG_DIFFUSION_SYNC_STAGE_PROFILING", "0") == "1"
and torch.cuda.is_initialized()
):
torch.cuda.synchronize()
output_batch.metrics.record_stage(
"GPUWorker.frame_materialize_for_return",
time.perf_counter() - start_time,
)
@staticmethod
def _materialize_frame_output(
output: Any, output_batch: OutputBatch, req: Req
) -> np.ndarray:
if (
isinstance(output, torch.Tensor)
and not req.enable_frame_interpolation
and not req.enable_upscaling
):
if output.dim() == 3:
output = output.unsqueeze(1)
output = (output * 255).clamp(0, 255).to(torch.uint8)
return output.permute(1, 2, 3, 0).cpu().numpy()
if (
isinstance(output, np.ndarray)
and output.dtype == np.uint8
and output.ndim == 4
and output.shape[-1] in (1, 3, 4)
):
return output
materialized = materialize_output_sample(
output,
req.data_type,
req.fps,
enable_frame_interpolation=req.enable_frame_interpolation,
frame_interpolation_exp=req.frame_interpolation_exp,
frame_interpolation_scale=req.frame_interpolation_scale,
frame_interpolation_model_path=req.frame_interpolation_model_path,
enable_upscaling=req.enable_upscaling,
upscaling_model_path=req.upscaling_model_path,
upscaling_scale=req.upscaling_scale,
)
return np.asarray(materialized.frames)
def _record_output_peak_memory(self, output_batch: OutputBatch) -> None:
if self.rank != 0 or current_platform.is_cpu():
return
peak_reserved_bytes = torch.get_device_module().max_memory_reserved()
output_batch.peak_memory_mb = peak_reserved_bytes / (1024**2)
def _forward_group(self, batch: list[Req]) -> OutputBatch:
assert self.pipeline is not None
results = self.pipeline.forward_batch(batch, self.server_args)
output_batches = [self._to_output_batch(result) for result in results]
return self._merge_expanded_output_batches(output_batches)
def _save_output_paths(self, req: Req, output_batch: OutputBatch) -> None:
"""save outputs to files"""
if self.rank != 0 or output_batch.output is None:
return
dynamic_output_paths = None
if req.extra:
dynamic_output_paths = req.extra.get("dynamic_batch_output_paths")
if dynamic_output_paths is not None and (
len(dynamic_output_paths) != len(output_batch.output)
):
logger.warning(
"dynamic_batch_output_paths length mismatch (got=%d, expected=%d). "
"Falling back to merged request output file naming.",
len(dynamic_output_paths),
len(output_batch.output),
)
dynamic_output_paths = None
if dynamic_output_paths is not None:
def build_output_path(idx: int) -> str:
return dynamic_output_paths[idx]
else:
num_outputs = len(output_batch.output)
def build_output_path(idx: int) -> str:
return req.output_file_path(num_outputs, idx)
output_batch.output_file_paths = save_outputs(
output_batch.output,
req.data_type,
req.fps,
True,
build_output_path,
audio=output_batch.audio,
audio_sample_rate=output_batch.audio_sample_rate,
output_compression=req.output_compression,
enable_frame_interpolation=req.enable_frame_interpolation,
frame_interpolation_exp=req.frame_interpolation_exp,
frame_interpolation_scale=req.frame_interpolation_scale,
frame_interpolation_model_path=req.frame_interpolation_model_path,
enable_upscaling=req.enable_upscaling,
upscaling_model_path=req.upscaling_model_path,
upscaling_scale=req.upscaling_scale,
)
def _save_group_output_paths(
self,
reqs: list[Req],
output_batch: OutputBatch,
) -> None:
if self.rank != 0 or output_batch.output is None:
return
if len(output_batch.output) != len(reqs):
raise RuntimeError(
f"Expected {len(reqs)} grouped outputs, got {len(output_batch.output)}"
)
first_req = reqs[0]
output_batch.output_file_paths = save_outputs(
output_batch.output,
first_req.data_type,
first_req.fps,
True,
lambda idx: reqs[idx].output_file_path(1, 0),
audio=output_batch.audio,
audio_sample_rate=output_batch.audio_sample_rate,
output_compression=first_req.output_compression,
enable_frame_interpolation=first_req.enable_frame_interpolation,
frame_interpolation_exp=first_req.frame_interpolation_exp,
frame_interpolation_scale=first_req.frame_interpolation_scale,
frame_interpolation_model_path=first_req.frame_interpolation_model_path,
enable_upscaling=first_req.enable_upscaling,
upscaling_model_path=first_req.upscaling_model_path,
upscaling_scale=first_req.upscaling_scale,
)
@staticmethod
def _validate_group_forward_reqs(reqs: list[Req]) -> None:
"""Validate fields that the grouped output/save path treats as shared."""
first_req = reqs[0]
shared_output_fields = (
"save_output",
"return_frames",
"return_file_paths_only",
"data_type",
"fps",
"output_compression",
"enable_frame_interpolation",
"frame_interpolation_exp",
"frame_interpolation_scale",
"frame_interpolation_model_path",
"enable_upscaling",
"upscaling_model_path",
"upscaling_scale",
)
for req in reqs[1:]:
mismatched = [
field
for field in shared_output_fields
if getattr(req, field, None) != getattr(first_req, field, None)
]
if mismatched:
raise ValueError(
"Grouped execute_forward requires matching output settings; "
f"mismatched fields: {mismatched}"
)
@staticmethod
def _iter_output_metrics(output_batch: OutputBatch):
"""Return all metrics objects carried by an output batch."""
if output_batch.metrics_list is not None:
return [
metrics for metrics in output_batch.metrics_list if metrics is not None
]
if output_batch.metrics is not None:
return [output_batch.metrics]
return []
@staticmethod
def _to_output_batch(result: Req | OutputBatch) -> OutputBatch:
if isinstance(result, Req):
return GPUWorker._req_to_output_batch(result)
return result
@staticmethod
def _req_to_output_batch(result: Req) -> OutputBatch:
return OutputBatch(
output=result.output,
audio=getattr(result, "audio", None),
audio_sample_rate=getattr(result, "audio_sample_rate", None),
metrics=result.metrics,
trajectory_timesteps=getattr(result, "trajectory_timesteps", None),
trajectory_latents=getattr(result, "trajectory_latents", None),
rollout_trajectory_data=getattr(result, "rollout_trajectory_data", None),
noise_pred=getattr(result, "noise_pred", None),
trajectory_decoded=getattr(result, "trajectory_decoded", None),
)
@staticmethod
def _merge_expanded_output_batches(
output_batches: list[OutputBatch],
) -> OutputBatch:
"""Merge per-output batches produced by grouped execution."""
merged = OutputBatch()
parts = _ExpandedOutputParts()
for output_batch in output_batches:
GPUWorker._merge_expanded_singletons(merged, output_batch)
GPUWorker._collect_expanded_parts(parts, output_batch)
GPUWorker._finalize_expanded_parts(
merged,
parts,
audio_sample_rate=output_batches[0].audio_sample_rate,
)
return merged
@staticmethod
def _merge_expanded_singletons(
merged: OutputBatch, output_batch: OutputBatch
) -> None:
if output_batch.error is not None and merged.error is None:
merged.error = output_batch.error
merged.peak_memory_mb = max(merged.peak_memory_mb, output_batch.peak_memory_mb)
if (
merged.trajectory_timesteps is None
and output_batch.trajectory_timesteps is not None
):
merged.trajectory_timesteps = output_batch.trajectory_timesteps
if (
merged.rollout_trajectory_data is None
and output_batch.rollout_trajectory_data is not None
):
merged.rollout_trajectory_data = output_batch.rollout_trajectory_data
@staticmethod
def _collect_expanded_parts(
parts: _ExpandedOutputParts, output_batch: OutputBatch
) -> None:
"""Collect expanded outputs"""
parts.metrics_list.append(output_batch.metrics)
if output_batch.output_file_paths:
parts.output_file_paths.extend(output_batch.output_file_paths)
if isinstance(output_batch.output, torch.Tensor):
parts.tensor_outputs.append(output_batch.output)
elif output_batch.output is not None:
parts.list_outputs.extend(output_batch.output)
if isinstance(output_batch.audio, torch.Tensor):
parts.tensor_audio.append(output_batch.audio)
if isinstance(output_batch.trajectory_latents, torch.Tensor):
parts.trajectory_latents.append(output_batch.trajectory_latents)
if isinstance(output_batch.noise_pred, torch.Tensor):
parts.noise_preds.append(output_batch.noise_pred)
if output_batch.trajectory_decoded:
GPUWorker._collect_trajectory_decoded(
parts, output_batch.trajectory_decoded
)
@staticmethod
def _collect_trajectory_decoded(
parts: _ExpandedOutputParts, trajectory_decoded: list[torch.Tensor]
) -> None:
if parts.trajectory_decoded_parts is None:
parts.trajectory_decoded_parts = [[] for _ in trajectory_decoded]
for index, decoded in enumerate(trajectory_decoded):
parts.trajectory_decoded_parts[index].append(decoded)
@staticmethod
def _finalize_expanded_parts(
merged: OutputBatch,
parts: _ExpandedOutputParts,
*,
audio_sample_rate: int | None,
) -> None:
"""
merge batched output
"""
if parts.output_file_paths:
merged.output_file_paths = parts.output_file_paths
if any(metrics is not None for metrics in parts.metrics_list):
merged.metrics_list = parts.metrics_list
merged.metrics = next(
metrics for metrics in parts.metrics_list if metrics is not None
)
if parts.tensor_outputs:
merged.output = torch.cat(parts.tensor_outputs, dim=0)
elif parts.list_outputs:
merged.output = parts.list_outputs
if parts.tensor_audio:
merged.audio = torch.cat(parts.tensor_audio, dim=0)
merged.audio_sample_rate = audio_sample_rate
if parts.trajectory_latents:
merged.trajectory_latents = torch.cat(parts.trajectory_latents, dim=0)
if parts.noise_preds:
merged.noise_pred = torch.cat(parts.noise_preds, dim=0)
if parts.trajectory_decoded_parts:
merged.trajectory_decoded = [
torch.cat(decoded_step, dim=0)
for decoded_step in parts.trajectory_decoded_parts
]
def get_can_stay_resident_components(
self, remaining_gpu_mem_gb: float
) -> List[str]:
"""
Calculate which components can stay resident on GPU without being offloaded.
"""
can_stay_resident = []
if not self.pipeline:
return can_stay_resident
# Map memory_usage keys to server_args offload flags.
# If the flag is False, the component is already resident, so we do not suggest it.
# If the flag is True, it is currently offloaded, so it is a candidate to stay resident.
offload_flags = {
"transformer": self.server_args.dit_cpu_offload
or self.server_args.is_dit_layerwise_offload_selected,
"vae": self.server_args.vae_cpu_offload,
"text_encoder": self.server_args.text_encoder_cpu_offload,
"text_encoder_2": self.server_args.text_encoder_cpu_offload,
"image_encoder": self.server_args.image_encoder_cpu_offload,
}
for name in OFFLOAD_DISABLE_RECOMMENDATION_ORDER:
# Only consider components that are currently configured to be offloaded
is_offload_configured = offload_flags.get(name, False)
if not is_offload_configured:
continue
usage = self.pipeline.memory_usages.get(name)
if usage is None:
continue
if usage <= remaining_gpu_mem_gb:
can_stay_resident.append(name)
remaining_gpu_mem_gb -= usage
return can_stay_resident
def set_lora(
self,
lora_nickname: Union[str, List[str]],
lora_path: Union[str, None, List[Union[str, None]]] = None,
target: Union[str, List[str]] = "all",
strength: Union[float, List[float]] = 1.0,
merge_mode: str | None = None,
) -> OutputBatch:
"""
Set the LoRA adapter(s) for the pipeline.
Supports both single LoRA (backward compatible) and multiple LoRA adapters.
Args:
lora_nickname: The nickname(s) of the adapter(s). Can be a string or a list of strings.
lora_path: Path(s) to the LoRA adapter(s). Can be a string, None, or a list of strings/None.
target: Which transformer(s) to apply the LoRA to. Can be a string or a list of strings.
strength: LoRA strength(s) for merge, default 1.0. Can be a float or a list of floats.
merge_mode: Optional per-request LoRA merge mode.
"""
if not isinstance(self.pipeline, LoRAPipeline):
return OutputBatch(error="Lora is not enabled")
self.pipeline.set_lora(
lora_nickname, lora_path, target, strength, merge_mode=merge_mode
)
return OutputBatch()
def merge_lora_weights(
self, target: str = "all", strength: float = 1.0
) -> OutputBatch:
"""
Merge LoRA weights.
Args:
target: Which transformer(s) to merge.
strength: LoRA strength for merge, default 1.0.
"""
if not isinstance(self.pipeline, LoRAPipeline):
return OutputBatch(error="Lora is not enabled")
self.pipeline.merge_lora_weights(target, strength)
return OutputBatch()
def unmerge_lora_weights(self, target: str = "all") -> OutputBatch:
"""
Unmerge LoRA weights.
Args:
target: Which transformer(s) to unmerge.
"""
if not isinstance(self.pipeline, LoRAPipeline):
return OutputBatch(error="Lora is not enabled")
self.pipeline.unmerge_lora_weights(target)
return OutputBatch()
def list_loras(self) -> OutputBatch:
"""
List loaded LoRA adapters and current application status per module.
"""
from sglang.multimodal_gen.runtime.pipelines_core.lora_pipeline import (
LoRAPipeline,
)
if not isinstance(self.pipeline, LoRAPipeline):
return OutputBatch(error="Lora is not enabled")
status = self.pipeline.get_lora_status()
return OutputBatch(output=status)
def release_memory_occupation(self) -> dict:
return self._get_memory_occupation().release_memory_occupation()
def resume_memory_occupation(self) -> dict:
if self.memory_occupation is None:
return {
"success": True,
"sleeping": False,
"message": "already awake",
}
return self.memory_occupation.resume_memory_occupation()
OOM_MSG = """
OOM detected. Possible solutions:
- If the OOM occurs during loading:
1. Check available memory on every selected GPU, not only total capacity.
In multi-GPU runs, the least-free selected GPU is the bottleneck.
2. For single-GPU deployment, use `--performance-mode memory`, component CPU offload,
or `--dit-layerwise-offload` for supported Wan/MOVA DiTs.
3. For multi-GPU deployment, keep the default `--performance-mode auto` or set
`--use-fsdp-inference true` to shard DiT weights with FSDP. FSDP is not a
single-GPU substitute for CPU offload.
- If the OOM occurs during runtime:
1. Reduce resolution, `--num-frames`, or batch size.
2. Use `--performance-mode memory` for lower memory usage.
3. Enable SP/Ulysses/Ring for sequence-heavy workloads in multi-GPU setups.
4. Use FSDP, with CFG parallelism when supported, for validated multi-GPU workloads.
5. Use a lower-memory attention backend or quantization when available.
Or, open an issue on GitHub https://github.com/sgl-project/sglang/issues/new/choose
"""
def _oom_exceptions():
# torch.OutOfMemoryError exists only in some PyTorch builds
types = [torch.cuda.OutOfMemoryError]
if hasattr(torch, "OutOfMemoryError"):
types.append(torch.OutOfMemoryError)
return tuple(types)
def run_scheduler_process(
local_rank: int,
rank: int,
master_port: int,
server_args: ServerArgs,
pipe_writer: mp.connection.Connection,
# For all workers: pipe to receive tasks from rank 0
task_pipe_r: mp.connection.Connection,
# For slave workers: pipe to send results back to rank 0
result_pipe_w: mp.connection.Connection | None,
# For rank 0 worker only: pipes to send tasks to slaves
task_pipes_to_slaves: list[mp.connection.Connection] | None = None,
# For rank 0 worker only: pipes to receive results from slaves
result_pipes_from_slaves: list[mp.connection.Connection] | None = None,
) -> None:
"""
The entry point for the worker process.
Rank 0 acts as the master, handling ZMQ requests and coordinating slaves.
Ranks > 0 act as slaves, waiting for tasks from the master.
"""
kill_itself_when_parent_died()
configure_logger(server_args)
globally_suppress_loggers()
if current_platform.is_cuda():
set_cuda_arch()
elif current_platform.is_musa():
set_musa_arch()
init_diffusion_tracing(server_args, f"DiffWorker_rank{rank}")
port_args = PortArgs.from_server_args(server_args)
# start the scheduler event loop
assert task_pipes_to_slaves is not None
assert result_pipes_from_slaves is not None
from sglang.multimodal_gen.runtime.managers.scheduler import Scheduler
try:
scheduler = Scheduler(
server_args,
gpu_id=rank,
port_args=port_args,
task_pipes_to_slaves=task_pipes_to_slaves,
result_pipes_from_slaves=result_pipes_from_slaves,
local_rank=local_rank,
)
logger.info(f"Worker {rank}: Scheduler loop started.")
pipe_writer.send(
{
"status": "ready",
}
)
scheduler.event_loop()
except _oom_exceptions() as _e:
logger.warning(OOM_MSG)
raise
finally:
# Clean up resources to speed up shutdown
if "scheduler" in locals():
del scheduler
gc.collect()
if torch.cuda.is_initialized():
torch.cuda.empty_cache()
if torch.distributed.is_available() and torch.distributed.is_initialized():
torch.distributed.destroy_process_group()
logger.info(f"Worker {rank}: Shutdown complete.")