Files
sglang/python/sglang/multimodal_gen/benchmarks/bench_offline_throughput.py
T
a69b943356 [SGLang-Diffusion] Add offline throughput benchmark script for multi-modal models (#18154)
Co-authored-by: Hao Jin <Hao Jin>
Co-authored-by: zhaochenyang20 <zhaochen20@outlook.com>
Co-authored-by: Xiaoyu Zhang <35585791+BBuf@users.noreply.github.com>
2026-03-04 08:39:46 +08:00

444 lines
14 KiB
Python

"""
Benchmark offline throughput for multimodal generation models (Image/Video Generation).
This script benchmarks generation throughput without running a server, using low-level APIs.
It provides detailed metrics on throughput, latency, and resource utilization.
# Usage Examples
## Text-to-Video with VBench dataset
python -m sglang.multimodal_gen.benchmarks.bench_offline_throughput \\
--model-path Wan-AI/Wan2.1-T2V-1.3B-Diffusers \\
--dataset vbench \\
--num-prompts 20 \\
--batch-size 1 \\
--width 512 --height 512 --num-frames 16
## Random dataset for stress testing
python -m sglang.multimodal_gen.benchmarks.bench_offline_throughput \\
--model-path Wan-AI/Wan2.1-T2V-1.3B-Diffusers \\
--dataset random \\
--num-prompts 100 \\
--batch-size 1 \\
--num-inference-steps 20 \\
--output-file results.json
"""
import argparse
import dataclasses
import json
import time
from dataclasses import dataclass
from typing import Any, Dict, List, Tuple
import torch
from tqdm import tqdm
from sglang.multimodal_gen.benchmarks.datasets import RandomDataset, VBenchDataset
from sglang.multimodal_gen.runtime.entrypoints.diffusion_generator import DiffGenerator
from sglang.multimodal_gen.runtime.server_args import ServerArgs, set_global_server_args
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
logger = init_logger(__name__)
@dataclass
class BatchOutput:
"""Container for batch generation results."""
latency: float = 0.0
latency_per_sample: float = 0.0
num_samples: int = 0
total_frames: int = 0
peak_memory_mb: float = 0.0
success: bool = False
error: str = ""
@dataclass
class BenchArgs:
"""Benchmark configuration for multimodal generation."""
# Diffusion Model Configuration
num_inference_steps: int = 20
guidance_scale: float = 7.5
seed: int = 42
disable_safety_checker: bool = False
# Output Configuration
width: int = 32
height: int = 32
num_frames: int = 1
fps: int = 24
# Dataset & Benchmark
dataset: str = "random"
dataset_path: str = ""
task_name: str = "unknown"
num_prompts: int = 10
batch_size: int = 1
# Benchmark Execution
skip_warmup: bool = False
output_file: str = ""
disable_tqdm: bool = False
@staticmethod
def add_cli_args(parser: argparse.ArgumentParser):
"""Add benchmark-specific CLI arguments."""
# Diffusion Model Configuration
parser.add_argument(
"--num-inference-steps",
type=int,
default=20,
help="Number of denoising steps",
)
parser.add_argument(
"--guidance-scale",
type=float,
default=7.5,
help="Classifier-free guidance scale",
)
parser.add_argument("--seed", type=int, default=42, help="Random seed")
parser.add_argument(
"--disable-safety-checker",
action="store_true",
help="Disable NSFW detection",
)
# Output Configuration
parser.add_argument("--width", type=int, default=32, help="Image/video width")
parser.add_argument("--height", type=int, default=32, help="Image/video height")
parser.add_argument(
"--num-frames", type=int, default=1, help="Number of frames for video"
)
parser.add_argument("--fps", type=int, default=24, help="FPS for video")
# Dataset & Benchmark
parser.add_argument(
"--dataset",
type=str,
default="random",
choices=["vbench", "random"],
help="Dataset to use",
)
parser.add_argument(
"--dataset-path",
type=str,
default="",
help="Path to dataset (prompts file or image directory)",
)
parser.add_argument(
"--task-name",
type=str,
default="unknown",
help="Task name for benchmark identification",
)
parser.add_argument(
"--num-prompts",
type=int,
default=10,
help="Total number of prompts to benchmark",
)
parser.add_argument(
"--batch-size",
type=int,
default=1,
help="Batch size per generation call (currently only bs=1 is supported)",
)
# Benchmark Execution
parser.add_argument(
"--skip-warmup", action="store_true", help="Skip warmup batch"
)
parser.add_argument(
"--output-file",
type=str,
default="",
help="Output JSON file for results (append mode)",
)
parser.add_argument(
"--disable-tqdm",
action="store_true",
help="Disable progress bar",
)
@classmethod
def from_cli_args(cls, args: argparse.Namespace):
"""Create BenchArgs from parsed CLI arguments."""
attrs = [attr.name for attr in dataclasses.fields(cls)]
return cls(**{attr: getattr(args, attr) for attr in attrs})
def initialize_engine(server_args: ServerArgs) -> DiffGenerator:
"""Initialize diffusion pipeline engine."""
logger.info("Initializing engine...")
engine = DiffGenerator.from_server_args(server_args, local_mode=True)
logger.info("Engine initialized successfully")
return engine
def generate_batch(
engine: DiffGenerator,
bench_args: BenchArgs,
prompts: List[str],
user_sampling_params: Dict[str, Any],
) -> BatchOutput:
"""Generate batch of images/videos synchronously."""
output = BatchOutput()
start_time = time.perf_counter()
torch.cuda.reset_peak_memory_stats()
for prompt in prompts:
try:
sampling_params_kwargs = dict(user_sampling_params)
sampling_params_kwargs["prompt"] = prompt
result = engine.generate(sampling_params_kwargs=sampling_params_kwargs)
if result is not None:
if isinstance(result, list):
output.total_frames += len(result)
else:
output.total_frames += 1
output.num_samples += 1
except Exception as e:
logger.error(f"Generation failed for prompt '{prompt[:50]}...': {e}")
output.error = str(e)
output.latency = time.perf_counter() - start_time
output.latency_per_sample = output.latency / len(prompts) if prompts else 0.0
output.success = output.num_samples > 0
output.peak_memory_mb = torch.cuda.max_memory_allocated() / (1024 * 1024)
logger.debug(
f"Batch generated: {output.num_samples}/{len(prompts)} samples in {output.latency:.2f}s"
)
return output
def calculate_metrics(
outputs: List[BatchOutput],
total_duration: float,
resolution: Tuple[int, int, int],
num_requests: int,
) -> Dict[str, Any]:
"""Calculate generation-specific throughput metrics."""
successful = [o for o in outputs if o.success]
num_success = sum(o.num_samples for o in successful)
total_frames = sum(o.total_frames for o in successful)
peak_memory = max((o.peak_memory_mb for o in outputs), default=0)
width, height, frames = resolution
pixels_per_sample = width * height * frames
total_pixels = num_success * pixels_per_sample
metrics = {
"num_requests": num_requests,
"successful_requests": num_success,
"failed_requests": num_requests - num_success,
"total_duration_seconds": total_duration,
"total_frames_generated": total_frames,
"total_pixels_generated": total_pixels,
"images_per_second": num_success / total_duration if total_duration > 0 else 0,
"frames_per_second": total_frames / total_duration if total_duration > 0 else 0,
"megapixels_per_second": (
total_pixels / (total_duration * 1e6) if total_duration > 0 else 0
),
"requests_per_second": (
num_success / total_duration if total_duration > 0 else 0
),
"latency_per_request_seconds": (
total_duration / num_success if num_success > 0 else 0
),
"peak_memory_mb": peak_memory,
}
return metrics
def throughput_test(
server_args: ServerArgs,
bench_args: BenchArgs,
) -> Dict[str, Any]:
"""Main throughput benchmark function."""
configure_logger(server_args=server_args)
logger.info("Starting offline throughput benchmark...")
engine = initialize_engine(server_args)
logger.info(f"Loading {bench_args.dataset} dataset...")
if bench_args.dataset == "vbench":
bench_args.task_name = engine.server_args.pipeline_config.task_type
dataset = VBenchDataset(bench_args)
elif bench_args.dataset == "random":
dataset = RandomDataset(bench_args)
else:
raise ValueError(f"Unknown dataset: {bench_args.dataset}")
sampling_params = {
"guidance_scale": bench_args.guidance_scale,
"num_inference_steps": bench_args.num_inference_steps,
"height": bench_args.height,
"width": bench_args.width,
"num_frames": bench_args.num_frames,
"seed": bench_args.seed,
}
if bench_args.disable_safety_checker:
sampling_params["safety_checker"] = None
if not bench_args.skip_warmup:
logger.info("Running warmup batch...")
warmup_count = min(bench_args.batch_size, len(dataset))
warmup_prompts = [dataset[i].prompt for i in range(warmup_count)]
generate_batch(engine, bench_args, warmup_prompts, sampling_params)
logger.info(f"Running benchmark with {bench_args.num_prompts} prompts...")
outputs: List[BatchOutput] = []
total_count = min(bench_args.num_prompts, len(dataset))
all_prompts = [dataset[i].prompt for i in range(total_count)]
start_time = time.perf_counter()
num_batches = (total_count + bench_args.batch_size - 1) // bench_args.batch_size
pbar = tqdm(
total=num_batches,
disable=bench_args.disable_tqdm,
desc="Benchmark",
)
for batch_start in range(0, total_count, bench_args.batch_size):
batch_end = min(batch_start + bench_args.batch_size, total_count)
batch_prompts = all_prompts[batch_start:batch_end]
batch_output = generate_batch(
engine, bench_args, batch_prompts, sampling_params
)
outputs.append(batch_output)
pbar.update(1)
pbar.close()
total_duration = time.perf_counter() - start_time
resolution = (bench_args.width, bench_args.height, bench_args.num_frames)
metrics = calculate_metrics(
outputs,
total_duration,
resolution=resolution,
num_requests=total_count,
)
display_results(
metrics,
bench_args,
model_path=server_args.model_path,
)
if bench_args.output_file:
save_results(metrics, bench_args, server_args)
return metrics
def display_results(
metrics: Dict[str, Any],
bench_args: BenchArgs,
model_path: str,
):
"""Display benchmark results in console."""
print(
"\n{s:{c}^{n}}".format(s=" Offline Throughput Benchmark Result ", n=110, c="=")
)
print_value_formatted("Model:", model_path)
print_value_formatted("Dataset:", bench_args.dataset)
print_value_formatted(
"Resolution:",
f"{bench_args.width}x{bench_args.height}x{bench_args.num_frames}",
)
print_value_formatted("Num Inference Steps:", bench_args.num_inference_steps)
print_divider(75)
print_value_formatted("Total Requests:", metrics["num_requests"])
print_value_formatted("Successful Requests:", metrics["successful_requests"])
print_value_formatted("Failed Requests:", metrics["failed_requests"])
print_value_formatted(
"Total Duration (seconds):", metrics["total_duration_seconds"]
)
print_divider(75)
print_value_formatted("Frames Generated:", metrics["total_frames_generated"])
print_value_formatted(
"Megapixels Generated:", metrics["total_pixels_generated"] / 1e6
)
print_divider(75)
print_value_formatted(
"Frame Throughput (frames/sec):", metrics["frames_per_second"]
)
print_value_formatted("MP Throughput (MP/sec):", metrics["megapixels_per_second"])
print_value_formatted("Requests Per Second:", metrics["requests_per_second"])
print_value_formatted(
"Latency Per Request (sec):", metrics["latency_per_request_seconds"]
)
print_value_formatted("Peak Memory (MB):", metrics["peak_memory_mb"])
print_divider(110, "=")
def save_results(
metrics: Dict[str, Any],
bench_args: BenchArgs,
server_args: ServerArgs,
):
"""Save benchmark results to JSON file."""
result = {
"metadata": {
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%S"),
"model_path": server_args.model_path,
"task_type": bench_args.task_name,
"backend": "engine",
},
"configuration": {
"num_inference_steps": bench_args.num_inference_steps,
"guidance_scale": bench_args.guidance_scale,
"seed": bench_args.seed,
"batch_size": bench_args.batch_size,
"num_prompts": bench_args.num_prompts,
"resolution": f"{bench_args.width}x{bench_args.height}x{bench_args.num_frames}",
"dataset": bench_args.dataset,
},
"results": metrics,
}
with open(bench_args.output_file, "a") as f:
f.write(json.dumps(result) + "\n")
logger.info(f"Results saved to {bench_args.output_file}")
def main():
"""Main entry point."""
parser = argparse.ArgumentParser(
description="Offline throughput benchmark for multimodal generation models"
)
ServerArgs.add_cli_args(parser)
BenchArgs.add_cli_args(parser)
args = parser.parse_args()
server_args = ServerArgs.from_cli_args(args)
bench_args = BenchArgs.from_cli_args(args)
set_global_server_args(server_args)
result = throughput_test(server_args, bench_args)
return result
if __name__ == "__main__":
main()