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
453 lines
16 KiB
Python
453 lines
16 KiB
Python
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
|
|
import asyncio
|
|
import inspect
|
|
import json
|
|
import os
|
|
import shutil
|
|
import tempfile
|
|
import time
|
|
from contextlib import contextmanager
|
|
from typing import Any, Generator, List, Optional, Union
|
|
|
|
import httpx
|
|
from fastapi import HTTPException, UploadFile
|
|
|
|
from sglang.multimodal_gen.configs.sample.sampling_params import (
|
|
DataType,
|
|
SamplingParams,
|
|
)
|
|
from sglang.multimodal_gen.runtime.entrypoints.utils import (
|
|
ListLorasReq,
|
|
MergeLoraWeightsReq,
|
|
SetLoraReq,
|
|
ShutdownReq,
|
|
UnmergeLoraWeightsReq,
|
|
format_lora_message,
|
|
save_outputs,
|
|
)
|
|
from sglang.multimodal_gen.runtime.pipelines_core.schedule_batch import OutputBatch
|
|
from sglang.multimodal_gen.runtime.scheduler_client import AsyncSchedulerClient
|
|
from sglang.multimodal_gen.runtime.server_args import get_global_server_args
|
|
from sglang.multimodal_gen.runtime.utils.common import parse_size
|
|
from sglang.multimodal_gen.runtime.utils.image_io import save_base64_image_to_path
|
|
from sglang.multimodal_gen.runtime.utils.logging_utils import (
|
|
init_logger,
|
|
log_batch_completion,
|
|
log_generation_timer,
|
|
)
|
|
from sglang.multimodal_gen.runtime.utils.trace_wrapper import trace_req
|
|
|
|
# re-export LoRA protocol types for backward compatibility
|
|
__all__ = [
|
|
"SetLoraReq",
|
|
"MergeLoraWeightsReq",
|
|
"UnmergeLoraWeightsReq",
|
|
"ListLorasReq",
|
|
"ShutdownReq",
|
|
"format_lora_message",
|
|
]
|
|
|
|
logger = init_logger(__name__)
|
|
|
|
OUTPUT_QUALITY_MAPPER = {"maximum": 100, "high": 90, "medium": 55, "low": 35}
|
|
DEFAULT_FPS = 24
|
|
DEFAULT_VIDEO_SECONDS = 4
|
|
|
|
|
|
def _bad_request(message: str) -> HTTPException:
|
|
return HTTPException(status_code=400, detail=message)
|
|
|
|
|
|
def _parse_size_or_raise(size: str) -> tuple[int, int]:
|
|
width, height = parse_size(size)
|
|
if width is None or height is None or width <= 0 or height <= 0:
|
|
raise _bad_request("size must be formatted as positive WIDTHxHEIGHT")
|
|
return width, height
|
|
|
|
|
|
def _validate_positive_int(kwargs: dict[str, Any], name: str) -> None:
|
|
value = kwargs.get(name)
|
|
if value is not None and int(value) <= 0:
|
|
raise _bad_request(f"{name} must be positive")
|
|
|
|
|
|
def flatten_extra_params(payload: Any) -> dict[str, Any]:
|
|
"""Promote vLLM-Omni-style extra_params into regular request fields."""
|
|
if not isinstance(payload, dict):
|
|
return {}
|
|
|
|
extra_params = payload.pop("extra_params", None)
|
|
if isinstance(extra_params, str):
|
|
try:
|
|
extra_params = json.loads(extra_params)
|
|
except Exception:
|
|
extra_params = None
|
|
if not isinstance(extra_params, dict):
|
|
if "guardrails" in payload:
|
|
payload.setdefault("use_guardrails", payload["guardrails"])
|
|
return payload
|
|
|
|
for key, value in extra_params.items():
|
|
payload.setdefault(key, value)
|
|
if "guardrails" in extra_params:
|
|
payload.setdefault("use_guardrails", extra_params["guardrails"])
|
|
|
|
return payload
|
|
|
|
|
|
@contextmanager
|
|
def temp_dir_if_disabled(
|
|
configured_path: str | None,
|
|
) -> Generator[str, None, None]:
|
|
"""Yield *configured_path* when it is set, otherwise create a temporary
|
|
directory that is automatically removed when the context exits."""
|
|
if configured_path is not None:
|
|
os.makedirs(configured_path, exist_ok=True)
|
|
yield configured_path
|
|
else:
|
|
tmp = tempfile.mkdtemp(prefix="sglang_")
|
|
try:
|
|
yield tmp
|
|
finally:
|
|
shutil.rmtree(tmp, ignore_errors=True)
|
|
|
|
|
|
def choose_output_image_ext(
|
|
output_format: Optional[str], background: Optional[str]
|
|
) -> str:
|
|
fmt = (output_format or "").lower()
|
|
if fmt in {"png", "webp", "jpeg", "jpg"}:
|
|
return "jpg" if fmt == "jpeg" else fmt
|
|
if (background or "auto").lower() == "transparent":
|
|
return "png"
|
|
return "jpg"
|
|
|
|
|
|
def build_sampling_params(request_id: str, **kwargs) -> SamplingParams:
|
|
"""Build SamplingParams from request parameters.
|
|
|
|
Handles size parsing, output_quality resolution, and None filtering before
|
|
delegating to SamplingParams.from_user_sampling_params_args. Callers pass
|
|
only the parameters they have; None values are stripped automatically so
|
|
that SamplingParams defaults apply.
|
|
"""
|
|
server_args = get_global_server_args()
|
|
|
|
# pop HTTP-layer params that aren't SamplingParams fields
|
|
output_quality = kwargs.pop("output_quality", None)
|
|
|
|
has_explicit_compression = kwargs.get("output_compression") is not None
|
|
|
|
# parse "WxH" size string if provided
|
|
size = kwargs.pop("size", None)
|
|
if size:
|
|
w, h = _parse_size_or_raise(size)
|
|
# treat None dimensions as unset so parsed size can fill them
|
|
if kwargs.get("width") is None:
|
|
kwargs["width"] = w
|
|
if kwargs.get("height") is None:
|
|
kwargs["height"] = h
|
|
|
|
for name in (
|
|
"width",
|
|
"height",
|
|
"num_frames",
|
|
"num_inference_steps",
|
|
"num_outputs_per_prompt",
|
|
):
|
|
_validate_positive_int(kwargs, name)
|
|
|
|
# filter out None values to let SamplingParams defaults apply
|
|
kwargs = {k: v for k, v in kwargs.items() if v is not None}
|
|
kwargs.setdefault("save_output", True)
|
|
|
|
sampling_params = SamplingParams.from_user_sampling_params_args(
|
|
model_path=server_args.model_path,
|
|
server_args=server_args,
|
|
request_id=request_id,
|
|
**kwargs,
|
|
)
|
|
|
|
# resolve output_quality → output_compression with the correct data_type.
|
|
# SamplingParams.__post_init__ may have resolved with the wrong data_type
|
|
# (default VIDEO) before _adjust() set the correct one.
|
|
if not has_explicit_compression and output_quality is not None:
|
|
resolved = adjust_output_quality(output_quality, sampling_params.data_type)
|
|
if resolved is not None:
|
|
sampling_params.output_compression = resolved
|
|
|
|
return sampling_params
|
|
|
|
|
|
async def save_image_to_path(
|
|
image: Union[UploadFile, bytes, str],
|
|
target_path: str,
|
|
*,
|
|
prefer_remote_source: bool = False,
|
|
) -> str:
|
|
input_path = await _maybe_url_image(
|
|
image, target_path, prefer_remote_source=prefer_remote_source
|
|
)
|
|
if input_path is None:
|
|
input_path = await _save_upload_to_path(image, target_path)
|
|
return input_path
|
|
|
|
|
|
# Helpers
|
|
async def _save_upload_to_path(
|
|
upload: Union[UploadFile, bytes], target_path: str
|
|
) -> str:
|
|
os.makedirs(os.path.dirname(target_path), exist_ok=True)
|
|
if isinstance(upload, bytes):
|
|
content = upload
|
|
elif isinstance(upload, (bytearray, memoryview)):
|
|
content = bytes(upload)
|
|
else:
|
|
read = getattr(upload, "read", None)
|
|
if not callable(read):
|
|
raise TypeError(f"Unsupported image upload type: {type(upload).__name__}")
|
|
content = read()
|
|
if inspect.isawaitable(content):
|
|
content = await content
|
|
if isinstance(content, (bytearray, memoryview)):
|
|
content = bytes(content)
|
|
if not isinstance(content, bytes):
|
|
raise TypeError(
|
|
f"Image upload read() returned {type(content).__name__}, expected bytes"
|
|
)
|
|
with open(target_path, "wb") as f:
|
|
f.write(content)
|
|
return target_path
|
|
|
|
|
|
async def _maybe_url_image(
|
|
img_url: str,
|
|
target_path: str,
|
|
*,
|
|
prefer_remote_source: bool = False,
|
|
) -> str | None:
|
|
if not isinstance(img_url, str):
|
|
return None
|
|
|
|
if img_url.lower().startswith(("http://", "https://")):
|
|
# Only bypass persistence when the caller explicitly disables input saves.
|
|
# Otherwise keep the prefetch outside the measured server stages.
|
|
if prefer_remote_source:
|
|
return img_url
|
|
# download image from URL and persist on disk
|
|
input_path = await _save_url_image_to_path(img_url, target_path)
|
|
return input_path
|
|
elif img_url.startswith("data:image"):
|
|
if prefer_remote_source:
|
|
return img_url
|
|
# encode image base64 url and persist on disk
|
|
input_path = save_base64_image_to_path(img_url, target_path)
|
|
return input_path
|
|
else:
|
|
raise ValueError("Unsupported image url format")
|
|
|
|
|
|
async def _save_url_image_to_path(image_url: str, target_path: str) -> str:
|
|
"""Download image from URL and save to target path."""
|
|
|
|
def _is_retryable_download_error(error: Exception) -> bool:
|
|
if isinstance(error, httpx.HTTPStatusError):
|
|
status_code = error.response.status_code
|
|
# Retry on rate limit and transient server-side failures.
|
|
return status_code == 429 or 500 <= status_code < 600
|
|
# Retry on transient network/protocol issues.
|
|
return isinstance(
|
|
error,
|
|
(
|
|
httpx.TimeoutException,
|
|
httpx.NetworkError,
|
|
httpx.RemoteProtocolError,
|
|
),
|
|
)
|
|
|
|
os.makedirs(os.path.dirname(target_path), exist_ok=True)
|
|
|
|
max_attempts = 3
|
|
backoff_seconds = 0.2
|
|
last_error: Exception | None = None
|
|
|
|
try:
|
|
async with httpx.AsyncClient(follow_redirects=True) as client:
|
|
for attempt in range(1, max_attempts + 1):
|
|
try:
|
|
response = await client.get(image_url, timeout=10.0)
|
|
response.raise_for_status()
|
|
|
|
# Determine file extension from content type or URL after downloading
|
|
if not os.path.splitext(target_path)[1]:
|
|
content_type = response.headers.get("content-type", "").lower()
|
|
|
|
url_path = image_url.split("?")[0]
|
|
_, url_ext = os.path.splitext(url_path)
|
|
url_ext = url_ext.lower()
|
|
|
|
if url_ext in {
|
|
".jpg",
|
|
".jpeg",
|
|
".png",
|
|
".webp",
|
|
".gif",
|
|
".bmp",
|
|
}:
|
|
ext = ".jpg" if url_ext == ".jpeg" else url_ext
|
|
elif content_type.startswith("image/"):
|
|
if "jpeg" in content_type or "jpg" in content_type:
|
|
ext = ".jpg"
|
|
elif "png" in content_type:
|
|
ext = ".png"
|
|
elif "webp" in content_type:
|
|
ext = ".webp"
|
|
else:
|
|
ext = ".jpg" # Default to jpg
|
|
elif content_type == "application/octet-stream":
|
|
# for octet-stream, if we couldn't get it from URL, default to jpg
|
|
ext = ".jpg"
|
|
else:
|
|
raise ValueError(
|
|
f"URL does not point to an image. Content-Type: {content_type}"
|
|
)
|
|
target_path = f"{target_path}{ext}"
|
|
|
|
with open(target_path, "wb") as f:
|
|
f.write(response.content)
|
|
|
|
return target_path
|
|
except Exception as e:
|
|
last_error = e
|
|
if attempt == max_attempts or not _is_retryable_download_error(e):
|
|
raise
|
|
wait_s = backoff_seconds * (2 ** (attempt - 1))
|
|
logger.warning(
|
|
"Retrying image download (%s/%s) for %s after %.1fs due to: %s",
|
|
attempt,
|
|
max_attempts,
|
|
image_url,
|
|
wait_s,
|
|
e,
|
|
)
|
|
await asyncio.sleep(wait_s)
|
|
except Exception as e:
|
|
final_error = last_error or e
|
|
raise Exception(
|
|
f"Failed to download image from URL {image_url}: {str(final_error)}"
|
|
)
|
|
|
|
|
|
async def process_generation_batch(
|
|
scheduler_client: AsyncSchedulerClient,
|
|
batch,
|
|
) -> tuple[list[str], OutputBatch]:
|
|
total_start_time = time.perf_counter()
|
|
with trace_req(batch.trace_ctx), log_generation_timer(logger, batch.prompt):
|
|
result = await scheduler_client.forward([batch])
|
|
|
|
if (
|
|
result.output is None
|
|
and result.output_file_paths is None
|
|
and result.raw_frame_batches is None
|
|
):
|
|
error_msg = result.error or "Unknown error"
|
|
raise RuntimeError(
|
|
f"Model generation returned no output. Error from scheduler: {error_msg}"
|
|
)
|
|
|
|
save_file_path_list = []
|
|
if result.output_file_paths:
|
|
save_file_path_list = result.output_file_paths
|
|
elif result.output is not None:
|
|
num_outputs = len(result.output)
|
|
save_file_path_list = save_outputs(
|
|
result.output,
|
|
batch.data_type,
|
|
batch.fps,
|
|
batch.save_output,
|
|
lambda idx: str(batch.output_file_path(num_outputs, idx)),
|
|
audio=result.audio,
|
|
audio_sample_rate=result.audio_sample_rate,
|
|
output_compression=batch.output_compression,
|
|
enable_frame_interpolation=batch.enable_frame_interpolation,
|
|
frame_interpolation_exp=batch.frame_interpolation_exp,
|
|
frame_interpolation_scale=batch.frame_interpolation_scale,
|
|
frame_interpolation_model_path=batch.frame_interpolation_model_path,
|
|
enable_upscaling=batch.enable_upscaling,
|
|
upscaling_model_path=batch.upscaling_model_path,
|
|
upscaling_scale=batch.upscaling_scale,
|
|
)
|
|
|
|
total_time = time.perf_counter() - total_start_time
|
|
if get_global_server_args().batching_max_size > 1:
|
|
log_batch_completion(
|
|
logger,
|
|
len(save_file_path_list),
|
|
total_time,
|
|
)
|
|
|
|
if result.peak_memory_mb and result.peak_memory_mb > 0:
|
|
logger.info(f"Peak memory usage: {result.peak_memory_mb:.2f} MB")
|
|
|
|
return save_file_path_list, result
|
|
|
|
|
|
def merge_image_input_list(*inputs: Union[List, Any, None]) -> List:
|
|
"""
|
|
Merge multiple image input sources into a single list.
|
|
|
|
This function handles both single items and lists of items, merging them
|
|
into a single flattened list. Useful for processing images, URLs, or other
|
|
multimedia inputs that can come as either single items or lists.
|
|
|
|
Args:
|
|
*inputs: Variable number of inputs, each can be None, single item, or list
|
|
|
|
Returns:
|
|
List: Flattened list of all non-None inputs
|
|
|
|
Example:
|
|
>>> merge_image_input_list(["img1", "img2"], "img3", None)
|
|
["img1", "img2", "img3"]
|
|
"""
|
|
result = []
|
|
for input_item in inputs:
|
|
if input_item is not None:
|
|
if isinstance(input_item, list):
|
|
result.extend(input_item)
|
|
else:
|
|
result.append(input_item)
|
|
return result
|
|
|
|
|
|
def add_common_data_to_response(
|
|
response: dict, request_id: str, result: OutputBatch
|
|
) -> dict:
|
|
if result.peak_memory_mb and result.peak_memory_mb > 0:
|
|
response["peak_memory_mb"] = result.peak_memory_mb
|
|
|
|
if result.metrics and result.metrics.total_duration_s > 0:
|
|
response["inference_time_s"] = result.metrics.total_duration_s
|
|
|
|
response["id"] = request_id
|
|
|
|
if result.action_pred is not None:
|
|
t = result.action_pred
|
|
response["action"] = {
|
|
"data": t.tolist(),
|
|
"shape": list(t.shape),
|
|
"dtype": str(t.dtype).replace("torch.", ""),
|
|
"raw_action_dim": result.action_raw_action_dim,
|
|
"action_mode": result.action_mode,
|
|
"domain_id": result.action_domain_id,
|
|
}
|
|
|
|
return response
|
|
|
|
|
|
def adjust_output_quality(output_quality: str, data_type: DataType = None) -> int:
|
|
if output_quality == "default":
|
|
return 50 if data_type == DataType.VIDEO else 75
|
|
return OUTPUT_QUALITY_MAPPER.get(output_quality, None)
|