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

843 lines
30 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Benchmark online serving for diffusion models (Image/Video Generation).
Usage:
# launch a server and benchmark on it
# T2V or T2I or any other multimodal generation model
sglang serve --model-path Wan-AI/Wan2.2-T2V-A14B-Diffusers --num-gpus 1 --port 1231
# benchmark it and make sure the port is the same as the server's port
python3 -m sglang.multimodal_gen.benchmarks.bench_serving --dataset vbench --num-prompts 20 --port 1231
# benchmark with SLO metrics enabled
python3 -m sglang.multimodal_gen.benchmarks.bench_serving --dataset vbench --num-prompts 20 --port 1231 --slo --slo-scale 3.0 --warmup-requests 2
"""
import argparse
import asyncio
import json
import os
import time
from dataclasses import replace
from typing import Any, Dict, List, Optional
import aiohttp
import numpy as np
import requests
from tqdm.asyncio import tqdm
from sglang.multimodal_gen.benchmarks.datasets import (
RandomDataset,
RequestFuncInput,
RequestFuncOutput,
VBenchDataset,
)
from sglang.multimodal_gen.runtime.utils.logging_utils import (
configure_logger,
init_logger,
)
from sglang.multimodal_gen.test.test_utils import print_divider, print_value_formatted
from sglang.srt.utils.network import NetworkAddress
logger = init_logger(__name__)
# Patch size used for computing area units (e.g. in latent diffusion models).
PATCH_SIZE = 16
PATCH_AREA = PATCH_SIZE * PATCH_SIZE
def _get_response_output_count(resp_json: Dict[str, Any]) -> int:
if isinstance(resp_json.get("num_outputs"), int):
return resp_json["num_outputs"]
if isinstance(resp_json.get("data"), list):
return len(resp_json["data"])
if isinstance(resp_json.get("file_paths"), list):
return len(resp_json["file_paths"])
if isinstance(resp_json.get("urls"), list):
return len(resp_json["urls"])
if resp_json.get("file_path") or resp_json.get("url"):
return 1
return 0
def _compute_scale_factor(req: RequestFuncInput, args) -> Optional[float]:
"""Computes the composite scale factor (area × frames × steps) for a request."""
width = req.width or args.width
height = req.height or args.height
if None in (width, height):
return None
frames = req.num_frames or args.num_frames
steps = req.num_inference_steps or args.num_inference_steps
frame_scale = frames if isinstance(frames, int) and frames > 0 else 1
step_scale = steps if isinstance(steps, int) and steps > 0 else 1
area_units = max((float(width) * float(height)) / float(PATCH_AREA), 1.0)
return area_units * float(frame_scale) * float(step_scale)
def _compute_expected_latency_ms_from_base(
req: RequestFuncInput, args, base_time_ms: Optional[float]
) -> Optional[float]:
"""Scales latency linearly by pixel area, frame count, and inference steps."""
if base_time_ms is None:
return None
scale = _compute_scale_factor(req, args)
if scale is None:
return None
return float(base_time_ms) * scale
def _infer_slo_base_time_ms_from_warmups(
warmup_pairs: List[tuple], args
) -> Optional[float]:
"""Derives median base latency from successful warmup runs."""
candidates_ms: List[float] = []
for req, out in warmup_pairs:
if not out.success or out.latency <= 0:
logger.warning(
f"Skipping warmup result: success={out.success}, latency={out.latency:.3f}"
)
continue
scale = _compute_scale_factor(req, args)
if scale is None or scale <= 0:
continue
candidates_ms.append((out.latency * 1000.0) / scale)
return float(np.median(candidates_ms)) if candidates_ms else None
def _populate_slo_ms_from_warmups(
requests_list: List[RequestFuncInput], warmup_pairs: List[tuple], args
) -> List[RequestFuncInput]:
"""Assigns estimated SLO targets to requests lacking them."""
if not any(req.slo_ms is None for req in requests_list):
return requests_list
base_time_ms = _infer_slo_base_time_ms_from_warmups(warmup_pairs, args)
if base_time_ms is None:
return requests_list
slo_scale = float(getattr(args, "slo_scale", 3.0))
if slo_scale <= 0:
raise ValueError(f"slo_scale must be positive, got {slo_scale}.")
updated: List[RequestFuncInput] = []
for req in requests_list:
if req.slo_ms is not None:
updated.append(req)
continue
expected_ms = _compute_expected_latency_ms_from_base(req, args, base_time_ms)
if expected_ms is not None:
# Create a new RequestFuncInput with updated slo_ms
updated.append(replace(req, slo_ms=expected_ms * slo_scale))
else:
updated.append(req)
return updated
async def async_request_image_sglang(
input: RequestFuncInput,
session: aiohttp.ClientSession,
pbar: Optional[tqdm] = None,
) -> RequestFuncOutput:
output = RequestFuncOutput()
output.start_time = time.perf_counter()
# Check if we need to use multipart (for image edits with input images)
if input.image_paths and len(input.image_paths) > 0:
# Use multipart/form-data for image edits
data = aiohttp.FormData()
data.add_field("model", input.model)
data.add_field("prompt", input.prompt)
data.add_field("response_format", "b64_json")
data.add_field("n", str(input.num_outputs_per_prompt))
if input.width and input.height:
data.add_field("size", f"{input.width}x{input.height}")
# Merge extra parameters
for key, value in input.extra_body.items():
data.add_field(key, str(value))
# Add image file(s)
for idx, img_path in enumerate(input.image_paths):
if os.path.exists(img_path):
data.add_field(
"image",
open(img_path, "rb"),
filename=os.path.basename(img_path),
content_type="application/octet-stream",
)
else:
output.error = f"Image file not found: {img_path}"
output.success = False
if pbar:
pbar.update(1)
return output
try:
async with session.post(input.api_url, data=data) as response:
if response.status == 200:
resp_json = await response.json()
output.response_body = resp_json
output.success = True
output.output_count = _get_response_output_count(resp_json)
if "peak_memory_mb" in resp_json:
output.peak_memory_mb = resp_json["peak_memory_mb"]
else:
output.error = f"HTTP {response.status}: {await response.text()}"
output.success = False
except Exception as e:
output.error = str(e)
output.success = False
else:
# Use JSON for text-to-image generation
payload = {
"model": input.model,
"prompt": input.prompt,
"n": input.num_outputs_per_prompt,
"response_format": "b64_json",
}
if input.width and input.height:
payload["size"] = f"{input.width}x{input.height}"
if input.num_inference_steps:
payload["num_inference_steps"] = input.num_inference_steps
# Merge extra parameters
payload.update(input.extra_body)
try:
async with session.post(input.api_url, json=payload) as response:
if response.status == 200:
resp_json = await response.json()
output.response_body = resp_json
output.success = True
output.output_count = _get_response_output_count(resp_json)
if "peak_memory_mb" in resp_json:
output.peak_memory_mb = resp_json["peak_memory_mb"]
else:
output.error = f"HTTP {response.status}: {await response.text()}"
output.success = False
except Exception as e:
output.error = str(e)
output.success = False
output.latency = time.perf_counter() - output.start_time
# Check SLO if defined
if input.slo_ms is not None and output.success:
output.slo_achieved = (output.latency * 1000.0) <= input.slo_ms
if pbar:
pbar.update(1)
return output
async def async_request_video_sglang(
input: RequestFuncInput,
session: aiohttp.ClientSession,
pbar: Optional[tqdm] = None,
) -> RequestFuncOutput:
output = RequestFuncOutput()
output.start_time = time.perf_counter()
# 1. Submit Job
job_id = None
# Check if we need to upload images (Multipart) or just send JSON
if input.image_paths and len(input.image_paths) > 0:
# Use multipart/form-data
data = aiohttp.FormData()
data.add_field("model", input.model)
data.add_field("prompt", input.prompt)
data.add_field("num_outputs_per_prompt", str(input.num_outputs_per_prompt))
if input.width and input.height:
data.add_field("size", f"{input.width}x{input.height}")
# Add extra body fields to form data if possible, or assume simple key-values
# Note: Nested dicts in extra_body might need JSON serialization if API expects it stringified
if input.extra_body:
data.add_field("extra_body", json.dumps(input.extra_body))
# Explicitly add fps/num_frames if they are not in extra_body (bench_serving logic overrides)
if input.num_frames:
data.add_field("num_frames", str(input.num_frames))
if input.fps:
data.add_field("fps", str(input.fps))
# Add image file
# Currently only support single image upload as 'input_reference' per API spec
img_path = input.image_paths[0]
if os.path.exists(img_path):
data.add_field(
"input_reference",
open(img_path, "rb"),
filename=os.path.basename(img_path),
content_type="application/octet-stream",
)
else:
output.error = f"Image file not found: {img_path}"
output.success = False
if pbar:
pbar.update(1)
return output
try:
async with session.post(input.api_url, data=data) as response:
if response.status == 200:
resp_json = await response.json()
job_id = resp_json.get("id")
else:
output.error = (
f"Submit failed HTTP {response.status}: {await response.text()}"
)
output.success = False
if pbar:
pbar.update(1)
return output
except Exception as e:
output.error = f"Submit exception: {str(e)}"
output.success = False
if pbar:
pbar.update(1)
return output
else:
# Use JSON
payload: Dict[str, Any] = {
"model": input.model,
"prompt": input.prompt,
"num_outputs_per_prompt": input.num_outputs_per_prompt,
}
if input.width and input.height:
payload["size"] = f"{input.width}x{input.height}"
if input.num_frames:
payload["num_frames"] = input.num_frames
if input.num_inference_steps:
payload["num_inference_steps"] = input.num_inference_steps
if input.fps:
payload["fps"] = input.fps
payload.update(input.extra_body)
try:
async with session.post(input.api_url, json=payload) as response:
if response.status == 200:
resp_json = await response.json()
job_id = resp_json.get("id")
else:
output.error = (
f"Submit failed HTTP {response.status}: {await response.text()}"
)
output.success = False
if pbar:
pbar.update(1)
return output
except Exception as e:
output.error = f"Submit exception: {str(e)}"
output.success = False
if pbar:
pbar.update(1)
return output
if not job_id:
output.error = "No job_id returned"
output.success = False
if pbar:
pbar.update(1)
return output
# 2. Poll for completion
# Assuming the API returns a 'status' field.
# We construct the check URL. Assuming api_url is like .../v1/videos
# The check url should be .../v1/videos/{id}
check_url = f"{input.api_url}/{job_id}"
while True:
try:
async with session.get(check_url) as response:
if response.status == 200:
status_data = await response.json()
status = status_data.get("status")
if status == "completed":
output.success = True
output.response_body = status_data
output.output_count = _get_response_output_count(status_data)
if "peak_memory_mb" in status_data:
output.peak_memory_mb = status_data["peak_memory_mb"]
break
elif status == "failed":
output.success = False
output.error = f"Job failed: {status_data.get('error')}"
break
else:
# queued or processing
await asyncio.sleep(1.0)
else:
output.success = False
output.error = (
f"Poll failed HTTP {response.status}: {await response.text()}"
)
break
except Exception as e:
output.success = False
output.error = f"Poll exception: {str(e)}"
break
output.latency = time.perf_counter() - output.start_time
# Check SLO if defined
if input.slo_ms is not None and output.success:
output.slo_achieved = (output.latency * 1000.0) <= input.slo_ms
if pbar:
pbar.update(1)
return output
def calculate_metrics(
outputs: List[RequestFuncOutput],
total_duration: float,
requests_list: List[RequestFuncInput],
args,
slo_enabled: bool,
):
success_outputs = [o for o in outputs if o.success]
error_outputs = [o for o in outputs if not o.success]
num_success = len(success_outputs)
latencies = [o.latency for o in success_outputs]
completed_outputs = sum(o.output_count for o in success_outputs)
peak_memories = [
o.peak_memory_mb
for o in success_outputs
if o.peak_memory_mb is not None and o.peak_memory_mb > 0
]
metrics = {
"duration": total_duration,
"completed_requests": num_success,
"completed_outputs": completed_outputs,
"failed_requests": len(error_outputs),
"throughput_qps": num_success / total_duration if total_duration > 0 else 0,
"output_throughput_ops": (
completed_outputs / total_duration if total_duration > 0 else 0
),
"latency_mean": np.mean(latencies) if latencies else 0,
"latency_median": np.median(latencies) if latencies else 0,
"latency_p50": np.percentile(latencies, 50) if latencies else 0,
"latency_p90": np.percentile(latencies, 90) if latencies else 0,
"latency_p95": np.percentile(latencies, 95) if latencies else 0,
"latency_p99": np.percentile(latencies, 99) if latencies else 0,
"num_outputs_per_prompt": args.num_outputs_per_prompt,
"peak_memory_mb_max": max(peak_memories) if peak_memories else 0,
"peak_memory_mb_mean": np.mean(peak_memories) if peak_memories else 0,
"peak_memory_mb_median": np.median(peak_memories) if peak_memories else 0,
}
if slo_enabled:
slo_defined_total = 0
slo_met_success = 0
for req, out in zip(requests_list, outputs):
if req.slo_ms is None:
continue
slo_defined_total += 1
if out.slo_achieved:
slo_met_success += 1
slo_attain_all = (
(slo_met_success / slo_defined_total) if slo_defined_total > 0 else 0.0
)
metrics.update(
{
"slo_attainment_rate": slo_attain_all,
"slo_met_success": slo_met_success,
"slo_scale": getattr(args, "slo_scale", 3.0),
}
)
return metrics
def wait_for_service(base_url: str, timeout: int = 1200) -> None:
logger.info(f"Waiting for service at {base_url}...")
start_time = time.time()
while True:
try:
# Try /health endpoint first
resp = requests.get(f"{base_url}/health", timeout=1)
if resp.status_code == 200:
logger.info("Service is ready.")
break
except requests.exceptions.RequestException:
pass
if time.time() - start_time > timeout:
raise TimeoutError(
f"Service at {base_url} did not start within {timeout} seconds."
)
time.sleep(1)
async def benchmark(args):
from huggingface_hub import model_info
# Construct base_url if not provided
if args.base_url is None:
args.base_url = NetworkAddress(args.host, args.port).to_url()
# Wait for service
wait_for_service(args.base_url)
# Fetch model info
try:
resp = requests.get(f"{args.base_url}/v1/model_info", timeout=5)
if resp.status_code == 200:
info = resp.json()
if "model_path" in info and info["model_path"]:
args.model = info["model_path"]
logger.info(f"Updated model name from server: {args.model}")
except Exception as e:
logger.info(f"Failed to fetch model info: {e}. Using default: {args.model}")
valid_tasks = (
"text-to-video",
"image-to-video",
"video-to-video",
"text-to-image",
"image-to-image",
)
# Resolve task_name with priority: args.task > local config > HF pipeline_tag
if args.task:
task_name = args.task
logger.info(f"Using task from --task: {task_name}")
elif os.path.exists(args.model):
config_path = os.path.join(args.model, "config.json")
if os.path.exists(config_path):
with open(config_path, "r") as f:
config = json.load(f)
task_name = config.get("pipeline_tag", "text-to-image")
logger.info(f"Inferred task from local config.json: {task_name}")
else:
task_name = "text-to-image"
logger.info(f"No config.json found, defaulting task to: {task_name}")
else:
task_name = model_info(args.model).pipeline_tag
logger.info(f"Inferred task from HuggingFace pipeline_tag: {task_name}")
if task_name not in valid_tasks:
raise ValueError(
f"Task '{task_name}' is not a valid multimodal generation task. "
f"Use --task to specify one of: {', '.join(valid_tasks)}"
)
if task_name in ("text-to-video", "image-to-video", "video-to-video"):
api_url = f"{args.base_url}/v1/videos"
request_func = async_request_video_sglang
else: # text-to-image or image-to-image
api_url = (
f"{args.base_url}/v1/images/edits"
if task_name == "image-to-image"
else f"{args.base_url}/v1/images/generations"
)
request_func = async_request_image_sglang
setattr(args, "task_name", task_name)
if args.random_request_config and args.dataset != "random":
raise ValueError(
"--random-request-config can only be used with --dataset random"
)
if args.dataset == "vbench":
dataset = VBenchDataset(args, api_url, args.model)
elif args.dataset == "random":
dataset = RandomDataset(args, api_url, args.model)
else:
raise ValueError(f"Unknown dataset: {args.dataset}")
logger.info("Loading requests...")
requests_list = dataset.get_requests()
logger.info(f"Prepared {len(requests_list)} requests from {args.dataset} dataset.")
# Limit concurrency
if args.max_concurrency is not None:
semaphore = asyncio.Semaphore(args.max_concurrency)
else:
semaphore = None
async def limited_request_func(req, session, pbar):
if semaphore:
async with semaphore:
return await request_func(req, session, pbar)
else:
return await request_func(req, session, pbar)
async with aiohttp.ClientSession() as session:
# Run warmup requests
warmup_pairs: List[tuple] = []
if args.warmup_requests and requests_list:
# The server always overrides warmup requests to use
# num_inference_steps=1 (see Req.set_as_warmup), so we match
# that here to keep the benchmark's SLO estimation consistent.
warmup_steps = 1
logger.info(
f"Running {args.warmup_requests} warmup request(s) with "
f"num_inference_steps={warmup_steps}..."
)
for i in range(args.warmup_requests):
warm_req = requests_list[i % len(requests_list)]
warm_req = replace(
warm_req,
num_inference_steps=warmup_steps,
)
warm_out = await limited_request_func(warm_req, session, None)
warmup_pairs.append((warm_req, warm_out))
logger.info(
f"Warmup {i+1}/{args.warmup_requests}: "
f"latency={warm_out.latency:.2f}s, success={warm_out.success}"
)
# Populate SLO values from warmups if enabled
if args.slo:
requests_list = _populate_slo_ms_from_warmups(
requests_list=requests_list, warmup_pairs=warmup_pairs, args=args
)
# Run benchmark
pbar = tqdm(total=len(requests_list), disable=args.disable_tqdm)
start_time = time.perf_counter()
tasks = []
for req in requests_list:
if args.request_rate != float("inf"):
# Poisson process: inter-arrival times follow exponential distribution
interval = np.random.exponential(1.0 / args.request_rate)
await asyncio.sleep(interval)
task = asyncio.create_task(limited_request_func(req, session, pbar))
tasks.append(task)
outputs = await asyncio.gather(*tasks)
total_duration = time.perf_counter() - start_time
pbar.close()
# Calculate metrics
metrics = calculate_metrics(outputs, total_duration, requests_list, args, args.slo)
print("\n{s:{c}^{n}}".format(s=" Serving Benchmark Result ", n=60, c="="))
# Section 1: Configuration
print_value_formatted("Task:", task_name)
print_value_formatted("Model:", args.model)
print_value_formatted("Dataset:", args.dataset)
# Section 2: Execution & Traffic
print_divider(50)
print_value_formatted("Benchmark duration (s):", metrics["duration"])
print_value_formatted("Request rate:", str(args.request_rate))
print_value_formatted(
"Max request concurrency:",
str(args.max_concurrency) if args.max_concurrency else "not set",
)
print_value_formatted(
"Successful requests:",
f"{metrics['completed_requests']}/{len(requests_list)}",
)
print_value_formatted("Completed outputs:", metrics["completed_outputs"])
print_value_formatted("Outputs per prompt:", metrics["num_outputs_per_prompt"])
# Section 3: Performance Metrics
print_divider(50)
print_value_formatted("Request throughput (req/s):", metrics["throughput_qps"])
print_value_formatted(
"Output throughput (outputs/s):", metrics["output_throughput_ops"]
)
print_value_formatted("Latency Mean (s):", metrics["latency_mean"])
print_value_formatted("Latency Median (s):", metrics["latency_median"])
print_value_formatted("Latency P90 (s):", metrics["latency_p90"])
print_value_formatted("Latency P95 (s):", metrics["latency_p95"])
print_value_formatted("Latency P99 (s):", metrics["latency_p99"])
if metrics["peak_memory_mb_max"] > 0:
print_divider(50)
print_value_formatted("Peak Memory Max (MB):", metrics["peak_memory_mb_max"])
print_value_formatted("Peak Memory Mean (MB):", metrics["peak_memory_mb_mean"])
print_value_formatted(
"Peak Memory Median (MB):", metrics["peak_memory_mb_median"]
)
if args.slo and "slo_attainment_rate" in metrics:
print_divider(50)
print(
"{:<40} {:<15.2%}".format(
"SLO Attainment Rate:", metrics["slo_attainment_rate"]
)
)
print("{:<40} {:<15}".format("SLO Met (Success):", metrics["slo_met_success"]))
print("{:<40} {:<15.2f}".format("SLO Scale:", metrics["slo_scale"]))
print_divider(60)
if args.output_file:
with open(args.output_file, "w") as f:
json.dump(metrics, f, indent=2)
print(f"Metrics saved to {args.output_file}")
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Benchmark serving for diffusion models."
)
parser.add_argument(
"--backend",
type=str,
default=None,
help="DEPRECATED: --task is deprecated and will be ignored. The task will be inferred from --model.",
)
parser.add_argument(
"--base-url",
type=str,
default=None,
help="Base URL of the server (e.g., http://localhost:30000). Overrides host/port.",
)
parser.add_argument("--host", type=str, default="localhost", help="Server host.")
parser.add_argument("--port", type=int, default=30000, help="Server port.")
parser.add_argument("--model", type=str, default="default", help="Model name.")
parser.add_argument(
"--dataset",
type=str,
default="vbench",
choices=["vbench", "random"],
help="Dataset to use.",
)
parser.add_argument(
"--task",
type=str,
choices=[
"text-to-video",
"image-to-video",
"text-to-image",
"image-to-image",
"video-to-video",
],
default=None,
help="The task will be inferred from huggingface pipeline_tag. When huggingface pipeline_tag is not provided, --task will be used.",
)
parser.add_argument(
"--dataset-path",
type=str,
default=None,
help="Path to local dataset file (optional).",
)
parser.add_argument(
"--num-prompts", type=int, default=10, help="Number of prompts to benchmark."
)
parser.add_argument(
"--num-outputs-per-prompt",
type=int,
default=1,
help="Number of generated outputs requested per prompt.",
)
parser.add_argument(
"--max-concurrency",
type=int,
default=1,
help="Maximum number of concurrent requests, default to `1`. This can be used "
"to help simulate an environment where a higher level component "
"is enforcing a maximum number of concurrent requests. While the "
"--request-rate argument controls the rate at which requests are "
"initiated, this argument will control how many are actually allowed "
"to execute at a time. This means that when used in combination, the "
"actual request rate may be lower than specified with --request-rate, "
"if the server is not processing requests fast enough to keep up.",
)
parser.add_argument(
"--request-rate",
type=float,
default=float("inf"),
help="Number of requests per second. If this is inf, then all the requests are sent at time 0. "
"Otherwise, we use Poisson process to synthesize the request arrival times. Default is inf.",
)
parser.add_argument("--width", type=int, default=None, help="Image/Video width.")
parser.add_argument("--height", type=int, default=None, help="Image/Video height.")
parser.add_argument(
"--random-request-config",
type=str,
default=None,
help=(
"JSON string defining random request profiles. "
"Each profile may contain: width, height, num_inference_steps, "
"num_outputs_per_prompt, etc. "
"The 'weight' field controls sampling probability (relative weight). "
"Example: "
'[{"width":512,"height":512,"num_inference_steps":20,"weight":0.15},'
'{"width":768,"height":768,"num_inference_steps":20,"weight":0.85}]'
),
)
parser.add_argument(
"--random-request-seed",
type=int,
default=42,
help="Random seed for sampling request profiles (default: 42).",
)
parser.add_argument(
"--num-frames", type=int, default=None, help="Number of frames (for video)."
)
parser.add_argument("--fps", type=int, default=None, help="FPS (for video).")
parser.add_argument(
"--output-file", type=str, default=None, help="Output JSON file for metrics."
)
parser.add_argument(
"--disable-tqdm", action="store_true", help="Disable progress bar."
)
parser.add_argument(
"--log-level",
type=str,
default="INFO",
choices=["DEBUG", "INFO", "WARNING", "ERROR"],
help="Log level.",
)
parser.add_argument(
"--slo",
action="store_true",
help="Enable SLO calculation. Uses trace-provided slo_ms or infers from warmups.",
)
parser.add_argument(
"--slo-scale",
type=float,
default=3.0,
help="SLO target multiplier: slo_ms = estimated_exec_time_ms * slo_scale (default: 3).",
)
parser.add_argument(
"--warmup-requests",
type=int,
default=1,
help="Number of warmup requests to run before measurement.",
)
parser.add_argument(
"--num-inference-steps",
type=int,
default=None,
help="Number of inference steps for diffusion models.",
)
args = parser.parse_args()
configure_logger(args)
asyncio.run(benchmark(args))