135 lines
5.7 KiB
Markdown
135 lines
5.7 KiB
Markdown
# 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:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# 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 via `results/.gitignore` (empty dir, nothing
|
|||
|
|
committed inside).
|
|||
|
|
- On Buildkite nightly, the step uploads `results/*.json` as 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):
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
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_cli` is replaced by an argv-recording spy (never executes).
|
|||
|
|
- `resolve_ref_audio` is stubbed to a local path.
|
|||
|
|
- Fake score files are pre-written under `tmp_path/scores/<split>/` from
|
|||
|
|
`sample_ids_per_split` (minus `exclude_ids`), so `summarize_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):
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
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:
|
|||
|
|
|
|||
|
|
1. Record `summarize_scores()` output (mean content / temporal / success).
|
|||
|
|
2. Set `_MIN_*` thresholds = `mean - n * std` (start `n=2`) in
|
|||
|
|
`test_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.
|
|||
|
|
3. 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)
|
|||
|
|
|
|||
|
|
1. `protocol_pin == PROTOCOL_PIN` (track upstream protocol changes)
|
|||
|
|
2. `summary["samples"]` == number of selected IDs
|
|||
|
|
3. RTD `by_task` keys == 6 RTD subtasks
|
|||
|
|
4. PR `by_task` keys == 3 PR subtasks
|
|||
|
|
5. RTD mean content / temporal scores >= thresholds
|
|||
|
|
6. 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.
|