Omni-DuplexEval CI Guard
Nightly end-to-end scoring regression for [Hothan/Omni-DuplexEval]
(RTD 6 subtasks + PR 3 subtasks), driven by vllm bench omni-duplex-eval
(generate → evaluate → summarize).
Files
| File | Purpose |
|---|---|
test_omni_duplex_eval_ci.py |
Main pytest: hardcoded thresholds + 6 assertions |
test_omni_duplex_eval_ci_mock.py |
CPU mock integration tests (no GPU / model needed) |
omni_duplex_eval_ci_config.json |
Dataset / revision / sample IDs / judge |
conftest.py |
judge_server fixture (endpoint resolution only) |
README.md |
This document |
results/.gitignore |
Track empty results dir for Buildkite artifact upload |
Run locally
Judge server must be pre-started; this test never launches it:
# CUDA (H100 / B200), or NPU (A3)
VLLM_DUPLEX_EVAL_JUDGE_URL=http://127.0.0.1:8001 \
pytest -s -v tests/e2e/accuracy/omni_duplex_eval/ \
-m "full_model and cuda and (H100 or B200) and cards_1"
The omni_server fixture boots MiniCPM-o-4_5 with the community duplex
params (tests/e2e/online_serving/helpers/minicpmo_4_5_duplex.py).
judge.base_url_env (default VLLM_DUPLEX_EVAL_JUDGE_URL) overrides
judge.base_url from the JSON config.
Results output
After Phase 3 (summarize), the full summarize_scores() dict is persisted
to results/summary.json (same pattern as PR#6817 perf tests) and printed
to stdout:
results/is tracked in git viaresults/.gitignore(empty dir, nothing committed inside).- On Buildkite nightly, the step uploads
results/*.jsonas artifacts (buildkite-agent artifact upload) even when assertions fail, so a failing run's scores remain downloadable.
Judge server
Judge is a pre-started, OpenAI-compatible endpoint (never launched by this
test). It can run on a separate machine / GPU from the omni_server.
Current judge: Qwen2.5-VL-7B-Instruct (judge.model in the JSON
config). The 7B model is chosen for Phase A to run on a single GPU (≈16 GB
FP16, or ≈5 GB with INT4 quantization); PR 0/1 classification and temporal
0–3 scoring are robust at this size. The 0.00–3.00 content scoring is the
weakest link (fine-grained counting / color / spatial checks) — if the
guard proves insensitive to regressions, upgrade the judge (e.g.
72B on FP8 H100 or an external API) and re-run the baseline to reset
_MIN_* thresholds. Changing the judge never affects the P0 assertions
(protocol_pin, sample count, by_task keys) or the CPU mock tests.
Example (any OpenAI-compatible vLLM server):
python -m vllm.entrypoints.openai.api_server \
--model Qwen/Qwen2.5-VL-7B-Instruct --port 8001 --max-model-len 65536 \
--allowed-local-media-path /tmp/omni_duplex_ci
If the judge is started on a different host or the media temporary directory
changes, adjust --allowed-local-media-path to match the path used by
vllm bench omni-duplex-eval evaluate for materialized videos.
CPU mock tests (no GPU / model / judge needed)
test_omni_duplex_eval_ci_mock.py drives the real
test_omni_duplex_eval_ci() guard on CPU by stubbing the heavy I/O:
_run_cliis replaced by an argv-recording spy (never executes).resolve_ref_audiois stubbed to a local path.- Fake score files are pre-written under
tmp_path/scores/<split>/fromsample_ids_per_split(minusexclude_ids), sosummarize_scores()and the Phase-4 assertions run for real.
It validates the CLI argument assembly (generate + evaluate), the
score-file-count check, protocol_pin, the RTD/PR by_task keys, and
all threshold comparisons — including the failure paths. Runs in CI on
every PR (core_model + cpu marks):
pytest -v tests/e2e/accuracy/omni_duplex_eval/test_omni_duplex_eval_ci_mock.py
Thresholds (record mode → tighten after first nightly run)
sample_ids_per_split contains real dataset IDs (verified against the
Hothan/Omni-DuplexEval splits; PR_correction / PR_event_reminder IDs are
inferred from the split row order and must be re-verified once the dataset
is reachable). exclude_ids has been removed.
All _MIN_* thresholds are currently 0.0 (record mode): the nightly run
produces real scores and persists results/summary.json without failing the
gate. Tighten after the first nightly run:
- Record
summarize_scores()output (mean content / temporal / success). - Set
_MIN_*thresholds =mean - n * std(startn=2) intest_omni_duplex_eval_ci.py. As a starting point, the paper's MiniCPM-o 4.5 scores (100-point scale) map to ≈1.15 content / ≈2.40 temporal / ≈0.20 PR on the code scales (see "Reference paper" below); ~50% of those is a reasonable lower bound. - If needed, exclude known bad samples (generate hang / judge failure) via
exclude_ids.
Thresholds are bound to the judge model: switching the judge requires
re-running steps 1–2. If the 7B judge shows high variance (small models
are more prompt-sensitive), widen n (e.g. n=3) or increase the sample
size before tightening.
Assertions (in order)
protocol_pin == PROTOCOL_PIN(track upstream protocol changes)summary["samples"]== number of selected IDs- RTD
by_taskkeys == 6 RTD subtasks - PR
by_taskkeys == 3 PR subtasks - RTD mean content / temporal scores >= thresholds
- PR
mean_all_success>= threshold
Reference paper & scoring scale
Reference paper: Omni-DuplexEval (arXiv:2605.17360). The paper reports
a 100-point scale, while the evaluation code
(vllm_omni/benchmarks/duplex/omni_duplex_eval_metrics.py) uses a 3-point
content scale (0.00–3.00), a 0–3 integer temporal scale, and a 0/1 PR
success flag. The CI thresholds are defined against the code's scales
(the source of truth for summarize_scores() output), not the paper's
100-point scale.