Vllm Pytorch CI Triage
pytorch/test-infra
Root-cause a vLLM torch-nightly CI regression report. An agent skill from pytorch/test-infra.
Generate and run tests for vllm-project/vllm-omni with CI-aligned levels and markers; wire new tests into Buildkite (test-ready.yml for L1/L2, test-merge.yml for L3, test-nightly.yml for L4).
$ npx skills add vllm-project/vllm-omni --skill vllm-omni-test -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install vllm-project/vllm-omni vllm-omni-test --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/vllm-project/vllm-omni.git skills-src && mkdir -p .claude/skills && cp -r skills-src/.claude/skills/vllm-omni-test .claude/skills/vllm-omni-test && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "vllm-omni-test" agent skill from https://github.com/vllm-project/vllm-omni/tree/main/.claude/skills/vllm-omni-test into .claude/skills/vllm-omni-test/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "vllm-omni-test", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/vllm-project/vllm-omni/tree/main/.claude/skills/vllm-omni-testType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add vllm-project/vllm-omni --skill vllm-omni-test -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install vllm-project/vllm-omni vllm-omni-test --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/vllm-project/vllm-omni.git skills-src && mkdir -p .agents/skills && cp -r skills-src/.claude/skills/vllm-omni-test .agents/skills/vllm-omni-test && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "vllm-omni-test" agent skill from https://github.com/vllm-project/vllm-omni/tree/main/.claude/skills/vllm-omni-test into .agents/skills/vllm-omni-test/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "vllm-omni-test", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add vllm-project/vllm-omni --skill vllm-omni-test -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install vllm-project/vllm-omni vllm-omni-test --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/vllm-project/vllm-omni.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/.claude/skills/vllm-omni-test .cursor/skills/vllm-omni-test && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "vllm-omni-test" agent skill from https://github.com/vllm-project/vllm-omni/tree/main/.claude/skills/vllm-omni-test into .cursor/skills/vllm-omni-test/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "vllm-omni-test", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/vllm-project/vllm-omni.git --path .claude/skills/vllm-omni-test--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add vllm-project/vllm-omni --skill vllm-omni-test -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install vllm-project/vllm-omni vllm-omni-test --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/vllm-project/vllm-omni.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/.claude/skills/vllm-omni-test .gemini/skills/vllm-omni-test && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "vllm-omni-test" agent skill from https://github.com/vllm-project/vllm-omni/tree/main/.claude/skills/vllm-omni-test into .gemini/skills/vllm-omni-test/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "vllm-omni-test", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install vllm-project/vllm-omni vllm-omni-testInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add vllm-project/vllm-omni --skill vllm-omni-test -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/vllm-project/vllm-omni.git skills-src && mkdir -p .github/skills && cp -r skills-src/.claude/skills/vllm-omni-test .github/skills/vllm-omni-test && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "vllm-omni-test" agent skill from https://github.com/vllm-project/vllm-omni/tree/main/.claude/skills/vllm-omni-test into .github/skills/vllm-omni-test/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "vllm-omni-test", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add vllm-project/vllm-omni --skill vllm-omni-test -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install vllm-project/vllm-omni vllm-omni-test --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/vllm-project/vllm-omni.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/.claude/skills/vllm-omni-test .opencode/skills/vllm-omni-test && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "vllm-omni-test" agent skill from https://github.com/vllm-project/vllm-omni/tree/main/.claude/skills/vllm-omni-test into .opencode/skills/vllm-omni-test/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "vllm-omni-test", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
vllm-omni-testGenerate and run tests for vllm-project/vllm-omni with CI-aligned levels and markers; wire new tests into Buildkite (test-ready.yml for L1/L2, test-merge.yml for L3, test-nightly.yml for L4).
Vllm Omni Test is an agent skill from vllm-project/vllm-omni. Generate and run tests for vllm-project/vllm-omni with CI-aligned levels and markers; wire new tests into Buildkite (test-ready.yml for L1/L2, test-merge.yml for L3, test-nightly.yml for L4). On completion, always provide copy-paste local and CI-like pytest commands plus prerequisites. Use when creating regression tests, adding L1-L4 coverage, selecting pytest markers, or validating fixes from issues/PRs.
Its SKILL.md is about 17k tokens, which your agent loads only when the skill is triggered. The skill folder holds 2 other files, including reference files (for example `references/test-routing.md`).
It sits in AI & LLM Engineering, covering LLM inference and serving and Unit testing. It works with vLLM and pytest. The repository describes itself as: A framework for efficient model inference with omni-modality models. The licence is Apache-2.0.
7 steps, taken from the step headings in SKILL.md.
Read from SKILL.md and the folder at commit 096988d. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
Shell commands in SKILL.md call:
pytestFrom the folder's file list and the shell code blocks in SKILL.md.
No URLs in SKILL.md.
From URLs in SKILL.md, links to its own repository left out.
Names these keys or tokens, usually read from environment variables:
HF_TOKENFrom names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Vllm Omni Test loads about 17k tokens when it runs, and up to ~23k if it reads all its reference files. Until then it costs about 106 tokens; SKILL.md has 5,880 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check found no risky patterns in SKILL.md.
Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.
The full file from vllm-project/vllm-omni at commit 096988d, republished under its Apache-2.0 licence (© vllm-project). 5,880 words, ~17,278 tokens.
.claude/skills/vllm-omni-test/SKILL.md (or your agent's skills folder). This skill also uses 1 other file; get the full folder from GitHub.Use this skill to generate minimal, stable test cases and run them with the correct marker/level strategy for vllm-project/vllm-omni.
Link convention: Paths such as .buildkite/ and docs/contributing/ live at the vllm-omni repo root. Markdown links use repo-relative paths from this skill file (e.g. ../../../.buildkite/cuda/test-ready.yml, ../../../docs/contributing/ci/test_system_overview.md).
Default priorities:
pytest lines (local: single file and/or single test; CI-like: markers + --run-level), plus short prerequisites (GPU tier, HF cache, optional model_prefix). Do not assume the reader will infer commands from test-routing.md alone.required / recommended / not_needed: required — stable logic/contract bug that should have been caught; recommended — environment-sensitive but a small regression still helps; not_needed — one-off external/config failure or existing tests already cover the path. Prefer the narrowest stable L1 (CPU) case; escalate to L2/L3 only when the bug needs real weights or serving.Use references/test-routing.md for level-to-marker and command mapping.
Always attach markers deliberately:
core_model (L1/L2) and/or advanced_model (L3) and/or full_model (L4 nightly)omni — end-to-end multimodal LLM pipelines (thinker/talker/stages; Qwen-Omni family)tts — speech synthesis / TTS-only models (/v1/audio/speech, voice clone, etc.)diffusion — generative diffusion models (image / audio / text / video from noise)parallel, cache, example, benchmarkcpu, gpu, cuda, rocm, npu, …; SKU (H100, L4, …) and cards_{n} via @hardware_test(...) / hardware_marks(...) only — never pytest.mark.H100 (check-mark rejects it)slow. Multi-card: num_cards=N on the helper (filter with cards_N / not cards_1)Baseline smoke (L2 + L3): The simplest e2e case per model — default deploy, minimal request — should usually carry both @pytest.mark.core_model and @pytest.mark.advanced_model on the same test function so test-ready.yml and test-merge.yml share one test. send_*_request picks validation depth from --run-level. References: test_voxcpm2_tts.py::test_text_to_audio_001, test_qwen3_tts_customvoice.py::test_text_to_audio_001. Heavier scenarios use advanced_model only; L4 expansion uses full_model.
For hardware-aware tests, prefer @hardware_test(...) or hardware_marks(...) in tests/helpers/mark.py.
Diffusion nightly split (test-nightly.yml): all diffusion tests use pytest.mark.diffusion, but CI groups them by output modality, not by separate pytest markers:
| Nightly group | Scope | Typical models / paths |
|---|---|---|
| Diffusion X2I(&A&T) Model Test | x2i (image), x2a (audio), x2t (text) and other non-video diffusion | Qwen-Image*, BAGEL, FLUX, SD3, Z-Image, LongCat, DreamZero, … — test_*_expansion.py under X2I shards; L4 sweep uses -k "not test_wan and not test_bagel_expansion and not hunyuan" for L4 |
| Diffusion X2V Model Test | x2v (video) only | Wan2.2, HunyuanVideo 1.5, Wan VACE, … — e.g. test_wan22_expansion.py, test_hunyuan_video_15_expansion.py |
Wire new video diffusion expansion tests into the X2V group; wire image/audio/text diffusion expansion tests into X2I(&A&T). Do not place x2v modules in X2I shards (see comment in test-nightly.yml above the X2I group).
When adding new pytest modules whose primary scope is a specific model (typical under tests/e2e/offline_inference/ or tests/e2e/online_serving/), use this filename pattern:
| Level | Filename pattern | Example (model Qwen/Qwen2.5-Omni-7B) |
|---|---|---|
| L2, L3 | test_{lowercase_model_slug}.py | test_qwen2_5_omni.py |
| L4 | test_{lowercase_model_slug}_expansion.py | test_qwen2_5_omni_expansion.py |
Slug rules for {lowercase_model_slug}:
Qwen/Qwen2.5-Omni-7B), but do not put the org into the filename: use the repo segment only (Qwen2.5-Omni-7B), not Qwen_... / qwen_qwen2_5_....., -, and whitespace with a single _ (e.g. Qwen2.5-Omni-7B → qwen2_5_omni). Omit trailing size tokens such as 7b / 30b in the basename when a single file covers that model line in the directory (matches test_qwen2_5_omni.py in-tree)._7b vs _3b) only then.tests/<area>/test_<feature>.py as today.Routing tables and commands: references/test-routing.md § Model-centric e2e filename convention.
Existing references: tests/e2e/offline_inference/test_qwen2_5_omni.py (L2-style omni), tests/e2e/offline_inference/test_qwen3_5_9b.py (L2-style omni, single-stage VL), tests/e2e/online_serving/test_qwen3_omni_expansion.py (L4-style omni), tests/e2e/online_serving/test_qwen_image_edit_expansion.py / test_qwen_image_expansion.py (L4-style diffusion).
1. Pick the functional scenario (then choose directory, fixtures, and markers):
| Scenario | Typical location | Fixtures / runner pattern | Baseline markers & level |
|---|---|---|---|
| Offline inference e2e | tests/e2e/offline_inference/ | Module (default): omni_runner + offline_client. Function (isolation only): omni_runner_function + offline_client_function. Diffusion/TTS may use Omni(...).generate directly | L2: core_model + one of omni / tts / diffusion; @hardware_test(...) when GPU/NPU is required |
| Online serving e2e | tests/e2e/online_serving/ | Module (default): omni_server + online_client. Function (isolation only): omni_server_function + online_client_function. Clients: send_omni_request (omni), send_audio_speech_request (tts), send_diffusion_request / send_video_diffusion_request / send_images_generations_request (diffusion) | Baseline smoke: core_model + advanced_model; heavier paths: advanced_model only; L4 expansion: full_model |
| Documentation / runnable examples | tests/examples/offline_inference/, tests/examples/online_serving/ | Offline docs (preferred): extract Python/Bash blocks from the doc README (e.g. ReadmeSnippet.extract_readme_snippets), pytest.mark.parametrize each snippet, run via example_runner.run with a stable output_subfolder. Online docs: copy client/request scripts into dedicated tests and keep them in sync with the doc page. | Usually L4: advanced_model, often example plus hardware marks matching the nightly docs-example job (see .buildkite/cuda/test-nightly.yml). Full conventions: docs/contributing/ci/test_examples/l4_doc_example_tests.inc.md (introduced in PR #1910: naming, output directory layout, skip rules, avoid trimming num_inference_steps without a strong CI reason). |
| Performance / benchmark | tests/dfx/perf/tests/*.json + run_*_benchmark.py | JSON or script-driven server + load config; assert explicit metrics / baselines | L4 Perf: JSON mark with full_model + omni/tts/diffusion; wire test-nightly.yml Perf steps |
| Invalid parameter / negative HTTP validation | tests/dfx/reliability/invalid_param_test/ | Live omni_server + low-level send_*_http_request with err_code / err_message | pytest.mark.slow + omni / tts / diffusion + @hardware_marks (H100 or L4); CI in test-weekly.yml (not ready/merge/nightly) |
If the user’s test plan includes invalid parameter validation / invalid params / negative HTTP / 400 validation: do not add those test_* functions to tests/e2e/online_serving/test_*.py or *_expansion.py. Move or author them under tests/dfx/reliability/invalid_param_test/ in the endpoint-matching script (see Invalid parameter validation below). Success-path e2e and invalid-param dfx tests must stay in separate modules.
1b. Model type — omni vs tts vs diffusion
After choosing offline/online/docs/perf, classify the product under test and attach exactly one model-type marker. All three can live under the same tests/e2e/ trees; conventions diverge:
| Dimension | Omni (pytest.mark.omni) | TTS (pytest.mark.tts) | Diffusion (pytest.mark.diffusion) |
|---|---|---|---|
| What it is | Multimodal LLM pipeline (thinker/talker/stages; text + vision + audio I/O) | Speech synthesis / voice models | Generative diffusion (noise → image, audio, text, or video) |
| Examples | Qwen2.5-Omni, Qwen3-Omni | Qwen3-TTS, VoxCPM2, Higgs-Audio, Step-Audio2 | Qwen-Image, BAGEL, Wan2.2, HunyuanVideo |
| Offline runner | omni_runner + offline_client, generate_multimodal | Omni(...).generate with deploy YAML + TTS params | Omni(...).generate + OmniDiffusionSamplingParams |
| Online client | online_client.send_omni_request (chat completions, modalities) | online_client.send_audio_speech_request (/v1/audio/speech) | send_diffusion_request (chat/T2I), send_video_diffusion_request (/v1/videos, X2V), or send_images_generations_http_request / send_images_edits_http_request (DALL-E routes) |
| Typical assertions | Stage outputs, text/audio keywords via OfflineOmniClient / response handler | WAV bytes, stream chunks, speech endpoint contract | Image/video dimensions, final_output_type, assert_diffusion_response |
| Deploy YAML | Per-model deploy configs (ci/qwen3_omni_moe.yaml, …) | qwen3_tts.yaml, voxcpm2.yaml, … | Often default serve; parallel/offload YAML for heavy DiT |
Nightly group (test-nightly.yml) | Omni Model Test — -m "full_model and omni" | TTS Model Test — -m "full_model and tts" | Diffusion X2I(&A&T) or Diffusion X2V (see Step 3 table; same diffusion marker, different YAML group / file shard) |
| L4 pressure | Expansion per modality/model as needed | Expansion + accuracy/perf in TTS group | X2I: merge feature combos per #1832; X2V: separate nightly group |
Do not mix fixtures across types (e.g. do not use omni_runner layout for a pure diffusion or TTS model without mirroring an in-tree test in that family).
Diffusion only — X2I(&A&T) vs X2V (nightly routing, not extra markers):
When adding a new test_*_expansion.py, place it in the matching nightly group step (explicit file list in test-nightly.yml), not only by marker expression.
2. Use the narrowest deterministic skeleton for the scenario
L1 unit / logic (CPU-first):
Mocking rule (L1 only): use pytest integration — mocker (pytest-mock) or monkeypatch (built-in). Do not import or call unittest.mock (patch, MagicMock, @patch, with patch(...), etc.) in L1 tests; patches must auto-revert with the test lifecycle.
import pytest
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
def test_<scenario_name>(mocker):
# Prefer mocker.patch / mocker.spy / mocker.Mock — not unittest.mock.patch
fake_fn = mocker.patch("vllm_omni.some.module.expensive_call", return_value=...)
# Act
# Assert
fake_fn.assert_called_once()
def test_<env_or_attr>(monkeypatch):
# For simple env / attribute substitution without a Mock object
monkeypatch.setenv("SOME_FLAG", "1")
monkeypatch.setattr("vllm_omni.some.module.CONST", 42)
...Avoid in L1:
# BAD — do not use in L1 unit tests
from unittest.mock import patch, MagicMock
@patch("vllm_omni.some.module.fn")
def test_bad(mock_fn): ...
def test_also_bad():
with patch("...") as m: ...See L1 unit test constraints (mocking) below for the full do/don't list.
Offline multimodal e2e — Omni (representative):
@pytest.mark.core_model
@pytest.mark.omni
@hardware_test(...)
@pytest.mark.parametrize("omni_runner", test_params, indirect=True)
def test_<scenario>(omni_runner, offline_client) -> None:
request_config = {"prompts": ..., "modalities": [...]} # optional: images, videos, audios
offline_client.send_omni_request(request_config)Offline generative e2e — Diffusion (representative):
@pytest.mark.core_model
@pytest.mark.diffusion
@hardware_test(...)
@pytest.mark.parametrize("omni_runner", test_params, indirect=True)
def test_text_to_image_001(offline_client) -> None:
offline_client.send_diffusion_request({"prompt": "...", "extra_body": {"num_inference_steps": 4, ...}})Offline TTS e2e — Qwen3-TTS (two-stage; representative):
@pytest.mark.advanced_model
@pytest.mark.tts
@hardware_test(...)
@pytest.mark.parametrize("omni_runner", tts_server_params, indirect=True)
def test_text_to_audio_001(omni_runner, offline_client) -> None:
offline_client.send_audio_speech_request({
"input": "...",
"task_type": "Base",
"ref_audio": REF_AUDIO_URL,
"ref_text": REF_TEXT,
})Offline TTS e2e — single-stage (Coqui XTTS, MOSS-TTS-Nano; representative):
@pytest.mark.advanced_model
@pytest.mark.tts
@hardware_test(...)
@pytest.mark.parametrize("omni_runner", tts_server_params, indirect=True)
def test_voice_clone_001(offline_client) -> None:
offline_client.send_single_stage_tts_request({
"input": "...",
"language": "en",
"prompt_audio_path": REF_AUDIO_PATH,
"response_format": "wav",
"run_level": "advanced_model",
})Online serving e2e — Omni (representative):
@pytest.mark.core_model
@pytest.mark.omni
@hardware_test(...)
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
def test_text_to_text_001(omni_server, online_client) -> None:
request_config = {"model": omni_server.model, "messages": ..., "modalities": ["text"]}
online_client.send_omni_request(request_config)Online serving e2e — TTS (representative):
@pytest.mark.core_model
@pytest.mark.advanced_model
@pytest.mark.tts
@hardware_test(...)
@pytest.mark.parametrize("omni_server", tts_server_params, indirect=True)
def test_text_to_audio_001(omni_server, online_client) -> None:
request_config = {"model": omni_server.model, "input": "...", "response_format": "wav", ...}
online_client.send_audio_speech_request(request_config)Online serving e2e — Diffusion X2I (representative):
@pytest.mark.core_model
@pytest.mark.diffusion
@pytest.mark.parametrize("omni_server", _get_default_case(MODEL), indirect=True)
def test_text_to_image_001(omni_server, online_client) -> None:
online_client.send_diffusion_request({...}) # chat completions + extra_bodyOnline serving e2e — Diffusion X2V (representative):
@pytest.mark.core_model
@pytest.mark.diffusion
def test_text_to_video_001(omni_server, online_client) -> None:
online_client.send_video_diffusion_request({"model": ..., "form_data": {...}}) # /v1/videosDocumentation example tests: follow the Preferred Test Strategy in l4_doc_example_tests.inc.md: dynamic extraction for offline READMEs; explicit copied client code for online pages until extraction is justified; use the documented naming, output directory (page folder + case id), and skipping rules (e.g. Gradio-only scripts).
Performance tests: add or extend entries under tests/dfx/perf/tests/ (and JSON configs where the project uses them), with explicit baselines, mark on each case (hardware_marks + full_model + type marker), and the same nightly Perf step pattern as in-tree configs.
3. Cross-cutting rules
.py files need the Omni SPDX header (Copyright contributors to the vLLM-Omni project, not vLLM project). tests/ may keep stdlib re/base64; pickle is still banned unless the file is already on the pickle allowlist. Every collected tests/**/test_*.py needs a CI level mark and a hardware platform mark/helper (check-mark runs locally; GHA skips it). Do not write pytest.mark.H100 / pytest.mark.L4; use the helpers so cards_{n} is attached. Do not grow allowed_files to land a test. Policy: docs/contributing/README.md.OmniRunner tests without a clear reason.test_<scenario> function with a name that states what is validated (endpoint, size/n, server flag, or route). Do not merge multiple cases into a single test that branches on request.node.callspec.id, param.id, or if case_id == .... Use @pytest.mark.parametrize("omni_server", [...], indirect=True) per function (usually one OmniServerParams per test). A loop inside one test is OK only when it serves that function’s single intent (e.g. three standard sizes in test_*_sizes_256_512_1024).tests/helpers/fixtures/runtime.py): default omni_server / omni_runner (module) + matching client/handler; use omni_server_function / omni_runner_function only when each test_* must start a fresh instance (see below).unittest.mock; use mocker or monkeypatch only (see below).send_*_request in tests/helpers/runtime.py when it exists; otherwise add the helper in runtime.py first, then call it from the test. General assert_* inside send_*_request; special assert_* only in the test. See Runtime send helpers below — do not call omni.generate, raw HTTP, or SDK clients from test_*.py.tests/helpers/assertions.py — not as private _assert_* helpers inside test_*.py (see below).MODEL, deploy path, REF_AUDIO_URL, get_prompt(), _build_request(), and inline request_config dicts live in test_{slug}.py / test_{slug}_expansion.py (and offline/L1 siblings). Do not create tests/helpers/{slug}.py to deduplicate them; a little copy across files is preferred (see test_glm_tts.py, test_cosyvoice3_tts_expansion.py). tests/helpers/ is repo-wide harness only (mark, media, runtime, stage_config, assertions, fixtures/).L1 tests (core_model and cpu, under tests/diffusion/, tests/engine/, tests/model_executor/, etc.) must follow the repo’s pytest-mock convention (pytest-mock>=3.10.0 in pyproject.toml [project.optional-dependencies] dev).
| Do | Don't |
|---|---|
def test_foo(mocker): + mocker.patch(...), mocker.spy(...), mocker.Mock(), mocker.MagicMock(), mocker.AsyncMock() | from unittest.mock import patch, MagicMock, Mock, AsyncMock |
def test_bar(monkeypatch): + monkeypatch.setattr / setenv / delenv / setitem | @patch(...) decorator |
Let mocker auto-stop patches after the test | with patch(...): / patch.object(...) context managers |
| Mirror neighboring L1 tests in the same directory | unittest.mock.create_autospec unless an existing file already documents an exception |
Rationale: mocker ties patch lifecycle to pytest fixtures (no leaked patches across tests). unittest.mock decorators/context managers are easy to compose incorrectly with parametrized or async tests and are inconsistent with in-tree L1 style.
Minimal patterns:
def test_returns_cached_config(mocker):
loader = mocker.patch(
"vllm_omni.foo.load_yaml",
return_value={"stages": []},
)
result = get_deploy_config("ci/foo.yaml")
assert result["stages"] == []
loader.assert_called_once_with("ci/foo.yaml")
def test_skips_when_env_unset(monkeypatch):
monkeypatch.delenv("VLLM_OMNI_FEATURE", raising=False)
assert should_enable_feature() is FalseE2E levels (L2+) generally avoid mocks; if a rare L2 stub is unavoidable, still prefer mocker over unittest.mock for consistency.
tests/helpers/fixtures/runtime.py)vLLM-Omni e2e tests start a real OmniServer (online) or OmniRunner (offline). Pick scope by how often the process must be recreated, not by test level alone.
| Scope | Online fixtures | Offline fixtures | When to use |
|---|---|---|---|
| Module (default) | omni_server → online_client | omni_runner → offline_client | Default for L2/L3/L4 expansion — amortize model/server init across test_* in the same module. Same OmniServerParams / runner config can be reused by multiple tests. |
| Function | omni_server_function → online_client_function | omni_runner_function → offline_client_function | Only when required — each test_* must get a clean server/runner (no shared engine/GPU state). Typical: tests/dfx/reliability/, sleep/wakeup, crash/restart, tests that mutate global server state. |
Rules:
omni_server / omni_runner) unless the scenario explicitly needs a fresh instance per test function.@pytest.mark.parametrize("omni_server", ...) with omni_server + online_client; @pytest.mark.parametrize("omni_server_function", ...) with omni_server_function + online_client_function. Do not mix module fixture with function client (or vice versa).OmniServerParams per test_* (e.g. default vs --enable-cpu-offload) is still OK with module omni_server — pytest parametrizes per test node; only switch to _function when isolation between tests matters, not merely because server_args differ.test_* with many server configs (expansion matrix) → single function + @pytest.mark.parametrize("omni_server", [...], indirect=True) + module omni_server (see in-tree test_qwen_image_expansion.py).# Default — module-scoped server (L4 expansion)
@pytest.mark.parametrize("omni_server", [pytest.param(OmniServerParams(model=MODEL), marks=H100)], indirect=True)
def test_foo_images_generations_default_1024(omni_server, online_client) -> None:
online_client.send_images_generations_request({...})
# Function-scoped — reliability / per-test clean state only
@pytest.mark.parametrize(
"omni_server_function",
[pytest.param(OmniServerParams(model=MODEL), marks=H100)],
indirect=True,
)
def test_foo_sleep_wakeup_cycle(omni_server_function, online_client_function) -> None:
online_client_function.send_omni_sleep_http_request({...})
online_client_function.send_omni_wakeup_http_request({...})tests/helpers/runtime.py)L2+ e2e (online serving and offline inference) must call APIs through tests/helpers/runtime.py. Fixtures live in tests/helpers/fixtures/runtime.py; send/assert implementation lives in tests/helpers/runtime.py + tests/helpers/assertions.py.
| Principle | Action |
|---|---|
| Reuse first | Grep runtime.py for an existing send_*_request on OnlineOmniClient (online) or OfflineOmniClient (offline) that matches the endpoint / pipeline shape. |
| Extend when close | If an existing send_*_request almost fits (e.g. missing one optional field), extend it in runtime.py — do not fork logic in the test file. |
| Add when missing | No suitable helper → add send_<feature>_request (high-level: call + general assert_*) or send_<route>_<verb>_http_request (low-level HTTP for negative/dfx) in runtime.py first, then call it from tests. |
| Test module owns payload only | In test_*.py: MODEL, deploy path, vendored media, get_prompt(), and inline request_config dicts only. No omni.generate(...), raw requests.post, OpenAI SDK calls, or _collect_audio() / _process_output() in e2e tests. |
Online (online_client from omni_server): OnlineOmniClient.send_*_request.
Offline (offline_client from omni_runner): OfflineOmniClient.send_*_request.
| Do | Don't |
|---|---|
Online: online_client.send_omni_request, send_diffusion_request, send_audio_speech_request, send_video_diffusion_request, send_images_generations_request, … | requests.post(f"{base_url}/v1/...", json=…) or client.chat.completions.create(...) inside a test |
Offline: offline_client.send_omni_request, send_diffusion_request, send_audio_speech_request, send_single_stage_tts_request, send_single_stage_tts_batch_request, … | omni_runner.omni.generate(...) + hand-rolled tensor/WAV extraction in test_*.py |
Add missing send_* to runtime.py first; bundle general assert_* inside the send helper | A one-off def _post_* / def _collect_audio at the bottom of a test module |
Mirror naming/style of neighboring send_* (docstring, request_config dict, optional run_level, err_code / err_message for negative cases) | Different parameter shapes per test file for the same endpoint |
Workflow when generating online/offline e2e tests:
send_*) or special (one case / one parameter combo only). See General vs special assert placement below.runtime.py for a matching send_*_request; if missing, add it (with general assert_* inside) before writing the test body.request_config → call send_*_request only for the general contract; call assert_* in the test file only for special, case-specific checks (import from assertions.py).send_*_http_request for negative/dfx tests (err_code / err_message) — not for ordinary L2+ success-path e2e.source_file_dependencies, include tests/helpers/runtime.py and/or tests/helpers/assertions.py when new helpers were added.Exceptions (document in the test docstring why): models whose offline prompt path cannot go through existing handlers yet (e.g. test_higgs_audio_v2.py, test_voxtral_tts.py with custom tokenizer compose) may call omni.generate directly until a send_*_request is added to runtime.py — treat as debt, not the default for new TTS/diffusion/omni e2e.
General vs special assert placement:
| Kind | Definition | Where it lives | Test file calls |
|---|---|---|---|
| General | Default success contract for this endpoint/client method — e.g. HTTP 200, data[] shape, n count, size dimensions, decodable image/audio, bundled omni/diffusion fields | Implement in assertions.py, invoke inside send_*_request in runtime.py (after low-level send / SDK call, when err_code is not set) | send_*_request only — do not repeat the same assert_* |
| Special | Extra check tied to this test case only — e.g. seed byte-identical replay, accuracy/CLIP threshold, perf ceiling, model-specific optional field | Add or reuse assert_* in assertions.py (never inline in test) | send_*_request then assert_<special>(...) once for that case |
Changing request_config fields (size, n, seed, server flags) is not special validation — the general assert_* should read those from request_config inside send_*.
send_* ↔ assert_* pairing (online OnlineOmniClient):
High-level send_* (prefer in L2+ e2e) | Assert already invoked inside runtime.py | Low-level HTTP-only sibling |
|---|---|---|
send_omni_request | assert_omni_response | send_chat_completions_http_request → assert_http_error only |
send_diffusion_request | assert_diffusion_response | — |
send_audio_speech_request | assert_audio_speech_response | send_audio_speech_http_request → assert_http_error only |
send_video_diffusion_request | assert_diffusion_response | send_videos_*_http_request → assert_http_error only |
send_images_generations_request (add when needed) | assert_images_generations_response (general — inside send) | send_images_generations_http_request → assert_http_error only |
send_images_edits_request (add when needed) | assert_images_edits_response (general — inside send) | send_images_edits_http_request → assert_http_error only |
Rule: Tests call high-level send_*_request for the general contract. Never call the same bundled assert_* again. Call an extra assert_* in the test only for special, case-specific validation.
Common OnlineOmniClient entry points (non-exhaustive — grep runtime.py before adding):
| Area | High-level (SDK + assert) | Low-level HTTP (*_http_request) |
|---|---|---|
| Omni chat | send_omni_request | send_chat_completions_http_request |
| Diffusion T2I (chat route) | send_diffusion_request | — |
| Diffusion X2V | send_video_diffusion_request | send_videos_create_http_request, send_video_content_http_request, … |
| DALL-E T2I / edit | send_images_generations_request, send_images_edits_request | send_images_generations_http_request, send_images_edits_http_request |
| TTS (online) | send_audio_speech_request | send_audio_speech_http_request, send_audio_generate_http_request, … |
| Ops / meta | — | send_health_http_request, send_models_http_request, send_omni_sleep_http_request, … |
Common OfflineOmniClient entry points (offline — grep runtime.py before adding):
| Area | High-level (send_*_request + assert) | Notes |
|---|---|---|
| Omni multimodal | send_omni_request | generate_multimodal path |
| Diffusion offline | send_diffusion_request | chat-route / OmniTextPrompt offline |
| Qwen-style TTS | send_audio_speech_request | two-stage, generate_multimodal + mm_processor_kwargs |
| Single-stage TTS (Coqui XTTS, MOSS-TTS-Nano, …) | send_single_stage_tts_request, send_single_stage_tts_batch_request | prompt + additional_information + omni.generate — do not duplicate in tests |
| New model family | Add send_<family>_request here first | Then call from tests/e2e/offline_inference/test_*.py |
L1 tests under tests/entrypoints/ may use FastAPI TestClient or direct handler calls with mocks; they do not need OnlineOmniClient / OfflineOmniClient. L2+ online and offline e2e must use runtime.py send_* helpers.
tests/dfx/reliability/invalid_param_test/)When the user asks for invalid parameter validation, invalid request bodies, HTTP 4xx contract tests, or any case that sends malformed / out-of-range / mismatched API payloads against a live server:
tests/e2e/online_serving/ or *_expansion.py. If already drafted there, move the test_* into the correct invalid_param_test script and delete the duplicate from e2e.test_invalid_<area>.py only when no in-tree script covers that route family):| API / area | Script |
|---|---|
| Omni chat completions, WebSocket video/realtime paths | test_invalid_omni_chat.py |
POST /v1/audio/speech, stream, batch, voices | test_invalid_audio_speech.py |
| Audio diffusion endpoints | test_invalid_audio_diffusion.py |
POST /v1/images/generations | test_invalid_image_generation.py |
POST /v1/images/edits | test_invalid_image_editing.py |
POST/GET/DELETE /v1/videos* | test_invalid_video_generation.py |
| Sleep / wakeup / server control | test_invalid_server_control.py |
| Element | Convention |
|---|---|
| Module markers | pytestmark = [pytest.mark.slow, pytest.mark.<omni or tts or diffusion>] |
| Server fixture | _PARAMS / _QWEN3_TTS_SPEECH-style list of pytest.param(OmniServerParams(...), id="...", marks=hardware_marks(...)); @pytest.mark.parametrize("omni_server", _PARAMS, indirect=True) |
| Hardware | hardware_marks(res={"cuda": "H100"}) for heavy diffusion/omni/video; hardware_marks(res={"cuda": "L4"}) for smaller TTS models (must match weekly -m "slow and L4" step) |
| HTTP client | Low-level online_client.send_*_http_request({..., "err_code": 400, "err_message": (...)} ) — not send_*_request (success path) |
| Case shape | Prefer one test_* per route family + @pytest.mark.parametrize("body_spec, err_message", [...]) with stable id= per case; or dedicated test_<route>_malformed_json when not parametrized |
| Body helpers | _minimal_<endpoint>_json(omni_server) / _minimal_*_form_data(); merge overrides with body.update(body_spec) |
| Known gaps | pytest.mark.skip(reason="…#3649") as _SKIP_ISSUE_3649 when server validation is not yet strict (mirror neighboring cases) |
| Sections | Route banner comments (# ─── POST /v1/images/generations ───) like existing files |
| Shared fixtures | Reuse tests/dfx/reliability/invalid_param_test/conftest.py (tiny_png_bytes, env defaults) |
Adding a new model to invalid-param coverage: append a pytest.param(OmniServerParams(model="...", stage_config_path=..., server_args=...), ...) entry to the script’s _PARAMS list — do not create test_invalid_<model>.py unless the route is new.
CI — .buildkite/cuda/test-weekly.yml only (not test-ready.yml / test-merge.yml / test-nightly.yml):
| Weekly step | Command | When your cases run |
|---|---|---|
| Invalid parameters Test · H100 | pytest -s -v tests/dfx/reliability/invalid_param_test/ -m "slow and H100" | Diffusion / omni / video invalid-param tests with H100 hardware mark |
| Invalid parameters Test · L4 | pytest -s -v tests/dfx/reliability/invalid_param_test/ -m "slow and L4" | TTS / lighter models marked L4 |
build.env("WEEKLY") == "1" (all weekly jobs) or PR label weekly-test (Reliability / Perf, then filtered by source_file_dependencies).invalid_param_test script needs no YAML edit — the weekly steps already sweep the whole directory.source_file_dependencies. Filtering applies only on the weekly-test label; WEEKLY=1 on main keeps every job in those groups. CPU coverage (WEEKLY=1 only) and the E2E Tests group (NON_CRITICAL=1 only) do not use this field.Example — append to test_invalid_image_generation.py:
@pytest.mark.parametrize(
"body_spec, err_message",
[
pytest.param({"seed": -1}, ("seed", "greater_than_equal", "0"), id="seed_negative"),
],
)
@pytest.mark.parametrize("omni_server", _PARAMS, indirect=True)
def test_images_generations_invalid_requests(
omni_server: OmniServer,
online_client: OnlineOmniClient,
body_spec: dict[str, object],
err_message: str | tuple[str, ...],
) -> None:
body = _minimal_images_gen_json(omni_server)
body.update(body_spec)
online_client.send_images_generations_http_request(
{"json": body, "timeout": 300, "err_code": 400, "err_message": err_message}
)See references/test-routing.md Invalid parameter / weekly CI.
tests/helpers/assertions.py)Do not add module-local helpers such as _assert_images_generations_payload or _send_and_assert_* in e2e test files. Response/media validation belongs in tests/helpers/assertions.py, grouped by category:
| Category | Existing anchors | When to extend vs add |
|---|---|---|
| Image (chat diffusion + DALL-E JSON) | assert_image_diffusion_response, assert_image_valid | DALL-E /v1/images/generations JSON → add assert_images_generations_response beside image helpers (reuses assert_image_valid); do not fork decode logic into tests |
| Video | assert_video_diffusion_response, assert_video_valid | Extend these for new video contracts |
| Audio | assert_audio_diffusion_response, assert_audio_speech_response, assert_audio_valid | Extend for new TTS/audio endpoints |
| Omni multimodal | assert_omni_response | Extend for new modality combos |
| HTTP errors | assert_http_error, assert_err_message_in_text | Used inside low-level send_*_http_request and negative tests |
| Do | Don't |
|---|---|
Put general assert_* inside send_*_request in runtime.py; tests call send_*_request only | Call assert_diffusion_response after send_diffusion_request (general assert already bundled) |
Put special assert_* in assertions.py and call it in the test after send_*_request when that case needs extra checks | Put general decode/count/size logic in the test because “this case uses n=4” (that belongs in general assert reading request_config) |
Extend the matching category function, or add assert_<endpoint>_response next to its category | _send_and_assert_* or PIL/base64 loops in test_*.py |
Use low-level send_*_http_request in dfx/negative tests (err_code) | Use low-level HTTP send + manual general assert in every L2+ expansion file |
Workflow when generating tests:
assert_* in assertions.py (by category).send_*_request in runtime.py if not already bundled.request_config → send_*_request. Add a separate assert_* import/call only for special case validation.source_file_dependencies when runtime.py or assertions.py changes.Example — DALL-E /v1/images/generations:
# tests/helpers/assertions.py — general contract (image category)
def assert_images_generations_response(resp_body: dict, request_config: dict, *, run_level: str | None = None) -> None:
... # data[], n, size → assert_image_valid
# tests/helpers/runtime.py — general assert INSIDE send
def send_images_generations_request(self, request_config: dict[str, Any], ...) -> list[HttpResponse]:
responses = self.send_images_generations_http_request(request_config, ...)
if request_config.get("err_code") is None:
body = responses[0].json_body
assert isinstance(body, dict)
assert_images_generations_response(body, request_config, run_level=self.run_level)
return responses
# tests/e2e/.../test_foo_expansion.py — one case per test_* (no case_id branching)
@pytest.mark.parametrize("omni_server", [pytest.param(OmniServerParams(model=MODEL), marks=SINGLE_L4)], indirect=True)
def test_foo_images_generations_default_1024(omni_server, online_client) -> None:
online_client.send_images_generations_request({"json": body, "timeout": 300})
@pytest.mark.parametrize("omni_server", [pytest.param(OmniServerParams(model=MODEL), marks=SINGLE_L4)], indirect=True)
def test_foo_images_generations_sizes_256_512_1024(omni_server, online_client) -> None:
for size in ("256x256", "512x512", "1024x1024"):
online_client.send_images_generations_request({"json": {**body, "size": size}, "timeout": 300})
# chat diffusion — separate test; send_diffusion_request bundles assert_diffusion_response
@pytest.mark.parametrize("omni_server", [pytest.param(OmniServerParams(model=MODEL), marks=SINGLE_L4)], indirect=True)
def test_foo_chat_completions_t2i_fallback(omni_server, online_client) -> None:
online_client.send_diffusion_request({...})L1 tests under tests/entrypoints/ may keep minimal asserts next to the handler under test when validating a single branch; still prefer assertions.py when the same JSON/media check appears in more than one file or level (e.g. L1 protocol test + L4 e2e).
When the goal is a general Omni/multimodal test case, prioritize mapping the test to the correct purpose, directory, and resource assumptions aligned with the layers (see test_system_overview.md and test_writing_guide.md):
core_model and cpu). Cover input validation, branches, and exception paths (tests/<component>/test_*.py). Mock with mocker / monkeypatch only — not unittest.mock.tests/e2e/online_serving/ and tests/e2e/offline_inference/).*_expansion.py plus related expansion cases).Also keep markers consistent with the run level: use core_model for L1/L2 and advanced_model for L3/L4, and pair with --run-level to select the intended CI strategy.
When the task involves diffusion models/features, organize L4 test cases following #1832: combine multiple diffusion features into as few test cases as possible to fit limited CI GPU resources.
Implementation strategy:
advanced_model) to broaden coverage under resource constraints.L4 is not only test_*_expansion.py. In test-nightly.yml, each model-type group typically runs separate jobs:
| Sub-pillar | Nightly step label pattern | What you add | Typical paths |
|---|---|---|---|
| Function | · Function Test with … | E2e expansion / feature matrix | tests/e2e/online_serving/test_<model>_expansion.py (or offline) |
| Accuracy | · Accuracy Test | Quality / similarity vs baseline | tests/e2e/accuracy/test_<model>*.py |
| Perf | · Perf Test · <Model> | Throughput / latency / memory benchmarks | tests/dfx/perf/tests/test_<model>_vllm_omni.json + runner script |
| Doc (optional) | · Doc Test | Runnable doc examples | tests/examples/*/test_text_to_image.py, … |
Default when the user asks for “L4 functional cases”: deliver Function pillar only (*_expansion.py + Function Test shard in test-nightly.yml). Do not silently add Perf or Accuracy unless the user also asks for performance / benchmark / accuracy / full L4 / full L4 coverage.
When the user explicitly asks for L4 perf (or full L4 including perf):
test_*_expansion.py — perf uses the dfx benchmark harness, not omni_server e2e fixtures.tests/dfx/perf/tests/ (mirror in-tree names: test_qwen_image_vllm_omni.json, test_cosmos3_vllm_omni.json, test_qwen3_omni_async_chunk.json).test_name, optional mark (hardware_marks required when present; marks: full_model + omni/tts/diffusion), server_params (model, serve_args or stage_config_name), benchmark_params[] with name and explicit baseline (throughput_qps, latency_mean, peak_memory_mb_mean, …). Diffusion cases also set server_type (e.g. "vllm-omni") and usually benchmark_endpoint.| Model type | Runner script | Example config |
|---|---|---|
| Diffusion X2I/X2V | tests/dfx/perf/scripts/run_diffusion_benchmark.py | tests/dfx/perf/tests/test_qwen_image_vllm_omni.json |
| TTS | tests/dfx/perf/scripts/run_benchmark.py | tests/dfx/perf/tests/test_tts.json (shared) or test_{slug}.json (dedicated; use when the model must not join the shared nightly matrix before integration — e.g. test_voxcpm2.json, test_coqui_tts.json) |
| Omni | tests/dfx/perf/scripts/run_benchmark.py | test_qwen3_omni_no_async_chunk.json, test_qwen3_omni_async_chunk.json, test_qwen3_omni_vllm_text.json, test_qwen3_omni_multi_replicas.json |
test-nightly.yml · Perf Test step for that model (separate from Function Test): export DIFFUSION_BENCHMARK_DIR / BENCHMARK_DIR, run pytest on the script with --test-config-file (nightly perf steps do not use -m), upload artifacts (buildkite-agent artifact upload), often multi-GPU H100 for diffusion perf.test_name block in JSON (same spirit as one test_* per function case). Combine server serve_args + workload in one entry; use multiple benchmark_params rows for size/step sweeps under the same server config.Perf local commands:
cd tests
export DIFFUSION_BENCHMARK_DIR=tests/dfx/perf/results
export DIFFUSION_ATTENTION_BACKEND=FLASH_ATTN
# Single file (CI-like)
pytest -s -v dfx/perf/scripts/run_diffusion_benchmark.py \
--test-config-file dfx/perf/tests/test_<model>_vllm_omni.json
# Bulk load + filter by JSON mark
pytest -sv dfx/perf/scripts/run_diffusion_benchmark.py -m "full_model and diffusion and H100"
pytest -sv dfx/perf/scripts/run_benchmark.py -m "full_model and omni and H100"Clarify in the test plan which L4 pillars you deliver: Function only | Function + Perf | Function + Accuracy + Perf.
See references/test-routing.md L4 nightly pillars for Buildkite step patterns.
If the test is not already collected by an existing pipeline command (for example, L1 tests marked core_model and cpu are already covered by the Simple Unit Test step in ready/merge), update the appropriate pipeline under .buildkite/:
| Test level | Edit this file | Typical trigger / intent |
|---|---|---|
| L1 and L2 | .buildkite/cuda/test-ready.yml | PR ready label; L1 CPU + L2 GPU/basic e2e (steps labeled Omni ·, TTS ·, Diffusion ·) |
| L3 | .buildkite/cuda/test-merge.yml | Post-merge; advanced_model integration per model type |
| L4 | .buildkite/cuda/test-nightly.yml | Nightly; grouped by model type (see below) |
| Invalid param / reliability (weekly) | .buildkite/cuda/test-weekly.yml | Weekly; tests/dfx/reliability/invalid_param_test/ — not L1–L4 e2e pipelines |
Level-specific Buildkite delivery (follow the user’s requested level):
| User asks for | Document / edit only | Do not add by default |
|---|---|---|
L4 (*_expansion.py, full_model) | test-nightly.yml — append file to the matching nightly shard (X2I / X2V / Omni / TTS) or note it is already collected by an existing -m / -k sweep | test-merge.yml (L3 / advanced_model) or test-ready.yml (L2) |
| L3 | test-merge.yml with advanced_model + source_file_dependencies | test-nightly.yml unless user also wants nightly |
| L2 | test-ready.yml with core_model + source_file_dependencies | merge / nightly pipelines |
| Invalid param | test-weekly.yml — Invalid parameters Test group (-m "slow and H100" / -m "slow and L4"). Usually no YAML edit when appending cases to existing invalid_param_test scripts | test-ready.yml, test-merge.yml, test-nightly.yml |
test-nightly.yml shards use explicit pytest file paths in commands plus source_file_dependencies; PR trigger is nightly-test (path filter), or NIGHTLY=1 on main (all jobs). Only suggest merge/ready YAML when the requested level is L3/L2 (or the user explicitly asks for multi-level CI).
test-nightly.yml top-level groups (each uses full_model + type marker in pytest -m):
| Group | Type marker | Notes |
|---|---|---|
| Omni Model Test | omni | Function / doc / accuracy / perf for Qwen-Omni family |
| TTS Model Test | tts | -m "full_model and L4 and tts" (and H100 variants when enabled); Perf Test uses run_benchmark.py + test_tts.json |
| Diffusion X2I(&A&T) Model Test | diffusion | Image/audio/text diffusion; explicit file shards for Function; Perf Test · <Model> uses run_diffusion_benchmark.py + JSON config |
| Diffusion X2V Model Test | diffusion | Video-only (Wan, HunyuanVideo, …); separate Perf steps when in-tree |
Guidelines when editing YAML:
--run-level to the test: L1/L2 in test-ready.yml use core_model + omni / tts / diffusion as appropriate; L3 merge uses advanced_model; L4 nightly uses full_model with the same type marker.tests/e2e/ sweep alone (shards exist to save GPU and avoid wrong queue).source_file_dependencies (required for new E2E jobs in test-ready.yml and test-merge.yml): every step under the :card_index_dividers: E2E Test group must declare source_file_dependencies so Buildkite only schedules the job when relevant paths change. Omitting it breaks path-based CI skipping for GPU e2e jobs.steps: entry with the correct agents.queue, timeout_in_minutes, and docker/kubernetes plugin blocks consistent with neighboring jobs.timeout_in_minutes on the step, not timeout before pytest: when adding or documenting a new E2E job in test-ready.yml / test-merge.yml, set timeout_in_minutes on the step and run pytest directly in commands. Do not wrap pytest in timeout 40m bash -c "..." / timeout 20m bash -c "..."; Buildkite already enforces the step deadline via timeout_in_minutes.plugins in skill examples or PR YAML snippets: H100 E2E steps on mithril-h100-pool use the full kubernetes block (resources, volumeMounts, env with HF_TOKEN secret, nodeSelector: gpu-h100-sxm, devshm + hf-cache volumes). L4/docker steps on gpu_1_queue use the full docker#v5.2.0 block (shm-size, HF_HOME, volumes). Do not write # ... plugins unchanged — paste the complete block from a sibling step.source_file_dependencies for E2E Test jobs (test-ready.yml, test-merge.yml)When adding or splitting a step inside the E2E Test group, always include source_file_dependencies on that step. List paths the job actually depends on — at minimum:
tests/e2e/... file the pytest command runs (online and offline if both are in one step).vllm_omni/model_executor/models/<family>/ and/or vllm_omni/diffusion/models/<family>/ as applicable.vllm_omni/model_executor/stage_input_processors/<family>.py when present.vllm_omni/deploy/<config>.yaml or vllm_omni/deploy/ci/<config>.yaml referenced by the test’s OmniServerParams / get_deploy_config_path(...).tests/helpers/runtime.py for new send_* client methods; tests/helpers/assertions.py for new shared assert_* helpers.Use repo-relative paths (no leading ./). Prefer directory entries (.../models/bagel/) when the whole package matters; use file entries for single test modules and YAML configs. Mirror a neighboring step for the same model family (e.g. Diffusion · Bagel Test in test-ready.yml).
Example (L2 ready — diffusion online smoke, H100 kubernetes):
- label: "Diffusion · Bagel Test"
source_file_dependencies:
- tests/e2e/online_serving/test_bagel.py
- vllm_omni/model_executor/models/bagel/
- vllm_omni/diffusion/models/bagel/
- vllm_omni/model_executor/stage_input_processors/bagel.py
- vllm_omni/deploy/bagel.yaml
timeout_in_minutes: 40
commands:
- |
export VLLM_IMAGE_FETCH_TIMEOUT=60
pytest -s -v tests/e2e/online_serving/test_bagel.py -m 'core_model' --run-level 'core_model'
agents:
queue: "mithril-h100-pool"
plugins:
- kubernetes:
podSpec:
containers:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
resources:
limits:
nvidia.com/gpu: 1
volumeMounts:
- name: devshm
mountPath: /dev/shm
- name: hf-cache
mountPath: /root/.cache/huggingface
env:
- name: HF_HOME
value: /root/.cache/huggingface
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token-secret
key: token
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
- name: devshm
emptyDir:
medium: Memory
- name: hf-cache
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreateExample (L3 merge — TTS online + offline, L4 docker):
- label: "TTS · Qwen3-TTS Base Test"
source_file_dependencies:
- tests/e2e/online_serving/test_qwen3_tts_base.py
- tests/e2e/offline_inference/test_qwen3_tts_base.py
- vllm_omni/model_executor/models/qwen3_tts/
- vllm_omni/model_executor/stage_input_processors/qwen3_tts.py
- vllm_omni/deploy/qwen3_tts.yaml
timeout_in_minutes: 20
commands:
- |
export VLLM_LOGGING_LEVEL=DEBUG
export VLLM_ALLOW_LONG_MAX_MODEL_LEN=1
pytest -s -v tests/e2e/online_serving/test_qwen3_tts_base.py tests/e2e/offline_inference/test_qwen3_tts_base.py -m 'advanced_model and cuda' --run-level 'advanced_model'
agents:
queue: "gpu_1_queue" # g6.4xlarge instance on AWS, has 1 L4 GPU
plugins:
- docker#v5.2.0:
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
always-pull: true
propagate-environment: true
shm-size: "8gb"
environment:
- "HF_HOME=/fsx/hf_cache"
volumes:
- "/fsx/hf_cache:/fsx/hf_cache"Example (L3 merge — diffusion online + offline, H100 kubernetes):
- label: "Diffusion · Qwen Image Test"
source_file_dependencies:
- tests/e2e/online_serving/test_qwen_image.py
- tests/e2e/offline_inference/test_qwen_image.py
- vllm_omni/diffusion/models/qwen_image/
timeout_in_minutes: 40
commands:
- |
export VLLM_IMAGE_FETCH_TIMEOUT=60
pytest -s -v \
tests/e2e/online_serving/test_qwen_image.py \
tests/e2e/offline_inference/test_qwen_image.py \
-m 'advanced_model and cuda' --run-level 'advanced_model'
agents:
queue: "mithril-h100-pool"
plugins:
- kubernetes:
podSpec:
containers:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
resources:
limits:
nvidia.com/gpu: 1
volumeMounts:
- name: devshm
mountPath: /dev/shm
- name: hf-cache
mountPath: /root/.cache/huggingface
env:
- name: HF_HOME
value: /root/.cache/huggingface
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token-secret
key: token
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
- name: devshm
emptyDir:
medium: Memory
- name: hf-cache
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreateWhen extending an existing E2E step to run an additional test file, append that file (and any new model/deploy paths) to source_file_dependencies on the same step. When authoring a PR, mention the dependency list in the Buildkite change section of your test plan.
cuda/pipeline.yml)Production behavior is driven by .buildkite/cuda/pipeline.yml: it builds the CI image, then uploads L2 (test-ready.yml on non-main), L3 (test-merge.yml when build.branch == "main"), and L4 (test-nightly.yml when build.env("NIGHTLY") == "1").
When you add or debug L3 or L4 tests and need those child pipelines to run off main or without NIGHTLY=1, apply temporary edits (revert before merge):
Point the upload at the right file (optional shortcut)
In the Upload Ready Pipeline step, the command is buildkite-agent pipeline upload .buildkite/cuda/test-ready.yml. For a one-off run you may change that path to .buildkite/cuda/test-merge.yml (L3) or .buildkite/cuda/test-nightly.yml (L4), and adjust the step’s if: so it does not conflict with the other upload steps (avoid double-uploading unless intentional).
L3 — merge pipeline
The gate if: build.branch == "main" lives on the Upload Merge Pipeline step in cuda/pipeline.yml (not inside test-merge.yml). Comment out that if line so test-merge.yml is uploaded on your feature branch. Alternatively rely on step (1) instead of the dedicated merge upload step.
L4 — nightly pipeline
Comment out if: build.env("NIGHTLY") == "1" on the Upload Nightly Pipeline step in cuda/pipeline.yml so the nightly definition is uploaded without setting the env var.
Child steps in test-nightly.yml often repeat if: build.env("NIGHTLY") == "1"; comment out those lines on the steps you need to run, otherwise they will still be skipped after upload.
Pick one command path:
file.py::test_name--run-levelFull templates live in references/test-routing.md. After authoring tests, always emit concrete commands (see Output Format).
Typical copy-paste examples (run from repo tests/ directory; requires matching vLLM/vllm-omni install and hardware):
| Area | Example |
|---|---|
| Omni offline L2 | pytest -s -v e2e/offline_inference/test_qwen2_5_omni.py -m "core_model and omni and not cpu" --run-level=core_model |
| Omni online L2 | pytest -s -v e2e/online_serving/test_qwen3_omni.py -m "core_model and omni" --run-level=core_model |
| TTS online L2 | pytest -s -v e2e/online_serving/test_qwen3_tts_base.py -m "core_model and tts" --run-level=core_model |
| Diffusion X2I L2 online | pytest -s -v e2e/online_serving/test_qwen_image.py -m "core_model and diffusion" --run-level=core_model |
| Diffusion X2V L2 online | pytest -s -v e2e/online_serving/test_wan22_t2v.py -m "core_model and diffusion" --run-level=core_model |
| Diffusion X2I L4 nightly | pytest -s -v e2e/online_serving/test_qwen_image_expansion.py -m "full_model and diffusion and H100" --run-level=full_model |
| Diffusion X2I L4 perf (nightly) | pytest -s -v dfx/perf/scripts/run_diffusion_benchmark.py --test-config-file dfx/perf/tests/test_qwen_image_vllm_omni.json |
| Diffusion X2V L4 nightly | pytest -s -v e2e/online_serving/test_wan22_expansion.py -m "full_model and cuda" --run-level=full_model |
| Invalid param (weekly H100) | pytest -s -v dfx/reliability/invalid_param_test/ -m "slow and H100" |
| Invalid param (weekly L4) | pytest -s -v dfx/reliability/invalid_param_test/ -m "slow and L4" |
| L1 CPU | pytest -s -v -m "core_model and cpu" |
Prerequisites to mention when relevant: GPU model (e.g. L4 vs H100), HF_HOME / token for hub weights, module-level skipif (NPU/XPU-only gaps), and whether CI already collects the path (e.g. test_*_expansion.py glob in test-nightly.yml).
Before finishing:
runtime.py send_*_request (not inline HTTP/SDK in the test file)?assert_* bundled inside send_*_request (not duplicated in the test)?test_* function (no case_id / if param.id branching across cases)?omni_server / omni_runner (module); _function variants only when each test must restart the instance?--run-level consistent?tests/dfx/reliability/invalid_param_test/ (not e2e), using send_*_http_request + err_code, with pytest.mark.slow and correct H100 / L4 hardware mark?When completing a request, return:
invalid_param_test/test_invalid_<area>.py; module basename for e2e: test_{slug}.py / test_{slug}_expansion.py)tests/helpers/{slug}.py). Extend tests/helpers/runtime.py / tests/helpers/assertions.py only when new repo-wide send_* or shared assert helpers are required (do not leave ad-hoc HTTP or _assert_* in test modules). If L4 Perf was requested, also list tests/dfx/perf/tests/*.json. If invalid-param cases were requested, list the invalid_param_test/ script and parametrized test_* / new id= rows — not e2e paths.test-nightly.yml Function Test shard (file list / note existing sweep). No test-merge.yml unless user also asked for L3.test-nightly.yml Perf Test · <Model> step (new step or extend commands); include env exports + artifact upload pattern from a sibling perf job.test-nightly.yml Accuracy Test step under the same model-type group.test-merge.yml + full source_file_dependencies + agents + plugins.test-ready.yml + same E2E block requirements.test-weekly.yml — note Invalid parameters Test · H100/L4 group; usually no YAML change when only extending existing scripts.
If the user asked only for L4 functional cases, state explicitly that Perf / Accuracy / Invalid param were not included (offer to add if needed). If the plan mixes success e2e and invalid param, split deliverables across e2e vs invalid_param_test/ and call out both CI files.bash blocks:cd tests then pytest -s -v <path> …pytest -s -v path::test_func …--run-level pairing the level uses (see references/test-routing.md and Step 6 table above)HF_TOKEN”; “Syntax-only validation when vllm is not installed locally”© vllm-project, Apache-2.0. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
SKILL.md and 1 other file (references) in .claude/skills/vllm-omni-test of vllm-project/vllm-omni.
Open the folder on GitHubat commit 096988d
Vllm Omni Test next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Vllm Omni Test this skillvllm-project/vllm-omni | 7.1k | — | ~17k | Automated safety check: Pass | Apache-2.0 | |
| Vllm Pytorch CI Triagepytorch/test-infra | 113 | — | ~2.4k | Automated safety check: Pass | Custom licence | |
| Build Zendnnamd/ZenDNN | 158 | — | ~2k | Automated safety check: Pass | Custom licence | |
| Debug Failing GPUfacebookexperimental/triton | 201 | — | ~709 | Automated safety check: Pass | MIT | |
| Langchain CI Integrationjeremylongshore/tons-of-skills-marketplace | 2.8k | — | ~4.4k | Automated safety check: Pass | MIT | |
| SageMaker Serving Image Selectionhuggingface/skills | 11k | 1 repos | ~4.6k | Automated safety check: Pass | Apache-2.0 |
pytorch/test-infra
Root-cause a vLLM torch-nightly CI regression report. An agent skill from pytorch/test-infra.
amd/ZenDNN
Build the standalone ZenDNN native library (zendnnl) from source with the alternate compute backends OFF (no oneDNN, libxsmm, parlooper, fbgemm), keeping AOCL DLP, which is the GEMM backend zendnnl…
facebookexperimental/triton
Recover from GPU-busy / GPU-unavailable failures. An agent skill from facebookexperimental/triton.
jeremylongshore/tons-of-skills-marketplace
Wire LangChain 1.0 / LangGraph 1.0 tests into a GitHub Actions pipeline — unit tests with FakeListChatModel, VCR-gated integration tests, warning-filter policy, and eval-regression merge gates.
huggingface/skills
Chooses the right serving container and current image URI for deploying a Hugging Face model to a SageMaker endpoint, preferring Hugging Face images over generic ones.
amElnagdy/delegate-skills
Delegate a coding task to Aider (aider) as a background implementer, then review its diff and land it yourself.
vllm-project/vllm-omni
Diagnose and optimize vLLM Omni diffusion workloads, especially Wan/Qwen/Flux-style image and video generation.
vllm-project/vllm-omni
Self-check your branch before creating a PR — catch dead code, prevent new model-specific Python examples, verify accuracy/perf claims, validate PR title format, and confirm merge readiness.
vllm-project/vllm-omni
Work on vLLM-Omni quantization for diffusion, autoregressive, omni, or multi-stage models.
vllm-project/vllm-omni
Review pull requests and local branches for vllm-project/vllm-omni with a frozen snapshot, module-design ownership, feature-design overlays, targeted validation, and concise evidence-backed findings.
vllm-project/vllm-omni
Write MiniMax H3 video generation prompts for T2VA, I2VA, FL2VA, L2VA, and Ref2VA.
vllm-project/vllm-omni
Add a new diffusion model (text-to-image, text-to-video, image-to-video, text-to-audio, image editing) to vLLM-Omni, including native non-Diffusers ports, reference-parity validation, Cache-DiT…
Categories
Generate and run tests for vllm-project/vllm-omni with CI-aligned levels and markers; wire new tests into Buildkite (test-ready.yml for L1/L2, test-merge.yml for L3, test-nightly.yml for L4). Vllm Omni Test is an agent skill from vllm-project/vllm-omni.yml for L4).
Vllm Omni Test fits situations like: creating regression tests; adding L1-L4 coverage; selecting pytest markers; validating fixes from issues/PRs.
Run `npx skills add vllm-project/vllm-omni --skill vllm-omni-test -a claude-code`. Or copy the skill folder (.claude/skills/vllm-omni-test in vllm-project/vllm-omni) into .claude/skills/vllm-omni-test in your project. Claude Code loads it when a task matches its description.
Run `npx skills add vllm-project/vllm-omni --skill vllm-omni-test -a codex`. Or copy the skill folder (.claude/skills/vllm-omni-test in vllm-project/vllm-omni) into .agents/skills/vllm-omni-test in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add vllm-project/vllm-omni --skill vllm-omni-test -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/vllm-omni-test, .gemini/skills/vllm-omni-test, .github/skills/vllm-omni-test and .opencode/skills/vllm-omni-test in your project.
Going by SKILL.md and its folder, Vllm Omni Test needs the command-line tools its instructions call (pytest) and credentials named HF_TOKEN.
SKILL.md contains no URLs. Any network use would come from the scripts or tools the agent runs. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.
Vllm Omni Test is published under the Apache-2.0 licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.
About 17k tokens (SKILL.md is roughly 69k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full. Its references folder adds about 5.9k tokens, read only when the agent opens those files.
Skills that share tags, products or a category with Vllm Omni Test: Vllm Pytorch CI Triage (pytorch/test-infra, 113 stars), Build Zendnn (amd/ZenDNN, 158 stars), Debug Failing GPU (facebookexperimental/triton, 201 stars) and Langchain CI Integration (jeremylongshore/tons-of-skills-marketplace, 2.8k stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
vllm-project (a GitHub organization) maintains it in vllm-project/vllm-omni, which has 7,119 GitHub stars. The repository holds 20 skills in this directory. The repository was last updated on October 11, 2026.
Source: vllm-project/vllm-omni on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.