Files
vllm-omni/benchmarks/lingbot_world/README.md
T
2026-09-21 13:12:00 +00:00

225 lines
11 KiB
Markdown

# LingBot-World Benchmarks — Realtime Streaming Cadence
`benchmark_lingbot_world_realtime.py` measures LingBot-World 2.0 as it is
actually deployed: one `WS /v1/realtime/video` session that streams one video
chunk per AR block, for as long as the world keeps running.
## Why this is not the diffusion serving benchmark
[`benchmarks/diffusion`](../diffusion/README.md) measures "one request, one
finished video" and reports request throughput. LingBot-World has neither shape:
- **One request is the whole rollout.** `prepare_encode` runs once, then every AR
block is four DMD steps plus one clean-KV commit forward, and each block
streams out as a chunk. Throughput in requests per second is meaningless.
- **There is exactly one session.** The stepwise deploy config pins
`max_num_seqs: 1`, and the AR-Diffusion consumer rejects a stage with more than
one replica, because session-affine routing does not exist yet. There is no
concurrency to sweep.
- **What matters is cadence.** A deployment is healthy when chunk *N + 1* arrives
before a viewer finishes watching chunk *N*. That is a latency distribution
against a wall clock, not a throughput number.
## Quick start
Serve the model, then point the benchmark at it. The benchmark never starts a
server.
```bash
# Terminal 1 — one GPU, eager (portable smoke configuration)
vllm serve robbyant/lingbot-world-v2-14b-causal-fast-diffusers \
--omni \
--deploy-config benchmarks/lingbot_world/configs/single_gpu_eager.yaml \
--port 8000
# Terminal 2
pip install websockets
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
```
With no `--image`, the first frame comes from the shared vLLM image asset, so the
default workload needs no setup and this repository carries no benchmark binary.
Pass `--image /path/to/frame.png` to condition the rollout on your own frame.
### Realtime topology
The single-GPU config is a smoke configuration, not a realtime one: one card runs
all five forwards of every block serially and nothing is compiled. Use the
four-GPU config for numbers that mean anything about realtime behavior.
```bash
vllm serve robbyant/lingbot-world-v2-14b-causal-fast-diffusers \
--omni \
--deploy-config benchmarks/lingbot_world/configs/usp4_compiled.yaml \
--port 8000
python benchmarks/lingbot_world/benchmark_lingbot_world_realtime.py \
--port 8000 --num-chunks 40 --warmup-sessions 1 --sessions 3 \
--target-fps 12 --output-json /tmp/lingbot_world_repeated.json
```
`usp4_compiled.yaml` sets pure Ulysses sequence parallelism over four ranks and
`enforce_eager: false`. The pipeline validates the parallel shape itself:
`sequence_parallel_size` must equal `ulysses_degree`, `ring_degree` and
`allgather_degree` must be 1, `ulysses_mode` must be `strict`, and pipeline,
CFG, and VAE parallel sizes above 1 are rejected. This benchmark config sets
`tensor_parallel_size: 1`; the pipeline validator does not reject larger TP sizes.
Compiled mode can pay compilation and capture costs on the first rollout.
Use `--warmup-sessions 1` before measured repetitions. Excluding the first six
chunks addresses the attention-window ramp; it does not guarantee that
compilation and capture have finished.
## Metrics
| Metric | Meaning |
| --- | --- |
| `ttfc_ms` | **Time to first chunk**: `session.start` sent until the first chunk's bytes arrive. Pays for prompt encode, first-frame VAE encode, and any compile or capture. |
| `interval_all` | Inter-arrival latency across every chunk: mean, median, std, min, p90, p95, p99, max. |
| `interval_steady` | The same summary over steady-state chunks only. |
| `video_rtf` | Wall seconds spent per second of video, matching vLLM-Omni's own RTF (`generation_s / video_duration`). **Lower is better; below 1.0 is real time.** |
| `video_rtfx` | The reciprocal, under the name this repo uses for it: video seconds per wall second, where higher is better. |
| `steady_video_rtf` | `video_rtf` restricted to steady-state chunks. |
| `chunk_deadline_ms` | Wall-clock budget one steady chunk must meet to hold real time: 12 frames ÷ `--target-fps`. Override directly with `--chunk-slo-ms`. |
| `slo_attainment` | Fraction of steady chunks that met the deadline. |
| `playback` | Underrun count, total stall, and stall ratio from a simulated viewer. |
### Which frame rate counts as real time
**The checkpoint declares no frame rate.** There is none in `model_index.json`, the
scheduler config, or the transformer config, and the pipeline never reads one. A
chunk is twelve pixel frames; how long those frames *last* is a property of how the
output is played, not of the model.
That choice moves the verdict without moving anything measured:
| basis | video per chunk | deadline | RTF at a measured 1357 ms/chunk |
| --- | --- | --- | --- |
| 16 fps | 0.750 s | 750 ms | 1.809 |
| 12 fps | 1.000 s | 1000 ms | 1.357 |
So `--fps` (sent to the server, which uses it to label the muxed MP4) and
`--target-fps` (the real-time basis for RTF, the deadline, and the playback
simulation) are separate flags. `--target-fps` defaults to `--fps`; set it when the
two differ, and the report prints both. State the basis whenever you quote a number
— an RTF without its fps is not a measurement. (RTF is a cost here, so a bigger number is a
worse result; a steady chunk's RTF is simply its interval divided by its video duration.)
An RTF above 1 means the measured deployment is slower than real time at the
stated target frame rate. The benchmark measures this failure as well as success.
### Why warmup chunks are not optional
The DiT attends over a sliding window of `sliding_window_num_frames = 18` latent
frames with `sink_size = 9`, so the key count a block attends over keeps growing
until eighteen latent frames of history exist — six chunks at three frames per
block. **Chunk cost is not stationary before chunk six.** A mean over a short
rollout is therefore lower than anything a running session will sustain, which is
why `--warmup-chunks` defaults to 6 and steady-state metrics are reported
separately. Keep `--num-chunks` comfortably above the warmup, or the benchmark
tells you no chunk reached steady state. The terminal chunk skips next-chunk
preparation, so it is excluded from steady metrics too; it remains in overall
RTF, all-chunk intervals, and playback simulation.
The default 16-chunk rollout provides only nine steady intervals. Percentiles
are interpolated sample summaries; the report warns when fewer than 100
intervals contribute, and even 100 samples do not guarantee a stable p99.
Use longer rollouts for tail analysis and at least `--sessions 3` after
`--warmup-sessions 1` for reported comparisons. Session warmup pays compilation
cost; chunk warmup separately excludes the attention-window ramp. The command
above yields 33 steady intervals per session (indices 6 through 38), or 99 across
three sessions. Report each session's mean and the spread across sessions; this
sample still does not establish a reliable p99.
### Why playback is simulated
A p99 interval alone cannot say whether a viewer saw a stall: the same p99 is
invisible behind a three-chunk buffer and a visible freeze behind none. The
benchmark replays chunk arrivals against a wall clock — playback starts once
`--playback-buffer-chunks` chunks have landed and then consumes video at `--target-fps`
— and reports every moment the player ran dry.
### What the client-side number is worth
On a four-GPU Ulysses run the client's mean inter-arrival over all fifteen
post-first-chunk intervals was **1326.743 ms**, against the server's own
`StageRequestStats.inter_output_latency_ms` of **1327.479 ms** for the same
request — a difference of **0.74 ms**. This cross-check shows agreement in
average output cadence for that run, including warmup and the terminal chunk.
It does not measure encoding or transport latency: a fixed delivery delay can
cancel out of inter-arrival differences. Attributing that latency requires
per-chunk timestamps at both boundaries or server-side profiling.
Compare matching interval populations when investigating discrepancies; both
measurement bugs and delivery buffering can change the observed cadence.
### What this benchmark cannot see
Chunk metadata carries no server-side timestamps, so the benchmark cannot split
DiT time from VAE decode time; use `--enable-diffusion-pipeline-profiler` on the
server for that. It also reports no GPU memory, which is a server-side quantity.
## Custom rollouts
`--workload rollout.json` replaces the built-in workload, including mid-rollout
prompt changes, which the CLI flags cannot express:
```json
{
"prompt": "The camera moves slowly forward through the scene.",
"image": "/path/to/first_frame.png",
"num_chunks": 24,
"camera_pattern": "orbit",
"fps": 16,
"seed": 42,
"prompt_updates": [
{"after_chunk": 8, "prompt": "Rain begins to fall", "transition_chunks": 2}
]
}
```
Supply `camera_action_script` directly instead of `num_chunks` to control every
frame's action. It must hold exactly one three-entry action list per chunk; one
AR block is three latent frames, and the server rejects any other shape.
Explicit `--image`, `--prompt`, `--negative-prompt`, `--width`, `--height`,
`--fps`, `--seed`, and `--flow-shift` flags override the matching workload-file
values. `--num-chunks` and `--camera-pattern` apply only to the built-in
workload; when using `--workload`, set those fields in the file instead.
An explicit `camera_action_script` takes precedence over the file's
`num_chunks` and `camera_pattern`. Each prompt update must use a distinct
`after_chunk` boundary so its interaction event ID is unique.
Camera actions change what the world does, not what it costs, so `--camera-pattern`
(`forward`, `orbit`, `hold`) keeps a rollout representative rather than sweeping a
cost dimension.
## Options worth knowing
| Flag | Purpose |
| --- | --- |
| `--target-fps` | Playback rate the real-time verdict is measured against, independent of the mux label. |
| `--sessions N` | N sequential measured rollouts; the report adds spread across them. |
| `--warmup-sessions N` | Unmeasured rollouts first, to pay compile and capture cost. |
| `--print-chunks` | One line per chunk as it arrives. |
| `--save-video PATH` | Remux the last measured session for eyeballing (needs vLLM-Omni). |
| `--ping-interval` | `session.ping` cadence keeping the server's stall clock fresh during a slow first chunk. `0` disables. |
| `--first-chunk-timeout` | Raise it when the server is still compiling or capturing graphs. |
## Output
`--output-json` writes the run's configuration, every chunk's arrival time and
size, per-session metrics, and the aggregate, along with the git revision and
hostname so a number can be traced back to what produced it.
## Tests
`tests/benchmarks/test_lingbot_world_realtime.py` covers the chunk and frame
arithmetic against the server's own formula, the metric math, and the protocol
handling against a scripted WebSocket server. It needs no GPU:
```bash
pytest tests/benchmarks/test_lingbot_world_realtime.py
```