Signed-off-by: tzhouam <tzhouam@connect.ust.hk> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
225 lines
11 KiB
Markdown
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
|
|
```
|