Signed-off-by: QI JIA <qi.jia@shengshu.ai> Co-authored-by: QI JIA <qi.jia@shengshu.ai> Co-authored-by: Cursor <cursoragent@cursor.com>
203 lines
7.2 KiB
Python
203 lines
7.2 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
|
"""Benchmark newline-split and fixed-size chunking of image file responses.
|
|
|
|
Both variants stream the exact payload `ImageGenerationResponse.stream_response()`
|
|
produces, so the measurement isolates the response layer: PNG encoding and ZIP
|
|
building are shared by the variants and excluded from the timings.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import asyncio
|
|
import base64
|
|
import hashlib
|
|
import io
|
|
import json
|
|
import statistics
|
|
import time
|
|
from collections.abc import AsyncIterator, Callable, Iterable
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import cast
|
|
|
|
import numpy as np
|
|
from PIL import Image
|
|
from starlette.responses import StreamingResponse
|
|
|
|
from vllm_omni.entrypoints.openai.protocol.images import (
|
|
_FILE_RESPONSE_CHUNK_SIZE,
|
|
ImageData,
|
|
ImageGenerationResponse,
|
|
_iter_file_chunks,
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class BenchmarkVariant:
|
|
label: str
|
|
content: Callable[[bytes], Iterable[bytes] | AsyncIterator[memoryview]]
|
|
|
|
|
|
def _positive_int(value: str) -> int:
|
|
parsed = int(value)
|
|
if parsed < 1:
|
|
raise argparse.ArgumentTypeError("value must be at least 1")
|
|
return parsed
|
|
|
|
|
|
def _encode_png(*, width: int, height: int, seed: int) -> bytes:
|
|
"""Encode incompressible noise, so the payload looks like a real photo."""
|
|
rng = np.random.default_rng(seed)
|
|
pixels = rng.integers(0, 256, size=(height, width, 3), dtype=np.uint8)
|
|
buffer = io.BytesIO()
|
|
Image.fromarray(pixels, mode="RGB").save(buffer, format="png")
|
|
return buffer.getvalue()
|
|
|
|
|
|
async def _drain(response: StreamingResponse) -> tuple[bytes, int]:
|
|
"""Run a response through ASGI, returning its body and non-empty frame count."""
|
|
body = bytearray()
|
|
frames = 0
|
|
|
|
async def send(message: dict[str, object]) -> None:
|
|
nonlocal frames
|
|
if message["type"] == "http.response.body" and message["body"]:
|
|
frames += 1
|
|
body.extend(cast(bytes, message["body"]))
|
|
|
|
async def receive() -> dict[str, str]:
|
|
return {"type": "http.disconnect"}
|
|
|
|
scope = {"type": "http", "method": "GET", "asgi": {"spec_version": "2.4"}}
|
|
await response(scope, receive, send)
|
|
return bytes(body), frames
|
|
|
|
|
|
async def _build_payload(*, images: int, width: int, height: int, seed: int) -> bytes:
|
|
png = _encode_png(width=width, height=height, seed=seed)
|
|
response = ImageGenerationResponse(
|
|
created=0,
|
|
data=[ImageData(b64_json=base64.b64encode(png).decode()) for _ in range(images)],
|
|
output_format="png",
|
|
size=f"{width}x{height}",
|
|
)
|
|
payload, _ = await _drain(response.stream_response())
|
|
return payload
|
|
|
|
|
|
async def _measure(variant: BenchmarkVariant, payload: bytes) -> dict[str, object]:
|
|
response = StreamingResponse(variant.content(payload), media_type="application/octet-stream")
|
|
cpu_start = time.process_time_ns()
|
|
wall_start = time.perf_counter_ns()
|
|
body, frames = await _drain(response)
|
|
wall_ms = (time.perf_counter_ns() - wall_start) / 1_000_000
|
|
process_cpu_ms = (time.process_time_ns() - cpu_start) / 1_000_000
|
|
return {
|
|
"label": variant.label,
|
|
"wall_ms": wall_ms,
|
|
"process_cpu_ms": process_cpu_ms,
|
|
"body_frames": frames,
|
|
"output_bytes": len(body),
|
|
"output_sha256": hashlib.sha256(body).hexdigest(),
|
|
}
|
|
|
|
|
|
def _summarize(records: list[dict[str, object]], label: str) -> dict[str, object]:
|
|
selected = [record for record in records if record["label"] == label]
|
|
wall_values = [cast(float, record["wall_ms"]) for record in selected]
|
|
cpu_values = [cast(float, record["process_cpu_ms"]) for record in selected]
|
|
return {
|
|
"runs": len(selected),
|
|
"body_frames": cast(int, selected[0]["body_frames"]),
|
|
"wall_ms": {
|
|
"median": statistics.median(wall_values),
|
|
"min": min(wall_values),
|
|
"max": max(wall_values),
|
|
},
|
|
"process_cpu_ms": {
|
|
"median": statistics.median(cpu_values),
|
|
"min": min(cpu_values),
|
|
"max": max(cpu_values),
|
|
},
|
|
}
|
|
|
|
|
|
def _parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--images", type=_positive_int, default=1, help="2 or more streams a ZIP archive")
|
|
parser.add_argument("--width", type=_positive_int, default=1024)
|
|
parser.add_argument("--height", type=_positive_int, default=1024)
|
|
parser.add_argument("--warmup", type=int, default=1)
|
|
parser.add_argument("--rounds", type=_positive_int, default=3)
|
|
parser.add_argument("--seed", type=int, default=20260912)
|
|
parser.add_argument("--output-json", type=Path)
|
|
args = parser.parse_args()
|
|
if args.warmup < 0:
|
|
parser.error("--warmup must be non-negative")
|
|
return args
|
|
|
|
|
|
async def _run(args: argparse.Namespace) -> dict[str, object]:
|
|
payload = await _build_payload(images=args.images, width=args.width, height=args.height, seed=args.seed)
|
|
baseline = BenchmarkVariant("newline_split", io.BytesIO)
|
|
candidate = BenchmarkVariant("fixed_size_chunks", _iter_file_chunks)
|
|
|
|
for _ in range(args.warmup):
|
|
for variant in (baseline, candidate):
|
|
await _measure(variant, payload)
|
|
|
|
records: list[dict[str, object]] = []
|
|
for round_index in range(args.rounds):
|
|
order = (baseline, candidate) if round_index % 2 == 0 else (candidate, baseline)
|
|
for variant in order:
|
|
record = await _measure(variant, payload)
|
|
record["round"] = round_index + 1
|
|
records.append(record)
|
|
|
|
output_hashes = {str(record["output_sha256"]) for record in records}
|
|
if len(output_hashes) != 1:
|
|
raise RuntimeError(f"benchmark variants produced different outputs: {sorted(output_hashes)}")
|
|
|
|
baseline_summary = _summarize(records, baseline.label)
|
|
candidate_summary = _summarize(records, candidate.label)
|
|
baseline_ms = cast(float, cast(dict[str, object], baseline_summary["wall_ms"])["median"])
|
|
candidate_ms = cast(float, cast(dict[str, object], candidate_summary["wall_ms"])["median"])
|
|
return {
|
|
"config": {
|
|
"images": args.images,
|
|
"width": args.width,
|
|
"height": args.height,
|
|
"warmup": args.warmup,
|
|
"rounds": args.rounds,
|
|
"seed": args.seed,
|
|
"chunk_size": _FILE_RESPONSE_CHUNK_SIZE,
|
|
"payload_bytes": len(payload),
|
|
"payload_newline_bytes": payload.count(b"\n"),
|
|
},
|
|
"records": records,
|
|
"summary": {
|
|
baseline.label: baseline_summary,
|
|
candidate.label: candidate_summary,
|
|
"median_wall_speedup": baseline_ms / candidate_ms,
|
|
"body_frame_ratio": (
|
|
cast(int, baseline_summary["body_frames"]) / cast(int, candidate_summary["body_frames"])
|
|
),
|
|
"output_sha256": output_hashes.pop(),
|
|
},
|
|
}
|
|
|
|
|
|
def main() -> None:
|
|
args = _parse_args()
|
|
result = asyncio.run(_run(args))
|
|
output_json = json.dumps(result, indent=2, sort_keys=True)
|
|
if args.output_json is not None:
|
|
args.output_json.write_text(output_json + "\n", encoding="utf-8")
|
|
print(output_json)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|