#!/usr/bin/env python3 # SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """Realtime streaming benchmark for LingBot-World 2.0 over ``WS /v1/realtime/video``. Every other diffusion benchmark in this repository measures "one request, one finished video". LingBot-World is not that: one request is a whole rollout, and the thing a deployment lives or dies by is whether chunk *N + 1* keeps arriving before a viewer has finished watching chunk *N*. This benchmark therefore measures a **cadence**, not a throughput: * ``TTFC`` -- session.start to the first chunk's bytes, which pays for prompt encode, the first-frame VAE encode, and any compile or capture the deploy config asks for; * **inter-arrival latency** over all chunks and, separately, over the steady state after the attention window saturates; * ``VIDEO_RTF`` -- wall seconds spent per second of video generated, matching how vLLM-Omni computes RTF everywhere else (``generation_s / video_duration``). **Lower is better and below 1.0 is real time**; ``VIDEO_RTFX`` is reported alongside it as the reciprocal, under the repo's own name for that; * **playback continuity** -- a simulated viewer with a start buffer, because a p99 that is harmless behind a three-chunk buffer is a visible stall behind none. The server is not started here. Bring one up first (see the README), then point this script at it. Requires ``websockets``; ``--save-video`` additionally needs a vLLM-Omni installation for the remux helper. Example: python benchmarks/lingbot_world/benchmark_lingbot_world_realtime.py \\ --host 127.0.0.1 --port 8000 \\ --num-chunks 16 \\ --output-json /tmp/lingbot_world_realtime.json """ from __future__ import annotations import argparse import asyncio import base64 import json import mimetypes import os import platform import subprocess import sys import time from dataclasses import dataclass from datetime import datetime, timezone from pathlib import Path from typing import Any REPO_ROOT = Path(__file__).resolve().parents[2] if str(REPO_ROOT) not in sys.path: sys.path.insert(0, str(REPO_ROOT)) from benchmarks.lingbot_world.workload import ( # noqa: E402 CAMERA_PATTERNS, DEFAULT_FLOW_SHIFT, DEFAULT_FPS, DEFAULT_HEIGHT, DEFAULT_MODEL, DEFAULT_PROMPT, DEFAULT_SEED, DEFAULT_WARMUP_CHUNKS, DEFAULT_WIDTH, ChunkRecord, Workload, aggregate_metrics, compute_metrics, default_workload, load_workload, steady_chunk_deadline_ms, ) # The shared vLLM asset keeps the default workload runnable with no setup and # adds no binary to this repository. Any real photograph conditions a rollout. _DEFAULT_IMAGE_ASSET = "2560px-Gfp-wisconsin-madison-the-nature-boardwalk" _WS_PATH = "/v1/realtime/video" class BenchmarkError(RuntimeError): """A session failed in a way that invalidates its numbers.""" # --------------------------------------------------------------------------- # # Input resolution # --------------------------------------------------------------------------- # def resolve_image_reference(value: str | None) -> str: """Return an ``http(s)`` or ``data:`` URL for the session's first frame.""" if value is None: from vllm.assets.image import ImageAsset value = str(ImageAsset(_DEFAULT_IMAGE_ASSET).get_path("jpg")) if value.startswith(("http://", "https://", "data:")): return value path = Path(value).expanduser() if not path.is_file(): raise BenchmarkError(f"--image is neither a URL nor a readable file: {value}") media_type = mimetypes.guess_type(path.name)[0] or "image/png" return f"data:{media_type};base64,{base64.b64encode(path.read_bytes()).decode()}" def build_workload(args: argparse.Namespace) -> Workload: if args.workload is not None: # The file names its own first frame, so resolving happens inside # load_workload -- never eagerly, which would fetch the default asset a # --workload run has no use for. return load_workload( args.workload, image_resolver=lambda value: resolve_image_reference(value if args.image is None else args.image), overrides={ "width": args.width_override, "height": args.height_override, "fps": args.fps_override, "seed": args.seed_override, "prompt": args.prompt_override, "flow_shift": args.flow_shift, "negative_prompt": args.negative_prompt, }, ) return default_workload( image_reference=resolve_image_reference(args.image), num_chunks=args.num_chunks, camera_pattern=args.camera_pattern, prompt=args.prompt_override if args.prompt_override is not None else DEFAULT_PROMPT, width=args.width_override if args.width_override is not None else DEFAULT_WIDTH, height=args.height_override if args.height_override is not None else DEFAULT_HEIGHT, fps=args.fps_override if args.fps_override is not None else DEFAULT_FPS, seed=args.seed_override if args.seed_override is not None else DEFAULT_SEED, flow_shift=args.flow_shift if args.flow_shift is not None else DEFAULT_FLOW_SHIFT, negative_prompt=args.negative_prompt, ) # --------------------------------------------------------------------------- # # One streaming session # --------------------------------------------------------------------------- # @dataclass class SessionResult: request_id: str | None records: list[ChunkRecord] media: bytes ttfc_s: float | None video_start_s: float | None wall_s: float done_event: dict[str, Any] async def _keepalive(websocket: Any, send_lock: asyncio.Lock, interval: float) -> None: """Refresh the server's stall clock while a slow first chunk is generating. A compiled deploy config records CUDA graphs on the first block, which can outlast the server's ~60 s stall timeout. ``session.ping`` is the documented way to say the client is still there. """ while True: await asyncio.sleep(interval) async with send_lock: await websocket.send(json.dumps({"type": "session.ping"})) async def run_session( url: str, model: str, workload: Workload, *, first_chunk_timeout: float, chunk_timeout: float, ping_interval: float, collect_media: bool, print_chunks: bool, ) -> SessionResult: """Drive one rollout and timestamp every chunk as its bytes land.""" try: from websockets.asyncio.client import connect except ImportError: # pragma: no cover - exercised only without websockets try: from websockets import connect # type: ignore[attr-defined] except ImportError as exc: raise BenchmarkError("This benchmark needs the websockets package: pip install websockets") from exc payload = workload.session_start_payload(model) records: list[ChunkRecord] = [] media = bytearray() pending: dict[str, Any] | None = None request_id: str | None = None ttfc_s: float | None = None video_start_s: float | None = None last_arrival: float | None = None done_event: dict[str, Any] = {} send_lock = asyncio.Lock() keepalive: asyncio.Task[None] | None = None # Fire remaining prompt updates in chunk order as their boundary passes. updates = sorted(workload.prompt_updates, key=lambda update: update.after_chunk) update_index = 0 async with connect(url, max_size=None, ping_interval=None) as websocket: async with send_lock: await websocket.send(json.dumps(payload, ensure_ascii=False)) started = time.perf_counter() if ping_interval > 0: keepalive = asyncio.create_task(_keepalive(websocket, send_lock, ping_interval)) try: while True: timeout = first_chunk_timeout if not records else chunk_timeout try: deadline = started + (last_arrival if last_arrival is not None else 0.0) + timeout remaining = deadline - time.perf_counter() if remaining <= 0: raise asyncio.TimeoutError message = await asyncio.wait_for(websocket.recv(), timeout=remaining) except asyncio.TimeoutError as exc: raise BenchmarkError( f"No media chunk for {timeout:g} s after {len(records)} chunk(s); " "the server may still be loading, compiling, or capturing graphs." ) from exc now = time.perf_counter() if isinstance(message, (bytes, bytearray)): if pending is None: raise BenchmarkError("Received a binary frame with no preceding video.chunk_metadata event.") if collect_media: media.extend(message) if pending.get("kind") == "media": index = int(pending["generation_chunk_index"]) arrival = now - started if ttfc_s is None: ttfc_s = arrival records.append( ChunkRecord( index=index, arrival_s=arrival, inter_arrival_s=arrival - (last_arrival if last_arrival is not None else 0.0), byte_length=len(message), num_frames=int(pending["num_frames"]), ) ) last_arrival = arrival if print_chunks: print( f" chunk {index:3d} t={arrival:8.3f}s " f"interval={records[-1].inter_arrival_s * 1000:8.1f}ms " f"frames={records[-1].num_frames:3d} bytes={len(message)}", flush=True, ) while update_index < len(updates) and updates[update_index].after_chunk <= index: async with send_lock: await websocket.send(json.dumps(updates[update_index].to_payload(), ensure_ascii=False)) update_index += 1 pending = None continue event = json.loads(message) event_type = event.get("type") if event_type == "video.chunk_metadata": pending = event elif event_type == "video.start": request_id = event.get("request_id") video_start_s = now - started elif event_type == "error": # Say what was asked for alongside what the server refused: # some builds cap a rollout's num_frames, and the operator # needs to know which --num-chunks would fit. raise BenchmarkError( f"Server rejected the session after {len(records)} chunk(s): " f"{event.get('message', event)} " f"(requested {workload.num_chunks} chunks = num_frames {workload.num_frames})" ) elif event_type == "session.done": done_event = event break finally: if keepalive is not None: keepalive.cancel() try: await keepalive except asyncio.CancelledError: pass wall_s = time.perf_counter() - started if not records: raise BenchmarkError("The session finished without delivering a single media chunk.") if done_event.get("stopped"): raise BenchmarkError(f"The session was stopped before it finished: {done_event}") observed = [record.index for record in records] if observed != list(range(len(records))): raise BenchmarkError(f"Chunk indices are not contiguous from zero: {observed}") if len(records) != workload.num_chunks: raise BenchmarkError( f"Expected {workload.num_chunks} chunks for num_frames={workload.num_frames}, received {len(records)}." ) return SessionResult( request_id=request_id, records=records, media=bytes(media), ttfc_s=ttfc_s, video_start_s=video_start_s, wall_s=wall_s, done_event=done_event, ) # --------------------------------------------------------------------------- # # Reporting # --------------------------------------------------------------------------- # def _git_revision() -> str | None: try: return subprocess.check_output( ["git", "-C", str(REPO_ROOT), "rev-parse", "HEAD"], text=True, stderr=subprocess.DEVNULL, ).strip() except (OSError, subprocess.CalledProcessError): return None def _print_row(label: str, value: str) -> None: print(f"{label:<44}{value:>16}") def print_report( metrics: dict[str, Any], *, workload: Workload, endpoint: str, sessions: int, target_fps: float, ) -> None: header = " LingBot-World realtime streaming benchmark " print("\n{s:{c}^{n}}".format(s=header, n=60, c="=")) _print_row("Endpoint:", endpoint) _print_row("Geometry:", f"{workload.width}x{workload.height}") _print_row("Chunks per session:", str(workload.num_chunks)) _print_row("Sessions:", str(sessions)) frames = workload.num_frames _print_row("Real-time basis:", f"{target_fps:g} fps") _print_row("Video seconds per session:", f"{frames / target_fps:.2f} ({frames} frames)") if abs(target_fps - workload.fps) > 1e-9: _print_row("Mux label sent to server:", f"{workload.fps} fps") if sessions == 1: print("{s:{c}^{n}}".format(s=" Cadence ", n=60, c="-")) if metrics.get("ttfc_ms") is not None: _print_row("TTFC (session.start -> first chunk):", f"{metrics['ttfc_ms']:.1f} ms") interval_all = metrics.get("interval_all") or {} interval_steady = metrics.get("interval_steady") or {} for title, summary in (("All chunks", interval_all), ("Steady state", interval_steady)): if not summary: continue print(f" {title} (n={int(summary['count'])}):") _print_row(" mean / median:", f"{summary['mean_ms']:.1f} / {summary['median_ms']:.1f} ms") _print_row(" std / max:", f"{summary['std_ms']:.1f} / {summary['max_ms']:.1f} ms") tail = f"{summary['p90_ms']:.0f} / {summary['p95_ms']:.0f} / {summary['p99_ms']:.0f} ms" _print_row(" p90 / p95 / p99:", tail) if summary["count"] < 100: print( " NOTE: fewer than 100 intervals; tail percentiles are descriptive, not reliable tail estimates." ) print("{s:{c}^{n}}".format(s=" Real time ", n=60, c="-")) _print_row("VIDEO_RTF (wall s / video s, <1 real time):", f"{metrics['video_rtf']:.3f}") _print_row("VIDEO_RTFX (video s / wall s):", f"{metrics['video_rtfx']:.3f}") if metrics.get("steady_video_rtf") is not None: _print_row("VIDEO_RTF steady state:", f"{metrics['steady_video_rtf']:.3f}") _print_row("Frames per second:", f"{metrics['frames_per_second']:.2f}") _print_row("Chunk deadline:", f"{metrics['chunk_deadline_ms']:.1f} ms") if metrics.get("slo_attainment") is not None: _print_row( "Steady chunks within deadline:", f"{metrics['slo_attainment'] * 100:.1f}% ({metrics['slo_violations']} late)", ) if not metrics.get("steady_state_reached", False): print( f" NOTE: no chunk reached the steady state; the attention window needs " f"{metrics['warmup_chunks']} chunks. Raise --num-chunks." ) playback = metrics.get("playback") or {} print("{s:{c}^{n}}".format(s=" Playback continuity ", n=60, c="-")) _print_row("Start buffer:", f"{playback.get('buffer_chunks')} chunk(s)") _print_row("Underruns:", str(playback.get("underrun_count"))) _print_row("Total stall:", f"{playback.get('total_stall_ms', 0.0):.1f} ms") _print_row("Stall ratio:", f"{playback.get('stall_ratio', 0.0) * 100:.2f}%") if playback.get("first_underrun_chunk") is not None: _print_row("First underrun at chunk:", str(playback["first_underrun_chunk"])) else: print("{s:{c}^{n}}".format(s=" Across sessions ", n=60, c="-")) if metrics.get("ttfc_ms_mean") is not None: _print_row("TTFC mean / max:", f"{metrics['ttfc_ms_mean']:.1f} / {metrics['ttfc_ms_max']:.1f} ms") if metrics.get("steady_interval_ms_mean") is not None: _print_row("Steady interval mean:", f"{metrics['steady_interval_ms_mean']:.1f} ms") _print_row("Steady interval spread:", f"{metrics['steady_interval_ms_spread']:.1f} ms") _print_row("VIDEO_RTF mean (lower is better):", f"{metrics['video_rtf_mean']:.3f}") _print_row( "VIDEO_RTF best / worst:", f"{metrics['video_rtf_best']:.3f} / {metrics['video_rtf_worst']:.3f}", ) _print_row("Chunk deadline:", f"{metrics['chunk_deadline_ms']:.1f} ms") _print_row("Underruns (all sessions):", str(metrics["underrun_count"])) print("=" * 60) def save_video(media: bytes, path: Path, *, fps: int) -> None: from vllm_omni.diffusion.utils.media_utils import finalize_streaming_video_bytes playback_bytes = finalize_streaming_video_bytes(media, input_format="m4s", fps=fps) path.parent.mkdir(parents=True, exist_ok=True) tmp_path = path.with_suffix(path.suffix + ".tmp") tmp_path.write_bytes(playback_bytes) os.replace(tmp_path, path) print(f"Saved {len(media)} streamed bytes -> {len(playback_bytes)} playback bytes at {path}") # --------------------------------------------------------------------------- # # CLI # --------------------------------------------------------------------------- # def parse_args(argv: list[str] | None = None) -> argparse.Namespace: parser = argparse.ArgumentParser( description="Benchmark LingBot-World 2.0 realtime streaming over WS /v1/realtime/video.", formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) endpoint = parser.add_argument_group("endpoint") endpoint.add_argument("--host", default="127.0.0.1") endpoint.add_argument("--port", type=int, default=8000) endpoint.add_argument("--base-url", default=None, help="Full ws:// URL; overrides --host/--port.") endpoint.add_argument("--model", default=DEFAULT_MODEL) workload = parser.add_argument_group("workload") workload.add_argument("--workload", default=None, help="JSON rollout spec; overrides the built-in workload.") workload.add_argument("--image", default=None, help="First frame: path or URL. Defaults to the shared vLLM asset.") workload.add_argument("--prompt", dest="prompt_override", default=None, help=f"Default: {DEFAULT_PROMPT!r}") workload.add_argument("--negative-prompt", default=None) workload.add_argument("--num-chunks", type=int, default=16, help="AR blocks per session; ignored with --workload.") workload.add_argument( "--camera-pattern", choices=CAMERA_PATTERNS, default="forward", help="Camera pattern for the built-in workload only; ignored with --workload.", ) workload.add_argument("--width", dest="width_override", type=int, default=None, help=f"Default: {DEFAULT_WIDTH}") workload.add_argument("--height", dest="height_override", type=int, default=None, help=f"Default: {DEFAULT_HEIGHT}") workload.add_argument("--fps", dest="fps_override", type=int, default=None, help=f"Default: {DEFAULT_FPS}") workload.add_argument("--seed", dest="seed_override", type=int, default=None, help=f"Default: {DEFAULT_SEED}") workload.add_argument("--flow-shift", type=float, default=None, help=f"Default: {DEFAULT_FLOW_SHIFT}") run = parser.add_argument_group("run") run.add_argument("--sessions", type=int, default=1, help="Sequential measured rollouts.") run.add_argument("--warmup-sessions", type=int, default=0, help="Unmeasured rollouts first (pays compile cost).") run.add_argument( "--warmup-chunks", type=int, default=DEFAULT_WARMUP_CHUNKS, help="Chunks excluded from steady state; the attention window saturates after 6.", ) run.add_argument( "--chunk-slo-ms", type=float, default=None, help="Deadline a steady chunk must meet. Default: one chunk of video at --target-fps.", ) run.add_argument( "--target-fps", type=float, default=None, help=( "Playback rate used for real-time factor, the chunk deadline, and the playback " "simulation. The checkpoint declares no frame rate, so this is a consumption " "choice, not a model property. Default: the --fps sent to the server." ), ) run.add_argument("--playback-buffer-chunks", type=int, default=1, help="Chunks buffered before playback starts.") run.add_argument("--first-chunk-timeout", type=float, default=900.0) run.add_argument("--chunk-timeout", type=float, default=300.0) run.add_argument("--ping-interval", type=float, default=20.0, help="session.ping cadence; 0 disables.") output = parser.add_argument_group("output") output.add_argument("--output-json", default=None) output.add_argument("--save-video", default=None, help="Write the last measured session's video here.") output.add_argument("--print-chunks", action="store_true") args = parser.parse_args(argv) if args.sessions < 1: parser.error("--sessions must be at least 1.") if args.warmup_sessions < 0: parser.error("--warmup-sessions must be non-negative.") if args.warmup_chunks < 0: parser.error("--warmup-chunks must be non-negative.") if args.playback_buffer_chunks < 1: parser.error("--playback-buffer-chunks must be at least 1.") if args.target_fps is not None and args.target_fps <= 0: parser.error("--target-fps must be positive.") if args.workload is None and args.num_chunks < 1: parser.error("--num-chunks must be at least 1.") return args def endpoint_url(args: argparse.Namespace) -> str: if args.base_url is not None: base = args.base_url.rstrip("/") for prefix, replacement in (("http://", "ws://"), ("https://", "wss://")): if base.startswith(prefix): base = replacement + base[len(prefix) :] break return base if base.endswith(_WS_PATH) else base + _WS_PATH return f"ws://{args.host}:{args.port}{_WS_PATH}" async def run_benchmark(args: argparse.Namespace) -> dict[str, Any]: workload = build_workload(args) url = endpoint_url(args) # The mux label and the real-time basis are different questions; only the # latter decides whether a run passes. target_fps = args.target_fps if args.target_fps is not None else float(workload.fps) if max(1, args.warmup_chunks) >= workload.num_chunks - 1: print( f"WARNING: --warmup-chunks={args.warmup_chunks} leaves no nonterminal steady-state chunk in a " f"{workload.num_chunks}-chunk rollout; steady-state metrics will be empty.", file=sys.stderr, ) print(f"Connecting to {url}") print( f"Rollout: {workload.num_chunks} chunks, num_frames={workload.num_frames}, " f"{workload.video_seconds:.2f}s of video" ) print( f"Real-time basis: {target_fps:g} fps -> a steady 12-frame chunk is " f"{steady_chunk_deadline_ms(target_fps):.1f} ms of video" + ("" if args.target_fps is None else f" (mux label stays {workload.fps} fps)") ) for index in range(args.warmup_sessions): print(f"Warmup session {index + 1}/{args.warmup_sessions} (not measured)...") await run_session( url, args.model, workload, first_chunk_timeout=args.first_chunk_timeout, chunk_timeout=args.chunk_timeout, ping_interval=args.ping_interval, collect_media=False, print_chunks=False, ) session_metrics: list[dict[str, Any]] = [] session_payloads: list[dict[str, Any]] = [] last_result: SessionResult | None = None for index in range(args.sessions): if args.sessions > 1: print(f"Session {index + 1}/{args.sessions}...") result = await run_session( url, args.model, workload, first_chunk_timeout=args.first_chunk_timeout, chunk_timeout=args.chunk_timeout, ping_interval=args.ping_interval, collect_media=args.save_video is not None, print_chunks=args.print_chunks, ) last_result = result metrics = compute_metrics( result.records, fps=target_fps, warmup_chunks=args.warmup_chunks, slo_ms=args.chunk_slo_ms, playback_buffer_chunks=args.playback_buffer_chunks, ttfc_s=result.ttfc_s, session_wall_s=result.wall_s, ) session_metrics.append(metrics) session_payloads.append( { "session_index": index, "request_id": result.request_id, "video_start_ms": (result.video_start_s * 1000.0) if result.video_start_s is not None else None, "chunks": [record.to_dict() for record in result.records], "metrics": metrics, } ) aggregate = aggregate_metrics(session_metrics, fps=target_fps) print_report(aggregate, workload=workload, endpoint=url, sessions=args.sessions, target_fps=target_fps) if args.save_video is not None and last_result is not None: save_video(last_result.media, Path(args.save_video), fps=workload.fps) document = { "benchmark": "lingbot_world_realtime", "timestamp": datetime.now(timezone.utc).isoformat(), "endpoint": url, "model": args.model, "git_revision": _git_revision(), "hostname": platform.node(), "workload": workload.describe(), "config": { "sessions": args.sessions, "warmup_sessions": args.warmup_sessions, "warmup_chunks": args.warmup_chunks, "camera_pattern": args.camera_pattern, "chunk_slo_ms": args.chunk_slo_ms, "target_fps": target_fps, "mux_fps": workload.fps, "playback_buffer_chunks": args.playback_buffer_chunks, "workload_file": args.workload, }, "sessions": session_payloads, "metrics": aggregate, } if args.output_json is not None: path = Path(args.output_json) path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(document, indent=2, sort_keys=True) + "\n") print(f"Wrote {path}") return document def main(argv: list[str] | None = None) -> int: args = parse_args(argv) try: asyncio.run(run_benchmark(args)) except BenchmarkError as error: print(f"ERROR: {error}", file=sys.stderr) return 1 return 0 if __name__ == "__main__": raise SystemExit(main())