Compare commits
188
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ae9bdd8c2 | ||
|
|
80a0d8d20a | ||
|
|
4b8dcac681 | ||
|
|
35a83a0885 | ||
|
|
3d71a2b3ad | ||
|
|
6e63ea875e | ||
|
|
0637454301 | ||
|
|
faad918588 | ||
|
|
121d0b9aee | ||
|
|
c4b37471c4 | ||
|
|
51cf142bc1 | ||
|
|
b02862beee | ||
|
|
504e5a6a6e | ||
|
|
f270ff7c3d | ||
|
|
fb94b87d4b | ||
|
|
a58825ed6c | ||
|
|
2f454823a6 | ||
|
|
ec3880df34 | ||
|
|
1439847f33 | ||
|
|
188d021023 | ||
|
|
efda9369a8 | ||
|
|
eef8e2bb94 | ||
|
|
26a37abfaf | ||
|
|
b751b6e62c | ||
|
|
b79911841f | ||
|
|
4653b49a0c | ||
|
|
51c5a284a6 | ||
|
|
90b7b6c5c9 | ||
|
|
9af0331b21 | ||
|
|
e4c47a2569 | ||
|
|
94914a1270 | ||
|
|
b5146fa981 | ||
|
|
85d1f92ee5 | ||
|
|
c1e32fc01d | ||
|
|
9a399adba8 | ||
|
|
08884c4cfb | ||
|
|
c306445ff2 | ||
|
|
f9879239ba | ||
|
|
87a1fd8da2 | ||
|
|
6a63f5cb99 | ||
|
|
6fb7b0a05e | ||
|
|
b9932aa17b | ||
|
|
06f67695f1 | ||
|
|
c94cf4ddde | ||
|
|
0e602509aa | ||
|
|
2f1845fa4e | ||
|
|
7c1b1e4189 | ||
|
|
6e79d582e3 | ||
|
|
038c9b948f | ||
|
|
cdd4638b17 | ||
|
|
ecdd762ff5 | ||
|
|
ffa991b24f | ||
|
|
c66df06c0f | ||
|
|
7712701578 | ||
|
|
ab56148505 | ||
|
|
5b002727b0 | ||
|
|
e0d0879794 | ||
|
|
09b720b0d2 | ||
|
|
968f3e379c | ||
|
|
f2e30f46b6 | ||
|
|
358b7d79c1 | ||
|
|
43b697541a | ||
|
|
2bae550c04 | ||
|
|
81c1c460b3 | ||
|
|
e74fe3c6e8 | ||
|
|
cdf027874c | ||
|
|
d7678e2bb3 | ||
|
|
5078c7ad66 | ||
|
|
f67cc52f74 | ||
|
|
b6f4df320d | ||
|
|
b3dd45874a | ||
|
|
5e4baea64d | ||
|
|
fdaf4e31bd | ||
|
|
7a80b79698 | ||
|
|
de1b0708bd | ||
|
|
7422a85cc7 | ||
|
|
a31690041d | ||
|
|
d59d166485 | ||
|
|
5378b77be4 | ||
|
|
d7740336ff | ||
|
|
c9c25b33c3 | ||
|
|
031612f250 | ||
|
|
b58ff5cb8b | ||
|
|
7c2e30afd3 | ||
|
|
f3fabe4145 | ||
|
|
c086554d1f | ||
|
|
c704aeccae | ||
|
|
087ac46318 | ||
|
|
1413eaab7c | ||
|
|
b1ec136bd7 | ||
|
|
e1c59edec6 | ||
|
|
043f03d576 | ||
|
|
c3498fb869 | ||
|
|
6942a5b600 | ||
|
|
3b335814cc | ||
|
|
cf36f5e474 | ||
|
|
7be014bce6 | ||
|
|
9b6a308914 | ||
|
|
30d6a0b4e6 | ||
|
|
4b96c57730 | ||
|
|
3204a0b2ad | ||
|
|
f080f41e03 | ||
|
|
85b0bad2d3 | ||
|
|
677265b64b | ||
|
|
91dfafcaab | ||
|
|
4b81f692d1 | ||
|
|
b78c31eda6 | ||
|
|
34aa1d2afa | ||
|
|
5cd925631c | ||
|
|
7f32c326c6 | ||
|
|
38abc0ba28 | ||
|
|
2df95de421 | ||
|
|
eeee257913 | ||
|
|
615f97b9c7 | ||
|
|
ff6e906a25 | ||
|
|
715b8b8746 | ||
|
|
14d2d00536 | ||
|
|
af74a5a155 | ||
|
|
039808e0d9 | ||
|
|
e8526cc552 | ||
|
|
06541d1f52 | ||
|
|
1cd92553cf | ||
|
|
32370968d0 | ||
|
|
946b843e61 | ||
|
|
f26f9c3a7f | ||
|
|
ea3cf99894 | ||
|
|
44d3ae100a | ||
|
|
ae70479619 | ||
|
|
142151fef5 | ||
|
|
5d7fdf9350 | ||
|
|
d4096e01fe | ||
|
|
13c617a140 | ||
|
|
66bbebd9d3 | ||
|
|
1e74807c35 | ||
|
|
ae57e406fd | ||
|
|
b6b992ac7b | ||
|
|
66d938100b | ||
|
|
348fd0fc07 | ||
|
|
ea14b04e9f | ||
|
|
0f9ee6afd8 | ||
|
|
22cefe04b4 | ||
|
|
7493cbd3c2 | ||
|
|
41f4f9d8c9 | ||
|
|
10701e1838 | ||
|
|
0e903e9695 | ||
|
|
175dec11be | ||
|
|
8d98cc0cb7 | ||
|
|
4179b0cae1 | ||
|
|
e43c787544 | ||
|
|
fda3af8bbc | ||
|
|
dff28e6867 | ||
|
|
1dae537313 | ||
|
|
ec22c7ac06 | ||
|
|
711daacabf | ||
|
|
2b47faf601 | ||
|
|
d1a5a7c178 | ||
|
|
24c49cf392 | ||
|
|
48ae412a2f | ||
|
|
b5e898f295 | ||
|
|
4927fd5412 | ||
|
|
8f284b343a | ||
|
|
376b28aced | ||
|
|
63ed0fdef4 | ||
|
|
ca6b4e2112 | ||
|
|
b9a88bf51e | ||
|
|
3ab582c993 | ||
|
|
3b6ce98421 | ||
|
|
95a9311696 | ||
|
|
d5fd9eb6bb | ||
|
|
5d20f6b900 | ||
|
|
c59b48b47e | ||
|
|
57c08d34c3 | ||
|
|
b6e72f3879 | ||
|
|
67ef0b64b9 | ||
|
|
e51fe6ec1b | ||
|
|
5f49848536 | ||
|
|
3cddda9781 | ||
|
|
0288c3f56e | ||
|
|
477083a103 | ||
|
|
e1b2f589c1 | ||
|
|
44c8d1c449 | ||
|
|
2298359f67 | ||
|
|
d1c8beeb18 | ||
|
|
e2b83db683 | ||
|
|
49c6fc1540 | ||
|
|
a65e89cbd4 | ||
|
|
7992c2213e | ||
|
|
8e26dce1f4 |
No files matched your search
@@ -14,6 +14,8 @@ set -euo pipefail
|
||||
# shellcheck disable=SC1091
|
||||
source .buildkite/common/scripts/resolve_skip_ci.sh
|
||||
|
||||
AMD_SUITE_SELECTOR=".buildkite/amd/scripts/select_test_suites.py"
|
||||
|
||||
if [[ -z "${RUN_ALL:-}" ]]; then
|
||||
RUN_ALL=0
|
||||
fi
|
||||
@@ -30,31 +32,20 @@ if [[ -z "${AMD_MIRROR_HW:-}" ]]; then
|
||||
AMD_MIRROR_HW="amdproduction"
|
||||
fi
|
||||
|
||||
fail_fast() {
|
||||
DISABLE_LABEL="ci-no-fail-fast"
|
||||
# If BUILDKITE_PULL_REQUEST != "false", then we check the PR labels using curl and jq
|
||||
if [ "$BUILDKITE_PULL_REQUEST" != "false" ]; then
|
||||
PR_LABELS=$(curl -s "https://api.github.com/repos/vllm-project/vllm-omni/pulls/$BUILDKITE_PULL_REQUEST" | jq -r '.labels[].name')
|
||||
if [[ $PR_LABELS == *"$DISABLE_LABEL"* ]]; then
|
||||
echo false
|
||||
else
|
||||
echo true
|
||||
fi
|
||||
else
|
||||
echo false # not a PR or BUILDKITE_PULL_REQUEST not set
|
||||
PR_LABELS=""
|
||||
# Explicit debug suites do not depend on PR labels.
|
||||
if [[ "${BUILDKITE_PULL_REQUEST:-false}" != "false" && -z "${DEBUG_TEST_YAML:-}" ]]; then
|
||||
if ! PR_LABELS=$(curl -fsSL \
|
||||
"https://api.github.com/repos/vllm-project/vllm-omni/pulls/$BUILDKITE_PULL_REQUEST" \
|
||||
| jq -r '.labels[].name'); then
|
||||
echo "ERROR: Could not read PR labels; refusing to select an incorrect AMD test tier." >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
fi
|
||||
|
||||
check_run_all_label() {
|
||||
RUN_ALL_LABEL="ready-run-all-tests"
|
||||
# If BUILDKITE_PULL_REQUEST != "false", then we check the PR labels using curl and jq
|
||||
if [ "$BUILDKITE_PULL_REQUEST" != "false" ]; then
|
||||
PR_LABELS=$(curl -s "https://api.github.com/repos/vllm-project/vllm-omni/pulls/$BUILDKITE_PULL_REQUEST" | jq -r '.labels[].name')
|
||||
if [[ $PR_LABELS == *"$RUN_ALL_LABEL"* ]]; then
|
||||
echo true
|
||||
else
|
||||
echo false
|
||||
fi
|
||||
fail_fast() {
|
||||
if [[ "${BUILDKITE_PULL_REQUEST:-false}" != "false" ]]; then
|
||||
echo true
|
||||
else
|
||||
echo false # not a PR or BUILDKITE_PULL_REQUEST not set
|
||||
fi
|
||||
@@ -64,6 +55,57 @@ if [[ -z "${COV_ENABLED:-}" ]]; then
|
||||
COV_ENABLED=0
|
||||
fi
|
||||
|
||||
resolve_test_specs() {
|
||||
local selected_specs
|
||||
if ! selected_specs=$(python3 "$AMD_SUITE_SELECTOR" \
|
||||
--branch "$BUILDKITE_BRANCH" \
|
||||
--labels "$PR_LABELS" \
|
||||
--debug-test-yaml "${DEBUG_TEST_YAML:-}"); then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TEST_SPECS=()
|
||||
while IFS= read -r suite_spec; do
|
||||
[[ -z "$suite_spec" ]] && continue
|
||||
TEST_SPECS+=("$suite_spec")
|
||||
done <<< "$selected_specs"
|
||||
}
|
||||
|
||||
filter_test_specs_by_skip_ci() {
|
||||
local suite_spec level decision
|
||||
local skip_all=0
|
||||
local -a runnable_specs=()
|
||||
|
||||
for suite_spec in "${TEST_SPECS[@]}"; do
|
||||
case "$suite_spec" in
|
||||
READY_TESTS:*) level="l2" ;;
|
||||
MERGE_TESTS:*) level="l3" ;;
|
||||
*)
|
||||
echo "ERROR: unknown AMD test suite spec '$suite_spec'" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if decision=$(python3 "$SKIP_CI_PY" gate amd "$level"); then
|
||||
echo "Skipping AMD $level suite due to $decision decision."
|
||||
[[ "$decision" == "skip-all" ]] && skip_all=1
|
||||
else
|
||||
runnable_specs+=("$suite_spec")
|
||||
fi
|
||||
done
|
||||
|
||||
TEST_SPECS=("${runnable_specs[@]}")
|
||||
if [[ ${#TEST_SPECS[@]} -eq 0 ]]; then
|
||||
if [[ $skip_all -eq 1 ]]; then
|
||||
buildkite-agent annotate \
|
||||
":memo: CI skipped — docs or pytest skip-mark changes only" \
|
||||
--style "info" 2>/dev/null || true
|
||||
fi
|
||||
echo "No AMD L2/L3 suites remain after skip-ci filtering."
|
||||
exit 0
|
||||
fi
|
||||
}
|
||||
|
||||
upload_pipeline() {
|
||||
echo "Uploading pipeline..."
|
||||
# Install minijinja
|
||||
@@ -84,7 +126,7 @@ upload_pipeline() {
|
||||
# (WIP) Use pipeline generator instead of jinja template
|
||||
if [ -e ".buildkite/amd/pipeline_generator/pipeline_generator.py" ]; then
|
||||
python -m pip install click pydantic
|
||||
python .buildkite/amd/pipeline_generator/pipeline_generator.py --run_all=$RUN_ALL --list_file_diff="$LIST_FILE_DIFF" --nightly="$NIGHTLY" --mirror_hw="$AMD_MIRROR_HW"
|
||||
python .buildkite/amd/pipeline_generator/pipeline_generator.py --run_all="$RUN_ALL" --list_file_diff="$LIST_FILE_DIFF" --nightly="$NIGHTLY" --mirror_hw="$AMD_MIRROR_HW"
|
||||
buildkite-agent pipeline upload .buildkite/amd/pipeline.yaml
|
||||
exit 0
|
||||
fi
|
||||
@@ -97,49 +139,12 @@ upload_pipeline() {
|
||||
|
||||
cd .buildkite/amd
|
||||
|
||||
# Select test definition file: merge suite for main, ready suite for PRs.
|
||||
# For debugging, DEBUG_TEST_YAML accepts a comma-separated list containing
|
||||
# "merge" and/or "ready" (case-insensitive). Multiple suites are combined
|
||||
# before rendering so they share one amd-build step.
|
||||
if [[ -n "${DEBUG_TEST_YAML:-}" ]]; then
|
||||
declare -a DEBUG_TEST_SPECS=()
|
||||
declare -A SEEN_DEBUG_TESTS=()
|
||||
IFS=',' read -ra REQUESTED_DEBUG_TESTS <<< "${DEBUG_TEST_YAML,,}"
|
||||
|
||||
for requested_test in "${REQUESTED_DEBUG_TESTS[@]}"; do
|
||||
# Permit readable values such as "ready, merge," and ignore the
|
||||
# empty item produced by a trailing comma.
|
||||
requested_test="${requested_test//[[:space:]]/}"
|
||||
[[ -z "$requested_test" ]] && continue
|
||||
|
||||
if [[ -n "${SEEN_DEBUG_TESTS[$requested_test]:-}" ]]; then
|
||||
echo "ERROR: duplicate DEBUG_TEST_YAML suite '$requested_test'" >&2
|
||||
exit 1
|
||||
fi
|
||||
SEEN_DEBUG_TESTS[$requested_test]=1
|
||||
|
||||
case "$requested_test" in
|
||||
ready)
|
||||
DEBUG_TEST_SPECS+=("READY_TESTS:test-amd-ready.yml")
|
||||
;;
|
||||
merge)
|
||||
DEBUG_TEST_SPECS+=("MERGE_TESTS:test-amd-merge.yml")
|
||||
;;
|
||||
*)
|
||||
echo "ERROR: DEBUG_TEST_YAML entries must be 'merge' or 'ready', got '$requested_test'" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ ${#DEBUG_TEST_SPECS[@]} -eq 0 ]]; then
|
||||
echo "ERROR: DEBUG_TEST_YAML did not contain a test suite" >&2
|
||||
exit 1
|
||||
elif [[ ${#DEBUG_TEST_SPECS[@]} -eq 1 ]]; then
|
||||
TEST_YAML="${DEBUG_TEST_SPECS[0]#*:}"
|
||||
else
|
||||
TEST_YAML=$(mktemp "${TMPDIR:-/tmp}/amd-debug-tests.XXXXXX.yml")
|
||||
python - "$TEST_YAML" "${DEBUG_TEST_SPECS[@]}" <<'PY'
|
||||
# Multiple label-selected or debug-selected suites share one image build.
|
||||
if [[ ${#TEST_SPECS[@]} -eq 1 ]]; then
|
||||
TEST_YAML="${TEST_SPECS[0]#*:}"
|
||||
else
|
||||
TEST_YAML=$(mktemp "${TMPDIR:-/tmp}/amd-selected-tests.XXXXXX.yml")
|
||||
python - "$TEST_YAML" "${TEST_SPECS[@]}" <<'PY'
|
||||
import sys
|
||||
|
||||
import yaml
|
||||
@@ -172,13 +177,8 @@ for suite_spec in suite_specs:
|
||||
with open(output_path, "w", encoding="utf-8") as output_file:
|
||||
yaml.safe_dump(combined, output_file, sort_keys=False)
|
||||
PY
|
||||
fi
|
||||
echo "DEBUG_TEST_YAML override: using ${DEBUG_TEST_SPECS[*]}"
|
||||
elif [[ $BUILDKITE_BRANCH == "main" ]]; then
|
||||
TEST_YAML="test-amd-merge.yml"
|
||||
else
|
||||
TEST_YAML="test-amd-ready.yml"
|
||||
fi
|
||||
echo "AMD test suites: ${TEST_SPECS[*]}"
|
||||
|
||||
(
|
||||
set -x
|
||||
@@ -198,7 +198,7 @@ PY
|
||||
> pipeline.yaml
|
||||
)
|
||||
cat pipeline.yaml
|
||||
if [[ "$TEST_YAML" == "${TMPDIR:-/tmp}/amd-debug-tests."*.yml ]]; then
|
||||
if [[ "$TEST_YAML" == "${TMPDIR:-/tmp}/amd-selected-tests."*.yml ]]; then
|
||||
rm -f -- "$TEST_YAML"
|
||||
fi
|
||||
buildkite-agent artifact upload pipeline.yaml
|
||||
@@ -219,12 +219,10 @@ if [[ $BUILDKITE_BRANCH == "main" ]]; then
|
||||
file_diff=$(get_diff_main)
|
||||
fi
|
||||
|
||||
# Early exit: unified skip-ci (docs / skip marks) and CI-yaml-only level targeting.
|
||||
if [[ $BUILDKITE_BRANCH == "main" ]]; then
|
||||
gate_bootstrap_ci amd l3
|
||||
else
|
||||
gate_bootstrap_ci amd l2
|
||||
fi
|
||||
# Resolve PR tier labels before skip-ci so ready and merge suites can be
|
||||
# filtered independently for CI-YAML-only changes.
|
||||
resolve_test_specs
|
||||
filter_test_specs_by_skip_ci
|
||||
|
||||
patterns=(
|
||||
"docker/Dockerfile"
|
||||
@@ -274,14 +272,6 @@ for file in $file_diff; do
|
||||
fi
|
||||
done
|
||||
|
||||
# Check for ready-run-all-tests label
|
||||
LABEL_RUN_ALL=$(check_run_all_label)
|
||||
if [[ $LABEL_RUN_ALL == true ]]; then
|
||||
RUN_ALL=1
|
||||
NIGHTLY=1
|
||||
echo "Found 'ready-run-all-tests' label. Running all tests including optional tests."
|
||||
fi
|
||||
|
||||
# Decide whether to use precompiled wheels
|
||||
# Relies on existing patterns array as a basis.
|
||||
if [[ -n "${VLLM_USE_PRECOMPILED:-}" ]]; then
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Select AMD L2/L3 suites after Buildkite has created an AMD build.
|
||||
|
||||
This repository-side selector does not decide which GitHub label events start
|
||||
the external ``vllm-omni-amd-ci`` pipeline. Its Buildkite PR build condition
|
||||
must admit the relevant label event before this code can inspect the labels.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from collections.abc import Iterable
|
||||
|
||||
SUITE_SPECS = {
|
||||
"ready": "READY_TESTS:test-amd-ready.yml",
|
||||
"merge": "MERGE_TESTS:test-amd-merge.yml",
|
||||
}
|
||||
|
||||
|
||||
def _parse_debug_suites(value: str) -> tuple[str, ...]:
|
||||
suites = tuple(item.strip().lower() for item in value.split(",") if item.strip())
|
||||
if not suites:
|
||||
raise ValueError("DEBUG_TEST_YAML did not contain a test suite")
|
||||
|
||||
seen: set[str] = set()
|
||||
for suite in suites:
|
||||
if suite not in SUITE_SPECS:
|
||||
raise ValueError(f"DEBUG_TEST_YAML entries must be 'merge' or 'ready', got {suite!r}")
|
||||
if suite in seen:
|
||||
raise ValueError(f"duplicate DEBUG_TEST_YAML suite {suite!r}")
|
||||
seen.add(suite)
|
||||
return suites
|
||||
|
||||
|
||||
def select_amd_test_suites(
|
||||
*,
|
||||
branch: str,
|
||||
labels: Iterable[str],
|
||||
debug_test_yaml: str = "",
|
||||
) -> tuple[str, ...]:
|
||||
"""Return ordered suite names while preserving legacy unlabeled PRs."""
|
||||
|
||||
if debug_test_yaml:
|
||||
return _parse_debug_suites(debug_test_yaml)
|
||||
if branch == "main":
|
||||
return ("merge",)
|
||||
|
||||
label_set = {label.strip() for label in labels if label.strip()}
|
||||
selected = tuple(suite for label, suite in (("ready", "ready"), ("merge-test", "merge")) if label in label_set)
|
||||
# AMD historically uploaded L2 for every PR build. Keep that behavior for
|
||||
# triggers such as amd-test while adding tier selection for ready/merge-test.
|
||||
return selected or ("ready",)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--branch", required=True)
|
||||
parser.add_argument("--labels", default="")
|
||||
parser.add_argument("--debug-test-yaml", default="")
|
||||
args = parser.parse_args()
|
||||
|
||||
labels = tuple(args.labels.splitlines())
|
||||
try:
|
||||
suites = select_amd_test_suites(
|
||||
branch=args.branch,
|
||||
labels=labels,
|
||||
debug_test_yaml=args.debug_test_yaml,
|
||||
)
|
||||
except ValueError as exc:
|
||||
parser.error(str(exc))
|
||||
|
||||
label_set = {label.strip() for label in labels if label.strip()}
|
||||
if not args.debug_test_yaml and args.branch != "main" and not ({"ready", "merge-test"} & label_set):
|
||||
print(
|
||||
"No AMD L2/L3 tier label found; preserving the legacy ready-suite fallback.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
if "nightly-test" in label_set:
|
||||
print(
|
||||
"AMD nightly-test has no suite yet and does not affect L2/L3 selection.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
for suite in suites:
|
||||
print(SUITE_SPECS[suite])
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -238,13 +238,10 @@ steps:
|
||||
grade: Blocking
|
||||
timeout_in_minutes: 50
|
||||
commands:
|
||||
- |
|
||||
timeout 40m bash -c '
|
||||
export VLLM_LOGGING_LEVEL=DEBUG
|
||||
export VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
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'
|
||||
'
|
||||
- export VLLM_LOGGING_LEVEL=DEBUG
|
||||
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
- export VLLM_ALLOW_LONG_MAX_MODEL_LEN="1"
|
||||
- timeout 40m 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"
|
||||
|
||||
# TODO: Bagel test on ROCm is very unstable. @tjtanaa
|
||||
# Need to debug before reneable numerical changes across large PRs
|
||||
|
||||
@@ -107,6 +107,11 @@ steps:
|
||||
- export VLLM_LOGGING_LEVEL=DEBUG
|
||||
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
- timeout 20m pytest -s -v tests/diffusion/distributed/test_sequence_parallel.py -m core_model
|
||||
- >-
|
||||
timeout 20m pytest -s -v
|
||||
tests/diffusion/models/ltx2/test_ltx2_transformer_ulysses.py
|
||||
-m 'core_model and rocm and cards_2'
|
||||
--run-level "core_model"
|
||||
|
||||
- label: "Diffusion GPU Worker Test"
|
||||
agent_pool: mi300_2
|
||||
@@ -122,7 +127,13 @@ steps:
|
||||
mirror_hardwares: [amdproduction]
|
||||
grade: Blocking
|
||||
commands:
|
||||
- timeout 15m pytest -sv tests/diffusion/models/ -m 'core_model and cuda' --run-level "core_model"
|
||||
# Preserve legacy one-GPU tests without cards_1 while excluding every
|
||||
# registered multi-GPU marker from this one-GPU job.
|
||||
- >-
|
||||
timeout 15m pytest -sv tests/diffusion/models/
|
||||
-m 'core_model and cuda and not
|
||||
(cards_2 or cards_3 or cards_4 or cards_5 or cards_6 or cards_7 or cards_8)'
|
||||
--run-level "core_model"
|
||||
|
||||
- label: "Diffusion · Wan22 Test"
|
||||
agent_pool: mi300_1
|
||||
|
||||
@@ -1,19 +1,36 @@
|
||||
# Uploader-only hardware presets for CUDA / NPU test YAML.
|
||||
# Referenced as ``mirror_hardwares: l4_1`` on a step; ``upload_pipeline.py`` expands to
|
||||
# ``agents`` (+ optional top-level ``image`` for NPU) + ``plugins``, then strips
|
||||
# ``mirror_hardwares`` before Buildkite upload.
|
||||
# ``agents`` (+ optional top-level ``image`` for NPU) + ``plugins`` (+ ``retry``
|
||||
# for ``l4_*``), then strips ``mirror_hardwares`` before Buildkite upload.
|
||||
#
|
||||
# Omit the key: compose {chip}_{n} from pytest -m SKU + cards_n (max of several
|
||||
# positives; not cards_1 with no positive cards_* → that chip's highest existing preset).
|
||||
# Unset MIRROR_HW matches H100 or L4 in -m (both → H100); no match skips the step.
|
||||
# MIRROR_HW=b200 matches B200 in -m (else the step is skipped).
|
||||
# MIRROR_HW must be empty or b200 (case-insensitive); unknown values fail the upload.
|
||||
# CUDA preset strings (h100_4, l4_1) are omitted when MIRROR_HW=b200.
|
||||
# Unset MIRROR_HW keeps string presets. NPU presets ignore MIRROR_HW.
|
||||
#
|
||||
# CUDA naming: {chip}_{count}
|
||||
# l4_1, l4_4, h100_1 … h100_4
|
||||
# l4_1, l4_2, l4_3, l4_4, h100_1 … h100_4, b200_1 … b200_4
|
||||
# NPU naming: {queue}_{resource_class}
|
||||
# a2b3_npu_1 — ascend-a2b3, resource_class npu-1 (A2/B3 CI image)
|
||||
# a2b3_npu_4 — ascend-a2b3, resource_class npu-4
|
||||
# a3_npu_2 — ascend-a3, resource_class npu-2
|
||||
# a3_npu_2 — ascend-a3, resource_class npu-2 (A3 CI image tag …:$BUILDKITE_COMMIT-a3)
|
||||
# a5_npu_2 — ascend-a5, resource_class npu-2
|
||||
# 310p_npu_1 — ascend-310p, resource_class npu-1
|
||||
#
|
||||
# The ``l4_*`` presets run under the ``docker`` plugin, which does not put
|
||||
# ``buildkite-agent`` on PATH inside the container the way the ``kubernetes``
|
||||
# presets do, so they set ``mount-buildkite-agent: true``; without it any step
|
||||
# calling ``buildkite-agent`` (artifact upload, annotate, meta-data) fails there.
|
||||
# B200 (b200-k8s): matches vLLM device ``b200-k8s`` from ci-infra
|
||||
# (AgentQueue.B200_K8S + b200_plugin_template). Queue routes the K8s pool;
|
||||
# podSpec uses hostNetwork + /raid|/mnt/shared (no H100-style nodeSelector).
|
||||
#
|
||||
# The ``l4_*`` presets run on the EKS ``l4-k8s`` queue (agent-stack-k8s),
|
||||
# packed onto g6.12xlarge L4 nodes (``vllm.ci/gpu-pool=l4x4``). GPU / CPU /
|
||||
# memory / shm requests scale with card count (1 GPU = 10 CPU / 40Gi / 24Gi shm).
|
||||
# Retry covers lost agents / killed pods, not pytest or OOM (exit 1 / 137).
|
||||
# The Kubernetes HF secret deliberately uses VLLM_CI_HF_TOKEN. Scheduled
|
||||
# build-level HF_TOKEN values can override pod env entries with the same name;
|
||||
# upload_pipeline.py restores the secret-backed value inside the job shell.
|
||||
|
||||
x-npu-k8s-plugins: &npu_k8s_plugins
|
||||
- kubernetes:
|
||||
@@ -21,39 +38,224 @@ x-npu-k8s-plugins: &npu_k8s_plugins
|
||||
imagePullSecrets:
|
||||
- name: swr-secret
|
||||
|
||||
# Infra-only retry. Omit exit 1 (pytest failed) and 137 (cgroup OOM / SIGKILL
|
||||
# of the test process). ci-infra ``K8S_RETRY`` still retries exit 1.
|
||||
x-l4-k8s-retry: &l4_k8s_retry
|
||||
automatic:
|
||||
- exit_status: -1
|
||||
limit: 1
|
||||
- exit_status: 128
|
||||
limit: 1
|
||||
- signal_reason: agent_stop
|
||||
limit: 1
|
||||
- signal_reason: agent_refused
|
||||
limit: 1
|
||||
|
||||
|
||||
mirror_hardwares:
|
||||
l4_1:
|
||||
agents:
|
||||
queue: gpu_1_queue
|
||||
queue: l4-k8s
|
||||
retry: *l4_k8s_retry
|
||||
plugins:
|
||||
- docker#v5.2.0:
|
||||
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
|
||||
always-pull: true
|
||||
propagate-environment: true
|
||||
mount-buildkite-agent: true
|
||||
shm-size: "8gb"
|
||||
environment:
|
||||
- HF_HOME=/fsx/hf_cache
|
||||
- HF_TOKEN
|
||||
volumes:
|
||||
- /fsx/hf_cache:/fsx/hf_cache
|
||||
- kubernetes:
|
||||
metadata:
|
||||
annotations:
|
||||
cluster-autoscaler.kubernetes.io/safe-to-evict: "false"
|
||||
karpenter.sh/do-not-disrupt: "true"
|
||||
podSpec:
|
||||
priorityClassName: ci
|
||||
serviceAccountName: buildkite-gpu-jobs
|
||||
tolerations:
|
||||
- key: nvidia.com/gpu
|
||||
operator: Exists
|
||||
effect: NoSchedule
|
||||
nodeSelector:
|
||||
vllm.ci/gpu-pool: l4x4
|
||||
containers:
|
||||
- image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
|
||||
resources:
|
||||
limits:
|
||||
nvidia.com/gpu: 1
|
||||
requests:
|
||||
nvidia.com/gpu: 1
|
||||
cpu: "10"
|
||||
memory: 40Gi
|
||||
volumeMounts:
|
||||
- name: devshm
|
||||
mountPath: /dev/shm
|
||||
- name: hf-cache
|
||||
mountPath: /fsx/hf_cache
|
||||
env:
|
||||
- name: HF_HOME
|
||||
value: /fsx/hf_cache
|
||||
- name: VLLM_CI_HF_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: hf-token-secret
|
||||
key: token
|
||||
volumes:
|
||||
- name: devshm
|
||||
emptyDir:
|
||||
medium: Memory
|
||||
sizeLimit: 24Gi
|
||||
- name: hf-cache
|
||||
hostPath:
|
||||
path: /fsx/hf_cache
|
||||
type: DirectoryOrCreate
|
||||
|
||||
l4_2:
|
||||
agents:
|
||||
queue: l4-k8s
|
||||
retry: *l4_k8s_retry
|
||||
plugins:
|
||||
- kubernetes:
|
||||
metadata:
|
||||
annotations:
|
||||
cluster-autoscaler.kubernetes.io/safe-to-evict: "false"
|
||||
karpenter.sh/do-not-disrupt: "true"
|
||||
podSpec:
|
||||
priorityClassName: ci
|
||||
serviceAccountName: buildkite-gpu-jobs
|
||||
tolerations:
|
||||
- key: nvidia.com/gpu
|
||||
operator: Exists
|
||||
effect: NoSchedule
|
||||
nodeSelector:
|
||||
vllm.ci/gpu-pool: l4x4
|
||||
containers:
|
||||
- image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
|
||||
resources:
|
||||
limits:
|
||||
nvidia.com/gpu: 2
|
||||
requests:
|
||||
nvidia.com/gpu: 2
|
||||
cpu: "20"
|
||||
memory: 80Gi
|
||||
volumeMounts:
|
||||
- name: devshm
|
||||
mountPath: /dev/shm
|
||||
- name: hf-cache
|
||||
mountPath: /fsx/hf_cache
|
||||
env:
|
||||
- name: HF_HOME
|
||||
value: /fsx/hf_cache
|
||||
- name: VLLM_CI_HF_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: hf-token-secret
|
||||
key: token
|
||||
volumes:
|
||||
- name: devshm
|
||||
emptyDir:
|
||||
medium: Memory
|
||||
sizeLimit: 48Gi
|
||||
- name: hf-cache
|
||||
hostPath:
|
||||
path: /fsx/hf_cache
|
||||
type: DirectoryOrCreate
|
||||
|
||||
l4_3:
|
||||
agents:
|
||||
queue: l4-k8s
|
||||
retry: *l4_k8s_retry
|
||||
plugins:
|
||||
- kubernetes:
|
||||
metadata:
|
||||
annotations:
|
||||
cluster-autoscaler.kubernetes.io/safe-to-evict: "false"
|
||||
karpenter.sh/do-not-disrupt: "true"
|
||||
podSpec:
|
||||
priorityClassName: ci
|
||||
serviceAccountName: buildkite-gpu-jobs
|
||||
tolerations:
|
||||
- key: nvidia.com/gpu
|
||||
operator: Exists
|
||||
effect: NoSchedule
|
||||
nodeSelector:
|
||||
vllm.ci/gpu-pool: l4x4
|
||||
containers:
|
||||
- image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
|
||||
resources:
|
||||
limits:
|
||||
nvidia.com/gpu: 3
|
||||
requests:
|
||||
nvidia.com/gpu: 3
|
||||
cpu: "30"
|
||||
memory: 120Gi
|
||||
volumeMounts:
|
||||
- name: devshm
|
||||
mountPath: /dev/shm
|
||||
- name: hf-cache
|
||||
mountPath: /fsx/hf_cache
|
||||
env:
|
||||
- name: HF_HOME
|
||||
value: /fsx/hf_cache
|
||||
- name: VLLM_CI_HF_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: hf-token-secret
|
||||
key: token
|
||||
volumes:
|
||||
- name: devshm
|
||||
emptyDir:
|
||||
medium: Memory
|
||||
sizeLimit: 72Gi
|
||||
- name: hf-cache
|
||||
hostPath:
|
||||
path: /fsx/hf_cache
|
||||
type: DirectoryOrCreate
|
||||
|
||||
l4_4:
|
||||
agents:
|
||||
queue: gpu_4_queue
|
||||
queue: l4-k8s
|
||||
retry: *l4_k8s_retry
|
||||
plugins:
|
||||
- docker#v5.2.0:
|
||||
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
|
||||
always-pull: true
|
||||
propagate-environment: true
|
||||
mount-buildkite-agent: true
|
||||
shm-size: "8gb"
|
||||
environment:
|
||||
- HF_HOME=/fsx/hf_cache
|
||||
- HF_TOKEN
|
||||
volumes:
|
||||
- /fsx/hf_cache:/fsx/hf_cache
|
||||
- kubernetes:
|
||||
metadata:
|
||||
annotations:
|
||||
cluster-autoscaler.kubernetes.io/safe-to-evict: "false"
|
||||
karpenter.sh/do-not-disrupt: "true"
|
||||
podSpec:
|
||||
priorityClassName: ci
|
||||
serviceAccountName: buildkite-gpu-jobs
|
||||
tolerations:
|
||||
- key: nvidia.com/gpu
|
||||
operator: Exists
|
||||
effect: NoSchedule
|
||||
nodeSelector:
|
||||
vllm.ci/gpu-pool: l4x4
|
||||
containers:
|
||||
- image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
|
||||
resources:
|
||||
limits:
|
||||
nvidia.com/gpu: 4
|
||||
requests:
|
||||
nvidia.com/gpu: 4
|
||||
cpu: "40"
|
||||
memory: 160Gi
|
||||
volumeMounts:
|
||||
- name: devshm
|
||||
mountPath: /dev/shm
|
||||
- name: hf-cache
|
||||
mountPath: /fsx/hf_cache
|
||||
env:
|
||||
- name: HF_HOME
|
||||
value: /fsx/hf_cache
|
||||
- name: VLLM_CI_HF_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: hf-token-secret
|
||||
key: token
|
||||
volumes:
|
||||
- name: devshm
|
||||
emptyDir:
|
||||
medium: Memory
|
||||
sizeLimit: 96Gi
|
||||
- name: hf-cache
|
||||
hostPath:
|
||||
path: /fsx/hf_cache
|
||||
type: DirectoryOrCreate
|
||||
|
||||
h100_1:
|
||||
agents:
|
||||
@@ -74,7 +276,7 @@ mirror_hardwares:
|
||||
env:
|
||||
- name: HF_HOME
|
||||
value: /root/.cache/huggingface
|
||||
- name: HF_TOKEN
|
||||
- name: VLLM_CI_HF_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: hf-token-secret
|
||||
@@ -109,7 +311,7 @@ mirror_hardwares:
|
||||
env:
|
||||
- name: HF_HOME
|
||||
value: /root/.cache/huggingface
|
||||
- name: HF_TOKEN
|
||||
- name: VLLM_CI_HF_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: hf-token-secret
|
||||
@@ -144,7 +346,7 @@ mirror_hardwares:
|
||||
env:
|
||||
- name: HF_HOME
|
||||
value: /root/.cache/huggingface
|
||||
- name: HF_TOKEN
|
||||
- name: VLLM_CI_HF_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: hf-token-secret
|
||||
@@ -179,7 +381,7 @@ mirror_hardwares:
|
||||
env:
|
||||
- name: HF_HOME
|
||||
value: /root/.cache/huggingface
|
||||
- name: HF_TOKEN
|
||||
- name: VLLM_CI_HF_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: hf-token-secret
|
||||
@@ -195,6 +397,211 @@ mirror_hardwares:
|
||||
path: /mnt/hf-cache
|
||||
type: DirectoryOrCreate
|
||||
|
||||
# vLLM device: b200-k8s → agents.queue b200-k8s + Buildkite K8s agent-stack podSpec
|
||||
b200_1:
|
||||
agents:
|
||||
queue: b200-k8s
|
||||
plugins:
|
||||
- kubernetes:
|
||||
podSpec:
|
||||
runtimeClassName: nvidia
|
||||
hostNetwork: true
|
||||
dnsPolicy: ClusterFirstWithHostNet
|
||||
imagePullSecrets:
|
||||
- name: k8s-ecr-login-renew-docker-secret
|
||||
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
|
||||
securityContext:
|
||||
capabilities:
|
||||
add:
|
||||
- IPC_LOCK
|
||||
- SYS_RESOURCE
|
||||
volumeMounts:
|
||||
- name: devshm
|
||||
mountPath: /dev/shm
|
||||
- name: raid
|
||||
mountPath: /raid
|
||||
- name: shared
|
||||
mountPath: /mnt/shared
|
||||
env:
|
||||
- name: NCCL_CUMEM_HOST_ENABLE
|
||||
value: "0"
|
||||
- name: HF_HOME
|
||||
value: /raid/hf_cache
|
||||
- name: VLLM_CI_HF_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: hf-token-secret
|
||||
key: token
|
||||
volumes:
|
||||
- name: devshm
|
||||
emptyDir:
|
||||
medium: Memory
|
||||
- name: raid
|
||||
hostPath:
|
||||
path: /raid
|
||||
type: DirectoryOrCreate
|
||||
- name: shared
|
||||
hostPath:
|
||||
path: /mnt/shared
|
||||
type: DirectoryOrCreate
|
||||
|
||||
b200_2:
|
||||
agents:
|
||||
queue: b200-k8s
|
||||
plugins:
|
||||
- kubernetes:
|
||||
podSpec:
|
||||
runtimeClassName: nvidia
|
||||
hostNetwork: true
|
||||
dnsPolicy: ClusterFirstWithHostNet
|
||||
imagePullSecrets:
|
||||
- name: k8s-ecr-login-renew-docker-secret
|
||||
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: 2
|
||||
securityContext:
|
||||
capabilities:
|
||||
add:
|
||||
- IPC_LOCK
|
||||
- SYS_RESOURCE
|
||||
volumeMounts:
|
||||
- name: devshm
|
||||
mountPath: /dev/shm
|
||||
- name: raid
|
||||
mountPath: /raid
|
||||
- name: shared
|
||||
mountPath: /mnt/shared
|
||||
env:
|
||||
- name: NCCL_CUMEM_HOST_ENABLE
|
||||
value: "0"
|
||||
- name: HF_HOME
|
||||
value: /raid/hf_cache
|
||||
- name: VLLM_CI_HF_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: hf-token-secret
|
||||
key: token
|
||||
volumes:
|
||||
- name: devshm
|
||||
emptyDir:
|
||||
medium: Memory
|
||||
- name: raid
|
||||
hostPath:
|
||||
path: /raid
|
||||
type: DirectoryOrCreate
|
||||
- name: shared
|
||||
hostPath:
|
||||
path: /mnt/shared
|
||||
type: DirectoryOrCreate
|
||||
|
||||
b200_3:
|
||||
agents:
|
||||
queue: b200-k8s
|
||||
plugins:
|
||||
- kubernetes:
|
||||
podSpec:
|
||||
runtimeClassName: nvidia
|
||||
hostNetwork: true
|
||||
dnsPolicy: ClusterFirstWithHostNet
|
||||
imagePullSecrets:
|
||||
- name: k8s-ecr-login-renew-docker-secret
|
||||
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: 3
|
||||
securityContext:
|
||||
capabilities:
|
||||
add:
|
||||
- IPC_LOCK
|
||||
- SYS_RESOURCE
|
||||
volumeMounts:
|
||||
- name: devshm
|
||||
mountPath: /dev/shm
|
||||
- name: raid
|
||||
mountPath: /raid
|
||||
- name: shared
|
||||
mountPath: /mnt/shared
|
||||
env:
|
||||
- name: NCCL_CUMEM_HOST_ENABLE
|
||||
value: "0"
|
||||
- name: HF_HOME
|
||||
value: /raid/hf_cache
|
||||
- name: VLLM_CI_HF_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: hf-token-secret
|
||||
key: token
|
||||
volumes:
|
||||
- name: devshm
|
||||
emptyDir:
|
||||
medium: Memory
|
||||
- name: raid
|
||||
hostPath:
|
||||
path: /raid
|
||||
type: DirectoryOrCreate
|
||||
- name: shared
|
||||
hostPath:
|
||||
path: /mnt/shared
|
||||
type: DirectoryOrCreate
|
||||
|
||||
b200_4:
|
||||
agents:
|
||||
queue: b200-k8s
|
||||
plugins:
|
||||
- kubernetes:
|
||||
podSpec:
|
||||
runtimeClassName: nvidia
|
||||
hostNetwork: true
|
||||
dnsPolicy: ClusterFirstWithHostNet
|
||||
imagePullSecrets:
|
||||
- name: k8s-ecr-login-renew-docker-secret
|
||||
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: 4
|
||||
securityContext:
|
||||
capabilities:
|
||||
add:
|
||||
- IPC_LOCK
|
||||
- SYS_RESOURCE
|
||||
volumeMounts:
|
||||
- name: devshm
|
||||
mountPath: /dev/shm
|
||||
- name: raid
|
||||
mountPath: /raid
|
||||
- name: shared
|
||||
mountPath: /mnt/shared
|
||||
env:
|
||||
- name: NCCL_CUMEM_HOST_ENABLE
|
||||
value: "0"
|
||||
- name: HF_HOME
|
||||
value: /raid/hf_cache
|
||||
- name: VLLM_CI_HF_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: hf-token-secret
|
||||
key: token
|
||||
volumes:
|
||||
- name: devshm
|
||||
emptyDir:
|
||||
medium: Memory
|
||||
- name: raid
|
||||
hostPath:
|
||||
path: /raid
|
||||
type: DirectoryOrCreate
|
||||
- name: shared
|
||||
hostPath:
|
||||
path: /mnt/shared
|
||||
type: DirectoryOrCreate
|
||||
|
||||
a2b3_npu_1:
|
||||
agents:
|
||||
queue: "ascend-a2b3"
|
||||
@@ -244,3 +651,52 @@ mirror_hardwares:
|
||||
resource_class: "npu-16"
|
||||
image: "swr.cn-southwest-2.myhuaweicloud.com/modelfoundry/vllm-omni-ci-npu:${BUILDKITE_COMMIT}-a3"
|
||||
plugins: *npu_k8s_plugins
|
||||
|
||||
a5_npu_2:
|
||||
agents:
|
||||
queue: "ascend-a5"
|
||||
resource_class: "npu-2"
|
||||
image: "swr.cn-southwest-2.myhuaweicloud.com/modelfoundry/vllm-omni-ci-npu:${BUILDKITE_COMMIT}-a5"
|
||||
plugins: *npu_k8s_plugins
|
||||
|
||||
a5_npu_4:
|
||||
agents:
|
||||
queue: "ascend-a5"
|
||||
resource_class: "npu-4"
|
||||
image: "swr.cn-southwest-2.myhuaweicloud.com/modelfoundry/vllm-omni-ci-npu:${BUILDKITE_COMMIT}-a5"
|
||||
plugins: *npu_k8s_plugins
|
||||
|
||||
a5_npu_8:
|
||||
agents:
|
||||
queue: "ascend-a5"
|
||||
resource_class: "npu-8"
|
||||
image: "swr.cn-southwest-2.myhuaweicloud.com/modelfoundry/vllm-omni-ci-npu:${BUILDKITE_COMMIT}-a5"
|
||||
plugins: *npu_k8s_plugins
|
||||
|
||||
a5_npu_16:
|
||||
agents:
|
||||
queue: "ascend-a5"
|
||||
resource_class: "npu-16"
|
||||
image: "swr.cn-southwest-2.myhuaweicloud.com/modelfoundry/vllm-omni-ci-npu:${BUILDKITE_COMMIT}-a5"
|
||||
plugins: *npu_k8s_plugins
|
||||
|
||||
310p_npu_1:
|
||||
agents:
|
||||
queue: "ascend-310p"
|
||||
resource_class: "npu-1"
|
||||
image: "swr.cn-southwest-2.myhuaweicloud.com/modelfoundry/vllm-omni-ci-npu:${BUILDKITE_COMMIT}-310p"
|
||||
plugins: *npu_k8s_plugins
|
||||
|
||||
310p_npu_2:
|
||||
agents:
|
||||
queue: "ascend-310p"
|
||||
resource_class: "npu-2"
|
||||
image: "swr.cn-southwest-2.myhuaweicloud.com/modelfoundry/vllm-omni-ci-npu:${BUILDKITE_COMMIT}-310p"
|
||||
plugins: *npu_k8s_plugins
|
||||
|
||||
310p_npu_4:
|
||||
agents:
|
||||
queue: "ascend-310p"
|
||||
resource_class: "npu-4"
|
||||
image: "swr.cn-southwest-2.myhuaweicloud.com/modelfoundry/vllm-omni-ci-npu:${BUILDKITE_COMMIT}-310p"
|
||||
plugins: *npu_k8s_plugins
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Shared Buildkite skip-ci and CI-YAML level targeting logic.
|
||||
|
||||
``resolve_ci_decision(changed_files, ...)`` buckets explicit file lists (tests, dry-run).
|
||||
@@ -10,12 +11,6 @@ CLI (for bootstrap scripts; exit code is the signal):
|
||||
python3 skip_ci.py gate <platform> <level>
|
||||
Exit 0 when bootstrap should stop (skip-all, or that L2/L3 target is off).
|
||||
Prints ``skip-all`` or ``skip-l23`` on stdout; logs the decision once.
|
||||
python3 skip_ci.py check-skip-all
|
||||
Exit 0 when only docs/skip-mark changes → bootstrap skips entire CI upload.
|
||||
python3 skip_ci.py check-skip-l2-l3 <platform> <level>
|
||||
Exit 0 when that L2/L3 pipeline should be skipped (CI config YAML-only diffs).
|
||||
python3 skip_ci.py print-annotate
|
||||
Print decision message for Buildkite annotation.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -258,11 +253,10 @@ class CiDecision:
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CiContext:
|
||||
"""Skip-ci decision plus git diff inputs used to compute it."""
|
||||
"""Skip-ci decision plus changed files used for pipeline filtering."""
|
||||
|
||||
decision: CiDecision
|
||||
changed_files: list[str] | None
|
||||
diff_range: str | None
|
||||
|
||||
|
||||
def _log(message: str) -> None:
|
||||
@@ -332,7 +326,7 @@ def resolve_ci_context_from_git() -> CiContext:
|
||||
diff_range = _resolve_diff_range()
|
||||
changed_files = _changed_files_for_diff_range(diff_range)
|
||||
decision = resolve_ci_decision(changed_files, diff_range=diff_range)
|
||||
return CiContext(decision=decision, changed_files=changed_files, diff_range=diff_range)
|
||||
return CiContext(decision=decision, changed_files=changed_files)
|
||||
|
||||
|
||||
# --- Doc / skip-mark helpers ---
|
||||
@@ -571,23 +565,6 @@ def main() -> int:
|
||||
gate_parser.add_argument("platform", help="cuda, amd, intel, or npu")
|
||||
gate_parser.add_argument("level", choices=("l2", "l3"), help="test level")
|
||||
|
||||
subparsers.add_parser(
|
||||
"check-skip-all",
|
||||
help="exit 0 when docs/skip-mark-only diff should skip entire CI upload",
|
||||
)
|
||||
|
||||
skip_parser = subparsers.add_parser(
|
||||
"check-skip-l2-l3",
|
||||
help="exit 0 when PLATFORM l2/l3 pipeline should be skipped (CI config YAML-only diffs)",
|
||||
)
|
||||
skip_parser.add_argument("platform", help="cuda, amd, intel, or npu")
|
||||
skip_parser.add_argument("level", choices=("l2", "l3"), help="test level")
|
||||
|
||||
subparsers.add_parser(
|
||||
"print-annotate",
|
||||
help="print human-readable skip reason for Buildkite annotation",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
decision = resolve_ci_context_from_git().decision
|
||||
|
||||
@@ -600,16 +577,6 @@ def main() -> int:
|
||||
return 0
|
||||
return 1
|
||||
|
||||
if args.command == "check-skip-all":
|
||||
return 0 if decision.skip_all else 1
|
||||
|
||||
if args.command == "check-skip-l2-l3":
|
||||
return 0 if not decision.is_run(args.platform, args.level) else 1
|
||||
|
||||
if args.command == "print-annotate":
|
||||
print(decision.message or "CI will run normally")
|
||||
return 0
|
||||
|
||||
return 1
|
||||
|
||||
|
||||
|
||||
@@ -12,8 +12,19 @@ Bootstrap mode (``bootstrap-upload-steps.yml``):
|
||||
|
||||
Test pipeline mode (e.g. test-merge.yml):
|
||||
- Drop steps whose ``source_file_dependencies`` do not match changed files.
|
||||
- Expand uploader-only ``mirror_hardwares: l4_1`` into ``agents`` (+ optional ``image``
|
||||
- Expand uploader-only ``mirror_hardwares`` into ``agents`` (+ optional ``image``
|
||||
for NPU) + ``plugins`` (see ci_mirror_hardwares.yml).
|
||||
- Omit ``mirror_hardwares`` to compose ``{chip}_{n}`` from pytest ``-m`` SKU
|
||||
markers plus ``cards_n`` (max of several positives; ``not cards_1`` with no
|
||||
positive ``cards_*`` uses that chip's highest existing preset). An explicit
|
||||
preset string (``h100_2``) is used as-is (marks are ignored). Names that
|
||||
are not keys in ``ci_mirror_hardwares.yml`` fail the upload.
|
||||
|
||||
Inferred chip: unset ``MIRROR_HW`` matches ``H100`` or ``L4`` in ``-m``
|
||||
(both → H100); no match skips the step. ``MIRROR_HW=b200`` matches
|
||||
``B200`` in ``-m`` (otherwise skipped). ``-m`` is not rewritten.
|
||||
``MIRROR_HW`` must be empty or ``b200``; unknown values fail the upload.
|
||||
A CUDA preset string such as ``h100_4`` is omitted when ``MIRROR_HW=b200``.
|
||||
|
||||
Usage:
|
||||
python3 upload_pipeline.py [--upload] [--all | --e2e] <pipeline.yml>
|
||||
@@ -25,11 +36,13 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import copy
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from typing import Any, Literal
|
||||
|
||||
try:
|
||||
import yaml
|
||||
@@ -45,11 +58,27 @@ from skip_ci import (
|
||||
resolve_ci_context_from_git,
|
||||
)
|
||||
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from tests.helpers.mark import ( # noqa: E402
|
||||
get_skus_for_platform,
|
||||
get_supported_card_counts,
|
||||
)
|
||||
|
||||
# --- Constants ---
|
||||
|
||||
LOG = "upload_pipeline"
|
||||
BOOTSTRAP_STEPS_FILENAME = "bootstrap-upload-steps.yml"
|
||||
BOOTSTRAP_IMAGE_BUILD_KEYS = frozenset({"image-build", "image-build-a2", "image-build-a3"})
|
||||
BOOTSTRAP_IMAGE_BUILD_KEYS = frozenset(
|
||||
{
|
||||
"image-build",
|
||||
"image-build-a2",
|
||||
"image-build-a3",
|
||||
"image-build-a5",
|
||||
"image-build-310p",
|
||||
}
|
||||
)
|
||||
BOOTSTRAP_UPLOAD_IF_KEYS = {
|
||||
"upload-ready-pipeline": "ready",
|
||||
"upload-merge-pipeline": "merge",
|
||||
@@ -58,6 +87,8 @@ BOOTSTRAP_UPLOAD_IF_KEYS = {
|
||||
}
|
||||
E2E_GROUP_MARKER = "E2E Test"
|
||||
CI_MIRROR_HARDWARES_PATH = ROOT / ".buildkite/common/ci_mirror_hardwares.yml"
|
||||
CUDA_HF_TOKEN_ENV = "VLLM_CI_HF_TOKEN"
|
||||
CUDA_HF_TOKEN_EXPORT = f'if [ -n "$${{{CUDA_HF_TOKEN_ENV}:-}}" ]; then export HF_TOKEN="$${{{CUDA_HF_TOKEN_ENV}}}"; fi'
|
||||
|
||||
# Bootstrap Buildkite ``if`` expressions.
|
||||
# ``*_MAIN_IF``: main + env schedule. ``*_LABEL_IF``: PR label (and/or composed with MAIN).
|
||||
@@ -233,32 +264,268 @@ def _load_mirror_hardwares() -> dict[str, dict[str, Any]]:
|
||||
return presets
|
||||
|
||||
|
||||
def _expand_mirror_hardwares(step: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Replace uploader-only ``mirror_hardwares`` with preset fields from ci_mirror_hardwares.yml."""
|
||||
hardware = step.get("mirror_hardwares")
|
||||
if hardware is None:
|
||||
return step
|
||||
@lru_cache(maxsize=1)
|
||||
def _cuda_mirror_chips() -> tuple[str, ...]:
|
||||
"""Lowercase CUDA SKUs that have at least one ``{chip}_{n}`` preset.
|
||||
|
||||
if not isinstance(hardware, str) or not hardware.strip():
|
||||
Longer tokens first so ``h100`` is not matched as a prefix of ``h200``.
|
||||
"""
|
||||
skus = get_skus_for_platform("cuda")
|
||||
known = _load_mirror_hardwares()
|
||||
chips = {
|
||||
sku.lower() for sku in skus if any(name == sku.lower() or name.startswith(f"{sku.lower()}_") for name in known)
|
||||
}
|
||||
return tuple(sorted(chips, key=lambda chip: (-len(chip), chip)))
|
||||
|
||||
|
||||
def _read_cards_marks(expr: str) -> tuple[set[int], bool]:
|
||||
"""Parse registered ``cards_n`` / ``not cards_n`` from a pytest ``-m`` expr.
|
||||
|
||||
Returns positive card counts and whether any ``not cards_*`` is present.
|
||||
"""
|
||||
counts = get_supported_card_counts()
|
||||
alt = "|".join(str(n) for n in sorted(counts, key=lambda n: (-len(str(n)), -n))) if counts else "(?!)"
|
||||
positives: set[int] = set()
|
||||
has_not_cards = False
|
||||
for match in re.finditer(rf"\b(?:(not)\s+)?cards_({alt})\b", expr):
|
||||
if match.group(1):
|
||||
has_not_cards = True
|
||||
else:
|
||||
positives.add(int(match.group(2)))
|
||||
return positives, has_not_cards
|
||||
|
||||
|
||||
def _read_hardware_marks(expr: str) -> set[str]:
|
||||
"""Return lowercase positive CUDA SKUs that have a mirror preset.
|
||||
|
||||
``not H100`` is ignored. SKUs with no ``{chip}_*`` preset (e.g. H200) are dropped.
|
||||
"""
|
||||
skus = sorted(get_skus_for_platform("cuda"), key=lambda s: (-len(s), s))
|
||||
if not skus:
|
||||
return set()
|
||||
alt = "|".join(re.escape(sku) for sku in skus)
|
||||
mirror = set(_cuda_mirror_chips())
|
||||
chips: set[str] = set()
|
||||
for match in re.finditer(rf"\b(?:(not)\s+)?({alt})\b", expr):
|
||||
if match.group(1):
|
||||
continue
|
||||
chip = match.group(2).lower()
|
||||
if chip in mirror:
|
||||
chips.add(chip)
|
||||
return chips
|
||||
|
||||
|
||||
# CI policy: not pytest-mark registry.
|
||||
_SUPPORTED_MIRROR_HW_SELECTORS = frozenset({"b200"})
|
||||
# Unset MIRROR_HW: match H100 then L4 in -m (skip if neither).
|
||||
_DEFAULT_INFER_CHIPS = ("h100", "l4")
|
||||
_PYTEST_MARKER_ARG = re.compile(r"-m\s+(?:\"([^\"]*)\"|'([^']*)'|(\S+))")
|
||||
_CARDS_CHIP_MAX: Literal["max"] = "max"
|
||||
|
||||
|
||||
def _get_mirror_hw_selector() -> str:
|
||||
"""Return lowercase ``MIRROR_HW``, or empty to keep string presets / marker chips.
|
||||
|
||||
A set value must be ``b200`` (case-insensitive). Unknown values fail closed
|
||||
so a typo cannot silently drop the CUDA pipeline.
|
||||
"""
|
||||
selector = os.environ.get("MIRROR_HW", "").strip().lower()
|
||||
if not selector:
|
||||
return ""
|
||||
if selector not in _SUPPORTED_MIRROR_HW_SELECTORS:
|
||||
allowed = " or ".join(repr(s) for s in sorted(_SUPPORTED_MIRROR_HW_SELECTORS))
|
||||
raise ValueError(f"unsupported MIRROR_HW={selector!r}; expected empty or {allowed}")
|
||||
return selector
|
||||
|
||||
|
||||
def _read_pytest_marks(commands: Any) -> tuple[set[str], int | Literal["max"] | None]:
|
||||
"""Read pytest ``-m`` from step commands: CUDA SKU chips and cards count.
|
||||
|
||||
*chips* are lowercase SKUs from positive markers (``not H100`` is ignored).
|
||||
*cards* is the max positive ``cards_n``, ``"max"`` when only ``not cards_*``
|
||||
is present, or ``None``.
|
||||
"""
|
||||
chunks: list[str] = []
|
||||
|
||||
def _collect(value: Any) -> None:
|
||||
if value is None:
|
||||
return
|
||||
if isinstance(value, str):
|
||||
chunks.append(value)
|
||||
elif isinstance(value, list):
|
||||
for part in value:
|
||||
_collect(part)
|
||||
else:
|
||||
chunks.append(str(value))
|
||||
|
||||
_collect(commands)
|
||||
text = "\n".join(chunks)
|
||||
|
||||
chips: set[str] = set()
|
||||
positives: set[int] = set()
|
||||
has_not_cards = False
|
||||
for match in _PYTEST_MARKER_ARG.finditer(text):
|
||||
expr = match.group(1) or match.group(2) or match.group(3) or ""
|
||||
chips |= _read_hardware_marks(expr)
|
||||
card_counts, not_cards = _read_cards_marks(expr)
|
||||
positives.update(card_counts)
|
||||
has_not_cards = has_not_cards or not_cards
|
||||
|
||||
if positives:
|
||||
cards: int | Literal["max"] | None = max(positives)
|
||||
elif has_not_cards:
|
||||
cards = _CARDS_CHIP_MAX
|
||||
else:
|
||||
cards = None
|
||||
return chips, cards
|
||||
|
||||
|
||||
def _compose_mirror_hardware_name(
|
||||
chips: set[str],
|
||||
cards: int | Literal["max"] | None,
|
||||
*,
|
||||
step_label: str,
|
||||
) -> str | None:
|
||||
"""Assemble ``{chip}_{n}`` from pytest ``-m`` chips/cards, or None to skip.
|
||||
|
||||
Unset ``MIRROR_HW``: ``H100`` if in ``-m``, else ``L4``, else skip.
|
||||
Set ``MIRROR_HW``: that chip if in ``-m``, else skip.
|
||||
``cards`` is a registered ``cards_n``, ``"max"`` for that chip's highest
|
||||
existing preset, or None (error after a chip matched — no ``cards_*``).
|
||||
"""
|
||||
selector = _get_mirror_hw_selector()
|
||||
if selector:
|
||||
chip = selector if selector in chips else None
|
||||
skip_reason = f"MIRROR_HW={selector!r} not in -m SKUs {sorted(chips)}"
|
||||
else:
|
||||
chip = next((c for c in _DEFAULT_INFER_CHIPS if c in chips), None)
|
||||
skip_reason = f"no H100 or L4 in -m SKUs {sorted(chips)}"
|
||||
if chip is None:
|
||||
_log(f"skip {step_label}: {skip_reason}")
|
||||
return None
|
||||
if cards is None:
|
||||
raise ValueError(
|
||||
f"mirror_hardwares must be a non-empty string in step {_get_step_label(step)!r}",
|
||||
f"step {step_label!r} has a pytest -m SKU marker but no cards_* "
|
||||
f"(or not cards_*); add cards_n or set mirror_hardwares to force a preset",
|
||||
)
|
||||
|
||||
preset = _load_mirror_hardwares().get(hardware)
|
||||
count_source = "not cards_*" if cards == _CARDS_CHIP_MAX else f"cards_{cards}"
|
||||
count = None if cards == _CARDS_CHIP_MAX else cards
|
||||
registered = get_supported_card_counts()
|
||||
if count is not None and count not in registered:
|
||||
supported = ", ".join(f"cards_{n}" for n in sorted(registered)) or "(none)"
|
||||
raise ValueError(
|
||||
f"{count_source} in step {step_label!r} is not a registered cards_* mark; supported: {supported}",
|
||||
)
|
||||
known = _load_mirror_hardwares()
|
||||
if count is None:
|
||||
available = [n for n in sorted(registered) if f"{chip}_{n}" in known]
|
||||
if not available:
|
||||
raise ValueError(
|
||||
f"{count_source} in step {step_label!r} has no CUDA count presets for {chip}",
|
||||
)
|
||||
count = max(available)
|
||||
chosen = f"{chip}_{count}"
|
||||
if chosen not in known:
|
||||
raise ValueError(
|
||||
f"{count_source} in step {step_label!r} has no preset {chosen!r}",
|
||||
)
|
||||
_log(f"{step_label}: {count_source} → {chosen!r}")
|
||||
return chosen
|
||||
|
||||
|
||||
def _resolve_mirror_hardware_name(hardware: Any, *, step_label: str) -> str | None:
|
||||
"""Map ``mirror_hardwares`` to a ``ci_mirror_hardwares.yml`` preset, or None to skip.
|
||||
|
||||
The value is a preset name (``h100_2``). Unknown names fail later when the
|
||||
registry is loaded. CUDA names are omitted when ``MIRROR_HW`` names a
|
||||
different chip; NPU strings ignore ``MIRROR_HW``.
|
||||
"""
|
||||
if isinstance(hardware, bool):
|
||||
raise ValueError(f"mirror_hardwares must not be a boolean in step {step_label!r}")
|
||||
if isinstance(hardware, str):
|
||||
name = hardware.strip()
|
||||
elif isinstance(hardware, int):
|
||||
name = str(hardware)
|
||||
else:
|
||||
raise ValueError(
|
||||
f"mirror_hardwares must be a preset string in step {step_label!r}",
|
||||
)
|
||||
if not name:
|
||||
raise ValueError(f"mirror_hardwares must be a non-empty string in step {step_label!r}")
|
||||
|
||||
selector = _get_mirror_hw_selector()
|
||||
token = name.lower()
|
||||
chip = next(
|
||||
(c for c in _cuda_mirror_chips() if token == c or token.startswith(f"{c}_")),
|
||||
None,
|
||||
)
|
||||
if selector and chip is not None and chip != selector:
|
||||
_log(
|
||||
f"skip {step_label}: preset {name!r} is {chip} hardware; MIRROR_HW={selector!r}",
|
||||
)
|
||||
return None
|
||||
return name
|
||||
|
||||
|
||||
def _expand_mirror_hardwares(step: dict[str, Any]) -> dict[str, Any] | None:
|
||||
"""Replace ``mirror_hardwares`` (or infer it from ``-m``) with preset agents/plugins.
|
||||
|
||||
Explicit preset string: used as-is; pytest marks are ignored.
|
||||
Omitted key: match H100/L4 (or ``MIRROR_HW``) in ``-m``, then compose
|
||||
``{chip}_{n}`` from ``cards_n``. No match skips the step. Steps with no
|
||||
SKU/cards and no ``mirror_hardwares`` are left unchanged (CPU jobs).
|
||||
|
||||
Returns None when the current ``MIRROR_HW`` selector does not apply.
|
||||
"""
|
||||
step_label = _get_step_label(step)
|
||||
has_pool = step.get("agents") is not None or step.get("plugins") is not None or step.get("image") is not None
|
||||
|
||||
if "mirror_hardwares" not in step:
|
||||
if has_pool:
|
||||
return step
|
||||
chips, cards = _read_pytest_marks(step.get("commands"))
|
||||
if not chips and cards is None:
|
||||
return step
|
||||
preset_name = _compose_mirror_hardware_name(chips, cards, step_label=step_label)
|
||||
else:
|
||||
if has_pool:
|
||||
raise ValueError(
|
||||
f"step {step_label!r} sets mirror_hardwares together with agents/plugins/image; "
|
||||
f"use mirror_hardwares only",
|
||||
)
|
||||
preset_name = _resolve_mirror_hardware_name(
|
||||
step.get("mirror_hardwares"),
|
||||
step_label=step_label,
|
||||
)
|
||||
|
||||
if preset_name is None:
|
||||
return None
|
||||
|
||||
preset = _load_mirror_hardwares().get(preset_name)
|
||||
if preset is None:
|
||||
known = ", ".join(sorted(_load_mirror_hardwares()))
|
||||
raise ValueError(
|
||||
f"unknown mirror_hardwares {hardware!r} in step {_get_step_label(step)!r}; known: {known}",
|
||||
)
|
||||
|
||||
if step.get("agents") is not None or step.get("plugins") is not None or step.get("image") is not None:
|
||||
raise ValueError(
|
||||
f"step {_get_step_label(step)!r} sets mirror_hardwares together with agents/plugins/image; "
|
||||
"use mirror_hardwares only",
|
||||
f"unknown mirror_hardwares {preset_name!r} in step {step_label!r}; known: {known}",
|
||||
)
|
||||
|
||||
expanded = copy.deepcopy(preset)
|
||||
return {key: value for key, value in step.items() if key != "mirror_hardwares"} | expanded
|
||||
merged = {key: value for key, value in step.items() if key != "mirror_hardwares"} | expanded
|
||||
# A scheduled build's HF_TOKEN may override a pod env entry with the same
|
||||
# name. The preset injects the Kubernetes secret under a collision-proof
|
||||
# alias; restore the conventional name after Buildkite has merged env.
|
||||
if any(preset_name == chip or preset_name.startswith(f"{chip}_") for chip in _cuda_mirror_chips()):
|
||||
commands = merged.get("commands")
|
||||
if isinstance(commands, list):
|
||||
merged["commands"] = [CUDA_HF_TOKEN_EXPORT, *commands]
|
||||
elif commands is None:
|
||||
merged["commands"] = [CUDA_HF_TOKEN_EXPORT]
|
||||
else:
|
||||
merged["commands"] = [CUDA_HF_TOKEN_EXPORT, commands]
|
||||
# Preset retry (K8S_RETRY on l4_*) must not clobber a step that opted out.
|
||||
if "retry" in step:
|
||||
merged["retry"] = step["retry"]
|
||||
return merged
|
||||
|
||||
|
||||
def _match_source_file(changed_files: list[str], prefixes: list[str]) -> bool:
|
||||
@@ -297,7 +564,7 @@ def _process_test_steps(
|
||||
nested = step.get("steps")
|
||||
if nested is not None:
|
||||
kept_nested = _process_test_steps(nested, changed_files)
|
||||
if changed_files is not None and not kept_nested:
|
||||
if not kept_nested:
|
||||
_log(f"omit empty group {_get_step_label(step)!r}")
|
||||
continue
|
||||
new_step = {key: value for key, value in step.items() if key != "source_file_dependencies"}
|
||||
@@ -305,14 +572,11 @@ def _process_test_steps(
|
||||
processed.append(new_step)
|
||||
continue
|
||||
|
||||
if deps is not None:
|
||||
processed.append(
|
||||
_expand_mirror_hardwares(
|
||||
{key: value for key, value in step.items() if key != "source_file_dependencies"},
|
||||
),
|
||||
)
|
||||
else:
|
||||
processed.append(_expand_mirror_hardwares(step))
|
||||
leaf = {key: value for key, value in step.items() if key != "source_file_dependencies"}
|
||||
expanded = _expand_mirror_hardwares(leaf)
|
||||
if expanded is None:
|
||||
continue
|
||||
processed.append(expanded)
|
||||
|
||||
return processed
|
||||
|
||||
@@ -338,6 +602,10 @@ def _render_test_pipeline(
|
||||
e2e_only: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
"""Filter steps by PR diff and strip uploader-only ``source_file_dependencies`` metadata."""
|
||||
# Validate MIRROR_HW once up front. Per-step skip must not run for typos
|
||||
# (e.g. b20o), or a pipeline can shrink to leftover CPU-only steps and
|
||||
# still upload successfully.
|
||||
_get_mirror_hw_selector()
|
||||
steps = doc.get("steps")
|
||||
if not isinstance(steps, list):
|
||||
return doc
|
||||
@@ -367,10 +635,7 @@ def _render_pipeline(
|
||||
|
||||
text = path.read_text(encoding="utf-8")
|
||||
ctx = resolve_ci_context_from_git()
|
||||
if force_all or e2e_only:
|
||||
changed_files = None
|
||||
else:
|
||||
changed_files = ctx.changed_files
|
||||
changed_files = None if force_all or e2e_only else ctx.changed_files
|
||||
|
||||
doc = yaml.safe_load(text)
|
||||
if not isinstance(doc, dict):
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# CUDA CI L3 (`advanced_model` e2e) plus L1 CPU Simple Test (`core_model and cpu`).
|
||||
# Uploaded by Upload Merge Pipeline: PR label `merge-test`, or post-merge on main.
|
||||
|
||||
env:
|
||||
VLLM_WORKER_MULTIPROC_METHOD: spawn
|
||||
VLLM_USE_FLASHINFER_MOE_FP16: 0
|
||||
HF_HUB_DOWNLOAD_TIMEOUT: 300
|
||||
HF_HUB_ETAG_TIMEOUT: 60
|
||||
|
||||
@@ -77,6 +79,9 @@ steps:
|
||||
steps:
|
||||
- label: "TTS · Qwen3-TTS CustomVoice Test"
|
||||
source_file_dependencies:
|
||||
- tests/e2e/features/prefill_phase/
|
||||
- vllm_omni/worker/gpu_model_runner.py
|
||||
- tests/helpers/media.py
|
||||
- tests/e2e/online_serving/test_qwen3_tts_customvoice.py
|
||||
- tests/e2e/offline_inference/test_qwen3_tts_customvoice.py
|
||||
- vllm_omni/model_executor/models/qwen3_tts/
|
||||
@@ -93,12 +98,13 @@ steps:
|
||||
timeout 40m bash -c "
|
||||
export VLLM_LOGGING_LEVEL=DEBUG
|
||||
export VLLM_ALLOW_LONG_MAX_MODEL_LEN=1
|
||||
pytest -s -v tests/e2e/online_serving/test_qwen3_tts_customvoice.py tests/e2e/offline_inference/test_qwen3_tts_customvoice.py -m 'advanced_model and cuda' --run-level 'advanced_model'
|
||||
pytest -s -v tests/e2e/online_serving/test_qwen3_tts_customvoice.py tests/e2e/offline_inference/test_qwen3_tts_customvoice.py tests/e2e/features/prefill_phase/test_chunked_prefill_tail.py -m 'advanced_model and cuda' --run-level 'advanced_model'
|
||||
"
|
||||
mirror_hardwares: l4_1
|
||||
|
||||
- label: "TTS · Qwen3-TTS Base Test"
|
||||
source_file_dependencies:
|
||||
- tests/helpers/media.py
|
||||
- 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/
|
||||
@@ -159,10 +165,11 @@ steps:
|
||||
commands:
|
||||
- pytest -s -v tests/e2e/offline_inference/test_minicpmo_4_5.py tests/e2e/online_serving/test_minicpmo_4_5.py -m 'advanced_model and cuda' --run-level 'advanced_model'
|
||||
# h100_2, not h100_1: advanced_model asserts transcribe every audio
|
||||
# response with Whisper, and tests/helpers/media.py only uses an
|
||||
# accelerator for Whisper when a spare GPU exists. On a single card it
|
||||
# grades on CPU: ~8-10 min per clip vs ~30 s, which alone overruns the
|
||||
# 50-minute job timeout (build 2956).
|
||||
# response with Whisper, and tests/helpers/media.py requires at least
|
||||
# 16 GiB free VRAM on a device to run Whisper on GPU. When the model
|
||||
# server leaves insufficient memory, Whisper falls back to CPU:
|
||||
# ~8-10 min per clip vs ~30 s, which overruns the 50-minute job timeout
|
||||
# (build 2956). h100_2 guarantees a second GPU with ample free VRAM.
|
||||
mirror_hardwares: h100_2
|
||||
|
||||
- label: "Omni · MiniCPM-o 4.5 Duplex Test"
|
||||
@@ -178,6 +185,21 @@ steps:
|
||||
- tests/e2e/online_serving/run_minicpmo_realtime_duplex_soft_interrupt.py
|
||||
- vllm_omni/deploy/minicpmo_4_5.yaml
|
||||
- tests/e2e/online_serving/helpers/minicpmo_4_5_duplex.py
|
||||
- vllm_omni/engine/duplex/
|
||||
- vllm_omni/entrypoints/duplex/
|
||||
# Public duplex client library + its unit suites: client-side
|
||||
# changes must re-run the duplex e2e group. The live client probe
|
||||
# (test_duplex_client_live.py, single GPU, core tier) runs in the
|
||||
# ready pipeline's duplex group.
|
||||
- vllm_omni/clients/
|
||||
- tests/clients/
|
||||
- vllm_omni/entrypoints/duplex_request_client.py
|
||||
- vllm_omni/outputs/duplex.py
|
||||
- vllm_omni/model_executor/duplex_sampling.py
|
||||
- tests/engine/duplex/
|
||||
- tests/entrypoints/duplex/
|
||||
- tests/model_executor/models/minicpmo_4_5/duplex/
|
||||
# Legacy Realtime probe client + video stacking used by the e2e drivers.
|
||||
- vllm_omni/experimental/fullduplex/
|
||||
- vllm_omni/model_executor/models/minicpmo_4_5/
|
||||
- vllm_omni/model_executor/models/step_audio2/
|
||||
@@ -237,3 +259,38 @@ steps:
|
||||
pytest -s -v tests/e2e/online_serving/test_hunyuan_video_15_expansion.py -m 'advanced_model and cuda and diffusion' --run-level 'advanced_model'
|
||||
"
|
||||
mirror_hardwares: h100_1
|
||||
|
||||
- label: "Diffusion · MiniMax-H3 L3 DLO DP2 and FastH3"
|
||||
source_file_dependencies:
|
||||
- tests/e2e/online_serving/minimax_h3/
|
||||
- tests/helpers/assertions.py
|
||||
- tests/helpers/mark.py
|
||||
- tests/helpers/media.py
|
||||
- tests/helpers/runtime.py
|
||||
- vllm_omni/diffusion/models/minimax_h3/
|
||||
- vllm_omni/diffusion/lora/
|
||||
- vllm_omni/diffusion/offloader/
|
||||
- vllm_omni/diffusion/distributed/
|
||||
- vllm_omni/entrypoints/openai/
|
||||
# Keep four server lifecycles in one Buildkite job, but never in one
|
||||
# pytest process. DLO uses multiprocessing shared-memory queues; the
|
||||
# parent's resource_tracker can retain a stale /psm_* entry during
|
||||
# teardown and strand the next Ref2VA collective.
|
||||
# A per-file process plus a hard timeout also makes a regression fail
|
||||
# instead of leaving the merge queue stuck indefinitely.
|
||||
timeout_in_minutes: 150
|
||||
commands:
|
||||
- |
|
||||
set -eu
|
||||
export VLLM_IMAGE_FETCH_TIMEOUT=60
|
||||
if [ -f vllm_omni/diffusion/models/minimax_h3/lora.py ]; then
|
||||
export VLLM_TEST_MINIMAX_H3_TURBO_LORA=$(python3 -c 'from huggingface_hub import hf_hub_download; print(hf_hub_download(repo_id="lightx2v/Minimax-h3-Turbo", filename="minimax_h3_fl2v_turbo_4step_v1.0_768p_bf16.safetensors"))')
|
||||
fi
|
||||
if [ -f vllm_omni/diffusion/models/minimax_h3/fasth3.py ]; then
|
||||
export VLLM_TEST_MINIMAX_H3_FASTH3_LORA=$(python3 -c 'from huggingface_hub import hf_hub_download; print(hf_hub_download(repo_id="FastVideo/FastVideo-FastH3-4-step-Preview-v1-LoRA", filename="dense-datafree/adapter_model.safetensors"))')
|
||||
fi
|
||||
timeout 35m pytest -s -v tests/e2e/online_serving/minimax_h3/test_minimax_h3_dlo_dp2_fl2va.py -m 'advanced_model and cuda and diffusion' --run-level 'advanced_model'
|
||||
timeout 35m pytest -s -v tests/e2e/online_serving/minimax_h3/test_minimax_h3_dlo_dp2_ref2va.py -m 'advanced_model and cuda and diffusion' --run-level 'advanced_model'
|
||||
timeout 35m pytest -s -v tests/e2e/online_serving/minimax_h3/test_minimax_h3_turbo_lora.py -m 'advanced_model and cuda and diffusion' --run-level 'advanced_model'
|
||||
timeout 35m pytest -s -v tests/e2e/online_serving/minimax_h3/test_minimax_h3_fasth3_lora.py -m 'advanced_model and cuda and diffusion' --run-level 'advanced_model'
|
||||
mirror_hardwares: h100_2
|
||||
@@ -1,6 +1,9 @@
|
||||
# CUDA CI L4 (`full_model`): nightly and release share this file.
|
||||
# Uploaded by Upload Nightly Pipeline: NIGHTLY=1, or PR labels (nightly-test / omni-test / ...).
|
||||
# Hardware: omit mirror_hardwares to compose from -m SKU + cards_n; or set a preset string to force.
|
||||
|
||||
env:
|
||||
VLLM_WORKER_MULTIPROC_METHOD: spawn
|
||||
VLLM_USE_FLASHINFER_MOE_FP16: 0
|
||||
HF_HUB_DOWNLOAD_TIMEOUT: 300
|
||||
HF_HUB_ETAG_TIMEOUT: 60
|
||||
|
||||
@@ -13,16 +16,14 @@ steps:
|
||||
steps:
|
||||
|
||||
- label: ":full_moon: Omni · Function Test with H100 · Single-GPU"
|
||||
timeout_in_minutes: 90
|
||||
timeout_in_minutes: 120
|
||||
commands:
|
||||
- pytest -sv tests/e2e --run-level "full_model" -m "full_model and H100 and omni and cards_1" --ignore=tests/e2e/accuracy --ignore=tests/e2e/online_serving/test_minicpmo_4_5_duplex_expansion.py
|
||||
mirror_hardwares: h100_1
|
||||
- pytest -sv tests/e2e --run-level "full_model" -m "full_model and H100 and B200 and omni and cards_1" --ignore=tests/e2e/accuracy --ignore=tests/e2e/online_serving/test_minicpmo_4_5_duplex_expansion.py
|
||||
|
||||
- label: ":full_moon: Omni · Function Test with H100 · 2-GPU"
|
||||
timeout_in_minutes: 90
|
||||
commands:
|
||||
- pytest -sv tests/e2e --run-level "full_model" -m "full_model and H100 and omni and cards_2" --ignore=tests/e2e/accuracy --ignore=tests/e2e/online_serving/test_qwen3_omni_multi_replicas.py
|
||||
mirror_hardwares: h100_2
|
||||
- pytest -sv tests/e2e --run-level "full_model" -m "full_model and H100 and B200 and omni and cards_2" --ignore=tests/e2e/accuracy --ignore=tests/e2e/online_serving/test_qwen3_omni_multi_replicas.py
|
||||
|
||||
- label: ":full_moon: Omni · MiniCPM-o 4.5 Duplex Test"
|
||||
timeout_in_minutes: 50
|
||||
@@ -30,11 +31,11 @@ steps:
|
||||
- pytest -s -v tests/e2e/online_serving/test_minicpmo_4_5_duplex_expansion.py -m "full_model and cuda and H100 and omni and cards_1" --run-level "full_model"
|
||||
mirror_hardwares: h100_1
|
||||
|
||||
- label: ":full_moon: Omni · Doc Test with L4"
|
||||
- label: ":full_moon: Omni · Doc Test with L4 · 4-GPU"
|
||||
timeout_in_minutes: 90
|
||||
commands:
|
||||
- export VLLM_ALLOW_LONG_MAX_MODEL_LEN="1"
|
||||
- pytest -s -v tests/examples/ -m "full_model and omni and L4" --run-level "full_model"
|
||||
- pytest -s -v tests/examples/ -m "full_model and omni and L4 and cards_4" --run-level "full_model"
|
||||
mirror_hardwares: l4_4
|
||||
|
||||
- label: ":full_moon: Omni · Doc Test with H100 · 2-GPU"
|
||||
@@ -178,7 +179,7 @@ steps:
|
||||
build.pull_request.labels includes "nightly-test"
|
||||
steps:
|
||||
|
||||
- label: ":full_moon: Tiny Model Tests · Diffusion (Multi-GPU accelerations)"
|
||||
- label: ":full_moon: Tiny Model Tests · Diffusion (Multi-GPU accelerations) · 2-GPU"
|
||||
timeout_in_minutes: 30
|
||||
# NOTE: Single-GPU (core_model) tiny model tests run on the ready label,
|
||||
# so nightly only adds the multi-GPU parallelism configurations.
|
||||
@@ -188,7 +189,14 @@ steps:
|
||||
# for the CI, since these tests should pass irrespective of whether or not we use
|
||||
# the full weights or not.
|
||||
commands:
|
||||
- timeout 25m pytest -sv tests/model_tests/diffusion -m 'full_model and cuda' --run-level "core_model"
|
||||
- pytest -sv tests/model_tests/diffusion -m 'full_model and cuda and cards_2' --run-level "core_model"
|
||||
mirror_hardwares: l4_2
|
||||
|
||||
- label: ":full_moon: Tiny Model Tests · Diffusion (Multi-GPU accelerations) · 4-GPU"
|
||||
timeout_in_minutes: 30
|
||||
# CFG+TP extra_test_groups request 2×2=4 devices (see get_required_device_count).
|
||||
commands:
|
||||
- pytest -sv tests/model_tests/diffusion -m 'full_model and cuda and cards_4' --run-level "core_model"
|
||||
mirror_hardwares: l4_4
|
||||
|
||||
- group: ":card_index_dividers: Diffusion X2I(&A&T) Model Test"
|
||||
@@ -220,10 +228,21 @@ steps:
|
||||
--run-level "full_model"
|
||||
mirror_hardwares: h100_2
|
||||
|
||||
- label: ":full_moon: Diffusion X2I(&A&T) · Function Test with L4 · Multi-GPU"
|
||||
- label: ":full_moon: Diffusion X2I(&A&T) · Boogu Edit CFG · 3-GPU"
|
||||
timeout_in_minutes: 120
|
||||
commands:
|
||||
- pytest -sv tests/e2e/ -k "not test_wan and not hunyuan" -m "full_model and diffusion and L4 and not cards_1" --run-level "full_model" --ignore=tests/e2e/accuracy
|
||||
- >-
|
||||
pytest -sv
|
||||
tests/e2e/online_serving/test_boogu_image_edit_expansion.py
|
||||
-k "test_double_guidance_cfg_parallel and cfg_parallel_3_full"
|
||||
-m "slow and diffusion and cuda and H100 and cards_3"
|
||||
--run-level "full_model"
|
||||
mirror_hardwares: h100_3
|
||||
|
||||
- label: ":full_moon: Diffusion X2I(&A&T) · Function Test with L4 · 4-GPU"
|
||||
timeout_in_minutes: 120
|
||||
commands:
|
||||
- pytest -sv tests/e2e/ -k "not test_wan and not hunyuan" -m "full_model and diffusion and L4 and cards_4" --run-level "full_model" --ignore=tests/e2e/accuracy
|
||||
mirror_hardwares: l4_4
|
||||
|
||||
- label: ":full_moon: Diffusion X2I(&A&T) · Doc Test"
|
||||
@@ -259,7 +278,6 @@ steps:
|
||||
timeout_in_minutes: 180
|
||||
commands:
|
||||
- export DIFFUSION_BENCHMARK_DIR=tests/dfx/perf/results
|
||||
- export DIFFUSION_ATTENTION_BACKEND=FLASH_ATTN
|
||||
- export CACHE_DIT_VERSION=1.5.0
|
||||
- |
|
||||
set +e
|
||||
@@ -325,6 +343,12 @@ steps:
|
||||
- pytest -s -v tests/e2e/online_serving/test_hunyuan_video_15_expansion.py -m "full_model and cuda" --run-level "full_model"
|
||||
mirror_hardwares: h100_2
|
||||
|
||||
- label: ":full_moon: Diffusion X2V · MAGI-2 Preview Serving Smoke"
|
||||
timeout_in_minutes: 180
|
||||
commands:
|
||||
- pytest -s -v tests/e2e/online_serving/test_magi2.py -m "advanced_model and diffusion and cuda and H100 and cards_4" --run-level "advanced_model"
|
||||
mirror_hardwares: h100_4
|
||||
|
||||
- label: ":full_moon: Diffusion X2V · Doc Test"
|
||||
timeout_in_minutes: 60
|
||||
commands:
|
||||
@@ -362,7 +386,6 @@ steps:
|
||||
timeout_in_minutes: 180
|
||||
commands:
|
||||
- export DIFFUSION_BENCHMARK_DIR=tests/dfx/perf/results
|
||||
- export DIFFUSION_ATTENTION_BACKEND=FLASH_ATTN
|
||||
- |
|
||||
set +e
|
||||
pytest -s -v tests/dfx/perf/scripts/run_diffusion_benchmark.py --test-config-file tests/dfx/perf/tests/test_wan22_i2v_vllm_omni.json -m "H100 and cards_1"
|
||||
@@ -412,7 +435,6 @@ steps:
|
||||
timeout_in_minutes: 180
|
||||
commands:
|
||||
- export DIFFUSION_BENCHMARK_DIR=tests/dfx/perf/results
|
||||
- export DIFFUSION_ATTENTION_BACKEND=FLASH_ATTN
|
||||
- |
|
||||
set +e
|
||||
pytest -s -v tests/dfx/perf/scripts/run_diffusion_benchmark.py --test-config-file tests/dfx/perf/tests/test_minimax_h3_vllm_omni.json
|
||||
@@ -427,10 +449,22 @@ steps:
|
||||
depends_on: upload-nightly-pipeline
|
||||
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test"
|
||||
steps:
|
||||
- label: ":full_moon: Diffusion · Distributed Test with L4"
|
||||
- label: ":full_moon: Diffusion · Distributed Test with L4 · 2-GPU"
|
||||
timeout_in_minutes: 60
|
||||
commands:
|
||||
- pytest -sv tests/diffusion/distributed/ -m 'full_model and cuda and L4' --run-level "full_model"
|
||||
- pytest -sv tests/diffusion/distributed/ -m 'full_model and cuda and L4 and cards_2' --run-level "full_model"
|
||||
mirror_hardwares: l4_2
|
||||
|
||||
- label: ":full_moon: Diffusion · Distributed Test with L4 · 3-GPU"
|
||||
timeout_in_minutes: 60
|
||||
commands:
|
||||
- pytest -sv tests/diffusion/distributed/ -m 'full_model and cuda and L4 and cards_3' --run-level "full_model"
|
||||
mirror_hardwares: l4_3
|
||||
|
||||
- label: ":full_moon: Diffusion · Distributed Test with L4 · 4-GPU"
|
||||
timeout_in_minutes: 60
|
||||
commands:
|
||||
- pytest -sv tests/diffusion/distributed/ -m 'full_model and cuda and L4 and cards_4' --run-level "full_model"
|
||||
mirror_hardwares: l4_4
|
||||
|
||||
- label: ":full_moon: Diffusion · Distributed Test with H100 · 2-GPU"
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# CUDA CI L2 (`core_model` e2e) plus L1 CPU Simple Test (`core_model and cpu`).
|
||||
# Uploaded by Upload Ready Pipeline: PR label `ready`, or main+WEEKLY=1 with --e2e.
|
||||
|
||||
env:
|
||||
VLLM_WORKER_MULTIPROC_METHOD: spawn
|
||||
VLLM_USE_FLASHINFER_MOE_FP16: 0
|
||||
HF_HUB_DOWNLOAD_TIMEOUT: 300
|
||||
HF_HUB_ETAG_TIMEOUT: 60
|
||||
|
||||
@@ -51,17 +53,20 @@ steps:
|
||||
- timeout 15m pytest -sv tests/diffusion/batching -m 'core_model and cuda' --run-level "core_model"
|
||||
mirror_hardwares: l4_1
|
||||
|
||||
- label: "Diffusion · Distributed & Attention Test"
|
||||
source_file_dependencies:
|
||||
- tests/diffusion/distributed/
|
||||
- tests/diffusion/attention/test_attention_sp.py
|
||||
- vllm_omni/diffusion/distributed/
|
||||
- vllm_omni/diffusion/attention/
|
||||
- vllm_omni/diffusion/data.py
|
||||
- vllm_omni/diffusion/models/
|
||||
- label: "Diffusion · Distributed Test · Single-GPU"
|
||||
commands:
|
||||
- timeout 20m pytest -sv tests/diffusion/distributed -m 'core_model and cuda' --run-level "core_model"
|
||||
- timeout 20m pytest -sv tests/diffusion/attention/test_attention_sp.py -m 'core_model and cuda' --run-level "core_model"
|
||||
- timeout 20m pytest -sv tests/diffusion/distributed -m 'core_model and cuda and L4 and cards_1' --run-level "core_model"
|
||||
mirror_hardwares: l4_1
|
||||
|
||||
- label: "Diffusion · Distributed Test · 2-GPU"
|
||||
commands:
|
||||
- timeout 20m pytest -sv tests/diffusion/distributed -m 'core_model and cuda and L4 and cards_2' --run-level "core_model"
|
||||
mirror_hardwares: l4_2
|
||||
|
||||
- label: "Diffusion · Distributed & Attention Test · 4-GPU"
|
||||
commands:
|
||||
- timeout 20m pytest -sv tests/diffusion/distributed -m 'core_model and cuda and L4 and cards_4' --run-level "core_model"
|
||||
- timeout 20m pytest -sv tests/diffusion/attention/test_attention_sp.py -m 'core_model and cuda and L4 and cards_4' --run-level "core_model"
|
||||
mirror_hardwares: l4_4
|
||||
|
||||
- label: "Diffusion · Model Test"
|
||||
@@ -71,7 +76,7 @@ steps:
|
||||
|
||||
- label: "Diffusion · Other Test"
|
||||
commands:
|
||||
- timeout 15m pytest -sv tests/diffusion/*.py -m 'core_model and cuda' --run-level "core_model"
|
||||
- timeout 15m pytest -sv tests/diffusion/*.py tests/diffusion/ar_diffusion -m 'core_model and cuda' --run-level "core_model"
|
||||
mirror_hardwares: l4_1
|
||||
|
||||
- group: ":card_index_dividers: Tiny Model Tests [Single GPU]"
|
||||
@@ -88,7 +93,7 @@ steps:
|
||||
commands:
|
||||
- |
|
||||
timeout 15m bash -c "
|
||||
pytest -sv tests/engine/ tests/model_executor/ -m 'core_model and cuda' --run-level 'core_model'
|
||||
pytest -sv tests/engine/ tests/model_executor/ tests/worker/test_batched_omni_output.py -m 'core_model and cuda' --run-level 'core_model'
|
||||
"
|
||||
mirror_hardwares: l4_1
|
||||
|
||||
@@ -209,6 +214,21 @@ steps:
|
||||
- tests/e2e/online_serving/run_minicpmo_realtime_duplex_soft_interrupt.py
|
||||
- vllm_omni/deploy/minicpmo_4_5.yaml
|
||||
- tests/e2e/online_serving/helpers/minicpmo_4_5_duplex.py
|
||||
- vllm_omni/engine/duplex/
|
||||
- vllm_omni/entrypoints/duplex/
|
||||
# Public duplex client library + its unit suites: client-side
|
||||
# changes must re-run the duplex e2e group, which includes the
|
||||
# live client probe (test_duplex_client_live.py, single GPU).
|
||||
- vllm_omni/clients/
|
||||
- tests/clients/
|
||||
- tests/e2e/online_serving/test_duplex_client_live.py
|
||||
- vllm_omni/entrypoints/duplex_request_client.py
|
||||
- vllm_omni/outputs/duplex.py
|
||||
- vllm_omni/model_executor/duplex_sampling.py
|
||||
- tests/engine/duplex/
|
||||
- tests/entrypoints/duplex/
|
||||
- tests/model_executor/models/minicpmo_4_5/duplex/
|
||||
# Legacy Realtime probe client + video stacking used by the e2e drivers.
|
||||
- vllm_omni/experimental/fullduplex/
|
||||
- vllm_omni/model_executor/models/minicpmo_4_5/
|
||||
- vllm_omni/model_executor/models/step_audio2/
|
||||
@@ -226,8 +246,8 @@ steps:
|
||||
- vllm_omni/config/stage_config.py
|
||||
commands:
|
||||
- |
|
||||
timeout 20m bash -c "
|
||||
pytest -s -v tests/e2e/online_serving/test_minicpmo_4_5_duplex.py -m 'core_model and cuda' --run-level 'core_model'
|
||||
timeout 35m bash -c "
|
||||
pytest -s -v tests/e2e/online_serving/test_minicpmo_4_5_duplex.py tests/e2e/online_serving/test_duplex_client_live.py -m 'core_model and cuda' --run-level 'core_model'
|
||||
"
|
||||
mirror_hardwares: h100_1
|
||||
|
||||
@@ -245,6 +265,7 @@ steps:
|
||||
|
||||
- label: "TTS · Qwen3-TTS CustomVoice Test"
|
||||
source_file_dependencies:
|
||||
- tests/helpers/media.py
|
||||
- tests/e2e/online_serving/test_qwen3_tts_customvoice.py
|
||||
- vllm_omni/model_executor/models/qwen3_tts/
|
||||
- vllm_omni/model_executor/models/common/qwen3_code_predictor.py
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# CUDA CI L5 (`slow` / reliability / extra coverage).
|
||||
# Uploaded by Upload Weekly Pipeline: WEEKLY=1 / NON_CRITICAL=1, or PR label `weekly-test`.
|
||||
|
||||
env:
|
||||
VLLM_WORKER_MULTIPROC_METHOD: spawn
|
||||
VLLM_USE_FLASHINFER_MOE_FP16: 0
|
||||
HF_HUB_DOWNLOAD_TIMEOUT: 300
|
||||
HF_HUB_ETAG_TIMEOUT: 60
|
||||
|
||||
@@ -111,17 +113,24 @@ steps:
|
||||
if: build.env("NON_CRITICAL") == "1"
|
||||
steps:
|
||||
|
||||
# Qwen2.5 expansion (CUDA 4-card / online 2-card) stays on this l4_4 job.
|
||||
- label: "Omni · L4 · Multi-GPU"
|
||||
# Online Qwen2.5 expansion is 2-card; offline expansion + AutoRound are 4-card.
|
||||
- label: "Omni · L4 · 2-GPU"
|
||||
timeout_in_minutes: 90
|
||||
commands:
|
||||
- export VLLM_ALLOW_LONG_MAX_MODEL_LEN="1"
|
||||
- pytest -s -v tests/e2e/*/test_qwen2_5_omni_expansion.py -m "slow and L4 and omni and not cards_1" --run-level "core_model"
|
||||
- pytest -s -v tests/e2e/online_serving/test_qwen2_5_omni_expansion.py -m "slow and L4 and omni and cards_2" --run-level "core_model"
|
||||
mirror_hardwares: l4_2
|
||||
|
||||
- label: "Omni · L4 · 4-GPU"
|
||||
timeout_in_minutes: 90
|
||||
commands:
|
||||
- export VLLM_ALLOW_LONG_MAX_MODEL_LEN="1"
|
||||
- pytest -s -v tests/e2e/offline_inference/test_qwen2_5_omni_expansion.py -m "slow and L4 and omni and cards_4" --run-level "core_model"
|
||||
- >-
|
||||
pytest -s -v tests/e2e/
|
||||
--ignore=tests/e2e/online_serving/test_qwen2_5_omni_expansion.py
|
||||
--ignore=tests/e2e/offline_inference/test_qwen2_5_omni_expansion.py
|
||||
-m "slow and L4 and omni and not cards_1"
|
||||
-m "slow and L4 and omni and cards_4"
|
||||
--run-level "full_model"
|
||||
mirror_hardwares: l4_4
|
||||
|
||||
@@ -194,12 +203,21 @@ steps:
|
||||
--run-level "full_model"
|
||||
mirror_hardwares: l4_1
|
||||
|
||||
- label: "Diffusion · L4 · Multi-GPU"
|
||||
- label: "Diffusion · L4 · 2-GPU"
|
||||
timeout_in_minutes: 60
|
||||
commands:
|
||||
- >-
|
||||
pytest -sv
|
||||
tests/e2e/ -m "slow and diffusion and L4 and not cards_1"
|
||||
tests/e2e/ -m "slow and diffusion and L4 and cards_2"
|
||||
--run-level "full_model"
|
||||
mirror_hardwares: l4_2
|
||||
|
||||
- label: "Diffusion · L4 · 4-GPU"
|
||||
timeout_in_minutes: 60
|
||||
commands:
|
||||
- >-
|
||||
pytest -sv
|
||||
tests/e2e/ -m "slow and diffusion and L4 and cards_4"
|
||||
--run-level "full_model"
|
||||
mirror_hardwares: l4_4
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
steps:
|
||||
- label: ":buildkit: Build and Push NPU Test Image - A2"
|
||||
key: image-build-a2
|
||||
agents:
|
||||
agents: &npu-image-build-agents
|
||||
queue: "ascend-a2b3"
|
||||
resource_class: "npu-1"
|
||||
plugins:
|
||||
plugins: &npu-image-build-plugins
|
||||
- kubernetes:
|
||||
metadata:
|
||||
annotations:
|
||||
@@ -46,123 +46,47 @@ steps:
|
||||
IMAGE_NAME: "vllm-omni-ci-npu"
|
||||
IMAGE_REGISTRY: "swr.cn-southwest-2.myhuaweicloud.com/modelfoundry"
|
||||
BUILDKITD_ADDR: "tcp://buildkitd-service.buildkitd:1234"
|
||||
command: |
|
||||
set -ex
|
||||
|
||||
echo "--- Building and pushing NPU Test Image"
|
||||
echo "Image: $${IMAGE_REGISTRY}/$${IMAGE_NAME}:$${VLLM_IMAGE_TAG}"
|
||||
echo "buildkitd address: $${BUILDKITD_ADDR}"
|
||||
|
||||
if ! command -v buildctl &> /dev/null; then
|
||||
echo "Installing buildctl..."
|
||||
mkdir -p /tmp/buildkit
|
||||
BUILDKIT_VERSION="v0.29.0"
|
||||
wget -q "https://github.com/moby/buildkit/releases/download/$${BUILDKIT_VERSION}/buildkit-$${BUILDKIT_VERSION}.linux-arm64.tar.gz" -O /tmp/buildkit.tar.gz
|
||||
tar -xzf /tmp/buildkit.tar.gz -C /tmp/buildkit
|
||||
cp /tmp/buildkit/bin/buildctl /usr/local/bin/
|
||||
fi
|
||||
|
||||
export DOCKER_CONFIG=/home/user/.docker
|
||||
buildctl \
|
||||
--addr="$${BUILDKITD_ADDR}" \
|
||||
--tlscacert=/home/user/.docker/ca.pem \
|
||||
--tlscert=/home/user/.docker/cert.pem \
|
||||
--tlskey=/home/user/.docker/key.pem \
|
||||
build \
|
||||
--frontend dockerfile.v0 \
|
||||
--local context=. \
|
||||
--local dockerfile=./docker \
|
||||
--opt filename=Dockerfile.npu.ci \
|
||||
--secret id=dockerconfig,src=/home/user/.docker/config.json \
|
||||
--output type=image,name=$${IMAGE_REGISTRY}/$${IMAGE_NAME}:$${VLLM_IMAGE_TAG},push=true \
|
||||
--progress=plain
|
||||
|
||||
echo "--- Image pushed successfully"
|
||||
echo "$${IMAGE_REGISTRY}/$${IMAGE_NAME}:$${VLLM_IMAGE_TAG}"
|
||||
command: bash .buildkite/npu/scripts/build-npu-image.sh Dockerfile.npu.ci "$${VLLM_IMAGE_TAG}"
|
||||
|
||||
- label: ":buildkit: Build and Push NPU Test Image - A3"
|
||||
key: image-build-a3
|
||||
agents:
|
||||
queue: "ascend-a2b3"
|
||||
resource_class: "npu-1"
|
||||
plugins:
|
||||
- kubernetes:
|
||||
metadata:
|
||||
annotations:
|
||||
vault.hashicorp.com/agent-init-first: "true"
|
||||
vault.hashicorp.com/agent-inject: "true"
|
||||
vault.hashicorp.com/agent-inject-perms-ca.pem: "0400"
|
||||
vault.hashicorp.com/agent-inject-perms-cert.pem: "0400"
|
||||
vault.hashicorp.com/agent-inject-perms-config.json: "0400"
|
||||
vault.hashicorp.com/agent-inject-perms-key.pem: "0400"
|
||||
vault.hashicorp.com/agent-inject-secret-ca.pem: internal/data/ascend/buildkitd
|
||||
vault.hashicorp.com/agent-inject-secret-cert.pem: internal/data/ascend/buildkitd
|
||||
vault.hashicorp.com/agent-inject-secret-config.json: internal/data/ascend/buildkitd
|
||||
vault.hashicorp.com/agent-inject-secret-key.pem: internal/data/ascend/buildkitd
|
||||
vault.hashicorp.com/agent-inject-template-ca.pem: "{{- with secret \"internal/data/ascend/buildkitd\" -}}\n{{ .Data.data.RootCA }}\n{{- end }}"
|
||||
vault.hashicorp.com/agent-inject-template-cert.pem: "{{- with secret \"internal/data/ascend/buildkitd\" -}}\n{{ .Data.data.ClientCaCert }}\n{{- end }}"
|
||||
vault.hashicorp.com/agent-inject-template-config.json: "{{- with secret \"internal/data/ascend/buildkitd\" -}}\n{{ .Data.data.dockerConfig }}\n{{- end }}"
|
||||
vault.hashicorp.com/agent-inject-template-key.pem: "{{- with secret \"internal/data/ascend/buildkitd\" -}}\n{{ .Data.data.ClientCaKey }}\n{{- end }}"
|
||||
vault.hashicorp.com/agent-pre-populate-only: "true"
|
||||
vault.hashicorp.com/agent-run-as-group: "1000"
|
||||
vault.hashicorp.com/agent-run-as-user: "1000"
|
||||
vault.hashicorp.com/agent-service-account-token-volume-name: token-vol
|
||||
vault.hashicorp.com/role: ascend-gha-runners
|
||||
vault.hashicorp.com/secret-volume-path: /home/user/.docker/
|
||||
vault.hashicorp.com/tls-skip-verify: "true"
|
||||
podSpecPatch:
|
||||
volumes:
|
||||
- name: token-vol
|
||||
projected:
|
||||
defaultMode: 420
|
||||
sources:
|
||||
- serviceAccountToken:
|
||||
audience: api
|
||||
expirationSeconds: 600
|
||||
path: token
|
||||
agents: *npu-image-build-agents
|
||||
plugins: *npu-image-build-plugins
|
||||
env:
|
||||
VLLM_IMAGE_TAG: "${BUILDKITE_COMMIT}-a3"
|
||||
IMAGE_NAME: "vllm-omni-ci-npu"
|
||||
IMAGE_REGISTRY: "swr.cn-southwest-2.myhuaweicloud.com/modelfoundry"
|
||||
BUILDKITD_ADDR: "tcp://buildkitd-service.buildkitd:1234"
|
||||
command: |
|
||||
set -ex
|
||||
command: bash .buildkite/npu/scripts/build-npu-image.sh Dockerfile.npu.ci "$${VLLM_IMAGE_TAG}" -a3
|
||||
|
||||
echo "--- Building and pushing NPU Test Image"
|
||||
echo "Image: $${IMAGE_REGISTRY}/$${IMAGE_NAME}:$${VLLM_IMAGE_TAG}"
|
||||
echo "buildkitd address: $${BUILDKITD_ADDR}"
|
||||
- label: ":buildkit: Build and Push NPU Test Image - A5"
|
||||
key: image-build-a5
|
||||
agents: *npu-image-build-agents
|
||||
plugins: *npu-image-build-plugins
|
||||
env:
|
||||
VLLM_IMAGE_TAG: "${BUILDKITE_COMMIT}-a5"
|
||||
IMAGE_NAME: "vllm-omni-ci-npu"
|
||||
IMAGE_REGISTRY: "swr.cn-southwest-2.myhuaweicloud.com/modelfoundry"
|
||||
BUILDKITD_ADDR: "tcp://buildkitd-service.buildkitd:1234"
|
||||
command: bash .buildkite/npu/scripts/build-npu-image.sh Dockerfile.npu.ci "$${VLLM_IMAGE_TAG}" -a5
|
||||
|
||||
if ! command -v buildctl &> /dev/null; then
|
||||
echo "Installing buildctl..."
|
||||
mkdir -p /tmp/buildkit
|
||||
BUILDKIT_VERSION="v0.29.0"
|
||||
wget -q "https://github.com/moby/buildkit/releases/download/$${BUILDKIT_VERSION}/buildkit-$${BUILDKIT_VERSION}.linux-arm64.tar.gz" -O /tmp/buildkit.tar.gz
|
||||
tar -xzf /tmp/buildkit.tar.gz -C /tmp/buildkit
|
||||
cp /tmp/buildkit/bin/buildctl /usr/local/bin/
|
||||
fi
|
||||
|
||||
export DOCKER_CONFIG=/home/user/.docker
|
||||
buildctl \
|
||||
--addr="$${BUILDKITD_ADDR}" \
|
||||
--tlscacert=/home/user/.docker/ca.pem \
|
||||
--tlscert=/home/user/.docker/cert.pem \
|
||||
--tlskey=/home/user/.docker/key.pem \
|
||||
build \
|
||||
--frontend dockerfile.v0 \
|
||||
--local context=. \
|
||||
--local dockerfile=./docker \
|
||||
--opt filename=Dockerfile.npu.ci.a3 \
|
||||
--secret id=dockerconfig,src=/home/user/.docker/config.json \
|
||||
--output type=image,name=$${IMAGE_REGISTRY}/$${IMAGE_NAME}:$${VLLM_IMAGE_TAG},push=true \
|
||||
--progress=plain
|
||||
|
||||
echo "--- Image pushed successfully"
|
||||
echo "$${IMAGE_REGISTRY}/$${IMAGE_NAME}:$${VLLM_IMAGE_TAG}"
|
||||
- label: ":buildkit: Build and Push NPU Test Image - 310P"
|
||||
key: image-build-310p
|
||||
agents: *npu-image-build-agents
|
||||
plugins: *npu-image-build-plugins
|
||||
env:
|
||||
VLLM_IMAGE_TAG: "${BUILDKITE_COMMIT}-310p"
|
||||
IMAGE_NAME: "vllm-omni-ci-npu"
|
||||
IMAGE_REGISTRY: "swr.cn-southwest-2.myhuaweicloud.com/modelfoundry"
|
||||
BUILDKITD_ADDR: "tcp://buildkitd-service.buildkitd:1234"
|
||||
command: bash .buildkite/npu/scripts/build-npu-image.sh Dockerfile.npu.ci "$${VLLM_IMAGE_TAG}" -310p
|
||||
|
||||
- label: "Upload Ready Pipeline"
|
||||
depends_on:
|
||||
- image-build-a2
|
||||
- image-build-a3
|
||||
- image-build-a5
|
||||
- image-build-310p
|
||||
key: upload-ready-pipeline
|
||||
agents:
|
||||
queue: "cpu_queue_premerge"
|
||||
@@ -174,6 +98,8 @@ steps:
|
||||
depends_on:
|
||||
- image-build-a2
|
||||
- image-build-a3
|
||||
- image-build-a5
|
||||
- image-build-310p
|
||||
key: upload-nightly-pipeline
|
||||
commands:
|
||||
- python3 .buildkite/common/scripts/upload_pipeline.py --upload .buildkite/npu/test-npu-nightly.yml
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env bash
|
||||
set -ex
|
||||
|
||||
dockerfile=${1:?Dockerfile is required}
|
||||
image_tag=${2:?Image tag is required}
|
||||
device_suffix=${3:-}
|
||||
|
||||
image_name=${IMAGE_NAME:-vllm-omni-ci-npu}
|
||||
image_registry=${IMAGE_REGISTRY:-swr.cn-southwest-2.myhuaweicloud.com/modelfoundry}
|
||||
buildkitd_addr=${BUILDKITD_ADDR:-tcp://buildkitd-service.buildkitd:1234}
|
||||
|
||||
echo "--- Building and pushing NPU Test Image"
|
||||
echo "Image: ${image_registry}/${image_name}:${image_tag}"
|
||||
echo "Dockerfile: ${dockerfile}"
|
||||
|
||||
if ! command -v buildctl &> /dev/null; then
|
||||
mkdir -p /tmp/buildkit
|
||||
buildkit_version=v0.29.0
|
||||
wget -q \
|
||||
"https://github.com/moby/buildkit/releases/download/${buildkit_version}/buildkit-${buildkit_version}.linux-arm64.tar.gz" \
|
||||
-O /tmp/buildkit.tar.gz
|
||||
tar -xzf /tmp/buildkit.tar.gz -C /tmp/buildkit
|
||||
cp /tmp/buildkit/bin/buildctl /usr/local/bin/
|
||||
fi
|
||||
|
||||
build_args=()
|
||||
if [[ -n "${device_suffix}" ]]; then
|
||||
build_args+=(--opt "build-arg:VLLM_ASCEND_DEVICE_SUFFIX=${device_suffix}")
|
||||
fi
|
||||
|
||||
export DOCKER_CONFIG=/home/user/.docker
|
||||
buildctl \
|
||||
--addr="${buildkitd_addr}" \
|
||||
--tlscacert=/home/user/.docker/ca.pem \
|
||||
--tlscert=/home/user/.docker/cert.pem \
|
||||
--tlskey=/home/user/.docker/key.pem \
|
||||
build \
|
||||
--frontend dockerfile.v0 \
|
||||
--local context=. \
|
||||
--local dockerfile=./docker \
|
||||
--opt "filename=${dockerfile}" \
|
||||
"${build_args[@]}" \
|
||||
--secret id=dockerconfig,src=/home/user/.docker/config.json \
|
||||
--output "type=image,name=${image_registry}/${image_name}:${image_tag},push=true" \
|
||||
--progress=plain
|
||||
|
||||
echo "--- Image pushed successfully"
|
||||
echo "${image_registry}/${image_name}:${image_tag}"
|
||||
@@ -1,26 +1,59 @@
|
||||
steps:
|
||||
- label: "Platforms Test"
|
||||
key: ready-platforms-test
|
||||
- group: ":card_index_dividers: Simple Test"
|
||||
depends_on: upload-ready-pipeline
|
||||
mirror_hardwares: a3_npu_2
|
||||
commands:
|
||||
- >-
|
||||
pytest -s -v
|
||||
tests/platforms/npu/test_minicpmo_code2wav_npugraph.py
|
||||
-m 'core_model and npu and A3 and omni'
|
||||
--run-level 'core_model'
|
||||
steps:
|
||||
- label: "Platforms Test"
|
||||
key: ready-platforms-test
|
||||
mirror_hardwares: a3_npu_2
|
||||
commands:
|
||||
- >-
|
||||
pytest -s -v
|
||||
tests/platforms/npu/test_minicpmo_code2wav_npugraph.py
|
||||
-m 'core_model and npu and A3 and omni'
|
||||
--run-level 'core_model'
|
||||
|
||||
- label: "Diffusion NPU layer tests"
|
||||
key: ready-diffusion-npu-layer-tests
|
||||
depends_on: upload-ready-pipeline
|
||||
mirror_hardwares: a3_npu_2
|
||||
commands:
|
||||
- >-
|
||||
pytest -s -v
|
||||
tests/diffusion/layers/test_activation.py
|
||||
tests/diffusion/layers/test_fused_qk_norm_rope_npu.py
|
||||
-m 'core_model and npu and A3'
|
||||
--run-level 'core_model'
|
||||
- label: "Diffusion NPU layer tests - A2"
|
||||
key: ready-diffusion-npu-layer-tests-a2
|
||||
mirror_hardwares: a2b3_npu_1
|
||||
commands:
|
||||
- >-
|
||||
pytest -s -v
|
||||
tests/diffusion/layers/test_activation.py
|
||||
-m 'core_model and npu and A2'
|
||||
--run-level 'core_model'
|
||||
|
||||
- label: "Diffusion NPU layer tests - A3"
|
||||
key: ready-diffusion-npu-layer-tests
|
||||
mirror_hardwares: a3_npu_2
|
||||
commands:
|
||||
- >-
|
||||
pytest -s -v
|
||||
tests/diffusion/layers/test_activation.py
|
||||
tests/diffusion/layers/test_fused_qk_norm_rope_npu.py
|
||||
tests/diffusion/models/minimax_h3/test_minimax_h3_qwen3vl_swiglu.py
|
||||
tests/diffusion/models/minimax_h3/test_minimax_h3_modulation.py
|
||||
-m 'core_model and npu and A3'
|
||||
--run-level 'core_model'
|
||||
|
||||
- label: "Diffusion NPU layer tests - A5"
|
||||
key: ready-diffusion-npu-layer-tests-a5
|
||||
mirror_hardwares: a5_npu_2
|
||||
commands:
|
||||
- >-
|
||||
pytest -s -v
|
||||
tests/diffusion/layers/test_activation.py
|
||||
-m 'core_model and npu and A5'
|
||||
--run-level 'core_model'
|
||||
|
||||
- label: "Diffusion NPU layer tests - 310P"
|
||||
key: ready-diffusion-npu-layer-tests-310p
|
||||
mirror_hardwares: 310p_npu_1
|
||||
commands:
|
||||
- >-
|
||||
pytest -s -v
|
||||
tests/diffusion/layers/test_activation.py
|
||||
-m 'core_model and npu and 310P'
|
||||
--run-level 'core_model'
|
||||
|
||||
- group: ":card_index_dividers: Omni Model Test"
|
||||
key: ready-omni-group
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: add-diffusion-model
|
||||
description: Add a new diffusion model (text-to-image, text-to-video, image-to-video, text-to-audio, image editing) to vLLM-Omni, including Cache-DiT acceleration and parallelism support (TP, SP/USP, CFG-Parallel, HSDP). Use when integrating a new diffusion model, porting a diffusers pipeline or a custom model repo to vllm-omni, creating a new DiT transformer adapter, adding diffusion model support, or enabling multi-GPU parallelism and cache acceleration for an existing model.
|
||||
description: 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, offload, and parallelism support (TP, SP/USP, CFG-Parallel, HSDP). Use when integrating or reviewing a new diffusion model, porting a Diffusers pipeline or custom model repository, creating a DiT adapter, reusing shared examples, or qualifying multi-GPU and memory optimizations.
|
||||
---
|
||||
|
||||
# Adding a Diffusion Model to vLLM-Omni
|
||||
@@ -26,9 +26,16 @@ Check the model's HF repo for `model_index.json`. This determines your path:
|
||||
|----------|----------------|----------------|
|
||||
| **Already supported** | `_class_name` in `model_index.json` matches a key in `_DIFFUSION_MODELS` in `registry.py` | Skip implementation, then validate model-specific examples, tests, and docs as needed |
|
||||
| **Diffusers-based** | Has standard `model_index.json` with `_diffusers_version`, subfolders for `transformer/`, `vae/`, etc. | Follow **Path A** below |
|
||||
| **Custom/private repo** | No diffusers `model_index.json`, weights in non-standard format, custom model code in a separate git repo | Follow **Path B** below |
|
||||
| **Native non-Diffusers model** | No Diffusers index, non-standard checkpoint hierarchy, or custom architecture in a separate repo | Follow **Path B** below; port the runtime natively unless an external adapter was explicitly requested |
|
||||
| **Hybrid** | Has some diffusers components (VAE) but custom transformer/fusion | Mix of Path A and Path B |
|
||||
|
||||
Before coding, write a short integration contract covering runtime ownership,
|
||||
checkpoint discovery, reference revision, I/O geometry, attention semantics,
|
||||
CFG behavior, auxiliary components, target hardware, and the default deployment.
|
||||
For Path B, hybrid models, or any optimization work, read
|
||||
[references/native-model-integration-checklist.md](references/native-model-integration-checklist.md)
|
||||
and use its phase gates.
|
||||
|
||||
## Path A: Diffusers-Based Model
|
||||
|
||||
For models with a standard diffusers layout. See [references/transformer-adaptation.md](references/transformer-adaptation.md) for detailed code patterns.
|
||||
@@ -103,9 +110,15 @@ For custom loop structures, follow `vllm_omni/diffusion/models/progress_bar.py`
|
||||
|
||||
---
|
||||
|
||||
## Path B: Custom/Private Repo Model
|
||||
## Path B: Native / Non-Diffusers Model
|
||||
|
||||
For models without a diffusers pipeline — weights in custom format, model code in a private repo. Real examples: BAGEL, HunyuanImage3. See [references/custom-model-patterns.md](references/custom-model-patterns.md) for concrete custom-model integration patterns.
|
||||
For models without a Diffusers pipeline—weights in custom formats and model
|
||||
code in another public or private repository. Treat that repository as a pinned
|
||||
correctness oracle. A request for native support means the vLLM-Omni runtime
|
||||
must not import the reference implementation; an external adapter is appropriate
|
||||
only when the requested scope explicitly permits that dependency. See
|
||||
[references/custom-model-patterns.md](references/custom-model-patterns.md) for
|
||||
concrete integration patterns.
|
||||
|
||||
### B1. Understand the reference repo
|
||||
|
||||
@@ -126,16 +139,25 @@ This is the key design decision for custom models. Follow these placement rules:
|
||||
| **Pipeline orchestration** (init, forward, denoise loop) | `vllm_omni/diffusion/models/<name>/pipeline_<name>.py` | Always required |
|
||||
| **Custom transformer/backbone** (ported and adapted to vllm-omni) | `vllm_omni/diffusion/models/<name>/<name>_transformer.py` or similar | `wan2_2.py`, `fusion.py`, `bagel_transformer.py` |
|
||||
| **Custom sub-models** (VAE, fusion, autoencoder) | `vllm_omni/diffusion/models/<name>/` as separate files | `autoencoder.py`, `fusion.py` |
|
||||
| **External dependency code** (original repo utilities) | **External repo**, installed via download script or pip | original-repo package via git clone |
|
||||
| **Reference-only code** | Keep outside the runtime; use a pinned revision for golden outputs and architecture analysis | Reference inference script |
|
||||
| **Explicit external adapter dependency** | External package, only when the requested scope and maintainer direction allow it | Compatibility adapter, not native support |
|
||||
| **Hardcoded model configs** | Module-level dicts in pipeline file | `VIDEO_CONFIG`, `AUDIO_CONFIG` dicts |
|
||||
| **Download/setup script** | `examples/offline_inference/<name>/download_<name>.py` | `download_<name>.py` |
|
||||
| **Custom `model_index.json`** | Generated by download script, placed at model root | Minimal: `{"_class_name": "YourPipeline", ...}` |
|
||||
|
||||
### B3. Handle external dependencies
|
||||
|
||||
If the model's code lives in a separate git repo:
|
||||
If the model's code lives in a separate git repo, first decide whether the
|
||||
requested deliverable is native support or an external adapter. Do not silently
|
||||
choose the adapter path.
|
||||
|
||||
**Option 1: Import with graceful fallback** (recommended for models with external utils)
|
||||
**Option 1: Port the code directly** (default for native support)
|
||||
|
||||
Copy the essential model files into `vllm_omni/diffusion/models/<name>/` and
|
||||
adapt them to shared vLLM-Omni contracts. Keep checkpoint loading strict and
|
||||
use the pinned reference only to generate parity evidence.
|
||||
|
||||
**Option 2: Import with graceful fallback** (adapter scope only)
|
||||
|
||||
```python
|
||||
try:
|
||||
@@ -147,11 +169,9 @@ except ImportError:
|
||||
)
|
||||
```
|
||||
|
||||
**Option 2: Port the code directly** (preferred when feasible)
|
||||
|
||||
Copy the essential model files into `vllm_omni/diffusion/models/<name>/` and adapt them. This avoids external dependencies. BAGEL does this — `autoencoder.py` and `bagel_transformer.py` are ported directly.
|
||||
|
||||
**Decision criteria**: Port if the code is self-contained and won't diverge. Use external deps if the model repo is actively maintained and the code is complex.
|
||||
Use an external runtime dependency only when the user explicitly requested an
|
||||
adapter or a maintainer approved the exception. Document the dependency,
|
||||
pinned revision, installation path, and unsupported native features.
|
||||
|
||||
### B4. Handle custom weight loading
|
||||
|
||||
@@ -221,7 +241,8 @@ class CustomPipeline(nn.Module):
|
||||
|
||||
### B5. Create the `model_index.json`
|
||||
|
||||
Custom models need a `model_index.json` at the model root for vllm-omni to discover them. For custom models, this is minimal:
|
||||
Prefer a `model_index.json` at the model root when vLLM-Omni owns an assembled
|
||||
checkpoint directory. For custom models, this is minimal:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -230,7 +251,14 @@ Custom models need a `model_index.json` at the model root for vllm-omni to disco
|
||||
}
|
||||
```
|
||||
|
||||
The `_class_name` must match a key in `_DIFFUSION_MODELS` in `registry.py`. Additional keys are model-specific (accessed via `od_config.model_config`).
|
||||
The `_class_name` must match a key in `_DIFFUSION_MODELS` in `registry.py`.
|
||||
Additional keys are model-specific (accessed via `od_config.model_config`).
|
||||
|
||||
If the released repository is immutable and has neither a root `config.json`
|
||||
nor a Diffusers index, add it to a generic native-checkpoint signature resolver:
|
||||
match the exact Hub ID or a distinctive set of local files and return the
|
||||
pipeline class. Do not use model-name substrings or add parallel one-off
|
||||
predicates in CLI, config, and serving consumers.
|
||||
|
||||
If the model's weights come from multiple HF repos, write a **download script** that:
|
||||
1. Downloads from each repo
|
||||
@@ -292,7 +320,9 @@ Use the appropriate existing example script:
|
||||
| Image-to-Image | `examples/offline_inference/image_to_image/image_edit.py` |
|
||||
| Text-to-Audio | `examples/offline_inference/text_to_audio/text_to_audio.py` |
|
||||
|
||||
For custom/Omni models that don't fit these categories, create a dedicated example script.
|
||||
Reuse these shared scripts even for custom models when their request and output
|
||||
contracts fit. Create a dedicated model script only when the shared category
|
||||
cannot represent the protocol, and document that gap in the PR.
|
||||
|
||||
**Validation**: No errors, output is meaningful, quality matches reference implementation.
|
||||
|
||||
@@ -300,13 +330,17 @@ See [references/troubleshooting.md](references/troubleshooting.md) for common er
|
||||
|
||||
### Step 6: Add Example Scripts
|
||||
|
||||
For Omni or custom models, create:
|
||||
Only when the shared category scripts cannot represent the model, create:
|
||||
- `examples/offline_inference/your_model_name/` — offline script + README
|
||||
- `examples/online_serving/your_model_name/` — server script + client
|
||||
- Download script if weights require assembly from multiple sources
|
||||
|
||||
### Step 7: Update Documentation
|
||||
|
||||
Follow the [`add-recipe` skill](../add-recipe/SKILL.md) to add or update the
|
||||
model-family recipe and its `recipes/README.md` row with verified specifications,
|
||||
hardware, commands, feature links, and qualification evidence.
|
||||
|
||||
Required updates:
|
||||
1. `docs/user_guide/diffusion/parallelism/overview.md` — parallelism support overview/table
|
||||
2. `docs/user_guide/diffusion/cpu_offload.md` — if CPU offload supported (add to supported models table)
|
||||
@@ -346,107 +380,20 @@ Classify the model's **CI priority** first:
|
||||
|
||||
**Keep model-specific code inside test modules — not `tests/helpers/{slug}.py`:** deploy constants, prompts, sampling dicts, and inline `request_config` / `form_data` belong in each `test_{slug}.py` and `test_{slug}_expansion.py`. Do not add per-model files under `tests/helpers/`; reuse only repo-wide harness (`mark`, `media`, `runtime`, `stage_config`, `assertions`). **L2+ online/offline e2e:** reuse or add `send_*_request` in `tests/helpers/runtime.py` — tests call the handler, not raw `omni.generate` / HTTP. See [vllm-omni-test skill](../vllm-omni-test/SKILL.md) § **Runtime send helpers**.
|
||||
|
||||
Keep the model suite proportional using the six distinct failure owners in the
|
||||
native integration checklist. Combine supported features into a few
|
||||
parametrized E2E rows instead of creating one test per optimization.
|
||||
|
||||
### Step 9: Add Cache-DiT Acceleration
|
||||
|
||||
Cache-DiT accelerates inference by caching intermediate computation results across denoising steps. After your model is working correctly on a single GPU, add cache-dit support.
|
||||
Add caching only after uncached single-device correctness. Read
|
||||
[references/cache-dit-patterns.md](references/cache-dit-patterns.md), use the
|
||||
automatic single-block-list path when possible, and add a registered
|
||||
`BlockAdapter` only for genuinely custom block topology.
|
||||
|
||||
See [references/cache-dit-patterns.md](references/cache-dit-patterns.md) for detailed code patterns.
|
||||
|
||||
#### 9a. Determine your model type
|
||||
|
||||
| Model Type | Description | Action |
|
||||
|------------|-------------|--------|
|
||||
| **Standard single-transformer** | One transformer with one `ModuleList` of blocks | No code needed — `CacheDiTBackend` auto-detects via `enable_cache_for_dit()` |
|
||||
| **Multi-block-list** | One transformer with multiple block lists (e.g., `transformer_blocks` + `single_transformer_blocks`) | Write custom enabler with `BlockAdapter` |
|
||||
| **Dual-transformer** | Two transformers (e.g., high-noise + low-noise) | Write custom enabler with `BlockAdapter` wrapping both |
|
||||
|
||||
#### 9b. Standard models — verify automatic support
|
||||
|
||||
For standard single-transformer models, test directly:
|
||||
|
||||
```python
|
||||
omni = Omni(
|
||||
model="your-model-name",
|
||||
cache_backend="cache_dit",
|
||||
cache_config={
|
||||
"Fn_compute_blocks": 1,
|
||||
"Bn_compute_blocks": 0,
|
||||
"max_warmup_steps": 4,
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
Check logs for "Cache-dit enabled successfully on xxx". If it works, skip to Step 9e.
|
||||
|
||||
#### 9c. Custom architectures — write a custom enabler
|
||||
|
||||
For multi-block-list or dual-transformer models, write a custom enabler function:
|
||||
|
||||
```python
|
||||
from cache_dit import BlockAdapter, ForwardPattern, ParamsModifier, DBCacheConfig
|
||||
|
||||
def enable_cache_for_your_model(pipeline, cache_config):
|
||||
db_cache_config = DBCacheConfig(
|
||||
num_inference_steps=None,
|
||||
Fn_compute_blocks=cache_config.Fn_compute_blocks,
|
||||
Bn_compute_blocks=cache_config.Bn_compute_blocks,
|
||||
max_warmup_steps=cache_config.max_warmup_steps,
|
||||
max_cached_steps=cache_config.max_cached_steps,
|
||||
max_continuous_cached_steps=cache_config.max_continuous_cached_steps,
|
||||
residual_diff_threshold=cache_config.residual_diff_threshold,
|
||||
)
|
||||
|
||||
cache_dit.enable_cache(
|
||||
BlockAdapter(
|
||||
transformer=pipeline.transformer,
|
||||
blocks=[
|
||||
pipeline.transformer.transformer_blocks,
|
||||
pipeline.transformer.single_transformer_blocks,
|
||||
],
|
||||
forward_pattern=[ForwardPattern.Pattern_1, ForwardPattern.Pattern_1],
|
||||
params_modifiers=[ParamsModifier(...)],
|
||||
),
|
||||
cache_config=db_cache_config,
|
||||
)
|
||||
|
||||
def refresh_cache_context(pipeline, num_inference_steps, verbose=True):
|
||||
cache_dit.refresh_context(
|
||||
pipeline.transformer, num_inference_steps=num_inference_steps, verbose=verbose
|
||||
)
|
||||
return refresh_cache_context
|
||||
```
|
||||
|
||||
#### 9d. Register the custom enabler
|
||||
|
||||
Add your enabler to `CUSTOM_DIT_ENABLERS` in
|
||||
`vllm_omni/diffusion/cache/cachedit/model_specific.py`:
|
||||
|
||||
```python
|
||||
CUSTOM_DIT_ENABLERS = {
|
||||
"Wan22Pipeline": enable_cache_for_wan22,
|
||||
"LongCatImagePipeline": enable_cache_for_longcat_image,
|
||||
"YourModelPipeline": enable_cache_for_your_model, # Add here
|
||||
}
|
||||
```
|
||||
|
||||
#### 9e. Test Cache-DiT
|
||||
|
||||
```python
|
||||
omni = Omni(
|
||||
model="your-model-name",
|
||||
cache_backend="cache_dit",
|
||||
cache_config={
|
||||
"Fn_compute_blocks": 1, "Bn_compute_blocks": 0,
|
||||
"max_warmup_steps": 4, "residual_diff_threshold": 0.24,
|
||||
}
|
||||
)
|
||||
images = omni.generate("a beautiful landscape",
|
||||
OmniDiffusionSamplingParams(num_inference_steps=50))
|
||||
```
|
||||
|
||||
**Verify**: 1) logs show cache enabled, 2) 1.5-2x speedup, 3) output quality acceptable vs baseline.
|
||||
|
||||
If quality degrades, lower `residual_diff_threshold` (try 0.12-0.18) or increase `max_warmup_steps` (try 6-8).
|
||||
Verify a real cache hit and compare quality with the uncached baseline. Make a
|
||||
speed claim only at a realistic step count where warmup permits hits; an
|
||||
all-warmup smoke proves integration, not acceleration.
|
||||
|
||||
---
|
||||
|
||||
@@ -460,107 +407,30 @@ See [references/parallelism-patterns.md](references/parallelism-patterns.md) for
|
||||
|
||||
#### 10a. Tensor Parallelism (TP)
|
||||
|
||||
Shards DiT linear layers across GPUs. Requires code changes in the transformer.
|
||||
|
||||
**What to change in the transformer**:
|
||||
1. Replace `nn.Linear` with `ColumnParallelLinear` / `RowParallelLinear` / `QKVParallelLinear`
|
||||
2. Update `load_weights()` to handle QKV fusion with `stacked_params_mapping`
|
||||
3. Use `self.to_qkv.num_heads` (local heads) instead of total heads for split sizes
|
||||
|
||||
```python
|
||||
from vllm.model_executor.layers.linear import (
|
||||
QKVParallelLinear, RowParallelLinear, ColumnParallelLinear,
|
||||
)
|
||||
|
||||
# Attention: QKV → RowParallel output
|
||||
self.to_qkv = QKVParallelLinear(dim, head_dim, num_heads, num_kv_heads)
|
||||
self.to_out = RowParallelLinear(dim, dim, input_is_parallel=True)
|
||||
|
||||
# FFN: ColumnParallel → RowParallel
|
||||
self.w1 = ColumnParallelLinear(dim, ffn_dim)
|
||||
self.w2 = RowParallelLinear(ffn_dim, dim, input_is_parallel=True)
|
||||
```
|
||||
|
||||
**Constraints**: `num_heads % tp_size == 0` and `num_kv_heads % tp_size == 0`.
|
||||
|
||||
**Test**: `--tensor-parallel-size 2`
|
||||
Replace compatible projections with vLLM parallel linears, preserve checkpoint
|
||||
fusion/loading, and use local head counts. Require query/KV head divisibility
|
||||
and compare a multi-rank forward with the one-rank oracle.
|
||||
|
||||
#### 10b. Sequence Parallelism (SP / USP)
|
||||
|
||||
Splits sequence tokens across GPUs. Non-intrusive via `_sp_plan` on the transformer class — no changes to `forward()`.
|
||||
|
||||
**What to change in the transformer**:
|
||||
|
||||
Add `_sp_plan` class attribute:
|
||||
|
||||
```python
|
||||
from vllm_omni.diffusion.distributed.sp_plan import (
|
||||
SequenceParallelInput, SequenceParallelOutput,
|
||||
)
|
||||
|
||||
class YourTransformer(nn.Module):
|
||||
_sp_plan = {
|
||||
"blocks.0": {
|
||||
"hidden_states": SequenceParallelInput(split_dim=1, expected_dims=3),
|
||||
},
|
||||
"proj_out": SequenceParallelOutput(gather_dim=1, expected_dims=3),
|
||||
}
|
||||
```
|
||||
|
||||
If inline tensor ops (e.g., `torch.cat`) exist between shard/gather points, extract them into `nn.Module` submodules so hooks can intercept them.
|
||||
|
||||
For RoPE that needs splitting, add an entry for the RoPE module with `split_output=True`.
|
||||
|
||||
**Test**: `--ulysses-degree 2` (offline) or `--usp 2` (online serving)
|
||||
Prefer the declarative `_sp_plan`. For packed variable-length attention or
|
||||
learned-sink LSE correction, keep model math explicit and reuse shared exchange
|
||||
utilities. Validate uneven sequence splits, RoPE coordinates, and outputs
|
||||
against the one-rank oracle.
|
||||
|
||||
#### 10c. CFG Parallel
|
||||
|
||||
Distributes positive/negative CFG branches across 2 GPUs. Requires the pipeline to inherit `CFGParallelMixin`.
|
||||
|
||||
**What to change in the pipeline**:
|
||||
|
||||
```python
|
||||
from vllm_omni.diffusion.distributed.cfg_parallel import CFGParallelMixin
|
||||
|
||||
class YourPipeline(nn.Module, CFGParallelMixin):
|
||||
def diffuse(self, ...) -> torch.Tensor:
|
||||
for i, t in enumerate(timesteps):
|
||||
positive_kwargs = {...}
|
||||
negative_kwargs = {...} if do_true_cfg else None
|
||||
noise_pred = self.predict_noise_maybe_with_cfg(
|
||||
do_true_cfg=do_true_cfg, true_cfg_scale=cfg_scale,
|
||||
positive_kwargs=positive_kwargs, negative_kwargs=negative_kwargs,
|
||||
)
|
||||
latents = self.scheduler_step_maybe_with_cfg(
|
||||
noise_pred, t, latents, do_true_cfg
|
||||
)
|
||||
return latents
|
||||
```
|
||||
|
||||
Override `predict_noise()` if your transformer call is non-standard. Override `combine_cfg_noise()` for multi-output models (e.g., video + audio).
|
||||
|
||||
**Constraint**: Exactly 2 GPUs. Only for models using classifier-free guidance.
|
||||
|
||||
**Test**: `--cfg-parallel-size 2`
|
||||
Confirm the model uses CFG. Then reuse `CFGParallelMixin`, overriding prediction
|
||||
or recombination only for non-standard or multi-output pipelines. Distinguish a
|
||||
packed positive/negative implementation from two independent branches and
|
||||
validate the two-rank result against the packed one-rank oracle.
|
||||
|
||||
#### 10d. HSDP (Hybrid Sharded Data Parallel)
|
||||
|
||||
Shards transformer weights via PyTorch FSDP2 to reduce per-GPU VRAM. No code changes to the forward pass — just add a class attribute.
|
||||
|
||||
**What to change in the transformer**:
|
||||
|
||||
```python
|
||||
class YourTransformer(nn.Module):
|
||||
@staticmethod
|
||||
def _is_transformer_block(name: str, module) -> bool:
|
||||
return "blocks" in name and name.split(".")[-1].isdigit()
|
||||
|
||||
_hsdp_shard_conditions = [_is_transformer_block]
|
||||
```
|
||||
|
||||
**Constraint**: Cannot combine with TP. For standalone HSDP, set `hsdp_shard_size` explicitly.
|
||||
|
||||
**Test**: `--use-hsdp` or `DiffusionParallelConfig(use_hsdp=True)`
|
||||
Declare layer shard conditions and ignored rank-local modules; preserve mixed
|
||||
checkpoint dtypes. HSDP cannot combine with TP. Measure parameter loading,
|
||||
FSDP materialization, warm HBM, and host PSS rather than assuming sharding saves
|
||||
peak memory. Keep the resident layout as default if HSDP is worse.
|
||||
|
||||
#### 10e. Update parallelism documentation
|
||||
|
||||
@@ -599,6 +469,12 @@ Pipelines without `SupportsComponentDiscovery` fall back to scanning
|
||||
well-known attribute names (`transformer`, `text_encoder`, `vae`,
|
||||
etc.), which fails for non-standard names.
|
||||
|
||||
Keep model-specific checkpoint paths, nested block aliases, layout transforms,
|
||||
and component lifecycles in the model package. Change a shared offloader only
|
||||
for a general contract, demonstrate another consumer or a framework-level bug,
|
||||
and add one focused shared regression. Avoid `if ModelName` branches in shared
|
||||
backends.
|
||||
|
||||
### Step 12: Performance Profiling
|
||||
|
||||
After verifying correctness and implementing parallelism/caching, profile the model's performance to identify bottlenecks and ensure optimal execution.
|
||||
@@ -608,6 +484,12 @@ See the [Profiling Single-Stage Diffusion](../../../docs/contributing/profiling.
|
||||
2. Using Nsight Systems (`nsys`) with `profiler: "cuda"` for low-overhead CUDA traces.
|
||||
3. Controlling profiling via `omni.start_profile()` and `omni.stop_profile()`.
|
||||
|
||||
Report cold E2E, warm user latency, steady-state wave time, peak allocated and
|
||||
reserved HBM, host PSS for the full process tree, and stage boundaries. State
|
||||
whether prompt encoding and VAE/audio decoding are included. For multi-device
|
||||
layouts, plot user latency against throughput per device and keep different
|
||||
denoising-step counts on separate Pareto frontiers.
|
||||
|
||||
---
|
||||
|
||||
## Pre-commit conventions
|
||||
@@ -635,7 +517,7 @@ shellcheck). In particular:
|
||||
3. **Use small models**: Test with smaller variants first
|
||||
4. **Check tensor shapes**: Most errors are reshape mismatches in attention
|
||||
5. **Add features incrementally**: Single GPU → TP → SP → CFG → HSDP → Cache-DiT
|
||||
6. **For custom models**: Get the model running with the original code first, then progressively replace components with vllm-omni equivalents
|
||||
6. **For custom models**: Run the pinned reference separately, then port the runtime natively; do not ship temporary imports from the reference implementation
|
||||
7. **Cache-DiT before parallelism tuning**: Cache-DiT is lossy — verify quality at baseline before combining with parallelism
|
||||
8. **Combine lossless + lossy**: e.g., TP + SP + Cache-DiT for maximum throughput
|
||||
|
||||
@@ -644,6 +526,7 @@ shellcheck). In particular:
|
||||
- [vllm-omni-test skill](../vllm-omni-test/SKILL.md) — L1–L4 markers, naming, Buildkite wiring, run commands
|
||||
- [Transformer Adaptation](references/transformer-adaptation.md) — porting transformers from diffusers
|
||||
- [Custom Model Patterns](references/custom-model-patterns.md) — patterns for non-diffusers models
|
||||
- [Native Model Integration Checklist](references/native-model-integration-checklist.md) — ownership boundaries, phase gates, qualification matrix, and review evidence
|
||||
- [Parallelism Patterns](references/parallelism-patterns.md) — TP, SP/USP, CFG parallel, HSDP implementation details
|
||||
- [Cache-DiT Patterns](references/cache-dit-patterns.md) — cache-dit acceleration for standard and custom architectures
|
||||
- [Troubleshooting](references/troubleshooting.md) — common errors and fixes
|
||||
@@ -9,7 +9,9 @@ Three caching strategies:
|
||||
- **TaylorSeer**: Calibration-based prediction using Taylor expansion to estimate block outputs
|
||||
- **SCM** (Step Computation Masking): Dynamic step skipping based on configurable policies
|
||||
|
||||
**Typical speedup**: 1.5-2.5x depending on model and configuration.
|
||||
Speedup is model-, step-, and policy-dependent. Treat a cache-enabled run as
|
||||
integration evidence until logs or a focused test show real hits and repeated
|
||||
timings show an improvement over the uncached baseline.
|
||||
|
||||
**Official docs**: https://docs.vllm.ai/projects/vllm-omni/en/latest/design/feature/cache_dit
|
||||
|
||||
@@ -243,8 +245,10 @@ vllm serve your-model --omni --port 8098 \
|
||||
|
||||
**Verification checklist**:
|
||||
1. Logs show "Cache-dit enabled successfully on xxx"
|
||||
2. Performance: 1.5-2x speedup vs no cache
|
||||
3. Quality: compare output with `cache_backend=None`
|
||||
2. A focused test or summary confirms at least one real cache hit
|
||||
3. Quality is compared with `cache_backend=None`
|
||||
4. Performance uses a realistic step count, repeated timings, and a policy
|
||||
whose warmup does not consume the whole run
|
||||
|
||||
## Excluded Models
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ vllm_omni/diffusion/models/wan2_2/
|
||||
|
||||
The transformer is loaded separately via `weights_sources` + `load_weights()`. Non-transformer components (VAE, text encoder) are loaded in `__init__` via `from_pretrained()`.
|
||||
|
||||
### Custom model with external deps
|
||||
### External adapter (not native support)
|
||||
|
||||
```
|
||||
vllm_omni/diffusion/models/<name>/
|
||||
@@ -28,7 +28,10 @@ examples/offline_inference/<task>/
|
||||
└── download_<name>.py # Downloads weights and/or clones code repo
|
||||
```
|
||||
|
||||
All weights loaded eagerly in `__init__`. `load_weights()` is a no-op. External dependency imported with try/except.
|
||||
All weights are loaded eagerly in `__init__`. `load_weights()` is a no-op. The
|
||||
external dependency is imported with try/except. Use this pattern only when the
|
||||
requested scope explicitly allows an adapter; it does not satisfy a request for
|
||||
a native vLLM-Omni implementation.
|
||||
|
||||
### Custom model with ported code (e.g., BAGEL)
|
||||
|
||||
@@ -86,7 +89,7 @@ init → load ALL weights eagerly via custom helpers → load_weights() = no-op
|
||||
- `load_weights()` is `pass`
|
||||
- Weights may come from multiple HF repos in different formats (`.pth`, `.safetensors`)
|
||||
|
||||
Use this when:
|
||||
Use this only for an explicitly approved external adapter when:
|
||||
- The original model has complex, well-tested loading code you don't want to rewrite
|
||||
- Weights span multiple HF repos
|
||||
- Weight format is non-standard (e.g., a single `.pth` file, not sharded safetensors)
|
||||
@@ -112,7 +115,14 @@ Custom model `model_index.json` (minimal):
|
||||
}
|
||||
```
|
||||
|
||||
The only **required** field is `_class_name` — it must match a key in `_DIFFUSION_MODELS` in `registry.py`. Other fields are model-specific and accessible via `od_config.model_config` dict.
|
||||
The only **required** field is `_class_name` — it must match a key in
|
||||
`_DIFFUSION_MODELS` in `registry.py`. Other fields are model-specific and
|
||||
accessible via `od_config.model_config`.
|
||||
|
||||
When the released repository cannot be modified and contains neither this file
|
||||
nor a root `config.json`, use the generic native-checkpoint signature resolver.
|
||||
Match the exact Hub ID or a distinctive local file set; never infer support from
|
||||
a model-name substring.
|
||||
|
||||
## External Dependency Management
|
||||
|
||||
@@ -141,6 +151,10 @@ def download_dependency():
|
||||
|
||||
Copy essential files from the original repo into `vllm_omni/diffusion/models/<name>/`. Adapt imports to use vllm-omni utilities. Benefits: no external dependency, no git clone step. Drawback: must maintain the ported code.
|
||||
|
||||
This is the default for native model support. Keep the reference repository at
|
||||
a pinned revision for architecture analysis and golden outputs, but do not
|
||||
import it from production code.
|
||||
|
||||
## Multi-Modal Input/Output Protocols
|
||||
|
||||
Custom models that handle images, audio, or video I/O should implement protocol classes:
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
# Native Diffusion Model Integration Checklist
|
||||
|
||||
Use this checklist for native non-Diffusers models, hybrid pipelines, and
|
||||
model-specific optimization work.
|
||||
|
||||
## 1. Freeze the contract before coding
|
||||
|
||||
Record:
|
||||
|
||||
- Reference repository and immutable revision.
|
||||
- Official checkpoint ID, local layout, license, and required auxiliary files.
|
||||
- Pipeline stages: prompt/image encoding, denoising, video/image/audio decoding,
|
||||
and muxing.
|
||||
- Token packing, RoPE coordinates, attention sinks/LSE behavior, MoE layout,
|
||||
scheduler equations, CFG formulation, dtypes, and checkpoint transforms.
|
||||
- Output geometry, frame count/FPS, audio rate/channels, supported tasks, and
|
||||
default generation steps.
|
||||
- Available local hardware versus the official qualification hardware.
|
||||
- Intended default deployment and optional optimization paths.
|
||||
|
||||
State whether the deliverable is a native implementation or an external
|
||||
adapter. For native support, use the reference as an oracle only.
|
||||
|
||||
## 2. Use phase gates
|
||||
|
||||
| Phase | Required exit evidence |
|
||||
|---|---|
|
||||
| Architecture | Component/weight inventory; unsupported reference dependencies identified; ownership decisions documented |
|
||||
| Native correctness | Strict checkpoint load; tiny forward or one-step golden vs pinned reference; output contract validated |
|
||||
| Framework integration | Registry/discovery, shared request types, existing category example, serving route, docs, and recipe work without model-name branching in shared scripts |
|
||||
| Lossless distribution | One-rank oracle vs each supported TP/SP/CFG/HSDP/VAE layout; released-checkpoint smoke for the recommended layout |
|
||||
| Memory optimization | Ordinary/layerwise/DLO behavior, peak HBM, full-tree host PSS, and stage timeline; shared backend unchanged unless a general contract is proven |
|
||||
| Lossy optimization | Cache hit demonstrated; quality delta measured; speed claim made only when warmup permits hits |
|
||||
| Full qualification | Default-resolution E2E at official step count for every supported task; media metadata and hashes recorded |
|
||||
| Review readiness | Focused tests, Ruff/format, strict docs, DCO, current-main rebase, issue/RFC for large changes, concise PR evidence and limitations |
|
||||
|
||||
Do not start performance tuning before native one-step parity. Do not claim a
|
||||
feature from topology validation alone when released weights can exercise it.
|
||||
|
||||
## 3. Keep ownership narrow
|
||||
|
||||
Default placement:
|
||||
|
||||
| Concern | Owner |
|
||||
|---|---|
|
||||
| Architecture, checkpoint remap, packed attention math, nested block alias, auxiliary lifecycle | Model package |
|
||||
| Kernel/version dispatch already useful to multiple models | Shared attention utility |
|
||||
| Request-dependent example defaults | Typed model-extra entry consumed through a registry |
|
||||
| Exact-ID/local-file detection for repositories without standard metadata | Generic native-checkpoint signature table |
|
||||
| General offload/cache/HSDP bug | Shared layer plus one focused shared regression |
|
||||
|
||||
Before changing shared code, answer:
|
||||
|
||||
1. Is this a framework contract or only this checkpoint's layout?
|
||||
2. Can the model expose an existing declarative hook instead?
|
||||
3. Is there a second consumer or a framework-level failing test?
|
||||
4. Can the shared change avoid importing or naming the model?
|
||||
|
||||
If the answers do not justify a shared change, keep it model-local.
|
||||
|
||||
## 4. Reuse examples without leaking model logic
|
||||
|
||||
- Reuse the existing `x_to_y.py` category script.
|
||||
- Resolve the pipeline class through standard metadata or native signature
|
||||
discovery, not path substrings.
|
||||
- Put dynamic model defaults and validation in `model_extras`; let shared
|
||||
examples consume only the generic typed API.
|
||||
- Do not create image/speech/default abstractions without a real consumer.
|
||||
- Add a dedicated example only when the shared request/output protocol cannot
|
||||
express the model.
|
||||
|
||||
## 5. Build a bounded evidence matrix
|
||||
|
||||
Start with the recommended resident layout. Add only supported, useful points:
|
||||
|
||||
- Single request: TP, SP, or hybrid TP/SP.
|
||||
- Throughput: DLO/DP layouts when concurrent requests are valid.
|
||||
- CFG: compare CFG-parallel output with the packed-CFG oracle only if the model
|
||||
actually uses CFG.
|
||||
- HSDP: record loading and materialization peaks; it can use more HBM than a
|
||||
resident SP layout.
|
||||
- VAE patch parallelism: compare decoded tensors or media exactly where the
|
||||
algorithm should be lossless.
|
||||
- Cache: separate cache integration from acceleration evidence.
|
||||
|
||||
With four devices, qualify four-device layouts such as DP4, DP2SP2, SP4, or
|
||||
CFG2xSP2. Do not present CFG2xSP4 or an official eight-device runtime as locally
|
||||
validated.
|
||||
|
||||
For each measured layout record:
|
||||
|
||||
- Cold E2E and warm user latency.
|
||||
- Steady-state wave time and throughput per device.
|
||||
- Prompt, denoise, video decode, audio decode, and mux times.
|
||||
- Peak allocated/reserved HBM and full-process-tree host PSS.
|
||||
- Step count, seed, output geometry, frame/FPS/audio metadata, and parity metric.
|
||||
|
||||
Plot latency versus throughput/device. Keep one-step, four-step, and full-step
|
||||
results separate because they are not quality-equivalent.
|
||||
|
||||
## 6. Keep tests proportional
|
||||
|
||||
Prefer a small set with distinct failure ownership:
|
||||
|
||||
1. Strict weight-key/dtype/load contract.
|
||||
2. Tiny native golden or one-step reference parity.
|
||||
3. One parametrized distributed-parity test covering supported layouts.
|
||||
4. Pipeline/request/output geometry and serving smoke.
|
||||
5. Shared-example/default test when the category script changes.
|
||||
6. One shared regression only for a genuine shared-layer fix.
|
||||
|
||||
Avoid duplicating the same shape or configuration assertion across
|
||||
model-specific files. Keep expensive released-checkpoint evidence in the PR or
|
||||
recipe unless the repository's CI tier explicitly requires it.
|
||||
|
||||
## 7. Write evidence, not marketing
|
||||
|
||||
In the PR description include:
|
||||
|
||||
- A compact architecture/data-flow explanation.
|
||||
- Default deployment and why it is recommended.
|
||||
- Code-change counts by component.
|
||||
- Accuracy and distributed-parity evidence tied to commands/scripts.
|
||||
- Separate Pareto figures for different step counts.
|
||||
- Supported versus validated versus planned optimizations.
|
||||
- Hardware gaps and untested official-runtime claims.
|
||||
|
||||
Call a result a bounded local qualification unless it includes repeated timing
|
||||
and variance. State whether latency includes text/image encoding and all
|
||||
decoders. Preserve the reference revision and exact commands needed to
|
||||
reproduce the evidence.
|
||||
@@ -0,0 +1,126 @@
|
||||
---
|
||||
name: add-recipe
|
||||
description: Add or update an in-repository vLLM-Omni model recipe with verified task, input, output, hardware, command, feature, and validation contracts. Use when creating files under recipes/, restructuring a recipe after review, documenting a newly supported model, or synchronizing recipe claims with support tables and shared feature guides.
|
||||
---
|
||||
|
||||
# Add a vLLM-Omni Recipe
|
||||
|
||||
## Source the contract
|
||||
|
||||
Read [`recipes/TEMPLATE.md`](../../../recipes/TEMPLATE.md) and
|
||||
[`recipes/README.md`](../../../recipes/README.md) before editing. Inspect the
|
||||
canonical model card/repository, implementation, shared examples, tests, and
|
||||
available local qualification evidence.
|
||||
|
||||
Do not infer support from a model name, sibling recipe, registry entry, or
|
||||
topology validator. Mark unexecuted configurations as configuration-only.
|
||||
|
||||
## Route by model family
|
||||
|
||||
Read only the contributor and user documentation relevant to the model:
|
||||
|
||||
| Family | Contributor guide | User-facing documentation to synchronize |
|
||||
|---|---|---|
|
||||
| Diffusion | [`adding a diffusion model`](../add-diffusion-model/SKILL.md) | `docs/user_guide/diffusion_features.md`, applicable diffusion feature guides, and shared image/video/audio examples |
|
||||
| TTS | [`adding a TTS model`](../add-tts-model/SKILL.md) | `examples/offline_inference/text_to_speech/README.md`, `examples/online_serving/text_to_speech/README.md`, and `docs/serving/speech_api.md` |
|
||||
| Omni | [`adding an omni model`](../../../docs/contributing/model/adding_omni_model.md) | model-family offline/online example docs, `docs/serving/chat_completions_api.md`, and `docs/user_guide/feature_compatibility.md` |
|
||||
|
||||
All families must update `docs/models/supported_models.md` and the matching row
|
||||
in `recipes/README.md`. Do not add a modality-specific support document when
|
||||
the repository has no such table; update the closest shared user contract.
|
||||
|
||||
## Structure the recipe
|
||||
|
||||
Keep the recipe task-oriented and use one hardware-specific file named
|
||||
`recipes/<vendor>/<model>-<hardware>.md`. A recipe may contain multiple
|
||||
deployment topologies on that hardware, but each additional accelerator model
|
||||
or platform requires its own suffixed file.
|
||||
|
||||
1. **Summary:** vendor, exact model ID, runtime, modes, named hardware,
|
||||
recommended deployment, and maintainer.
|
||||
2. **Supported model contract:** put task, input, output, and provided-profile
|
||||
tables before setup or commands.
|
||||
3. **References:** link the canonical model source, shared examples, supported
|
||||
model table, and feature matrix.
|
||||
4. **Checkpoint/setup:** pin revisions when possible and state required assets.
|
||||
5. **Hardware:** document the single accelerator named by the file suffix,
|
||||
including every validated device-count/topology profile on it.
|
||||
6. **Commands:** reuse shared `examples/` entrypoints unless the model contract
|
||||
truly requires a dedicated script. Include offline and online paths that
|
||||
were validated.
|
||||
7. **Supported features:** use a compact model-specific topology/status table
|
||||
with links to shared feature guides.
|
||||
8. **Verification:** provide a quick command and exact expected output
|
||||
contract.
|
||||
9. **Qualification evidence:** report correctness, memory, and timing evidence
|
||||
with its measurement scope and caveats.
|
||||
|
||||
## Separate hardware from software
|
||||
|
||||
For every locally validated hardware profile, record:
|
||||
|
||||
- accelerator vendor/model and per-device memory;
|
||||
- number of devices;
|
||||
- interconnect (`NVLink`, `PCIe`, or the platform equivalent);
|
||||
- host memory when CPU staging/offload is material;
|
||||
- whether the profile is runtime-qualified or configuration-only.
|
||||
|
||||
Record OS, Python, driver/runtime, framework versions, and the vLLM-Omni
|
||||
revision in a separate software-environment table. Keep precision, worker
|
||||
count, and DP/TP/SP/PP sizes with the deployment profile or exact command.
|
||||
|
||||
Never generalize a result from one accelerator family to another or combine
|
||||
NVIDIA, AMD, NPU, or distinct accelerator models in one recipe. Create a
|
||||
separate hardware-suffixed recipe and `recipes/README.md` row. Distinguish
|
||||
upstream requirements from hardware exercised by the PR.
|
||||
|
||||
## Keep shared documentation concise
|
||||
|
||||
Link the applicable diffusion, TTS, omni, serving, or design guide for generic
|
||||
feature semantics and launch instructions. The recipe should contain only the
|
||||
model-specific status, valid topology, required flag difference, and evidence
|
||||
boundary.
|
||||
|
||||
Put unsupported combinations in the same feature table. Avoid repeating the
|
||||
same model-specific prose below the global feature matrix; the recipe is the
|
||||
detailed source of truth.
|
||||
|
||||
## Report evidence precisely
|
||||
|
||||
For every performance or memory value, state:
|
||||
|
||||
- checkpoint/revision and workload;
|
||||
- accelerator and device count;
|
||||
- precision and topology;
|
||||
- cold/warm scope, step count, and concurrency;
|
||||
- peak HBM and host-memory metric when measured;
|
||||
- output/quality guard and sample count;
|
||||
- monitoring overhead or other known variance.
|
||||
|
||||
Call a single bounded run qualification evidence, not a benchmark. Do not add
|
||||
a benchmark script unless the contribution explicitly requires one.
|
||||
|
||||
## Synchronize repository documentation
|
||||
|
||||
Update all applicable locations:
|
||||
|
||||
- the row in `recipes/README.md`;
|
||||
- `docs/models/supported_models.md`;
|
||||
- the family-specific documentation selected in **Route by model family**;
|
||||
- shared example documentation when commands or flags change.
|
||||
|
||||
Keep global support tables compact. Link to the recipe instead of duplicating
|
||||
its deployment explanation.
|
||||
|
||||
## Validate
|
||||
|
||||
Run, at minimum:
|
||||
|
||||
```bash
|
||||
pre-commit run --files <changed recipe/docs/skill files>
|
||||
mkdocs build --strict
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Remove generated documentation artifacts after validation. Confirm every
|
||||
recipe link resolves and every claimed profile has matching code or evidence.
|
||||
@@ -499,6 +499,13 @@ Common pre-commit failures, recovery commands for missing sign-off, and the
|
||||
full `pre-commit run` invocation for a TTS model:
|
||||
[references/precommit-dco.md](references/precommit-dco.md).
|
||||
|
||||
## Documentation and recipe
|
||||
|
||||
Follow the [`add-recipe` skill](../add-recipe/SKILL.md) to add or update the
|
||||
in-repository model-family recipe, the `recipes/README.md` index, the supported
|
||||
models table, TTS example documentation, and the speech API contract using
|
||||
validated evidence.
|
||||
|
||||
## Integration Checklist
|
||||
|
||||
Use this checklist when integrating a new TTS model:
|
||||
@@ -543,6 +550,7 @@ Use this checklist when integrating a new TTS model:
|
||||
- [ ] Buildkite entries match level: `test-ready.yml` / `test-merge.yml` or nightly TTS job / `test-nightly.yml`
|
||||
- [ ] Gradio demo working
|
||||
- [ ] Documentation added (offline + online docs, nav, supported models)
|
||||
- [ ] In-repository recipe and `recipes/README.md` row added via the `add-recipe` skill
|
||||
|
||||
### Phase 4: Async Chunk
|
||||
- [ ] Pipeline declares the async handoff processor
|
||||
|
||||
@@ -0,0 +1,458 @@
|
||||
---
|
||||
name: production-add-diffusion-model
|
||||
description: Productionize a vLLM-Omni diffusion model after its Day-0 vertical slice works. Use when work requires official API parity and input limits, feature-combination evidence, online FP8, distributed layerwise offload, per-request Cache-DiT, CUDA/ROCm/NPU/XPU recipes, faster or sparse attention, fused operators, USP or disaggregation analysis, continuous batching and abort handling, long-running RPS stability, or accuracy/performance/reliability CI. For initial architecture porting, registry wiring, and basic weight loading, use add-diffusion-model first.
|
||||
---
|
||||
|
||||
# Productionizing a Diffusion Model
|
||||
|
||||
## Scope
|
||||
|
||||
Use this skill to move a working model integration from Day-0 support to a
|
||||
measured, fail-closed production deployment. It is deliberately separate from
|
||||
[`add-diffusion-model`](../add-diffusion-model/SKILL.md); do not modify or copy
|
||||
that skill when productionizing a model. If registry, pipeline, or basic loader
|
||||
work is incomplete, read that skill first and finish the Day-0 vertical slice.
|
||||
|
||||
MiniMax-H3 [PR #5691](https://github.com/vllm-project/vllm-omni/pull/5691)
|
||||
is the merged Day-0 case study. Its open optimization
|
||||
[issue #5700](https://github.com/vllm-project/vllm-omni/issues/5700) is an
|
||||
actively maintained roadmap and evidence map, not a list of already supported
|
||||
features. It can lag merged implementation: read its current revision, cited
|
||||
PRs, target source, and maintained recipes, then leave every unverified row
|
||||
`not tested` until the target revision has scoped evidence.
|
||||
|
||||
Read these references before the corresponding gate:
|
||||
|
||||
- [API and hardware recipes](references/api-and-recipes.md)
|
||||
- [Production feature patterns](references/feature-patterns.md)
|
||||
- [Performance patterns and code examples](references/performance-patterns.md)
|
||||
- [Serving, soak, and CI validation](references/production-validation.md)
|
||||
|
||||
## Non-negotiable evidence contract
|
||||
|
||||
Create a matrix whose row key is at least:
|
||||
|
||||
```text
|
||||
task x API mode x execution mode/capacity x shape/schedule x attention backend x
|
||||
packed layout x cache policy x quantization x offload mode x topology x
|
||||
hardware x dtype x output representation/transport
|
||||
```
|
||||
|
||||
Use exactly these states:
|
||||
|
||||
| State | Meaning |
|
||||
|---|---|
|
||||
| `validated` | The exact row passed correctness and measured deployment tests, with reproducible artifacts. |
|
||||
| `limited` | A bounded subset passed; the limitation and rejection/fallback behavior are explicit. |
|
||||
| `unsupported` | A stable architectural/platform restriction is proven and fails early with an actionable error. |
|
||||
| `not tested` | No adequate evidence exists. This is the default. |
|
||||
|
||||
Never infer compatibility from import success, server startup, another task,
|
||||
another card, or another topology. Record the model/checkpoint revision,
|
||||
vLLM-Omni commit, commands, request assets and hashes, seed/schedule/shape,
|
||||
raw output artifacts, environment, and result for every `validated` row.
|
||||
|
||||
## Workflow
|
||||
|
||||
Follow the gates in order. Keep a dense BF16 single-device path as the
|
||||
correctness oracle until every advertised fast path has parity evidence.
|
||||
|
||||
### Gate 0: Freeze the official contract
|
||||
|
||||
Pin the official implementation and checkpoint revision. Build a matrix for:
|
||||
|
||||
- constructor/call arguments, defaults, scheduler and sigma schedule;
|
||||
- task-to-source modality/count/order rules;
|
||||
- prompt, MIME, bytes, pixels, frames, duration, FPS, steps, seed, guidance,
|
||||
output count, and output format limits;
|
||||
- offline API and every public sync/async serving endpoint;
|
||||
- checkpoint partitions and task selection.
|
||||
|
||||
Normalize offline, sync, and async requests through one validation contract.
|
||||
Reject an invalid task/source combination before downloads, upload persistence,
|
||||
temporary files, async job creation, or engine submission. Use 400 for invalid
|
||||
semantics and 413 for payload limits; clean partial resources on every failure.
|
||||
See [API and hardware recipes](references/api-and-recipes.md).
|
||||
|
||||
### Gate 1: Re-prove correctness under production shapes
|
||||
|
||||
1. Make loading strict: track expected, loaded, unexpected, and intentionally
|
||||
ignored tensors. A fused destination is complete only after every source
|
||||
shard arrives. Missing retained parameters abort startup.
|
||||
2. Fix reference revision, asset hashes, seed, schedule, and shape. Compare
|
||||
component outputs, intermediate denoise latents, and final artifacts.
|
||||
3. Test unequal packed samples. Verify `cu_seqlens`, padding exclusion,
|
||||
modality/CFG ownership, split/gather boundaries, and one output per request.
|
||||
4. Test partial process groups with non-member ranks and group size smaller
|
||||
than world size. Do not construct a private group per component or request.
|
||||
5. Run negative API cases before positive E2E tests to prove side-effect-free
|
||||
rejection and cleanup.
|
||||
|
||||
### Gate 2: Wire shared fast primitives
|
||||
|
||||
Prefer explicit, testable vLLM-Omni/vLLM operators with native fallbacks.
|
||||
`torch.compile` remains a separately validated regional optimization; it is
|
||||
not a substitute for correct operator selection or lifecycle-safe boundaries.
|
||||
|
||||
#### RMSNorm + RoPE baseline
|
||||
|
||||
Use the shared cross-platform operators, preserving the official Q/K norm and
|
||||
RoPE order:
|
||||
|
||||
```python
|
||||
from vllm_omni.diffusion.layers.norm import RMSNorm
|
||||
from vllm_omni.diffusion.layers.rope import (
|
||||
RotaryEmbedding,
|
||||
apply_rope_to_qk,
|
||||
)
|
||||
|
||||
self.norm_q = RMSNorm(head_dim, eps=eps)
|
||||
self.norm_k = RMSNorm(head_dim, eps=eps)
|
||||
self.rope = RotaryEmbedding(is_neox_style=False)
|
||||
|
||||
query = self.norm_q(query) # [B, S, Hq, D] or packed [T, Hq, D]
|
||||
key = self.norm_k(key) # [B, S, Hkv, D] or packed [T, Hkv, D]
|
||||
query, key = apply_rope_to_qk(
|
||||
self.rope,
|
||||
query,
|
||||
key,
|
||||
(cos, sin),
|
||||
)
|
||||
```
|
||||
|
||||
Verify `is_neox_style`, `half_head_dim`, partial rotary dimensions, cos/sin
|
||||
layout, and packed row ownership against the official model. `False` means
|
||||
interleaved/GPT-J style; do not assume the default is NeoX. Shared RMSNorm and
|
||||
RoPE are two dispatched operators, not automatically one fused kernel.
|
||||
|
||||
For packed non-interleaved RoPE, use the shared fused boundary when its contract
|
||||
matches the model:
|
||||
|
||||
```python
|
||||
from vllm_omni.diffusion.layers.fused_qk_norm_rope import fused_qk_norm_rope
|
||||
|
||||
query, key = fused_qk_norm_rope(
|
||||
query, # [T, Hq, D]
|
||||
key, # [T, Hkv, D]
|
||||
self.norm_q.weight, # [D]
|
||||
self.norm_k.weight, # [D]
|
||||
rope_table, # [T, rotary_dim] = [cos | sin]
|
||||
self.norm_q.variance_epsilon,
|
||||
)
|
||||
```
|
||||
|
||||
The current CUDA fast path specializes BF16 `head_dim=128`, `rotary_dim=96`;
|
||||
the public function keeps an eager fallback for unsupported inputs. Verify the
|
||||
packed row/frequency contract and trace the actual fast path. Do not reshape an
|
||||
incompatible official RoPE layout merely to enter this kernel.
|
||||
|
||||
MiniMax-H3-style partial NeoX RoPE rotates a prefix and passes the suffix
|
||||
through:
|
||||
|
||||
```python
|
||||
self.q_norm = RMSNorm(head_dim, eps=eps, dtype=torch.bfloat16)
|
||||
self.k_norm = RMSNorm(head_dim, eps=eps, dtype=torch.bfloat16)
|
||||
self.rope = RotaryEmbedding(is_neox_style=True, half_head_dim=False)
|
||||
|
||||
def apply_partial_rope(self, x, freqs, rot_dim):
|
||||
x_rot, x_pass = x[..., :rot_dim], x[..., rot_dim:]
|
||||
cos, sin = torch.cos(freqs).to(x.dtype), torch.sin(freqs).to(x.dtype)
|
||||
return torch.cat((self.rope(x_rot, cos, sin), x_pass), dim=-1)
|
||||
```
|
||||
|
||||
The partial-RoPE snippet assumes `import torch` and lives on the owning
|
||||
attention module; adapt names and validated frequency shapes rather than
|
||||
copying it at module scope.
|
||||
|
||||
Call the modules normally so `CustomOp` selects CUDA/HIP/NPU/XPU/native
|
||||
implementations. Do not call `forward_cuda()` directly. Compare the shared
|
||||
path with native BF16 at operator, block, denoise-trajectory, and artifact
|
||||
levels before claiming either accuracy or speed.
|
||||
|
||||
#### Faster BF16 attention and fused projections
|
||||
|
||||
Use local TP head counts and role-aware shared attention:
|
||||
|
||||
```python
|
||||
from vllm.model_executor.layers.linear import QKVParallelLinear
|
||||
from vllm_omni.diffusion.attention.layer import Attention
|
||||
|
||||
self.qkv_proj = QKVParallelLinear(
|
||||
hidden_size=hidden_size,
|
||||
head_size=head_dim,
|
||||
total_num_heads=num_heads,
|
||||
total_num_kv_heads=num_kv_heads,
|
||||
bias=False,
|
||||
quant_config=quant_config,
|
||||
prefix=f"{prefix}.qkv_proj",
|
||||
)
|
||||
self.attn = Attention(
|
||||
num_heads=self.qkv_proj.num_heads,
|
||||
num_kv_heads=self.qkv_proj.num_kv_heads,
|
||||
head_size=head_dim,
|
||||
softmax_scale=head_dim**-0.5,
|
||||
causal=False,
|
||||
qkv_layout="BSND",
|
||||
role="self",
|
||||
prefix=prefix,
|
||||
)
|
||||
```
|
||||
|
||||
Prove the selected backend from runtime logs/trace and compare it with dense
|
||||
SDPA. Packed metadata must express real sample boundaries; do not build masks
|
||||
that the selected backend ignores. A backend that is faster for one shape or
|
||||
platform is not a global default. See [performance patterns](references/performance-patterns.md)
|
||||
for QKV, SwiGLU, AdaLN, sparse attention, redundancy, and fusion examples.
|
||||
|
||||
### Gate 3: Add online FP8 without breaking the loader
|
||||
|
||||
Route the active config to each component, then pass a stable runtime prefix
|
||||
and config into every quantizable vLLM linear:
|
||||
|
||||
```python
|
||||
def resolve_component_quant_config(quant_config, component):
|
||||
return quant_config.resolve(component) if hasattr(quant_config, "resolve") else quant_config
|
||||
|
||||
transformer_quant_config = resolve_component_quant_config(
|
||||
od_config.quantization_config,
|
||||
"transformer",
|
||||
)
|
||||
self.transformer = MyDiT(
|
||||
od_config,
|
||||
quant_config=transformer_quant_config,
|
||||
prefix="transformer",
|
||||
)
|
||||
|
||||
self.qkv_proj = QKVParallelLinear(
|
||||
hidden_size=hidden_size,
|
||||
head_size=head_dim,
|
||||
total_num_heads=num_heads,
|
||||
total_num_kv_heads=num_kv_heads,
|
||||
quant_config=quant_config,
|
||||
prefix=f"{prefix}.qkv_proj",
|
||||
)
|
||||
```
|
||||
|
||||
Use `build_quant_config()` for direct construction/testing:
|
||||
|
||||
```python
|
||||
from vllm_omni.quantization import build_quant_config
|
||||
|
||||
quant_config = build_quant_config({
|
||||
"transformer": {"method": "fp8"},
|
||||
"text_encoder": None,
|
||||
"vae": None,
|
||||
"default": None,
|
||||
})
|
||||
```
|
||||
|
||||
Preserve each parameter's vLLM `weight_loader`; perform checkpoint layout
|
||||
conversion before invoking it. Account for every QKV/gate-up source shard.
|
||||
Keep precision-sensitive norm/modulation/embedders in BF16 unless separately
|
||||
proven, using the current `safe_quant_config` pattern where applicable.
|
||||
|
||||
Validate resident DiT FP8 first: prove which modules are quantized, HBM saved,
|
||||
BF16-vs-FP8 trajectory/artifact quality, latency, and throughput. Text encoder,
|
||||
VAE, other hardware, TP/cache/DLO combinations are separate rows.
|
||||
Pre-quantized, pruned, rotated, or adapter-modified checkpoints are separate
|
||||
model/loading lanes; compare them with the released BF16 model without calling
|
||||
the result a pure quantization ablation. Read
|
||||
[`../quantization/SKILL.md`](../quantization/SKILL.md) if present and prefer the
|
||||
target revision's implementation/docs when sibling guidance differs.
|
||||
|
||||
### Gate 4: Make every component offloadable
|
||||
|
||||
Declare topology instead of relying on heuristic discovery:
|
||||
|
||||
```python
|
||||
from typing import ClassVar
|
||||
|
||||
import torch.nn as nn
|
||||
|
||||
from vllm_omni.diffusion.models.interface import SupportsComponentDiscovery
|
||||
from vllm_omni.diffusion.offloader import OffloadPlan
|
||||
|
||||
class MyPipeline(nn.Module, SupportsComponentDiscovery):
|
||||
_dit_modules: ClassVar[list[str]] = ["transformer"]
|
||||
_encoder_modules: ClassVar[list[str]] = ["text_encoder"]
|
||||
_vae_modules: ClassVar[list[str]] = ["vae"]
|
||||
_resident_modules: ClassVar[list[str]] = []
|
||||
_offload_plan: ClassVar[OffloadPlan] = OffloadPlan(
|
||||
on_demand_component_paths=frozenset({"text_encoder", "vae"}),
|
||||
block_attrs={"transformer": ("blocks",)},
|
||||
offload_submodules={"token_refiner": "blocks"},
|
||||
resident_dit_paths=frozenset({"transformer"}),
|
||||
encoder_block_attrs={"text_encoder": ("encoder.layers",)},
|
||||
)
|
||||
```
|
||||
|
||||
Add a test that resolves every dotted component/block path and fails if it is
|
||||
missing or is not the expected module/container; discovery may otherwise warn
|
||||
and skip. Validate ordinary layerwise offload and distributed layerwise
|
||||
offload (DLO) independently.
|
||||
|
||||
Keep component lifecycle and host-weight storage separate. `OffloadPlan`
|
||||
declares what can be streamed; the diffusion loader owns any `HostWeightPlan`
|
||||
and hands the exact prevalidated plan to DLO. Do not make the offloader rescan
|
||||
checkpoint files or duplicate loader name/shape/dtype decisions. Direct mmap is
|
||||
a fail-closed optimization: current preflight requires TP1, no HSDP, no online
|
||||
quantization, complete bindings, and represented transforms. Otherwise the
|
||||
ordinary loader remains authoritative.
|
||||
|
||||
If the target revision supports Host Weight Runtime (HWR), treat it as a
|
||||
separate, exact-identity startup/cache path rather than generic offload or
|
||||
zero-copy execution. Qualify `preferred` population/fallback and `required`
|
||||
consume-only failure independently; verify immutable manifests, final-layout
|
||||
restore, lease cleanup, corruption/quarantine, and node-local sharing. Current
|
||||
model contracts may be BF16 no-AllGather-only, so reject AllGather, online
|
||||
quantization, HSDP, LoRA/adapted weights, and unrecognized load formats unless
|
||||
the exact target revision explicitly adds them.
|
||||
|
||||
On small-HBM cards, record load/materialization peak, resident peak, encode,
|
||||
each denoise wave, decode, transient per-rank HBM, host PSS, H2D, and collective
|
||||
time. Test DLO AllGather and `--dlo-no-use-allgather` as different deployments.
|
||||
Sweep the supported resident-block policies and publish the non-dominated
|
||||
latency-HBM frontier; a single minimum-memory or fastest point is not the DLO
|
||||
trade-off. Keep request-specific preparation replica-local. Only weight
|
||||
materialization may enter the declared DLO collective group; never use WORLD
|
||||
for per-request work in a DP deployment.
|
||||
Direct checkpoint mmap preflight is limited to TP1 without HSDP or online
|
||||
quantization; TP>1 falls back to the ordinary TP-aware loader and can still feed
|
||||
DLO AllGather or no-AllGather after scoped E2E. HSDP remains incompatible with
|
||||
DLO AllGather. A target revision may allow finalized per-tensor online FP8
|
||||
through the ordinary loader and AllGather; keep every other online quantizer
|
||||
fail-closed, and require model/card/topology E2E before promotion.
|
||||
Use deploy YAML for DP under `--omni`; vLLM DP CLI flags are rejected.
|
||||
|
||||
### Gate 5: Add request-scoped acceleration safely
|
||||
|
||||
Server-wide Cache-DiT support does not imply a per-request quality API. Only
|
||||
add request policy after defining calibrated quality tiers and a safe hook
|
||||
lifecycle using `SupportsRequestScopedCacheDiT`, `CacheDiTRequestSpec`, and
|
||||
`RequestScopedCacheDiTRuntime`. Validate alternating and concurrent quality
|
||||
tiers, real cache hits, refresh/reset, success, error, disconnect, abort, and
|
||||
the next uncached request. Do not batch different quality policies unless the
|
||||
batch compatibility key and hook ownership make that safe.
|
||||
|
||||
Then test Cache-DiT against FP8, DLO, TP/SP, compile, and batching one
|
||||
combination at a time. See [production feature patterns](references/feature-patterns.md)
|
||||
for the lifecycle skeleton and compatibility matrix.
|
||||
|
||||
### Gate 6: Optimize only from profiles
|
||||
|
||||
Freeze one canonical workload manifest before profiling. Keep lossless runtime
|
||||
A/Bs, accelerated paths with numerical/quality trade-offs, and production
|
||||
topology studies in separate result lanes. Reconcile the client boundary as
|
||||
queue + encode + denoise + video/audio decode + output transport/codec +
|
||||
residual; do not hide a material residual in another stage.
|
||||
|
||||
Profile stage time, GPU kernels, CPU/GPU synchronization, allocations, H2D,
|
||||
output payload/copies, and collectives before choosing work. Apply and A/B one
|
||||
change at a time:
|
||||
|
||||
1. Remove synchronization and redundant denoise-loop work: hoist static prompt
|
||||
conditioning/token refinement, RoPE tables, masks/metadata, bounded AdaLN
|
||||
schedule projections, and reference scans; remove per-layer `.item()` and
|
||||
unused masks/allocations.
|
||||
2. Select the fastest correct BF16 attention backend for each role and shape.
|
||||
3. Prefer explicit fused QKV, gate-up + `SiluAndMul`, RMSNorm/RoPE, AdaLN, and
|
||||
layout/residual operators with guarded native fallbacks. Preserve every
|
||||
materialized dtype/rounding boundary consumed by the unfused graph.
|
||||
4. Enable sparse attention only when platform, shape, topology, realized
|
||||
sparsity, quality, fallback, and end-to-end speedup are all proven.
|
||||
5. Profile USP all-to-all count/bytes, packing, contiguous copies, overlap, and
|
||||
scaling efficiency. Audit replicated cross-attention before using
|
||||
`skip_sequence_parallel`. Qualify regular and accelerated Ulysses transport
|
||||
independently; activation logs, JIT/readiness time, workspace growth,
|
||||
stream ownership, maximum-shape warmup, and numerical drift are gates.
|
||||
Preserve the checkpoint's Q:KV head ratio when padding GQA for Ulysses, and
|
||||
validate strided QKV staging rather than assuming packed contiguity.
|
||||
6. Evaluate text-encoder or VAE disaggregation only if stage share, transfer
|
||||
volume, reuse/concurrency, and independent scaling justify it. Tiling,
|
||||
offload, or patch parallelism is not disaggregation.
|
||||
|
||||
Do not combine independent optimizations into one performance claim. Preserve
|
||||
raw before/after runs with fixed workload and warmup policy.
|
||||
|
||||
### Gate 7: Make serving interruptible and load-stable
|
||||
|
||||
Treat request-mode batching and step-wise execution as separate capabilities:
|
||||
|
||||
- request batching requires `supports_request_batch = True` and
|
||||
`DiffusionRequestBatch -> list[DiffusionOutput]`, exactly one output per
|
||||
request;
|
||||
- step execution requires the complete `SupportsStepExecution` contract:
|
||||
`prepare_encode`, `denoise_step`, `step_scheduler`, and `post_decode`.
|
||||
|
||||
Keep all mutable scheduler, RNG, latent, mask, cache, and step state request
|
||||
scoped. First validate `--step-execution --max-num-seqs 1` and step-boundary
|
||||
abort; only then test heterogeneous multi-request waves. Step continuous
|
||||
batching is experimental and is not automatically compatible with Cache-DiT.
|
||||
Before recommending it, predeclare a useful case and success threshold, then
|
||||
A/B it against request mode under the same arrival process. Structural support
|
||||
or a generic bridge is not latency, throughput, or cancellation evidence for a
|
||||
model.
|
||||
|
||||
Test queued and in-flight abort, client disconnect, timeout, OOM, worker error,
|
||||
and restart. Require one terminal result, idempotent cleanup, no post-decode
|
||||
after abort, no cache/temp/VRAM/request-ID leak, and a successful next request.
|
||||
Inject late worker results/exceptions after cancellation and prove the result
|
||||
pump stays alive instead of raising `InvalidStateError`. For large batched or
|
||||
long-window video outputs, freeze dtype, range, layout, contiguity, ownership,
|
||||
payload size, serializer/codec limits, and offline-versus-HTTP semantics.
|
||||
Validate device-side output preparation, D2H/IPC or shared handles, remote
|
||||
encoding, event-loop responsiveness, and client materialization as separate
|
||||
stages, including abort and consumer failure cleanup.
|
||||
A model/VAE callback that publishes ordered chunks is only a producer contract;
|
||||
it is not transport, backpressure, cancellation, public streaming, or
|
||||
time-to-first-frame evidence. Distinguish complete-response faster-than-playback
|
||||
(`client E2E / output duration <= 1`) from streaming and report first-chunk,
|
||||
steady cadence, finalization, and complete-artifact latency separately.
|
||||
Run below-, near-, and above-saturation mixed-RPS soaks and report success/error
|
||||
rate, throughput, queue time, memory slope, temp growth, and worker health.
|
||||
Report p50/p95/p99 only from a declared arrival model with enough measured
|
||||
requests for the claimed percentile. See
|
||||
[serving and CI validation](references/production-validation.md).
|
||||
|
||||
### Gate 8: Publish recipes and four independent CI tracks
|
||||
|
||||
For every target vendor/card/topology, publish exact environment, serve/deploy
|
||||
command, one complete JSON or multipart curl per supported task, expected
|
||||
output checks, benchmark command, warmup/repeat/concurrency policy, raw metrics,
|
||||
per-rank HBM/host PSS, accuracy artifact, and limitations. Mark each
|
||||
CUDA/ROCm/NPU/XPU row `validated`, `unsupported`, or `not tested`; never inherit
|
||||
support from the platform abstraction or another card. Re-run DLO on each
|
||||
small-HBM recipe.
|
||||
|
||||
Maintain separate CI gates:
|
||||
|
||||
1. Function: strict load, API/offline positive and negative contracts.
|
||||
2. Accuracy: fixed reference revision/assets/seed/schedule and scoped tolerance.
|
||||
3. Performance: fixed best deployment plus memory-constrained/DLO row, raw JSON,
|
||||
explicit warmup, enough samples for each claimed statistic, and owned
|
||||
regression thresholds.
|
||||
4. Reliability: long mixed-RPS soak, abort/disconnect/fault cleanup, memory and
|
||||
temporary-resource trend.
|
||||
|
||||
## Production Definition of Done
|
||||
|
||||
A model is production-ready only when:
|
||||
|
||||
- every official advertised task passes API parity, offline/online parity,
|
||||
strict loading, fixed-reference quality, and negative validation;
|
||||
- the recommended deployment row passes correctness, isolation, abort/error
|
||||
cleanup, benchmark, and soak gates on named hardware;
|
||||
- online FP8 and DLO are each implemented and either validated in scoped rows
|
||||
or explicitly limited/unsupported with fail-fast behavior;
|
||||
- per-request cache quality tiers have measured speed/quality frontiers and
|
||||
request isolation, if advertised;
|
||||
- CUDA/ROCm/NPU/XPU recipes describe evidence rather than inferred support;
|
||||
- Function, Accuracy, Performance, and Reliability CI have owners, artifacts,
|
||||
and actionable failure output;
|
||||
- accelerated paths identify every precision/quality trade-off, and the raw
|
||||
offline plus online encoded output contracts are versioned and tested;
|
||||
- all unverified task/backend/cache/quant/offload/topology/hardware combinations
|
||||
remain `not tested` and are not presented as supported.
|
||||
|
||||
Day-0 support is still a valid milestone, but title, docs, and matrix must say
|
||||
which representative task passed and which production gates remain.
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Production Add Diffusion Model"
|
||||
short_description: "Productionize vLLM-Omni diffusion models"
|
||||
default_prompt: "Use $production-add-diffusion-model to take this integrated diffusion model from Day-0 support to evidence-backed production readiness."
|
||||
@@ -0,0 +1,315 @@
|
||||
# API Contract and Hardware Recipes
|
||||
|
||||
## Contents
|
||||
|
||||
1. [Freeze the upstream contract](#freeze-the-upstream-contract)
|
||||
2. [Validate before side effects](#validate-before-side-effects)
|
||||
3. [Task request templates](#task-request-templates)
|
||||
4. [Serving benchmark template](#serving-benchmark-template)
|
||||
5. [Hardware recipe contract](#hardware-recipe-contract)
|
||||
6. [Best deployment selection](#best-deployment-selection)
|
||||
|
||||
## Freeze the upstream contract
|
||||
|
||||
Pin three revisions independently:
|
||||
|
||||
- official source implementation commit or package version;
|
||||
- checkpoint/model-card revision and partition;
|
||||
- vLLM-Omni target commit.
|
||||
|
||||
Build a table before editing code. One row is one official task/partition.
|
||||
|
||||
| Field | Required content |
|
||||
|---|---|
|
||||
| Task and partition | Official task name, checkpoint subdirectory, task selection rules |
|
||||
| Call contract | Constructor and call arguments, types, required/optional, official defaults |
|
||||
| Prompt/input | Prompt count/length, modality, MIME, order, count, decoded shape |
|
||||
| Size/time | Width/height alignment, pixels, frames, duration, FPS, start offset |
|
||||
| Denoise | Scheduler, sigma schedule, steps, CFG/guidance, seed behavior |
|
||||
| Output | Count, format, codec/sample rate, return type |
|
||||
| API | Offline mapping, sync endpoint, async endpoint/job lifecycle |
|
||||
| Rejection | Status code and stable error class/message for every invalid boundary |
|
||||
|
||||
Official API parity includes limits and defaults, not just names. Do not add a
|
||||
generic default that changes a distilled checkpoint's fixed schedule, or accept
|
||||
an input combination the official task rejects.
|
||||
|
||||
For every accepted boundary, add the neighboring rejected values. Examples:
|
||||
`max_images` and `max_images + 1`, smallest/largest legal duration, aligned and
|
||||
misaligned dimensions, omitted and illegal steps, one and too many outputs.
|
||||
|
||||
## Validate before side effects
|
||||
|
||||
Use one normalization/validation path for offline, sync, and async requests.
|
||||
The ordering must be:
|
||||
|
||||
```text
|
||||
parse -> normalize aliases -> validate task/source matrix -> validate declared
|
||||
limits -> stream with byte caps -> inspect/decode with frame/pixel/duration caps
|
||||
-> persist only if needed -> submit to engine
|
||||
```
|
||||
|
||||
Fail before download, persistence, job creation, or engine submit whenever the
|
||||
available metadata is sufficient. For streamed uploads, enforce:
|
||||
|
||||
- total reference count and per-modality count before reading bodies;
|
||||
- per-file bytes while streaming;
|
||||
- aggregate bytes across all files;
|
||||
- decoded frames, duration, pixels, and media type before engine submission.
|
||||
|
||||
On rejection, close streams and remove every partial temporary resource. Use
|
||||
HTTP 400 for invalid task/field combinations and 413 for payload limits. Test
|
||||
both sync and async paths; an invalid async request must not create a job ID.
|
||||
|
||||
Do not trust client MIME alone. Sniff/validate content and keep ordered repeated
|
||||
multipart fields ordered. Avoid logging prompts, raw references, signed URLs,
|
||||
or base64 payloads.
|
||||
|
||||
## Task request templates
|
||||
|
||||
These are endpoint-shape templates, not model recipes. Replace placeholders
|
||||
with the exact official defaults and validated assets for the target model.
|
||||
Every production recipe must include all advertised tasks and an expected
|
||||
output check.
|
||||
|
||||
### Text to image: JSON
|
||||
|
||||
```bash
|
||||
curl --fail-with-body -sS -X POST http://127.0.0.1:8091/v1/images/generations \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"model": "<model>",
|
||||
"prompt": "<validated prompt>",
|
||||
"size": "1024x1024",
|
||||
"num_inference_steps": 30,
|
||||
"seed": 42,
|
||||
"response_format": "b64_json"
|
||||
}' | jq -er '.data[0].b64_json' | base64 -d > output.png
|
||||
file output.png
|
||||
```
|
||||
|
||||
### Image edit: multipart
|
||||
|
||||
```bash
|
||||
curl --fail-with-body -sS -X POST http://127.0.0.1:8091/v1/images/edits \
|
||||
-F 'model=<model>' \
|
||||
-F 'prompt=<validated edit prompt>' \
|
||||
-F 'image=@/path/to/reference.png;type=image/png' \
|
||||
-F 'size=1024x1024' \
|
||||
-F 'num_inference_steps=30' \
|
||||
-F 'seed=42' \
|
||||
-F 'output_format=png' \
|
||||
| jq -er '.data[0].b64_json' | base64 -d > edited.png
|
||||
file edited.png
|
||||
```
|
||||
|
||||
Use repeated `image` fields only if the model's official input matrix allows
|
||||
multiple ordered sources. Never describe a file upload as JSON-only.
|
||||
|
||||
### Public request fields versus internal sampling fields
|
||||
|
||||
Use the field exposed by the target revision's endpoint schema; do not rename
|
||||
it from the pipeline's internal attribute. For example, the current video JSON
|
||||
and multipart contracts expose `extra_params`, while serving translates that
|
||||
object into `DiffusionSamplingParams.extra_args`. Other request paths may
|
||||
expose canonical `extra_args` or nest it under `extra_body`. Inspect the exact
|
||||
Pydantic model/FastAPI form dependency and exercise the generated curl before
|
||||
publishing a recipe. A deprecation warning in a different endpoint is not
|
||||
evidence that an unrecognized multipart form field works here.
|
||||
|
||||
Current endpoint map (re-check it at the target commit):
|
||||
|
||||
| Endpoint | Public model-specific field |
|
||||
|---|---|
|
||||
| `/v1/videos`, `/v1/videos/sync` | Multipart `extra_params` JSON string |
|
||||
| `/v1/images/generations` | JSON `extra_params` object |
|
||||
| `/v1/chat/completions` diffusion | `extra_body.extra_args` |
|
||||
| `/v1/images/edits` | Declared typed form fields only |
|
||||
| `/v1/audio/generate` | Declared typed JSON fields only |
|
||||
|
||||
Prefer declared top-level fields. Use a generic model-specific envelope only
|
||||
when the endpoint actually exposes it.
|
||||
|
||||
### Text to video: multipart
|
||||
|
||||
```bash
|
||||
curl --fail-with-body -sS -X POST http://127.0.0.1:8091/v1/videos/sync \
|
||||
-H 'Accept: video/mp4' \
|
||||
-F 'model=<model>' \
|
||||
-F 'prompt=<validated prompt>' \
|
||||
-F 'size=1280x720' \
|
||||
-F 'num_frames=81' \
|
||||
-F 'fps=16' \
|
||||
-F 'num_inference_steps=30' \
|
||||
-F 'seed=42' \
|
||||
-F 'extra_params={"task":"<official-task>"}' \
|
||||
-o output.mp4
|
||||
ffprobe -v error -show_entries stream=codec_name,width,height,r_frame_rate \
|
||||
-of json output.mp4
|
||||
```
|
||||
|
||||
### Image/video to video: multipart
|
||||
|
||||
```bash
|
||||
curl --fail-with-body -sS -X POST http://127.0.0.1:8091/v1/videos/sync \
|
||||
-H 'Accept: video/mp4' \
|
||||
-F 'model=<model>' \
|
||||
-F 'prompt=<validated prompt>' \
|
||||
-F 'num_frames=81' \
|
||||
-F 'fps=16' \
|
||||
-F 'num_inference_steps=30' \
|
||||
-F 'seed=42' \
|
||||
-F 'extra_params={"task":"<official-task>","start_time_seconds":0}' \
|
||||
-F 'input_reference=@/path/to/reference.png;type=image/png' \
|
||||
-o i2v.mp4
|
||||
|
||||
curl --fail-with-body -sS -X POST http://127.0.0.1:8091/v1/videos/sync \
|
||||
-H 'Accept: video/mp4' \
|
||||
-F 'model=<model>' \
|
||||
-F 'prompt=<validated prompt>' \
|
||||
-F 'num_frames=81' \
|
||||
-F 'num_inference_steps=30' \
|
||||
-F 'seed=43' \
|
||||
-F 'extra_params={"task":"<official-task>","start_time_seconds":0}' \
|
||||
-F 'input_reference=@/path/to/reference.mp4;type=video/mp4' \
|
||||
-o v2v.mp4
|
||||
```
|
||||
|
||||
For multiple references, repeat the model's accepted multipart field in
|
||||
official order. Include complete downloadable or repository fixtures plus
|
||||
SHA256 hashes. A placeholder-only recipe is not runnable evidence.
|
||||
|
||||
### Text to audio: JSON
|
||||
|
||||
```bash
|
||||
curl --fail-with-body -sS -X POST http://127.0.0.1:8091/v1/audio/generate \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"model": "<model>",
|
||||
"input": "<validated audio prompt>",
|
||||
"audio_length": 10.0,
|
||||
"num_inference_steps": 100,
|
||||
"seed": 42,
|
||||
"response_format": "wav"
|
||||
}' -o output.wav
|
||||
ffprobe -v error -show_entries stream=codec_name,sample_rate,channels \
|
||||
-of json output.wav
|
||||
```
|
||||
|
||||
Use `/v1/audio/generate` for general diffusion audio. Do not substitute the
|
||||
speech API unless the model is actually served through the speech contract.
|
||||
|
||||
### Async video lifecycle
|
||||
|
||||
Alongside `/v1/videos/sync`, include `POST /v1/videos`, status polling,
|
||||
content retrieval, cancellation/abort if public, and cleanup assertions. The
|
||||
same request must normalize to the same engine sampling parameters in both
|
||||
modes.
|
||||
|
||||
## Serving benchmark template
|
||||
|
||||
Run a smoke first, then fixed-concurrency and arrival-rate tests. Video task
|
||||
names for the harness are `t2v`, `i2v`, `ti2v`, or `v2v`; model-specific task
|
||||
selection belongs in `--extra-body` when required.
|
||||
|
||||
```bash
|
||||
python benchmarks/diffusion/diffusion_benchmark_serving.py \
|
||||
--base-url http://127.0.0.1:8091 \
|
||||
--endpoint /v1/videos \
|
||||
--model '<model>' \
|
||||
--dataset random \
|
||||
--task t2v \
|
||||
--num-prompts 100 \
|
||||
--width 1280 --height 720 \
|
||||
--num-frames 81 --fps 16 \
|
||||
--num-inference-steps 30 \
|
||||
--request-rate 0.2 \
|
||||
--max-concurrency 4 \
|
||||
--warmup-requests 4 \
|
||||
--warmup-concurrency 4 \
|
||||
--extra-body '{"extra_params":"{\"task\":\"<official-task>\"}"}' \
|
||||
--output-file result.json
|
||||
```
|
||||
|
||||
The video backend currently stringifies each multipart value. Pre-serialize
|
||||
the nested `extra_params` object as above; passing it as a nested object would
|
||||
produce Python dict syntax rather than valid JSON. If the target revision
|
||||
changes the backend to JSON-encode dict/list values, update and smoke-test this
|
||||
command against that revision.
|
||||
|
||||
For i2v/v2v, use the harness's validated dataset/trace path or provide the
|
||||
documented media source; do not silently benchmark t2v and label it i2v. Keep
|
||||
prompt, assets, seed, dimensions, frames, steps, guidance, warmup, concurrency,
|
||||
and arrival process identical across A/B runs.
|
||||
|
||||
Keep three benchmark lanes distinct:
|
||||
|
||||
1. lossless runtime A/B with the released schedule/precision/attention policy;
|
||||
2. accelerated-path A/B that changes one adapter, quantization, cache, or
|
||||
sparsity policy and adds same-seed quality evidence;
|
||||
3. production-topology studies that vary placement or parallelism and report a
|
||||
latency/throughput/memory frontier rather than a kernel speedup.
|
||||
|
||||
For a fixed single-request A/B, retain every measured run and report median or
|
||||
mean with range after an explicit warmup; do not infer p95/p99 from a few
|
||||
repetitions. Tail latency requires a declared arrival process, request count,
|
||||
concurrency, and enough successful samples. Reconcile client E2E with queue,
|
||||
encoder, denoise, video/audio decode, D2H/IPC, codec, and residual timings.
|
||||
|
||||
For generated media, define `real-time` rather than relying on the label. A
|
||||
complete-response claim uses `client E2E / validated output duration <= 1` and
|
||||
does not imply streaming or low time-to-first-frame. Report first chunk/fragment
|
||||
and cadence separately when a public streaming route exists. Never add gains
|
||||
across benchmark lanes with different schedules, prompts, seeds, artifacts, or
|
||||
topologies.
|
||||
|
||||
The diffusion serving harness currently covers image/video task families, not
|
||||
`/v1/audio/generate`. For text-to-audio, use the target model's task-specific
|
||||
audio benchmark if one exists; otherwise add a serving harness that records
|
||||
request rate, concurrency, latency, generated-audio seconds per second, error
|
||||
rate, and output validation. Do not relabel the TTS harness or a video workload
|
||||
as diffusion-audio evidence.
|
||||
|
||||
## Hardware recipe contract
|
||||
|
||||
Create a separate row and runnable section for each vendor/card/topology:
|
||||
|
||||
| Evidence | Required detail |
|
||||
|---|---|
|
||||
| Identity | Vendor, exact SKU, device count, HBM, interconnect |
|
||||
| Software | Driver, CUDA/ROCm/CANN/oneAPI, framework/container, dependency versions |
|
||||
| Source | vLLM-Omni commit, official/checkpoint revision and hashes |
|
||||
| Deployment | dtype, attention backend per role, TP/SP/CFG/HSDP/DP, cache, quant, offload |
|
||||
| Commands | Complete deploy YAML/serve command, every task curl, benchmark command |
|
||||
| Method | Warmup exclusion, repetitions or request count, concurrency, arrival distribution, duration, statistic definitions |
|
||||
| Results | Success rate, p50/p95/p99, RPS/throughput per device, stage latency |
|
||||
| Memory | Load/materialize, resident, encode, denoise, decode, transient per-rank HBM and host PSS |
|
||||
| Quality | Metric/tolerance, artifact links/hashes, temporal/audio checks |
|
||||
| Output | Raw/offline dtype, range, layout and ownership; online codec, payload bytes, transport route and client boundary |
|
||||
| Limits | `validated`/`limited`/`unsupported`/`not tested`, fallback/rejection behavior |
|
||||
|
||||
Use current paths such as `recipes/<vendor>/`, `recipes/README.md`, supported
|
||||
models, and feature compatibility tables. Top-level documentation must link to
|
||||
the scoped matrix; it must not use an unqualified checkmark that hides a ROCm,
|
||||
NPU, XPU, task, topology, or dtype limitation.
|
||||
|
||||
DLO evidence is per card, especially on small-HBM devices. A DLO result on one
|
||||
CUDA card does not establish another CUDA SKU, ROCm, NPU, or XPU support.
|
||||
|
||||
## Best deployment selection
|
||||
|
||||
Benchmark a correctness-approved candidate matrix, not every theoretical
|
||||
combination. Choose the recommended row by the production objective—latency,
|
||||
throughput, cost, or small-HBM fit—and document that objective.
|
||||
|
||||
Do not recommend a row until it passes:
|
||||
|
||||
- all advertised task/API contracts;
|
||||
- fixed-reference quality;
|
||||
- request isolation and abort/error cleanup;
|
||||
- sustained arrival-rate tests;
|
||||
- per-rank HBM and host PSS limits;
|
||||
- hardware-specific accuracy and performance CI.
|
||||
|
||||
Keep the most memory-constrained DLO row even when it is not the fastest; it is
|
||||
a separate production recipe, not a failed latency candidate.
|
||||
@@ -0,0 +1,462 @@
|
||||
# Production Feature Patterns
|
||||
|
||||
## Contents
|
||||
|
||||
1. [Compatibility matrix](#compatibility-matrix)
|
||||
2. [Online FP8](#online-fp8)
|
||||
3. [Strict fused-weight loading](#strict-fused-weight-loading)
|
||||
4. [Distributed layerwise offload](#distributed-layerwise-offload)
|
||||
5. [Per-request Cache-DiT](#per-request-cache-dit)
|
||||
6. [Combination gates](#combination-gates)
|
||||
|
||||
## Compatibility matrix
|
||||
|
||||
Create rows at the granularity below. Add evidence links and a reason for every
|
||||
non-validated state.
|
||||
|
||||
| Task/shape | Execution mode/capacity | Backend/layout | Quant | Cache policy | Offload | Topology | Hardware/dtype | Output contract/transport | State | Evidence |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| `<task>/<shape>/<schedule>` | serial request / 1 | SDPA, padded | BF16 | none | resident | 1 device | `<card>` BF16 | raw float / local | `not tested` | — |
|
||||
| `<task>/<shape>/<schedule>` | step / N | `<fast>`, packed | BF16 | none | resident | `<TP/SP>` | `<card>` BF16 | encoded / subprocess | `not tested` | — |
|
||||
| `<task>/<shape>/<schedule>` | step / N | `<fast>`, packed | FP8 | high | DLO no-AG | `<TP/SP>` | `<card>` | device uint8 / SHM | `not tested` | — |
|
||||
|
||||
Initialize every row as `not tested`; the table is a work queue, not evidence.
|
||||
Promote the dense BF16 row to the oracle only after its scoped parity passes.
|
||||
Add one axis at a time. If the runtime cannot
|
||||
enforce a known-incompatible combination, add construction/admission
|
||||
validation before writing a recipe.
|
||||
|
||||
## Online FP8
|
||||
|
||||
### Component routing
|
||||
|
||||
The public builder accepts one method or a longest-prefix component map:
|
||||
|
||||
```python
|
||||
from vllm_omni.quantization import build_quant_config
|
||||
|
||||
config = build_quant_config({
|
||||
"transformer": {
|
||||
"method": "fp8",
|
||||
"ignored_layers": ["transformer.final_layer"],
|
||||
},
|
||||
"text_encoder": None,
|
||||
"vae": None,
|
||||
"default": None,
|
||||
})
|
||||
|
||||
assert config.resolve("transformer.blocks.0.attn.qkv_proj") is not None
|
||||
assert config.resolve("text_encoder.layers.0.mlp") is None
|
||||
assert config.resolve("vae.decoder.conv_out") is None
|
||||
```
|
||||
|
||||
Use actual runtime prefixes. Multi-DiT or nested pipelines may not use the
|
||||
generic names `transformer` and `vae`. A prefix that falls through to a default
|
||||
is not component validation. Add tests for every included and ignored prefix.
|
||||
|
||||
For CLI serving, first validate resident DiT-only FP8:
|
||||
|
||||
```bash
|
||||
vllm serve '<model>' --omni --quantization fp8 --max-num-seqs 1
|
||||
```
|
||||
|
||||
For scoped routing:
|
||||
|
||||
```bash
|
||||
vllm serve '<model>' --omni \
|
||||
--diffusion-quantization-config \
|
||||
'{"<dit-runtime-prefix>":{"method":"fp8","ignored_layers":["<exact-linear-prefix>"]},"<vae-runtime-prefix>":null,"default":null}' \
|
||||
--max-num-seqs 1
|
||||
```
|
||||
|
||||
Do not publish these placeholders. A recipe must contain the target model's
|
||||
verified prefixes.
|
||||
|
||||
### Quantizable module wiring
|
||||
|
||||
Pass `quant_config` and a stable prefix through every vLLM linear. Keep
|
||||
precision-sensitive modulation/norm layers safe unless proven otherwise.
|
||||
|
||||
```python
|
||||
from vllm.model_executor.layers.activation import SiluAndMul
|
||||
from vllm.model_executor.layers.linear import (
|
||||
MergedColumnParallelLinear,
|
||||
QKVParallelLinear,
|
||||
RowParallelLinear,
|
||||
)
|
||||
|
||||
self.qkv_proj = QKVParallelLinear(
|
||||
hidden_size=hidden_size,
|
||||
head_size=head_dim,
|
||||
total_num_heads=num_heads,
|
||||
total_num_kv_heads=num_kv_heads,
|
||||
bias=False,
|
||||
quant_config=quant_config,
|
||||
prefix=f"{prefix}.attn.qkv_proj",
|
||||
)
|
||||
self.gate_up_proj = MergedColumnParallelLinear(
|
||||
hidden_size,
|
||||
[intermediate_size, intermediate_size],
|
||||
bias=False,
|
||||
quant_config=quant_config,
|
||||
prefix=f"{prefix}.mlp.gate_up_proj",
|
||||
)
|
||||
self.act_fn = SiluAndMul()
|
||||
self.down_proj = RowParallelLinear(
|
||||
intermediate_size,
|
||||
hidden_size,
|
||||
bias=False,
|
||||
input_is_parallel=True,
|
||||
quant_config=quant_config,
|
||||
prefix=f"{prefix}.mlp.down_proj",
|
||||
)
|
||||
```
|
||||
|
||||
Set required checkpoint parameters to fail rather than silently initialize if
|
||||
the current loader supports `missing_param_init = "error"`.
|
||||
|
||||
### FP8 evidence
|
||||
|
||||
For each task/hardware row, capture:
|
||||
|
||||
- startup proves the FP8 quant method is attached to intended linear modules;
|
||||
- no unexpected BF16 fallback and no meta/uninitialized parameters;
|
||||
- same-seed component, trajectory, and final-artifact comparison with BF16;
|
||||
- HBM at load, resident, encode, denoise, decode, and peak;
|
||||
- cold load time; fixed-work warm-latency raw runs; and serving p50/p95/p99 plus
|
||||
throughput only from a declared arrival load with enough samples;
|
||||
- named ignored layers and why their BF16 retention is required.
|
||||
|
||||
DiT FP8 evidence does not cover the text encoder or VAE. CUDA evidence does not
|
||||
cover ROCm/NPU/XPU. A pre-quantized checkpoint is a different loading path from
|
||||
online FP8 and needs its own state. If it is also pruned, rotated, distilled, or
|
||||
adapter-modified, report that model delta explicitly; BF16-versus-artifact
|
||||
quality and speed are deployment comparisons, not pure quantization ablations.
|
||||
|
||||
## Strict fused-weight loading
|
||||
|
||||
Never use `param.data.copy_()` for quantizable vLLM parameters. Transform the
|
||||
checkpoint layout before invoking the parameter loader so the online quant
|
||||
loader remains outermost:
|
||||
|
||||
```python
|
||||
from vllm.model_executor.model_loader.weight_utils import default_weight_loader
|
||||
|
||||
param = params[target_name]
|
||||
weight_loader = getattr(param, "weight_loader", default_weight_loader)
|
||||
|
||||
if target_name.endswith(".qkv_proj.weight"):
|
||||
packed_qkv = reorder_checkpoint_qkv(checkpoint_weight)
|
||||
weight_loader(param, packed_qkv)
|
||||
elif target_name.endswith(".gate_up_proj.weight"):
|
||||
gate, up = checkpoint_weight.chunk(2, dim=0)
|
||||
weight_loader(param, gate, 0)
|
||||
weight_loader(param, up, 1)
|
||||
else:
|
||||
weight_loader(param, checkpoint_weight)
|
||||
```
|
||||
|
||||
Track source shards per fused destination:
|
||||
|
||||
```python
|
||||
expected = {
|
||||
"blocks.0.attn.qkv_proj.weight": {"q", "k", "v"},
|
||||
"blocks.0.mlp.gate_up_proj.weight": {0, 1},
|
||||
}
|
||||
seen = {name: set() for name in expected}
|
||||
|
||||
# After mapping/loading each source:
|
||||
seen[target_name].add(shard_id)
|
||||
|
||||
incomplete = {
|
||||
name: sorted(shards - seen[name], key=str)
|
||||
for name, shards in expected.items()
|
||||
if shards - seen[name]
|
||||
}
|
||||
if incomplete:
|
||||
raise RuntimeError(f"incomplete fused checkpoint parameters: {incomplete}")
|
||||
```
|
||||
|
||||
Also compare retained model parameters with the returned loaded set. Explicit
|
||||
checkpoint ignores need a reason and test; an unknown or missing weight is not
|
||||
an ignore policy.
|
||||
|
||||
Load-time adapter fusion creates a dedicated model, not a request-switchable
|
||||
LoRA. Pin the base and adapter revisions, validate every low-rank and full-rank
|
||||
delta target, freeze the adapter-declared schedule/task, and fuse before
|
||||
sharding only when the loader contract proves identical rank-local weights.
|
||||
Reject offload or host-weight paths that bypass that fusion rather than serving
|
||||
the unfused base model silently.
|
||||
|
||||
## Distributed layerwise offload
|
||||
|
||||
### Declare component topology
|
||||
|
||||
Use `SupportsComponentDiscovery` and `OffloadPlan`. Validate all dotted paths
|
||||
at construction/test time because discovery may otherwise warn and skip.
|
||||
|
||||
Treat a future/requested `--layerwise-offload-components` selector as a public
|
||||
API only if it exists in the target revision. Today, express component topology
|
||||
with `OffloadPlan` (`on_demand_component_paths`, `block_attrs`,
|
||||
`encoder_block_attrs`, `resident_dit_paths`, and `offload_submodules`). Do not
|
||||
invent a CLI flag or claim component-selective DLO from discovery declarations
|
||||
alone; validate the selector separately when it lands.
|
||||
|
||||
```python
|
||||
from operator import attrgetter
|
||||
|
||||
import torch
|
||||
|
||||
def assert_offload_path(root, path):
|
||||
value = attrgetter(path)(root)
|
||||
if not isinstance(value, torch.nn.Module):
|
||||
raise TypeError(f"offload path {path!r} is not an nn.Module")
|
||||
return value
|
||||
|
||||
for path in (
|
||||
*pipeline._dit_modules,
|
||||
*pipeline._encoder_modules,
|
||||
*pipeline._vae_modules,
|
||||
*pipeline._offload_plan.on_demand_component_paths,
|
||||
):
|
||||
assert_offload_path(pipeline, path)
|
||||
```
|
||||
|
||||
For `block_attrs`, resolve the DiT first and then each declared block attribute.
|
||||
Require an indexable block container with at least one `nn.Module`. Validate
|
||||
`offload_submodules`, `resident_dit_paths`, and `encoder_block_attrs` similarly.
|
||||
|
||||
### Keep host storage loader-owned
|
||||
|
||||
Current DLO accepts a loader-produced `HostWeightPlan`. Pipeline authors should
|
||||
not instantiate this plan or add ad hoc mmap capability flags. The loader must
|
||||
preflight complete runtime/checkpoint bindings, names, shapes, dtypes, persistent
|
||||
buffers, and represented transforms before it skips ordinary materialization.
|
||||
The exact accepted plan is consumed once by DLO; a rejected plan records an
|
||||
observable fallback reason and continues through the ordinary loader.
|
||||
|
||||
Direct checkpoint mmap currently requires TP1 without HSDP or online
|
||||
quantization. TP>1 falls back to the ordinary TP-aware loader and may feed DLO
|
||||
AllGather or no-AllGather; the current design records a bounded DP2xTP2
|
||||
AllGather smoke, but that path does not receive direct-mmap host-page sharing.
|
||||
Per-tensor online FP8 must use the ordinary loader. Current revisions can admit
|
||||
its finalized weight/scale layout to DLO AllGather, including transposed runtime
|
||||
weights, while unsupported online quantizers still fail closed. Direct mmap
|
||||
remains unavailable, so capture the temporary full-model host materialization
|
||||
before rank sharding.
|
||||
|
||||
Host Weight Runtime is a distinct opt-in path. Current final-layout consumers
|
||||
use no-AllGather DLO and exact model-declared BF16 representations. In
|
||||
`preferred` mode, consume an exact local hit or canonically load and publish for
|
||||
a future startup; in `required` mode, consume only and fail on a miss or unusable
|
||||
artifact. HWR must not interact with DLO AllGather, online quantization, HSDP,
|
||||
LoRA/adapted weights, or an unrecognized load format unless the target revision
|
||||
adds an exact producer/restore contract. It is immutable node-local CPU backing,
|
||||
not zero-copy GPU execution; the DLO transport still owns staging or direct H2D.
|
||||
Qualify identity inputs, cold population, warm reuse, atomic concurrent
|
||||
publication, source digests, corruption/quarantine, lease cleanup, capacity,
|
||||
node-local PSS, H2D, and E2E latency. A host-memory saving that materially
|
||||
regresses transfer or request latency is a separate memory-first deployment.
|
||||
|
||||
### DLO deployment modes
|
||||
|
||||
Treat these paths separately:
|
||||
|
||||
| Path | Weight/runtime behavior | Required boundaries |
|
||||
|---|---|---|
|
||||
| DLO AllGather | Rank-sharded pinned host tensors; reconstruct layer through collective | TP>1 uses ordinary TP-aware loader output; HSDP and unsupported online quantizers are rejected; finalized per-tensor online FP8 is eligible only when recognized by the target revision; concurrent ranks must participate consistently |
|
||||
| DLO no-AllGather | Loader-approved checkpoint mmap or ordinary runtime tensors; each rank streams a complete block | Direct mmap is TP1/non-HSDP/non-online-quant only; TP/HSDP/online quant use ordinary runtime tensors and require scoped E2E |
|
||||
| SP + DLO | SP group can supply the DLO collective group | Packed boundaries and collective order require E2E |
|
||||
|
||||
Single-stage examples:
|
||||
|
||||
```bash
|
||||
# SP + DLO AllGather candidate; validate exact model/card before publishing.
|
||||
vllm serve '<model>' --omni \
|
||||
--enable-distributed-layerwise-offload \
|
||||
--usp 4 \
|
||||
--max-num-seqs 1
|
||||
|
||||
# TP + DLO rank-local candidate; not a support statement.
|
||||
vllm serve '<model>' --omni \
|
||||
--tensor-parallel-size 2 \
|
||||
--enable-distributed-layerwise-offload \
|
||||
--dlo-no-use-allgather \
|
||||
--max-num-seqs 1
|
||||
```
|
||||
|
||||
Under `--omni`, configure DP in a deploy YAML rather than passing vLLM DP CLI
|
||||
flags:
|
||||
|
||||
```yaml
|
||||
pipeline: <registered-pipeline>
|
||||
async_chunk: false
|
||||
data_parallel_size: 4
|
||||
stages:
|
||||
- stage_id: 0
|
||||
devices: "0,1,2,3"
|
||||
max_num_seqs: 1
|
||||
enable_distributed_layerwise_offload: true
|
||||
dlo_use_allgather: true
|
||||
parallel_config:
|
||||
tensor_parallel_size: 1
|
||||
sequence_parallel_size: 1
|
||||
```
|
||||
|
||||
```bash
|
||||
vllm serve '<model>' --omni --deploy-config /path/to/dlo_dp4.yaml
|
||||
```
|
||||
|
||||
Keep the YAML next to the validated recipe. Match its schema to the target
|
||||
revision; do not assume a model accepts another pipeline's deploy config.
|
||||
|
||||
### DLO validation sequence
|
||||
|
||||
1. Resident BF16 reference.
|
||||
2. Ordinary layerwise offload parity.
|
||||
3. DLO enable, warmup, same-seed generation, disable/restart.
|
||||
4. AllGather and no-AllGather separately.
|
||||
5. Concurrent requests with different prompts and identical collective-affecting
|
||||
parameters; then reject or schedule incompatible steps/shapes safely.
|
||||
6. Queued/in-flight abort, error, idle rank, worker exit, and next request.
|
||||
7. TP, SP, cache, compile, online FP8 one axis at a time.
|
||||
8. Loader-plan selection/fallback reason, cold/warm storage lifecycle, and
|
||||
corruption/stale-entry recovery when mmap/cache backing is advertised.
|
||||
9. Replica-local request preparation plus DLO-group-only weight collectives in
|
||||
DP; exercise concurrent replicas to catch accidental WORLD rendezvous.
|
||||
10. Sweep resident block counts; publish raw latency/HBM pairs, dominated
|
||||
points, and the non-dominated Pareto frontier.
|
||||
11. Per-rank HBM, host PSS for the full process tree, H2D and collective trace.
|
||||
|
||||
On a small-HBM card, measure transient encode/VAE peaks as well as resident DiT
|
||||
weights. If a non-DiT component still exceeds HBM, combine only independently
|
||||
validated on-demand component staging, VAE tiling/patching, or another topology;
|
||||
do not hide the remaining peak behind the phrase “DLO enabled.”
|
||||
|
||||
## Per-request Cache-DiT
|
||||
|
||||
### Policy and lifecycle skeleton
|
||||
|
||||
First verify that the target revision contains the shared protocol/runtime.
|
||||
If not, follow the nearest current model's lifecycle without inventing a new
|
||||
public API.
|
||||
|
||||
The following is a **serial/exclusive request-mode sketch only**. Pipeline-wide
|
||||
hooks are mutable; concurrent requests need an admission/batching key and
|
||||
runner-owned transition serialization before this shape is safe.
|
||||
|
||||
```python
|
||||
import torch.nn as nn
|
||||
|
||||
from vllm_omni.diffusion.cache.cachedit import (
|
||||
CacheDiTBackend,
|
||||
CacheDiTRequestSpec,
|
||||
RequestScopedCacheDiTRuntime,
|
||||
)
|
||||
|
||||
class MyPipeline(nn.Module):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__()
|
||||
# ... components ...
|
||||
self._cache_dit_runtime = RequestScopedCacheDiTRuntime(self)
|
||||
|
||||
def adopt_cache_dit_backend(self, backend: CacheDiTBackend) -> None:
|
||||
self._cache_dit_runtime.adopt(
|
||||
backend,
|
||||
installation_key="my_model.generic",
|
||||
)
|
||||
|
||||
def is_cache_dit_enabled(self) -> bool:
|
||||
return self._cache_dit_runtime.is_enabled
|
||||
|
||||
def _cache_spec(self, quality, steps):
|
||||
if quality == "lossless":
|
||||
return None
|
||||
if quality == "high":
|
||||
return CacheDiTRequestSpec(
|
||||
installation_key="my_model.high.v1",
|
||||
cache_config=self._validated_high_config,
|
||||
num_inference_steps=steps,
|
||||
)
|
||||
raise ValueError(f"unsupported quality tier: {quality!r}")
|
||||
|
||||
def forward(self, req):
|
||||
quality, steps = validate_and_resolve_request(req)
|
||||
self._cache_dit_runtime.prepare(self._cache_spec(quality, steps))
|
||||
return self._run_validated_request(req)
|
||||
```
|
||||
|
||||
The snippet shows the model-owned transition for exclusive execution. Put
|
||||
disable/restore handling in the runtime/runner lifecycle that owns success,
|
||||
exception, disconnect, and asynchronous abort; a `try/finally` inside
|
||||
`forward()` alone cannot observe every server cancellation boundary.
|
||||
|
||||
The `installation_key` must include every policy dimension that changes hook
|
||||
installation; changes in step count can use runtime refresh. `None` disables
|
||||
installed hooks. Do not mutate the global startup cache config for a request.
|
||||
|
||||
Centralize cleanup in the owning request/runner boundary. An exception, abort,
|
||||
disconnect, or admission failure must not leave a changed hook policy visible
|
||||
to the next request. The exact owner depends on the target revision; add a
|
||||
failure-injection test rather than relying on `forward()` alone to observe
|
||||
asynchronous abort.
|
||||
|
||||
Example model-specific request form after calibration:
|
||||
|
||||
```bash
|
||||
curl --fail-with-body -sS -X POST http://127.0.0.1:8091/v1/videos/sync \
|
||||
-F 'model=<model>' \
|
||||
-F 'prompt=<prompt>' \
|
||||
-F 'quality=lossless' \
|
||||
-F 'num_inference_steps=<validated-steps>' \
|
||||
-o lossless.mp4
|
||||
|
||||
curl --fail-with-body -sS -X POST http://127.0.0.1:8091/v1/videos/sync \
|
||||
-F 'model=<model>' \
|
||||
-F 'prompt=<prompt>' \
|
||||
-F 'quality=high' \
|
||||
-F 'num_inference_steps=<validated-steps>' \
|
||||
-o high.mp4
|
||||
```
|
||||
|
||||
Do not expose these tier names merely because another model uses them.
|
||||
|
||||
### Cache-DiT evidence
|
||||
|
||||
For every tier/task/shape/schedule:
|
||||
|
||||
- show real cache hits/skipped blocks, not only “backend enabled” logs;
|
||||
- measure fixed-work speed and quality against lossless/native;
|
||||
- publish a speed/quality frontier and chosen tier mapping;
|
||||
- alternate lossless/high/lossless and vary steps to prove restore/refresh;
|
||||
- interleave tasks and shapes; run concurrent requests only when scheduling
|
||||
guarantees exclusive compatible hook state;
|
||||
- inject validation error, denoise error, disconnect, and abort, then prove the
|
||||
next request starts with its requested policy;
|
||||
- bound any cached tensors and clear request-local state.
|
||||
|
||||
## Combination gates
|
||||
|
||||
Start every combination as `not tested`.
|
||||
|
||||
| Combination | Default production posture |
|
||||
|---|---|
|
||||
| Direct checkpoint mmap + TP>1/HSDP/online quant | Preflight falls back to the ordinary loader; do not claim direct-mmap savings |
|
||||
| Per-tensor online FP8 + DLO AllGather | Generic compatibility exists for finalized weights/scales; each model/card/topology remains a candidate until E2E, and other online quantizers fail closed |
|
||||
| Online FP8 + DLO no-AllGather | Candidate only after model/card/topology E2E |
|
||||
| TP>1 + DLO AllGather | Ordinary TP-aware loader only; bounded DP2xTP2 smoke exists, but each new model/card/topology still needs E2E |
|
||||
| TP>1 + DLO no-AllGather | Ordinary TP-aware loader only; candidate with limited generic coverage |
|
||||
| HSDP + DLO AllGather | Reject; it would double-shard HSDP-managed parameters |
|
||||
| HSDP + DLO no-AllGather | Configuration-compatible candidate with limited E2E coverage |
|
||||
| Cache-DiT + online FP8 | Separate trajectory, hit-rate, HBM, latency test |
|
||||
| Cache-DiT + DLO | Verify hooks, streamed blocks, memory peaks, abort cleanup |
|
||||
| Cache-DiT + step execution | Do not advertise until lifecycle/batching support is explicit |
|
||||
| Sparse attention + Ring | Verify backend execution; reject silently ignored sparse settings |
|
||||
| Load-time fused adapter + offload/HWR | Reject unless the selected loader path applies and validates the same fusion before sharding/storage |
|
||||
| Packed attention + any topology | Unequal-sample boundary parity is mandatory |
|
||||
| Device-side uint8 preparation + offline caller | Version the dtype/range/layout change; HTTP MP4 parity does not preserve the raw tensor contract |
|
||||
| Device-side preparation + remote codec | Validate route selection, strided/contiguous layouts, payload bytes, ordering, fallback, and encoded-media parity |
|
||||
|
||||
A startup-only test never upgrades a combination. Require request output,
|
||||
parity/quality, actual fast-path evidence, resource cleanup, and a benchmark on
|
||||
the named hardware.
|
||||
@@ -0,0 +1,481 @@
|
||||
# Performance Patterns and Code Examples
|
||||
|
||||
## Contents
|
||||
|
||||
1. [Measurement loop](#measurement-loop)
|
||||
2. [Faster BF16 attention](#faster-bf16-attention)
|
||||
3. [Sparse attention](#sparse-attention)
|
||||
4. [Fusion-first operator patterns](#fusion-first-operator-patterns)
|
||||
5. [Remove redundant computation](#remove-redundant-computation)
|
||||
6. [USP communication optimization](#usp-communication-optimization)
|
||||
7. [Text encoder and VAE disaggregation](#text-encoder-and-vae-disaggregation)
|
||||
8. [Performance acceptance](#performance-acceptance)
|
||||
|
||||
## Measurement loop
|
||||
|
||||
Do not start with a list of optimizations. Start with a fixed correctness and
|
||||
performance workload:
|
||||
|
||||
```text
|
||||
model/checkpoint revision + prompt/media hashes + seed + dimensions/frames +
|
||||
scheduler/sigma/steps/guidance + task + dtype + backend + topology + hardware
|
||||
```
|
||||
|
||||
Capture:
|
||||
|
||||
- stage time: load, encode, conditioning, denoise, VAE decode/postprocess;
|
||||
- GPU trace: kernel duration/count, launch gaps, allocations, H2D, collectives;
|
||||
- CPU trace: preprocessing, per-layer sync, Python dispatch, temporary files;
|
||||
- memory: per-rank HBM peaks and process-tree host PSS;
|
||||
- output: device preparation, D2H/IPC, payload bytes, codec wall/process CPU,
|
||||
event-loop wait, client materialization, and signed residual;
|
||||
- serving: queue time, throughput, errors, request mix, and distribution samples.
|
||||
|
||||
For a fixed-work single-request A/B, run one explicit warmup policy and at
|
||||
least three measured repetitions, keep every raw run, and report median or mean
|
||||
with range. Do not derive p95/p99 from three samples. For tail latency, declare
|
||||
the arrival model and collect enough successful requests for the percentile and
|
||||
confidence claimed. Implement one optimization at a time, re-run parity, and
|
||||
report both local kernel and end-to-end deltas. A kernel win that moves no E2E
|
||||
metric is not a production speed claim.
|
||||
|
||||
Use the pipeline profiler already exposed by the target revision when
|
||||
available, and use PyTorch/CUDA/ROCm/NPU/XPU profiling tools appropriate to the
|
||||
platform. Do not force every platform through a CUDA-only workflow.
|
||||
|
||||
## Faster BF16 attention
|
||||
|
||||
### Shared attention wiring
|
||||
|
||||
Use the shared role-aware attention layer and give it the true local head
|
||||
counts and QKV layout:
|
||||
|
||||
```python
|
||||
import torch
|
||||
|
||||
from vllm.model_executor.layers.linear import (
|
||||
QKVParallelLinear,
|
||||
RowParallelLinear,
|
||||
)
|
||||
from vllm_omni.diffusion.attention.layer import Attention
|
||||
|
||||
class SelfAttention(torch.nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
hidden_size,
|
||||
head_dim,
|
||||
num_heads,
|
||||
num_kv_heads,
|
||||
quant_config=None,
|
||||
prefix="",
|
||||
):
|
||||
super().__init__()
|
||||
self.head_dim = head_dim
|
||||
self.qkv_proj = QKVParallelLinear(
|
||||
hidden_size=hidden_size,
|
||||
head_size=head_dim,
|
||||
total_num_heads=num_heads,
|
||||
total_num_kv_heads=num_kv_heads,
|
||||
bias=False,
|
||||
quant_config=quant_config,
|
||||
prefix=f"{prefix}.qkv_proj",
|
||||
)
|
||||
self.attention = Attention(
|
||||
num_heads=self.qkv_proj.num_heads,
|
||||
num_kv_heads=self.qkv_proj.num_kv_heads,
|
||||
head_size=head_dim,
|
||||
softmax_scale=head_dim**-0.5,
|
||||
causal=False,
|
||||
qkv_layout="BSND",
|
||||
role="self",
|
||||
prefix=prefix,
|
||||
)
|
||||
self.out_proj = RowParallelLinear(
|
||||
num_heads * head_dim,
|
||||
hidden_size,
|
||||
bias=False,
|
||||
input_is_parallel=True,
|
||||
quant_config=quant_config,
|
||||
prefix=f"{prefix}.out_proj",
|
||||
)
|
||||
|
||||
def forward(self, x, attn_metadata=None):
|
||||
qkv, _ = self.qkv_proj(x)
|
||||
q_size = self.qkv_proj.num_heads * self.head_dim
|
||||
kv_size = self.qkv_proj.num_kv_heads * self.head_dim
|
||||
q, k, v = qkv.split([q_size, kv_size, kv_size], dim=-1)
|
||||
q = q.unflatten(-1, (self.qkv_proj.num_heads, self.head_dim))
|
||||
k = k.unflatten(-1, (self.qkv_proj.num_kv_heads, self.head_dim))
|
||||
v = v.unflatten(-1, (self.qkv_proj.num_kv_heads, self.head_dim))
|
||||
out = self.attention(q, k, v, attn_metadata)
|
||||
out = out.flatten(-2)
|
||||
out, _ = self.out_proj(out)
|
||||
return out
|
||||
```
|
||||
|
||||
Add Q/K norm and RoPE in the official order before the `Attention` call. Keep
|
||||
checkpoint mapping for the fused QKV and output projection complete.
|
||||
|
||||
### Backend qualification
|
||||
|
||||
For each role (`self`, cross-attention category, token refiner, etc.), record:
|
||||
|
||||
- actual selected backend from logs/trace;
|
||||
- supported dtype, head size, QKV layout, mask/metadata, packed varlen behavior;
|
||||
- platform and compute capability;
|
||||
- TP/SP/Ring/AllGather-KV restrictions;
|
||||
- fallback behavior and whether fallback changes the claimed performance.
|
||||
|
||||
Use unequal packed samples and compare with dense SDPA. Verify `cu_seqlens`,
|
||||
max sequence length, padding exclusion, modality boundaries, CFG ownership, and
|
||||
output slicing. Do not pass an `attn_mask` and assume it is consumed. Inspect
|
||||
the backend contract and trace.
|
||||
|
||||
TRTLLM, Flash Attention, cuDNN, NPU varlen, and other backends are independent
|
||||
qualification rows. The shared `Attention` class does not make every backend
|
||||
packed-safe or faster automatically. Ring and hybrid paths require their own
|
||||
boundary parity. AllGather-KV and specific backend combinations may fail fast;
|
||||
keep that behavior visible in docs.
|
||||
|
||||
## Sparse attention
|
||||
|
||||
Sparse attention is a backend/model/platform optimization, not a synonym for
|
||||
low-precision dense attention. Enable it only after dense BF16 parity.
|
||||
|
||||
For every candidate task/shape/topology:
|
||||
|
||||
1. State the sparsity policy, selected backend/kernel, block/head dimensions,
|
||||
supported hardware, external kernel/package revision, and fallback.
|
||||
2. Prove the sparse kernel executed and report realized sparsity; an enabled
|
||||
flag is insufficient.
|
||||
3. Compare same-seed per-block outputs, denoise trajectory, and final artifact
|
||||
with dense attention.
|
||||
4. Add modality-specific metrics: temporal consistency and audio/video sync for
|
||||
video, spectral/audio quality for audio, and prompt/image metrics as relevant.
|
||||
5. Report attention time and full E2E time. Include preprocessing/index-build
|
||||
overhead, peak memory, and the exact dense control.
|
||||
6. Validate each task and shape. Do not extrapolate a T2V result to I2V/Ref2V,
|
||||
or NPU sparse support to CUDA/ROCm/XPU.
|
||||
|
||||
If Ring bypasses the selected backend or a sparse setting would be silently
|
||||
ignored, reject the combination. Prefer a visible dense fallback only when the
|
||||
recipe labels the row `limited` and does not claim sparse speed.
|
||||
|
||||
## Fusion-first operator patterns
|
||||
|
||||
Prefer current shared/custom operators with explicit guards and native
|
||||
fallbacks. Prove parity at operator, block, trajectory, and artifact levels,
|
||||
then use a trace to prove fewer launches/bytes or lower latency.
|
||||
|
||||
### Q/K RMSNorm + RoPE
|
||||
|
||||
```python
|
||||
from vllm_omni.diffusion.layers.norm import RMSNorm
|
||||
from vllm_omni.diffusion.layers.rope import RotaryEmbedding, apply_rope_to_qk
|
||||
|
||||
self.norm_q = RMSNorm(head_dim, eps=eps)
|
||||
self.norm_k = RMSNorm(head_dim, eps=eps)
|
||||
# Example only: set these booleans from the official checkpoint contract.
|
||||
use_neox_layout = False
|
||||
cos_sin_are_half_dim = True
|
||||
self.rope = RotaryEmbedding(
|
||||
is_neox_style=use_neox_layout,
|
||||
half_head_dim=cos_sin_are_half_dim,
|
||||
)
|
||||
|
||||
q = self.norm_q(q)
|
||||
k = self.norm_k(k)
|
||||
q, k = apply_rope_to_qk(self.rope, q, k, (cos, sin))
|
||||
```
|
||||
|
||||
Replace the illustrative booleans with the official layout in real code.
|
||||
Preserve partial rotary dimensions by splitting the rotated prefix and concatenating
|
||||
the pass-through suffix. If 3D cos/sin includes a batch axis, the current
|
||||
shared path assumes the values are shared across the batch; per-sample
|
||||
different positions require a proven representation/path.
|
||||
|
||||
For packed `[tokens, heads, head_dim]` tensors with a non-interleaved
|
||||
`[cos | sin]` table, the current shared fused boundary is:
|
||||
|
||||
```python
|
||||
from vllm_omni.diffusion.layers.fused_qk_norm_rope import fused_qk_norm_rope
|
||||
|
||||
q, k = fused_qk_norm_rope(
|
||||
q,
|
||||
k,
|
||||
self.norm_q.weight,
|
||||
self.norm_k.weight,
|
||||
rope_table,
|
||||
self.norm_q.variance_epsilon,
|
||||
)
|
||||
```
|
||||
|
||||
Its CUDA fast path currently specializes BF16 `head_dim=128` and
|
||||
`rotary_dim=96`; other valid inputs take the eager implementation. The separate
|
||||
shared RMSNorm and RotaryEmbedding still provide the general cross-platform
|
||||
baseline. Treat NPU/MUSA and other vendor fusions as independent qualification
|
||||
rows, and do not generalize a model-specific Triton kernel without compatible
|
||||
shape, layout, frequency partition, dtype, and platform evidence.
|
||||
|
||||
### Fused QKV projection
|
||||
|
||||
Use `QKVParallelLinear` as shown above. Qualification includes:
|
||||
|
||||
- `num_heads` and `num_kv_heads` divisibility by TP;
|
||||
- local split sizes from the layer, never reusing global counts after TP;
|
||||
- Q/K/V checkpoint order conversion before the vLLM loader;
|
||||
- all Q, K, and V shards present;
|
||||
- quantization scale/loader mapping when FP8 or another method is enabled.
|
||||
|
||||
### Gate-up projection + fused SwiGLU
|
||||
|
||||
```python
|
||||
from vllm.model_executor.layers.activation import SiluAndMul
|
||||
from vllm.model_executor.layers.linear import (
|
||||
MergedColumnParallelLinear,
|
||||
RowParallelLinear,
|
||||
)
|
||||
|
||||
self.gate_up_proj = MergedColumnParallelLinear(
|
||||
input_size=hidden_size,
|
||||
output_sizes=[intermediate_size, intermediate_size],
|
||||
bias=False,
|
||||
quant_config=quant_config,
|
||||
prefix=f"{prefix}.gate_up_proj",
|
||||
)
|
||||
self.act_fn = SiluAndMul()
|
||||
self.down_proj = RowParallelLinear(
|
||||
input_size=intermediate_size,
|
||||
output_size=hidden_size,
|
||||
bias=False,
|
||||
input_is_parallel=True,
|
||||
quant_config=quant_config,
|
||||
prefix=f"{prefix}.down_proj",
|
||||
)
|
||||
|
||||
gate_up, _ = self.gate_up_proj(x)
|
||||
x = self.act_fn(gate_up)
|
||||
x, _ = self.down_proj(x)
|
||||
```
|
||||
|
||||
Map both checkpoint `gate_proj` and `up_proj` shards into the merged parameter
|
||||
and require both. A merged projection followed by `F.silu(gate) * up` is not
|
||||
the same claim as the `SiluAndMul` fused activation kernel; identify what the
|
||||
trace actually uses.
|
||||
|
||||
### AdaLN
|
||||
|
||||
Use a shared layer only when its mathematical/return contract matches the
|
||||
official model:
|
||||
|
||||
```python
|
||||
from vllm_omni.diffusion.layers.adalayernorm import AdaLayerNorm
|
||||
|
||||
self.adaln = AdaLayerNorm(
|
||||
hidden_size,
|
||||
elementwise_affine=False,
|
||||
eps=1e-6,
|
||||
)
|
||||
x = self.adaln(x, scale[:, None, :], shift[:, None, :])
|
||||
```
|
||||
|
||||
`AdaLayerNormZero`, `AdaLayerNormZeroSingle`, and continuous variants include
|
||||
different projections, broadcasts, and tuple outputs; they are not drop-in
|
||||
replacements. A shared AdaLN may have an NPU fast path while using native ops on
|
||||
CUDA/HIP/XPU. Do not call it a universal fused AdaLN kernel.
|
||||
|
||||
For sigma schedules with a small finite set of timesteps, profile whether
|
||||
precomputing the timestep/AdaLN projection once per request is worthwhile.
|
||||
Cache keys must include the exact schedule/config/device/dtype and stay bounded.
|
||||
|
||||
### Layout, residual, and other candidates
|
||||
|
||||
After profiling, inspect:
|
||||
|
||||
- bias/dropout/add or gated residual chains;
|
||||
- norm + linear or norm + modulation boundaries;
|
||||
- repeated transpose/reshape/contiguous around attention;
|
||||
- VAE normalization/activation/convolution boundaries;
|
||||
- MoE routing and fused experts for models that actually use MoE.
|
||||
|
||||
For fused residual-plus-norm or modulation chains, preserve the unfused
|
||||
materialization boundary. If the residual was stored in BF16 before RMSNorm,
|
||||
round to BF16 and promote again inside the fused kernel; using the unrounded
|
||||
FP32 temporary changes the denoising trajectory even when the stored residual
|
||||
looks correct.
|
||||
|
||||
Keep DLO materialize/release hooks, process-group collectives, request cache
|
||||
transitions, and cleanup outside compiled/fused regions unless lifecycle safety
|
||||
is explicitly tested. Compile only stable regions and retain eager fallback.
|
||||
|
||||
## Remove redundant computation
|
||||
|
||||
### Hoist request invariants
|
||||
|
||||
Audit the denoise loop for work that depends only on the request rather than
|
||||
the current timestep:
|
||||
|
||||
```python
|
||||
# Before the denoise loop.
|
||||
condition = self.condition_proj(text_embeddings)
|
||||
refined_condition = self.token_refiner(condition, packed_metadata)
|
||||
rope_tables = self.build_rope_tables(position_ids)
|
||||
attn_metadata = self.build_attention_metadata(layout)
|
||||
|
||||
for step, sigma in enumerate(sigmas):
|
||||
timestep_state = self.prepare_timestep_state(sigma)
|
||||
noise = self.transformer(
|
||||
latents,
|
||||
refined_condition,
|
||||
rope_tables=rope_tables,
|
||||
attn_metadata=attn_metadata,
|
||||
timestep_state=timestep_state,
|
||||
)
|
||||
latents = self.scheduler_step(noise, sigma, latents)
|
||||
```
|
||||
|
||||
Candidates include prompt conditioning, token-refiner output, reference
|
||||
encoding/scans, position IDs/RoPE tables, packed masks/metadata, static CFG
|
||||
branches, and finite-schedule AdaLN projections. First prove they are invariant
|
||||
for all official tasks, LoRA/adapters, prompt-update features, shapes, and
|
||||
schedules. In continuous batching, store request-specific results on request
|
||||
state rather than the pipeline singleton.
|
||||
|
||||
### Eliminate per-layer synchronization
|
||||
|
||||
Avoid device-to-host scalar extraction such as `.item()` inside every layer:
|
||||
|
||||
```python
|
||||
# Bad: forces a GPU/CPU synchronization in every attention layer.
|
||||
max_len = int(cu_seqlens[-1].item())
|
||||
|
||||
# Better: compute validated host metadata once during request packing and pass it.
|
||||
packed = PackedLayout(
|
||||
cu_seqlens=cu_seqlens,
|
||||
max_seqlen=max(sample_lengths),
|
||||
)
|
||||
for block in self.blocks:
|
||||
hidden = block(hidden, packed_layout=packed)
|
||||
```
|
||||
|
||||
Trace to verify the synchronization disappears. Do not replace a correct
|
||||
dynamic value with a stale constant merely to remove `.item()`.
|
||||
|
||||
### Remove unused masks and allocations
|
||||
|
||||
If packed attention consumes `cu_seqlens` and ignores a dense mask, do not
|
||||
construct the dense mask. Reuse immutable request metadata and preallocated
|
||||
buffers when safe. Prefer direct indexed scatter/gather over constructing large
|
||||
temporary tensors only when duplicate-index semantics and gradients (normally
|
||||
disabled for inference) remain correct.
|
||||
|
||||
Any cross-request cache needs a bounded lifetime and a complete key containing
|
||||
shape, layout, device, dtype, schedule, task, adapter/LoRA, and any value that
|
||||
changes the tensor. Otherwise keep it request-scoped.
|
||||
|
||||
### VAE and reference work
|
||||
|
||||
Profile duplicate VAE decode/postprocess, repeated reference decoding/scans,
|
||||
unnecessary host-device round trips, and full-frame materialization. Apply VAE
|
||||
tiling, slicing, patch parallelism, or on-demand staging only with seam,
|
||||
ordering, color/range, and memory parity tests.
|
||||
|
||||
## USP communication optimization
|
||||
|
||||
“USP works” is not evidence that USP communication is optimized. For Ulysses,
|
||||
Ring, hybrid, and AllGather-KV separately, profile:
|
||||
|
||||
- collective count, bytes, duration, stream, and overlap with compute;
|
||||
- Q/K/V and output all-to-all boundaries;
|
||||
- pack/unpack, padding, metadata broadcast, transpose, and contiguous copies;
|
||||
- head divisibility or advanced uneven-head support;
|
||||
- scaling efficiency from 1 to N devices on the same workload/interconnect;
|
||||
- load balance for unequal packed samples.
|
||||
|
||||
Declare `_sp_plan` split/gather boundaries around meaningful module outputs,
|
||||
not arbitrary Python statements. Keep packed metadata sharded/gathered in the
|
||||
same ownership model as tokens.
|
||||
|
||||
When the target revision offers accelerated Ulysses transport such as
|
||||
`--ulysses-a2a-permute`, qualify it separately from regular Ulysses. Prove the
|
||||
strict scatter-heads/gather-sequence layout selected the fast path, record
|
||||
one-time JIT/readiness cost, prewarm the maximum workspace shape before CUDA
|
||||
graph capture, keep the grow-only workspace on one stream, and exercise cleanup
|
||||
before process-group destruction. Compare collective and E2E deltas after the
|
||||
dense backend and topology are frozen; keep unsupported layouts on an explicit
|
||||
regular-Ulysses fallback.
|
||||
|
||||
For GQA, validate `Q_heads % KV_heads == 0`, pad KV heads first, and derive Q
|
||||
padding from the original ratio. Exercise both contiguous and row-strided QKV
|
||||
layouts; a fast copy path must preserve bitwise values and packed boundaries.
|
||||
|
||||
Cross-attention sometimes uses replicated text K/V while only video/audio Q is
|
||||
sequence-sharded. In that specific case, `skip_sequence_parallel=True` can
|
||||
avoid incorrect or wasteful Q/K/V redistribution:
|
||||
|
||||
```python
|
||||
self.cross_attention = Attention(
|
||||
num_heads=local_heads,
|
||||
num_kv_heads=local_kv_heads,
|
||||
head_size=head_dim,
|
||||
softmax_scale=head_dim**-0.5,
|
||||
causal=False,
|
||||
role="cross",
|
||||
skip_sequence_parallel=True,
|
||||
prefix=f"{prefix}.cross_attn",
|
||||
)
|
||||
```
|
||||
|
||||
Use it only after proving K/V are replicated and Q/output ownership is correct.
|
||||
Otherwise it disables needed communication. Report whether the optimization
|
||||
reduces collective bytes/time and improves E2E scaling.
|
||||
|
||||
## Text encoder and VAE disaggregation
|
||||
|
||||
Disaggregation is a measured architecture decision. First collect:
|
||||
|
||||
| Question | Required measurement |
|
||||
|---|---|
|
||||
| Is the stage large enough? | Encode/decode share at target concurrency and shape |
|
||||
| Is transfer economical? | Tensor/media bytes, serialization, connector latency and bandwidth |
|
||||
| Is there reuse? | Prompt/reference reuse rate, cacheability, fan-out |
|
||||
| Can it scale independently? | Queue saturation and resource utilization per stage |
|
||||
| Is the contract stable? | Shape, dtype, mask/tag/position metadata, ordering |
|
||||
|
||||
### Text encoder candidate
|
||||
|
||||
Define an explicit tensor contract containing hidden states, masks, tags or
|
||||
position IDs, dtype, shape, task/checkpoint revision, and request ID. Support
|
||||
disabling the local encoder so the DiT cannot accidentally recompute it. Test
|
||||
all tasks, prompt lengths, partial TP group membership, retries, backpressure,
|
||||
abort, connector cleanup, and independent replica scaling.
|
||||
|
||||
### VAE candidate
|
||||
|
||||
Separate VAE encode and decode contracts if both exist. Define latent shape,
|
||||
scale/shift, dtype/range, tiling/chunk order, output media ordering, and stage
|
||||
metadata. Measure latent/media transfer against local decode cost. Test tiling
|
||||
seams, RNG/determinism where relevant, chunk ordering, codec/postprocess,
|
||||
backpressure, abort, and worker failure.
|
||||
|
||||
Do not label VAE tiling, patch parallelism, compile, or offload as VAE
|
||||
disaggregation. Likewise, a separate text-encoder TP group is not a separate
|
||||
stage. If no reusable generic stage/connector exists in the target revision,
|
||||
produce a feasibility result or RFC and keep the capability `not tested` rather
|
||||
than inventing a model-local production architecture.
|
||||
|
||||
## Performance acceptance
|
||||
|
||||
For each accepted optimization, require:
|
||||
|
||||
- same fixed workload and correctness artifacts before/after;
|
||||
- actual fast-path execution proven by logs/trace/counters;
|
||||
- local metric plus E2E latency/throughput and memory;
|
||||
- at least three raw fixed-work repetitions after explicit warmup, or a
|
||||
declared arrival-load sample set large enough for every tail percentile;
|
||||
- named hardware, backend, topology, dtype, and task scope;
|
||||
- fallback/rejection tests for unsupported shapes/platforms;
|
||||
- no regression in abort/error cleanup or long-run memory trend.
|
||||
|
||||
Update the feature matrix narrowly. Do not convert a profile-driven direction
|
||||
into a generic “supported” feature merely because a model-specific PR exists.
|
||||
@@ -0,0 +1,464 @@
|
||||
# Production Serving and Validation
|
||||
|
||||
## Contents
|
||||
|
||||
1. [Execution-mode decision](#execution-mode-decision)
|
||||
2. [Request-level batching](#request-level-batching)
|
||||
3. [Step execution and continuous batching](#step-execution-and-continuous-batching)
|
||||
4. [Abort and cleanup](#abort-and-cleanup)
|
||||
5. [Mixed-RPS soak](#mixed-rps-soak)
|
||||
6. [Four CI tracks](#four-ci-tracks)
|
||||
7. [PR evidence checklist](#pr-evidence-checklist)
|
||||
|
||||
## Execution-mode decision
|
||||
|
||||
Request batching and step-wise execution solve different problems:
|
||||
|
||||
| Mode | Pipeline contract | Scheduling/abort boundary |
|
||||
|---|---|---|
|
||||
| Serial request | `forward(DiffusionRequestBatch)` for one request | Whole request; in-denoise preemption is unavailable |
|
||||
| Request batching | `supports_request_batch = True`; one forward consumes compatible independent requests | Whole batch/request |
|
||||
| Single-request step | Full `SupportsStepExecution`; `max_num_seqs=1` | Between denoise steps |
|
||||
| Step continuous batching | Full step contract plus heterogeneous batched-step evidence | Between denoise waves/steps |
|
||||
|
||||
Do not expose four stub methods as an opt-out. The current protocol is
|
||||
structural; if a native pipeline is not step-capable, do not define the four
|
||||
step methods. If it is capable, implement and test all four.
|
||||
|
||||
Treat structural support, a generic request-batch bridge, and model benefit as
|
||||
three separate claims. Before recommending step execution, name a hypothesis
|
||||
that request mode cannot satisfy, define a success threshold, and run a matched
|
||||
request-mode control. If throughput or latency regresses, retain step execution
|
||||
as functional/limited and keep request mode as the production recommendation.
|
||||
|
||||
Choose compatibility keys from every value that changes tensor shapes,
|
||||
semantics, hooks, or scheduler behavior: task, output shape/count, frames,
|
||||
steps/schedule, guidance/CFG, dtype/backend, LoRA/adapter, cache quality policy,
|
||||
and model-specific conditions. Reject or schedule separately rather than
|
||||
silently using the first request's settings.
|
||||
|
||||
## Request-level batching
|
||||
|
||||
A request-batched pipeline returns exactly one `DiffusionOutput` per logical
|
||||
request. This is model-owned pseudocode: `validate_batch_compatibility()` and
|
||||
`_generate_batch()` are placeholders to implement from the official contract,
|
||||
not shared APIs to import.
|
||||
|
||||
```python
|
||||
import torch
|
||||
|
||||
from vllm_omni.diffusion.data import DiffusionOutput
|
||||
from vllm_omni.diffusion.worker.request_batch import (
|
||||
DiffusionRequestBatch,
|
||||
split_diffusion_output_by_request,
|
||||
)
|
||||
|
||||
class MyPipeline(torch.nn.Module):
|
||||
supports_request_batch = True
|
||||
|
||||
def forward(self, req: DiffusionRequestBatch) -> list[DiffusionOutput]:
|
||||
validate_batch_compatibility(req.sampling_params_list, req.prompts)
|
||||
common = req.sampling_params_list[0]
|
||||
|
||||
text_prompts: list[str] | None = [
|
||||
prompt
|
||||
if isinstance(prompt, str)
|
||||
else (prompt.get("prompt") or "")
|
||||
for prompt in req.prompts
|
||||
]
|
||||
|
||||
prompt_fields = DiffusionRequestBatch.collate_prompt_field_map(
|
||||
req.prompts,
|
||||
{"prompt_embeds": None, "prompt_mask": None},
|
||||
)
|
||||
if prompt_fields["prompt_embeds"] is not None:
|
||||
text_prompts = None
|
||||
generators = req.collate_request_generators(
|
||||
common.num_outputs_per_prompt,
|
||||
default_generator=None,
|
||||
)
|
||||
output = self._generate_batch(
|
||||
prompt=text_prompts,
|
||||
prompt_fields=prompt_fields,
|
||||
generators=generators,
|
||||
sampling=common,
|
||||
)
|
||||
return split_diffusion_output_by_request(
|
||||
DiffusionOutput(output=output),
|
||||
req,
|
||||
num_outputs_per_prompt=common.num_outputs_per_prompt,
|
||||
)
|
||||
```
|
||||
|
||||
The prompt-field collator only handles tensor fields; extract raw text prompts
|
||||
and model-specific negative/alias fields separately, as current in-tree batched
|
||||
pipelines do. Set a raw prompt field to `None` when the matching precomputed
|
||||
embedding is supplied. The exact aliases and generator device depend on the
|
||||
model. Use current collators rather than manual `torch.cat` when they cover the
|
||||
contract; they reject mixed present/missing tensors and incompatible
|
||||
shape/dtype/device.
|
||||
|
||||
Before reading `sampling_params_list[0]`, validate every field that must be
|
||||
common. Tests must cover:
|
||||
|
||||
- two and N requests, independent seeds/generators, one output each;
|
||||
- mixed prompt lengths and packed/padded boundaries;
|
||||
- multiple outputs per prompt and correct result slicing;
|
||||
- compatible versus incompatible shapes, steps, guidance, LoRA, and quality;
|
||||
- one request failure without cross-request output or state corruption;
|
||||
- output ordering and request IDs under different completion times.
|
||||
|
||||
After the model advertises support, candidate serving is:
|
||||
|
||||
```bash
|
||||
vllm serve '<model>' --omni \
|
||||
--max-num-seqs 4 \
|
||||
--request-batch-max-wait-ms 20
|
||||
```
|
||||
|
||||
The wait window trades admission latency for batch formation. Benchmark at 0
|
||||
and candidate nonzero values under the target arrival process.
|
||||
|
||||
## Step execution and continuous batching
|
||||
|
||||
### Full protocol skeleton
|
||||
|
||||
Use the current shared state types and make every mutable value request-local:
|
||||
|
||||
The helpers beginning with `_` and `validate_step_batch()` below are
|
||||
model-owned placeholders; scheduler initialization is scheduler-specific.
|
||||
|
||||
```python
|
||||
import copy
|
||||
from collections.abc import Sequence
|
||||
from typing import Any, ClassVar
|
||||
|
||||
import torch
|
||||
|
||||
from vllm_omni.diffusion.data import DiffusionOutput
|
||||
from vllm_omni.diffusion.models.interface import SupportsStepExecution
|
||||
from vllm_omni.diffusion.worker.input_batch import InputBatch
|
||||
from vllm_omni.diffusion.worker.utils import StepRequestState
|
||||
|
||||
class MyPipeline(torch.nn.Module, SupportsStepExecution):
|
||||
supports_step_execution: ClassVar[bool] = True
|
||||
|
||||
def prepare_encode(
|
||||
self,
|
||||
state: StepRequestState,
|
||||
**kwargs: Any,
|
||||
) -> StepRequestState:
|
||||
prepared = self._prepare_request(state.prompt, state.sampling)
|
||||
state.prompt_embeds = prepared.prompt_embeds
|
||||
state.latents = prepared.latents
|
||||
state.timesteps = prepared.timesteps
|
||||
state.scheduler = copy.deepcopy(self.scheduler)
|
||||
state.scheduler.set_begin_index(0)
|
||||
state.step_index = 0
|
||||
state.extra["task_state"] = prepared.task_state
|
||||
return state
|
||||
|
||||
def denoise_step(
|
||||
self,
|
||||
input_batch: InputBatch,
|
||||
*,
|
||||
states: Sequence[StepRequestState] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> torch.Tensor | None:
|
||||
validate_step_batch(input_batch, states)
|
||||
return self.transformer(**build_batched_denoise_inputs(input_batch))
|
||||
|
||||
def step_scheduler(
|
||||
self,
|
||||
state: StepRequestState,
|
||||
noise_pred: torch.Tensor,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
timestep = state.current_timestep
|
||||
if timestep is None:
|
||||
raise RuntimeError("step_scheduler called without a current timestep")
|
||||
state.latents = state.scheduler.step(
|
||||
noise_pred,
|
||||
timestep,
|
||||
state.latents,
|
||||
return_dict=False,
|
||||
)[0].to(state.latents.dtype)
|
||||
state.step_index += 1
|
||||
|
||||
def post_decode(
|
||||
self,
|
||||
state: StepRequestState,
|
||||
**kwargs: Any,
|
||||
) -> DiffusionOutput:
|
||||
return DiffusionOutput(output=self.decode(state.latents))
|
||||
```
|
||||
|
||||
This is a shape guide, not drop-in code: verify the target revision's
|
||||
`StepRequestState.extra`, scheduler signature, CFG handling, state fields, and
|
||||
decode contract. Follow a current native pipeline such as Qwen-Image for the
|
||||
exact revision.
|
||||
|
||||
Never use the shared `self.scheduler` to advance a request. Store scheduler,
|
||||
latents, embeddings, timesteps, masks, RNG/generator, task metadata, cache
|
||||
policy, adapter, and output assembly state on `StepRequestState` or its
|
||||
request-local extension. Pipeline fields may hold immutable weights/config and
|
||||
bounded immutable caches only.
|
||||
|
||||
### Qualification sequence
|
||||
|
||||
1. Compare request-mode and step-mode at `max_num_seqs=1` for every task using
|
||||
identical seed/schedule/shape and intermediate latents.
|
||||
2. Abort queued and in-flight requests at several step boundaries.
|
||||
3. Increase to two compatible requests; verify independent scheduler/RNG/state
|
||||
and output order.
|
||||
4. Increase to the target capacity and mix prompt lengths.
|
||||
5. Exercise mixed shape, frames, steps/schedule, task, guidance, and quality.
|
||||
Batch only compatible rows; otherwise split/reject predictably.
|
||||
6. Inject a failure in encode, denoise, scheduler, and decode independently.
|
||||
7. Run the production soak and fault matrix.
|
||||
|
||||
Candidate commands only after capability validation:
|
||||
|
||||
```bash
|
||||
# Conservative step execution and step-boundary abort.
|
||||
vllm serve '<model>' --omni \
|
||||
--step-execution \
|
||||
--max-num-seqs 1
|
||||
|
||||
# Experimental batched-step candidate after multi-request E2E.
|
||||
vllm serve '<model>' --omni \
|
||||
--step-execution \
|
||||
--max-num-seqs 4
|
||||
```
|
||||
|
||||
If an attention backend is only correct for single-request step mode, fail
|
||||
early or keep the recipe at `max_num_seqs=1`. Do not describe generic runtime
|
||||
support as model support.
|
||||
|
||||
## Abort and cleanup
|
||||
|
||||
### Required semantics
|
||||
|
||||
For queued and in-flight abort/disconnect:
|
||||
|
||||
- emit exactly one terminal aborted/error result;
|
||||
- do not run `post_decode` after abort unless a documented partial-output mode
|
||||
explicitly requires it;
|
||||
- do not increment `step_index` or apply a scheduler result after cancellation;
|
||||
- remove scheduler/state cache entries and request IDs;
|
||||
- release latents, prompt/reference tensors, cache hooks, DLO buffers, connector
|
||||
messages, uploaded/temp files, and output buffers;
|
||||
- keep other requests alive;
|
||||
- make cleanup idempotent when disconnect, timeout, and worker error race;
|
||||
- drop late results/exceptions for already-resolved or cancelled futures
|
||||
without terminating the shared result-pump thread;
|
||||
- complete the next valid request with its requested cache/offload policy.
|
||||
|
||||
Test cancellation before engine admission, while queued, after encode, during
|
||||
early/middle/final denoise steps, before decode, and while returning/persisting
|
||||
output. Include async job cancellation and client socket disconnect.
|
||||
|
||||
### Failure injection matrix
|
||||
|
||||
| Location | Injection | Assertions |
|
||||
|---|---|---|
|
||||
| Validation/upload | bad MIME, count/bytes overflow, truncated media | 400/413, no job/engine submit, no temp leak |
|
||||
| Load/start | missing fused shard, unsupported topology | startup fails, actionable error, no silent init |
|
||||
| Encode | exception/OOM/timeout | one error, request state/temp released |
|
||||
| Denoise | exception/OOM/worker exit | peers handled, hooks/buffers released, health behavior explicit |
|
||||
| Scheduler | invalid state/shape | no latent/step partial commit |
|
||||
| Decode/post | exception/codec error | no corrupt successful result, partial output removed |
|
||||
| Result/IPC | late result after cancel, large split tensor, orphan SHM | pump remains alive, one terminal result, exact ownership cleanup |
|
||||
| Connector/disagg | timeout/retry/duplicate | idempotency, backpressure, no orphan payload |
|
||||
|
||||
After each injection, check server health and issue a known-good request. If
|
||||
the process must restart, document and test the restart policy; do not claim
|
||||
transparent recovery.
|
||||
|
||||
### Large-output transport
|
||||
|
||||
Treat worker-to-engine IPC and engine-to-client transport as separate hops.
|
||||
For batched video, prove that each logical output does not accidentally retain
|
||||
or serialize the complete batch storage. Exercise the target revision's shared
|
||||
memory or artifact-handle path above and below its threshold, including
|
||||
non-contiguous views, multiple outputs, consumer failure, timeout, abort, and
|
||||
worker exit. Verify every shared segment/file is released exactly once.
|
||||
|
||||
Freeze the media contract at each hop: dtype, value range, shape/layout,
|
||||
contiguity, request ownership, encoding/color model, payload bytes, and pending
|
||||
consumers. Device-side float-to-uint8 preparation, D2H/IPC, planar/interleaved
|
||||
conversion, CPU encode/mux, and delivery off the asyncio event loop are
|
||||
different optimizations. Validate their route selection and fallbacks
|
||||
independently. Byte-identical HTTP MP4 output does not prove that a raw offline
|
||||
tensor kept its dtype or range.
|
||||
|
||||
For long-window output, validate segment ordering and audio/video continuity,
|
||||
then exercise payloads around serializer, shared-memory, HTTP, and codec size
|
||||
limits. Chunking must reassemble exactly, reject malformed/missing chunks, and
|
||||
release partial state after abort, timeout, or peer failure.
|
||||
|
||||
Qualify chunk production, transport, and public streaming as separate layers.
|
||||
A VAE callback must define owned representation, frame offsets, final marker,
|
||||
rank ownership, source/config gate, and collective-safe failure, but it does not
|
||||
prove SHM/ZMQ delivery, bounded backpressure, cancellation, codec reuse, or
|
||||
client streaming. Measure time to first owned chunk, first encoded fragment,
|
||||
steady cadence, mux finalization, and complete client artifact independently.
|
||||
|
||||
Measure remote MP4 encoding, serialization/copies, network transfer, and client
|
||||
materialization separately from denoise/decode. An inference-kernel speedup does
|
||||
not establish serving throughput if output transport dominates or leaks.
|
||||
|
||||
## Mixed-RPS soak
|
||||
|
||||
Use a realistic request mix and arrival distribution. Include small/large
|
||||
shapes, short/long prompts, all advertised tasks, cache tiers, and a small rate
|
||||
of abort/disconnect/error injections. Run phases below, near, and above measured
|
||||
saturation so queue/backpressure behavior is visible.
|
||||
|
||||
Example serving benchmark phase:
|
||||
|
||||
```bash
|
||||
python benchmarks/diffusion/diffusion_benchmark_serving.py \
|
||||
--base-url http://127.0.0.1:8091 \
|
||||
--endpoint /v1/videos \
|
||||
--model '<model>' \
|
||||
--dataset trace \
|
||||
--dataset-path /path/to/versioned-production-trace.jsonl \
|
||||
--task t2v \
|
||||
--num-prompts 500 \
|
||||
--request-rate 0.5 \
|
||||
--max-concurrency 8 \
|
||||
--warmup-requests 8 \
|
||||
--warmup-concurrency 8 \
|
||||
--output-file soak-near-saturation.json
|
||||
```
|
||||
|
||||
Use a task-appropriate trace/media dataset. Run the same phase at rates selected
|
||||
from an initial saturation curve; a literal `0.5` is illustrative only.
|
||||
|
||||
On supported GPU hosts, wrap a test or soak with the repository monitor:
|
||||
|
||||
```bash
|
||||
bash tests/dfx/stability/scripts/resource_monitor.sh run --backend gpu -- \
|
||||
pytest -s -v tests/dfx/stability/scripts/test_stability_<model>.py -m slow
|
||||
```
|
||||
|
||||
Check the target revision's monitor backends. Do not claim CPU/NPU monitoring
|
||||
from a reserved CLI choice unless collection is implemented and verified.
|
||||
|
||||
Report:
|
||||
|
||||
- offered and achieved RPS, success/error/abort rates;
|
||||
- p50/p95/p99 and queue/service/stage times, with the successful sample count
|
||||
and arrival process used for each percentile;
|
||||
- throughput per device and batch/wave utilization;
|
||||
- per-rank HBM and process-tree PSS trend/slope;
|
||||
- cache size/hit rate, temp-file/disk growth, open FDs/handles;
|
||||
- outstanding shared-memory/artifact handles and encoded/network bytes;
|
||||
- worker/engine health, restarts, OOM/timeouts, backpressure response;
|
||||
- total duration, request count/mix, raw logs/JSON/monitor artifact hashes.
|
||||
|
||||
A short burst or repeated serial curl loop is not a stability test.
|
||||
|
||||
## Four CI tracks
|
||||
|
||||
Keep the tracks independent so a failure answers a specific question. These
|
||||
four readiness tracks are not the repository's numbered CI levels. Map them
|
||||
onto the current L1-L5 taxonomy:
|
||||
|
||||
| Readiness track | Repository CI placement |
|
||||
|---|---|
|
||||
| Function | L1 unit/logic plus L2 basic online/offline E2E; extend real-model scenarios in L3/L4 |
|
||||
| Accuracy | L3 real-model gates for important cases; deeper and broader nightly coverage in L4 |
|
||||
| Performance | Important thresholds in L3 where they fit the time budget; full baselines/regression jobs in nightly L4 |
|
||||
| Reliability | Cheap rejection/recovery assertions can run earlier, but long stability, fault injection, and reliability suites belong to weekly L5 |
|
||||
|
||||
Distributed topology is a test case within the appropriate level, not a
|
||||
replacement definition for a level.
|
||||
|
||||
### 1. Function CI
|
||||
|
||||
Cover:
|
||||
|
||||
- strict unquantized and advertised quantized loading;
|
||||
- fused source-shard completeness and prefix routing;
|
||||
- official task positive boundaries and neighboring negative cases;
|
||||
- offline, sync, async normalization/parity;
|
||||
- single-device reference plus every advertised topology smoke;
|
||||
- output type/count/order and request ID isolation;
|
||||
- feature rejection paths that must fail early.
|
||||
|
||||
### 2. Accuracy CI
|
||||
|
||||
Pin official implementation/checkpoint/vLLM-Omni revisions, prompts and media
|
||||
hashes, seed, scheduler/sigma/steps, dimensions/frames/FPS, guidance, dtype,
|
||||
backend, and topology. Compare at suitable levels:
|
||||
|
||||
- component outputs or selected blocks;
|
||||
- one or more intermediate denoise latents;
|
||||
- final image/video/audio artifact metrics;
|
||||
- temporal consistency and audio/video synchronization where relevant.
|
||||
|
||||
Set tolerance per modality/hardware/backend with rationale. Keep failure
|
||||
artifacts and metadata. A single visual inspection does not define CI.
|
||||
|
||||
For quantization trajectory tooling, inspect the target revision's current
|
||||
`vllm_omni/quantization/tools/compare_diffusion_trajectory_similarity.py` and
|
||||
reuse it when compatible instead of creating an ad hoc comparator.
|
||||
|
||||
### 3. Performance CI
|
||||
|
||||
Pin workload and best deployment. Include a resident recommended row and a
|
||||
small-HBM/DLO row when both are production targets. Declare:
|
||||
|
||||
- one explicit warmup exclusion and at least three measured fixed-work
|
||||
repetitions;
|
||||
- raw result JSON and environment identity;
|
||||
- median/mean plus range for small fixed-work A/Bs; p50/p95/p99 only for an
|
||||
arrival-load run with enough samples, plus throughput/RPS, stage time,
|
||||
HBM/PSS;
|
||||
- regression metric, threshold, baseline owner, and update procedure;
|
||||
- correctness/accuracy check linked to the same configuration.
|
||||
|
||||
Use the current harness config style, for example:
|
||||
|
||||
```bash
|
||||
pytest -s -v tests/dfx/perf/scripts/run_diffusion_benchmark.py \
|
||||
--test-config-file tests/dfx/perf/tests/test_<model>_vllm_omni.json
|
||||
```
|
||||
|
||||
Verify parser options on the target revision. Do not copy a JSON schema from a
|
||||
different model without matching its endpoint, task, dataset, and deploy path.
|
||||
|
||||
### 4. Reliability CI
|
||||
|
||||
Run a bounded CI soak plus a longer scheduled soak. Cover:
|
||||
|
||||
- below/near/above-saturation arrival phases;
|
||||
- queued/in-flight abort and disconnect;
|
||||
- OOM/timeout and worker/process failure appropriate to the platform;
|
||||
- DLO/cache/connector cleanup;
|
||||
- health, fast-fail/backpressure, restart/recovery policy;
|
||||
- memory/temp/FD trend and successful known-good request after each fault.
|
||||
|
||||
Keep reliability thresholds independent from latency thresholds. A fast server
|
||||
that leaks memory or hangs after cancellation fails production readiness.
|
||||
|
||||
## PR evidence checklist
|
||||
|
||||
The PR description must include or link:
|
||||
|
||||
- official source/checkpoint revisions and API/limit matrix;
|
||||
- scoped feature compatibility matrix with the four allowed states;
|
||||
- implementation notes for FP8, DLO, request cache, attention/fusion, batching;
|
||||
- exact environment, deploy/serve, every task curl, benchmark and soak commands;
|
||||
- fixed accuracy inputs and artifacts;
|
||||
- raw before/after performance runs and memory traces;
|
||||
- output-contract manifests and stage accounting from device preparation
|
||||
through the complete client artifact;
|
||||
- abort/disconnect/fault results and cleanup evidence;
|
||||
- Function/Accuracy/Performance/Reliability CI files and ownership;
|
||||
- limitations/TODOs that remain `not tested` rather than implied support.
|
||||
|
||||
Separate the Day-0 milestone from production readiness. If the PR only closes
|
||||
some gates, title and description should say which scoped gates it closes.
|
||||
@@ -26,6 +26,9 @@ self-check.
|
||||
|
||||
- [`add-diffusion-model`](add-diffusion-model/SKILL.md): guides integration of
|
||||
a new diffusion model into `vllm-omni`
|
||||
- [`add-recipe`](add-recipe/SKILL.md): creates or updates in-repository model
|
||||
recipes with verified task/input/output contracts, concrete hardware,
|
||||
shared feature-guide links, exact commands, and qualification evidence
|
||||
- [`add-tts-model`](add-tts-model/SKILL.md): covers integration of new TTS
|
||||
models and related serving workflows
|
||||
- [`diffusion-perf-opt`](diffusion-perf-opt/SKILL.md): guides diffusion model
|
||||
@@ -37,6 +40,10 @@ self-check.
|
||||
- [`precheck-pr`](precheck-pr/SKILL.md): self-checks a branch before creating a
|
||||
PR by validating title format, dead code, simplification opportunities,
|
||||
accuracy and performance claims, and merge readiness
|
||||
- [`production-add-diffusion-model`](production-add-diffusion-model/SKILL.md):
|
||||
takes a working Day-0 diffusion integration through API/limit parity,
|
||||
feature-combination evidence, quantization/offload, performance, production
|
||||
serving, hardware recipes, and CI readiness
|
||||
- [`quantization`](quantization/SKILL.md): guides quantization method selection,
|
||||
model integration, checkpoint loading, and quality/performance validation
|
||||
for vLLM-Omni
|
||||
|
||||
@@ -123,10 +123,11 @@ it changes again, report the churn and wait for a stable target.
|
||||
|
||||
For a trusted PR head, materialize the pinned head in an isolated detached
|
||||
worktree. A worktree freezes identity but is not a security sandbox. Treat fork
|
||||
heads as untrusted unless the user and environment policy explicitly establish
|
||||
otherwise: execute them only in a disposable, secret-free sandbox with restricted
|
||||
filesystem, network, and resources; without one, use static SHA-addressed reads
|
||||
and CI evidence only. For a local review, freeze the committed, index, worktree,
|
||||
heads as untrusted until the user and environment policy explicitly establish
|
||||
trust: execute their code only after that trust is recorded for this host,
|
||||
with the trusted SHA noted in the review state and secrets kept out of the
|
||||
execution scope; without recorded trust, use static SHA-addressed reads and CI
|
||||
evidence only. For a local review, freeze the committed, index, worktree,
|
||||
and NUL-safe in-scope untracked contents. Follow
|
||||
[review-execution.md](references/process/review-execution.md) for trust gates,
|
||||
state fingerprints, and byte-for-byte staleness checks.
|
||||
@@ -196,7 +197,7 @@ style preferences as simplification findings.
|
||||
|
||||
Before each validation group, verify the frozen SHA plus the tracked, index,
|
||||
untracked, and ignored-file fingerprint, or recreate a pristine snapshot. On a
|
||||
trusted head or inside the required sandbox, run an import/version preflight,
|
||||
head the user explicitly trusted for this host, run an import/version preflight,
|
||||
then the narrowest relevant tests and low-cost static checks. Bind every result
|
||||
to the head SHA, snapshot fingerprint, and environment fingerprint. Never run
|
||||
imports, tests, builds, hooks, or repo-configurable tooling from an untrusted
|
||||
|
||||
@@ -22,6 +22,19 @@ Keep exact commands and report variability rather than a cherry-picked run.
|
||||
Use the repository's benchmark when it covers the claim; otherwise use the
|
||||
smallest reproducible workload that reaches the changed production path.
|
||||
|
||||
## Isolation per claim
|
||||
|
||||
Isolate one claim per comparison. When a PR stacks several changes (a fused or
|
||||
restructured load path, a distilled step schedule, a new cache), request one
|
||||
column per claim at equal steps/workload instead of one blended base-vs-head
|
||||
delta, and name the unchanged stage or metric that pins equivalence.
|
||||
|
||||
When the changed path is runtime-switchable (feature flag or environment
|
||||
variable), run every column on the same head and toggle the switch rather than
|
||||
diffing against a base commit that cannot separate the stacked changes:
|
||||
base, head with the switch off, head with the switch on, then head with the
|
||||
switch on plus each further claim.
|
||||
|
||||
## Evidence table
|
||||
|
||||
| Dimension | Typical evidence |
|
||||
@@ -34,6 +47,100 @@ smallest reproducible workload that reaches the changed production path.
|
||||
Every optimization needs correctness or quality evidence. A faster result on a
|
||||
different workload, precision, seed, or topology is not a valid comparison.
|
||||
|
||||
## E2e stage-attribution table
|
||||
|
||||
For an end-to-end latency claim, request the stage-attribution table the
|
||||
repository's perf PRs use:
|
||||
|
||||
- Rows carry the benchmark's own field names: total e2e, per-stage execution,
|
||||
postprocess, response encoding, and a request-to-output roll-up. Add a stage
|
||||
handoff/idle row when the pipeline spans stages — connector transfer and
|
||||
scheduling gaps are invisible in per-stage latency deltas.
|
||||
- Sanity rows pin comparability: inference steps, output frames/pixels/samples,
|
||||
and generated tokens held identical across columns.
|
||||
- Report P50 and P100 from the stated warmup and measured counts. One
|
||||
population per number: never mix cold-start and warm samples into one value.
|
||||
- Name the baseline and candidate commit SHAs, and close with an isolation
|
||||
statement naming the metric that stayed unchanged ("X remained effectively
|
||||
unchanged, isolating the gain to Y").
|
||||
|
||||
When the serving path caches, captures, or regenerates at runtime (CUDA-graph
|
||||
bucket growth, cache rebuilds, recompilation), medians hide the resulting tail.
|
||||
Also request the event count and the maximum observed stall.
|
||||
|
||||
Component-level tables pair with, not replace, the e2e table: per-op reference
|
||||
vs candidate timings (with the numerical-equality result per op) plus one
|
||||
complete-pipeline check under the production shapes.
|
||||
|
||||
## Proportionality
|
||||
|
||||
When a PR adds a model and explicitly labels its numbers as smoke observations
|
||||
rather than claims, do not demand the attribution machinery. Ask only for
|
||||
evidence protecting the model's stated operating contract — for a
|
||||
realtime/duplex model that is sustained cadence over a realistic session
|
||||
(enough ticks to leave warmup), with any mid-session capture/regeneration
|
||||
event and its stall reported, since a cadence that collapses mid-session is a
|
||||
functional failure, not a perf claim.
|
||||
|
||||
## Realtime/duplex world models
|
||||
|
||||
Request the operating-contract evidence as serving metrics, not claims:
|
||||
|
||||
- Sustained per-tick latency over one realistic session: median, max, and the
|
||||
argmax tick.
|
||||
- Stall events with their causes.
|
||||
- Peak memory at first versus last tick — session-owned state must not grow
|
||||
per tick.
|
||||
- Control latency from a submitted action to the tick that applies it.
|
||||
- Realtime-versus-offline parity under the same controls, using the
|
||||
repository's video-similarity helpers. Parity plotted over tick index is the
|
||||
drift-over-horizon curve and the honest measure of session lifetime;
|
||||
distribution-level suites (FVD, VBench) belong to model evaluation, not the
|
||||
serving integration.
|
||||
|
||||
Watch the contract boundaries the model documents: if a control input is
|
||||
tick-API-only, the parity pair must use a horizon both paths can run.
|
||||
|
||||
## Speech-generation (TTS) models
|
||||
|
||||
The streaming contract outranks the average — a synthesis that pauses
|
||||
mid-stream is broken for realtime use regardless of its averages. Request:
|
||||
|
||||
- The benchmark's own speech metrics over one long utterance (enough audio to
|
||||
leave warmup, tens of seconds): `audio_ttfp` (time to first packet),
|
||||
`audio_rtf` (real-time factor; above 1.0 cannot keep up), `e2el`, and
|
||||
`ttft`/`tpot` where tokens precede audio.
|
||||
- Mid-utterance stalls as the benchmark's continuity fields, not prose:
|
||||
`audio_underrun` percentiles and the `audio_continuity_ok` rate
|
||||
(`vllm_omni/benchmarks/metrics/metrics.py`), computed by
|
||||
`vllm_omni/benchmarks/audio_continuity.py` — the companion to `audio_ttfp`
|
||||
and `audio_rtf`. A synthesis that bursts chunks fast enough on average but
|
||||
underruns mid-stream fails the streaming contract even when the averages
|
||||
pass.
|
||||
- Quality as a paired comparison against the reference implementation under
|
||||
the same text, speaker prompt, and seed; report sample rate and frame
|
||||
count, and assert output completeness — truncated or dropped trailing audio
|
||||
is a known failure class, not a quality nuance.
|
||||
- For realtime or duplex TTS: interruption (barge-in) latency and the
|
||||
per-session memory trend across turns.
|
||||
|
||||
## Omni models
|
||||
|
||||
Apply the e2e stage-attribution table directly to the voice-to-voice path of
|
||||
omni (speech-to-speech and multimodal AR) models:
|
||||
|
||||
- TTFT for text output, `audio_ttfp` for audio output, and per-stage
|
||||
ITL/TPOT cadence across the perception-understanding-generation chain.
|
||||
- Cross-modal parity — the omni analogue of realtime-versus-offline parity:
|
||||
the same semantic request through text and audio inputs, compared on output
|
||||
content or timing.
|
||||
- For duplex omni sessions: turn-taking latency, interruption handling, the
|
||||
concurrent session count actually validated, and a per-turn memory trend —
|
||||
session-owned KV state must not grow per turn. When either turn produces
|
||||
audio, continuity is judged with the same `audio_underrun` and
|
||||
`audio_continuity_ok` fields as TTS, since turn-taking latency alone does
|
||||
not expose mid-utterance underruns.
|
||||
|
||||
## Reviewer verification ladder
|
||||
|
||||
1. Run base/head A/B on suitable hardware when affordable.
|
||||
|
||||
@@ -121,6 +121,19 @@ Search bounded callers and sibling implementations before reporting dead code,
|
||||
duplication, or a missed consumer. Keep new abstractions only when they have a
|
||||
distinct live caller, invariant, or compatibility need.
|
||||
|
||||
Verify absence and equality claims with whole-file evidence. A line window, a
|
||||
grep for one symbol, or a commit message proves presence, never absence: to
|
||||
assert that a file matches another version or lacks a change, diff or hash the
|
||||
full files (for example `git diff <sha1>:<path> <sha2>:<path>` or the contents
|
||||
API's size and SHA fields) before anchoring the finding. A verification
|
||||
shortcut that would itself be wrong to run is not evidence.
|
||||
|
||||
For a value written or finalized in more than one place, establish which site
|
||||
executes first on each entry path — and whether the earlier site makes the
|
||||
later ones unreachable — before naming where a fix belongs. Enumerating all
|
||||
call sites correctly still misses the bug when a path the enumeration ranked
|
||||
last actually finalizes first and clears the state the proposed fix checks.
|
||||
|
||||
## Finding bar
|
||||
|
||||
Anchor each finding to a changed `path:line`; name the trigger or call path,
|
||||
|
||||
@@ -39,13 +39,13 @@ isolated detached worktree. Run every source read, `rg` search, import, and test
|
||||
from that snapshot, not the caller's checkout. A detached worktree provides
|
||||
snapshot isolation, not a security boundary.
|
||||
|
||||
Treat a fork head as untrusted unless the user and environment policy explicitly
|
||||
establish trust. Never run its imports, tests, builds, hooks, package setup, or
|
||||
repo-configurable linters/plugins on the reviewer host. Execute them only in a
|
||||
disposable sandbox or VM with no credentials or inherited secrets, no host agent
|
||||
or service sockets, minimal read-only host mounts, disabled or explicitly
|
||||
allowlisted network, resource/time limits, and destruction after the run. If
|
||||
that boundary is unavailable, limit the review to the remote diff, SHA-addressed
|
||||
Treat a fork head as untrusted until the user and environment policy
|
||||
explicitly establish trust. Without recorded trust, never run its imports,
|
||||
tests, builds, hooks, package setup, or repo-configurable linters/plugins on
|
||||
the reviewer host. Once the user explicitly trusts this pinned commit for
|
||||
this host, record the trusted SHA with the review state and execute with
|
||||
credentials, agent sockets, and other secrets out of scope. Without recorded
|
||||
trust, limit the review to the remote diff, SHA-addressed
|
||||
reads such as `git show <head_sha>:<path>`, and existing CI evidence; report all
|
||||
executable validation as a gap.
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
You may find information about contributing to vLLM-Omni on [Contributing](https://vllm-omni.readthedocs.io/en/latest/contributing/).
|
||||
|
||||
AI-assisted contributions must follow the [AI Usage Policy](docs/contributing/README.md#ai-usage-policy), including disclosure, human review, and validation requirements.
|
||||
|
||||
Local `pre-commit` gates (SPDX, forbidden imports, `torch.cuda`, shellcheck,
|
||||
mypy, test marks, TTS adapter ratchet, markdownlint, Buildkite schema, and
|
||||
how to extend allowlists) are documented in the
|
||||
|
||||
@@ -16,6 +16,7 @@ Easy, fast, and cheap omni-modality model serving for everyone
|
||||
|
||||
*Latest News* 🔥
|
||||
|
||||
- [2026/08] We released [0.28.0](https://github.com/vllm-project/vllm-omni/releases/tag/v0.28.0), featuring production-ready [MiniMax H3](recipes/MiniMaxAI/MiniMax-H3.md) serving on GPU and NPU, a unified AR/DiT paged KV cache runtime, and enhanced realtime full-duplex serving for the [MiniCPM-o series](recipes/OpenBMB/MiniCPM-o-4_5.md).
|
||||
- [2026/08] [VeRL-Omni](https://github.com/verl-project/verl-omni) `v0.2.0` is released: faster diffusion RL powered by vLLM-Omni (request-level/step-wise batching with FA3), rebuilt Qwen3-Omni multimodal training (DPO & GSPO), plus LTX-2.3, Qwen-Image-Edit support and more. See the [release notes](https://github.com/verl-project/verl-omni/releases/tag/v0.2.0).
|
||||
- [2026/08] We released [0.26.0](https://github.com/vllm-project/vllm-omni/releases/tag/v0.26.0) - aligned with the vLLM 0.26 release line, featuring [MiniMax H3](recipes/MiniMaxAI/MiniMax-H3.md) joint video/audio generation, an experimental full-duplex realtime runtime for [MiniCPM-o 4.5](recipes/OpenBMB/MiniCPM-o-4_5.md), distributed layerwise diffusion offload, and broader model, hardware, streaming, TTS, and quantization support.
|
||||
- [2026/07] We released [0.24.0](https://github.com/vllm-project/vllm-omni/releases/tag/v0.24.0) - aligned with the vLLM 0.24 release line, expanding production-ready coverage across TTS, speech, diffusion, image/video generation, and robot-policy serving, with major Omni stage runtime refactoring, diffusion request-level batching, async output materialization, quantization/cache/memory improvements, and broad CUDA/ROCm/XPU/NPU support.
|
||||
@@ -52,14 +53,14 @@ vLLM-Omni is flexible and easy to use with:
|
||||
- Tensor, pipeline, data and expert parallelism support for distributed inference
|
||||
- Streaming outputs
|
||||
- OpenAI-compatible API server
|
||||
- Full-duplex realtime serving with streaming audio input and output (experimental)
|
||||
- [Full-duplex realtime serving](docs/serving/realtime_duplex_api.md) with streaming audio input and output
|
||||
|
||||
vLLM-Omni seamlessly supports most popular open-source models on HuggingFace, including:
|
||||
|
||||
- **Omni-modality models** (e.g. Qwen3-Omni, MiniCPM-o 4.5, Cosmos3, HunyuanImage, BAGEL)
|
||||
- **TTS models** (e.g. Qwen3-TTS, VoxCPM2, Ming-Omni-TTS, CosyVoice3)
|
||||
- **Diffusion models** — image, video, and audio generation (e.g. MiniMax H3, Qwen-Image, Wan2.2, FLUX)
|
||||
- **Robot-policy and action models** (e.g. GR00T-N1.7, DreamZero-DROID, InternVLA-A1, Cosmos3 action policy)
|
||||
- **TTS models** (e.g. Qwen3-TTS, IndexTTS 2.5, dots.tts, CosyVoice3)
|
||||
- **Diffusion models** — image, video, and audio generation (e.g. MiniMax H3, LTX-2.5, SANA-Video, Wan2.2)
|
||||
- **Robot-policy and action models** (e.g. π0, GR00T-N1.7, DreamZero-DROID, InternVLA-A1)
|
||||
|
||||
## Getting Started
|
||||
|
||||
@@ -67,6 +68,7 @@ Visit our [documentation](https://vllm-omni.readthedocs.io/en/latest/) to learn
|
||||
|
||||
- [Installation](https://vllm-omni.readthedocs.io/en/latest/getting_started/installation/)
|
||||
- [Quickstart](https://vllm-omni.readthedocs.io/en/latest/getting_started/quickstart/)
|
||||
- [Nightly CUDA Docker images](https://hub.docker.com/r/vllm/vllm-omni/tags?name=nightly) built from the latest `main`
|
||||
- [List of Supported Models](https://vllm-omni.readthedocs.io/en/latest/models/supported_models/)
|
||||
- [Deployment Recipes](https://recipes.vllm.ai) for vLLM-Omni model serving
|
||||
|
||||
|
||||
@@ -17,18 +17,18 @@ The main entrypoint is:
|
||||
vllm serve Qwen/Qwen-Image --omni --port 8099
|
||||
```
|
||||
|
||||
2. Run a minimal benchmark:
|
||||
1. Run a minimal benchmark:
|
||||
|
||||
```bash
|
||||
python3 benchmarks/diffusion/diffusion_benchmark_serving.py \
|
||||
--base-url http://localhost:8099 \
|
||||
--model Qwen/Qwen-Image \
|
||||
--task t2i \
|
||||
--dataset vbench \
|
||||
--num-prompts 5
|
||||
--base-url http://localhost:8099 \
|
||||
--model Qwen/Qwen-Image \
|
||||
--task t2i \
|
||||
--dataset vbench \
|
||||
--num-prompts 5
|
||||
```
|
||||
|
||||
**Notes**
|
||||
### Notes
|
||||
|
||||
- By default, image tasks talk to `http://<host>:<port>/v1/chat/completions`; video tasks talk to `/v1/videos`.
|
||||
- If you run the server on another host or port, pass `--base-url` accordingly.
|
||||
@@ -52,14 +52,14 @@ Example (`t2v`):
|
||||
|
||||
```bash
|
||||
python3 benchmarks/diffusion/diffusion_benchmark_serving.py \
|
||||
--base-url http://localhost:8099 \
|
||||
--model Wan-AI/Wan2.2-T2V-A14B-Diffusers \
|
||||
--task t2v \
|
||||
--dataset vbench \
|
||||
--num-prompts 50 \
|
||||
--width 640 --height 480 \
|
||||
--num-frames 81 --fps 16 \
|
||||
--num-inference-steps 40
|
||||
--base-url http://localhost:8099 \
|
||||
--model Wan-AI/Wan2.2-T2V-A14B-Diffusers \
|
||||
--task t2v \
|
||||
--dataset vbench \
|
||||
--num-prompts 50 \
|
||||
--width 640 --height 480 \
|
||||
--num-frames 81 --fps 16 \
|
||||
--num-inference-steps 40
|
||||
```
|
||||
|
||||
Note: `vbench` can also be used for other tasks such as `t2i` / `i2v` (and `i2i`). For `t2i`, the loader reuses VBench t2v text prompts; for `i2v` / `i2i`, it loads the VBench i2v dataset (with image paths).
|
||||
@@ -140,6 +140,21 @@ Traffic / concurrency flags:
|
||||
- `--request-rate`: Target request rate (requests/second). If set to `inf`, the script sends all requests immediately.
|
||||
- `--max-concurrency`: Max number of in-flight requests (default: `1`). This can hard-cap the achieved QPS: if it is too small, requests will queue behind the semaphore, and both achieved throughput and observed SLO attainment can be skewed.
|
||||
|
||||
### Video job timeout and failures
|
||||
|
||||
`--video-job-timeout` controls how long each `/v1/videos` job may be polled,
|
||||
including time queued on the server (default: `900` seconds). For long video
|
||||
jobs at high concurrency, use a larger budget, for example
|
||||
`--video-job-timeout 1800`. This also applies to warmup jobs. The polling budget
|
||||
starts after job creation; it excludes time waiting for the client concurrency
|
||||
semaphore. Individual HTTP calls still use the aiohttp session timeout.
|
||||
|
||||
An expired job is counted as failed and deleted, which cancels queued or running
|
||||
server generation. The benchmark continues processing the remaining requests.
|
||||
The progress bar counts both successful and failed requests. The final report
|
||||
shows the failure count and the first 10 errors; `--output-file` saves all errors
|
||||
with their request IDs in `request_errors`.
|
||||
|
||||
### Batched warmup note
|
||||
|
||||
For batched serving runs, warm the same in-flight shape you plan to measure.
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
import json
|
||||
@@ -43,6 +46,7 @@ class RequestFuncInput:
|
||||
video_paths: list[str] | None = None
|
||||
request_id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
||||
default_bot_task: str | None = DEFAULT_EDITS_BOT_TASK
|
||||
video_job_timeout: float = 900.0
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -331,64 +335,64 @@ async def async_request_v1_videos(
|
||||
output = RequestFuncOutput()
|
||||
output.start_time = time.perf_counter()
|
||||
|
||||
files = dict(input.extra_body)
|
||||
if input.prompt:
|
||||
files.setdefault("prompt", input.prompt)
|
||||
if input.width and input.height:
|
||||
files.setdefault("height", input.height)
|
||||
files.setdefault("width", input.width)
|
||||
if input.num_frames:
|
||||
files.setdefault("num_frames", input.num_frames)
|
||||
if input.num_inference_steps:
|
||||
files.setdefault("num_inference_steps", input.num_inference_steps)
|
||||
if input.seed is not None:
|
||||
files.setdefault("seed", input.seed)
|
||||
if input.fps:
|
||||
files.setdefault("fps", input.fps)
|
||||
|
||||
form = aiohttp.FormData()
|
||||
for k, v in files.items():
|
||||
form.add_field(k, str(v))
|
||||
|
||||
image_file = None
|
||||
if input.image_paths and input.video_paths:
|
||||
output.error = "Only one of image_paths or video_paths can be provided"
|
||||
output.success = False
|
||||
return output
|
||||
job_id = None
|
||||
try:
|
||||
files = dict(input.extra_body)
|
||||
if input.prompt:
|
||||
files.setdefault("prompt", input.prompt)
|
||||
if input.width and input.height:
|
||||
files.setdefault("height", input.height)
|
||||
files.setdefault("width", input.width)
|
||||
if input.num_frames:
|
||||
files.setdefault("num_frames", input.num_frames)
|
||||
if input.num_inference_steps:
|
||||
files.setdefault("num_inference_steps", input.num_inference_steps)
|
||||
if input.seed is not None:
|
||||
files.setdefault("seed", input.seed)
|
||||
if input.fps:
|
||||
files.setdefault("fps", input.fps)
|
||||
|
||||
if input.image_paths and len(input.image_paths) > 0:
|
||||
image_path = input.image_paths[0]
|
||||
image_file = open(image_path, "rb")
|
||||
form.add_field(
|
||||
"input_reference",
|
||||
image_file,
|
||||
filename=os.path.basename(image_path),
|
||||
content_type=_guess_mime_type(image_path),
|
||||
)
|
||||
elif input.video_paths and len(input.video_paths) > 0:
|
||||
video_path = input.video_paths[0]
|
||||
if _is_minimax_h3_mixed_reference(input.model):
|
||||
with open(video_path, "rb") as reference_file:
|
||||
video_b64 = base64.b64encode(reference_file.read()).decode("ascii")
|
||||
form.add_field(
|
||||
"video_reference",
|
||||
json.dumps([{"video_url": f"data:video/mp4;base64,{video_b64}"}]),
|
||||
)
|
||||
else:
|
||||
image_file = open(video_path, "rb")
|
||||
form = aiohttp.FormData()
|
||||
for k, v in files.items():
|
||||
form.add_field(k, str(v))
|
||||
|
||||
if input.image_paths and input.video_paths:
|
||||
output.error = "Only one of image_paths or video_paths can be provided"
|
||||
output.success = False
|
||||
return output
|
||||
|
||||
if input.image_paths and len(input.image_paths) > 0:
|
||||
image_path = input.image_paths[0]
|
||||
image_file = open(image_path, "rb")
|
||||
form.add_field(
|
||||
"input_reference",
|
||||
image_file,
|
||||
filename=os.path.basename(video_path),
|
||||
content_type=_guess_mime_type(video_path),
|
||||
filename=os.path.basename(image_path),
|
||||
content_type=_guess_mime_type(image_path),
|
||||
)
|
||||
elif input.video_paths and len(input.video_paths) > 0:
|
||||
video_path = input.video_paths[0]
|
||||
if _is_minimax_h3_mixed_reference(input.model):
|
||||
with open(video_path, "rb") as reference_file:
|
||||
video_b64 = base64.b64encode(reference_file.read()).decode("ascii")
|
||||
form.add_field(
|
||||
"video_reference",
|
||||
json.dumps([{"video_url": f"data:video/mp4;base64,{video_b64}"}]),
|
||||
)
|
||||
else:
|
||||
image_file = open(video_path, "rb")
|
||||
form.add_field(
|
||||
"input_reference",
|
||||
image_file,
|
||||
filename=os.path.basename(video_path),
|
||||
content_type=_guess_mime_type(video_path),
|
||||
)
|
||||
|
||||
job_id = None
|
||||
job_status = None
|
||||
poll_json = {}
|
||||
resp_json = {}
|
||||
job_status = None
|
||||
poll_json = {}
|
||||
resp_json = {}
|
||||
|
||||
try:
|
||||
# invoke a post request (POST /v1/videos)
|
||||
async with session.post(input.api_url, data=form) as response:
|
||||
if response.status == 200:
|
||||
@@ -406,7 +410,7 @@ async def async_request_v1_videos(
|
||||
|
||||
# invoke a poll request (GET /v1/videos/{video_id})
|
||||
poll_interval = 2.0 # Unit(s)
|
||||
timeout_seconds = 600.0
|
||||
timeout_seconds = input.video_job_timeout
|
||||
deadline = time.perf_counter() + timeout_seconds
|
||||
job_url = f"{input.api_url}/{job_id}"
|
||||
|
||||
@@ -422,8 +426,11 @@ async def async_request_v1_videos(
|
||||
poll_json = await poll_response.json()
|
||||
job_status = poll_json.get("status")
|
||||
|
||||
if time.perf_counter() >= deadline:
|
||||
output.error = f"Timed out waiting for video job {job_id} to complete."
|
||||
if job_status not in {"completed", "failed"} and time.perf_counter() >= deadline:
|
||||
output.error = (
|
||||
f"Timed out after {timeout_seconds:g}s waiting for video job {job_id} to complete. "
|
||||
"Increase --video-job-timeout for long-running or queued video jobs."
|
||||
)
|
||||
output.success = False
|
||||
return output
|
||||
|
||||
@@ -452,7 +459,7 @@ async def async_request_v1_videos(
|
||||
elif "peak_memory_mb" in resp_json:
|
||||
output.peak_memory_mb = resp_json["peak_memory_mb"]
|
||||
except Exception as e:
|
||||
output.error = str(e)
|
||||
output.error = f"{type(e).__name__}: {e}"
|
||||
output.success = False
|
||||
finally:
|
||||
if image_file is not None:
|
||||
@@ -465,13 +472,13 @@ async def async_request_v1_videos(
|
||||
except Exception as e:
|
||||
print(f"Failed to clean up video job {job_id}: {e}")
|
||||
|
||||
output.latency = time.perf_counter() - output.start_time
|
||||
output.latency = time.perf_counter() - output.start_time
|
||||
if pbar is not None:
|
||||
pbar.update(1)
|
||||
|
||||
if output.success and input.slo_ms is not None:
|
||||
output.slo_achieved = (output.latency * 1000.0) <= float(input.slo_ms)
|
||||
|
||||
if pbar:
|
||||
pbar.update(1)
|
||||
return output
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# adapted from fastvideo
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
"""
|
||||
Benchmark online serving for diffusion models (Image/Video Generation).
|
||||
@@ -87,6 +87,7 @@ import base64
|
||||
import glob
|
||||
import json
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
import random
|
||||
import tempfile
|
||||
@@ -1116,6 +1117,11 @@ def calculate_metrics(
|
||||
"duration": total_duration,
|
||||
"completed_requests": num_success,
|
||||
"failed_requests": len(error_outputs),
|
||||
"request_errors": [
|
||||
{"request_id": req.request_id, "error": out.error}
|
||||
for req, out in zip(requests_list, outputs)
|
||||
if not out.success
|
||||
],
|
||||
"throughput_qps": num_success / total_duration if total_duration > 0 else 0,
|
||||
"latency_mean": np.mean(latencies) if latencies else 0,
|
||||
"latency_median": np.median(latencies) if latencies else 0,
|
||||
@@ -1261,6 +1267,9 @@ def _default_endpoint_for_task(task: str) -> str:
|
||||
|
||||
|
||||
async def benchmark(args):
|
||||
if not math.isfinite(args.video_job_timeout) or args.video_job_timeout <= 0:
|
||||
raise ValueError("--video-job-timeout must be a positive finite number of seconds.")
|
||||
|
||||
# Construct base_url if not provided
|
||||
if args.base_url is None:
|
||||
args.base_url = f"http://{args.host}:{args.port}"
|
||||
@@ -1312,6 +1321,8 @@ async def benchmark(args):
|
||||
print("Loading requests...")
|
||||
requests_list = dataset.get_requests()
|
||||
print(f"Prepared {len(requests_list)} requests from {args.dataset} dataset.")
|
||||
for req in requests_list:
|
||||
req.video_job_timeout = args.video_job_timeout
|
||||
|
||||
if args.return_stage_metrics and args.endpoint in _STAGE_METRICS_ENDPOINTS:
|
||||
for req in requests_list:
|
||||
@@ -1376,6 +1387,8 @@ async def benchmark(args):
|
||||
metrics["model"] = args.model
|
||||
metrics["dataset"] = args.dataset
|
||||
metrics["task"] = args.task
|
||||
if args.endpoint == "/v1/videos":
|
||||
metrics["video_job_timeout"] = args.video_job_timeout
|
||||
if args.endpoint == "/v1/images/edits":
|
||||
metrics["bot_task"] = args.bot_task
|
||||
|
||||
@@ -1398,6 +1411,11 @@ async def benchmark(args):
|
||||
)
|
||||
)
|
||||
print("{:<40} {}/{:<15}".format("Successful requests:", metrics["completed_requests"], len(requests_list)))
|
||||
print("{:<40} {:<15}".format("Failed requests:", metrics["failed_requests"]))
|
||||
for failure in metrics["request_errors"][:10]:
|
||||
print(f"Request {failure['request_id']} failed: {failure['error']}")
|
||||
if metrics["failed_requests"] > 10:
|
||||
print("Showing first 10 failures; use --output-file to save all request errors.")
|
||||
|
||||
# Section 3: Performance Metrics
|
||||
print(f"{'-' * 50}")
|
||||
@@ -1481,6 +1499,13 @@ if __name__ == "__main__":
|
||||
help="Path to local dataset file (optional).",
|
||||
)
|
||||
parser.add_argument("--num-prompts", type=int, default=10, help="Number of prompts to benchmark.")
|
||||
parser.add_argument(
|
||||
"--video-job-timeout",
|
||||
type=float,
|
||||
default=900.0,
|
||||
help="Maximum polling time in seconds per video job, including server queue time (default: 900). "
|
||||
"Increase this for long-running video generation or high concurrency.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--max-concurrency",
|
||||
type=int,
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Benchmark diffusion LoRA expand-and-accumulate implementations.
|
||||
|
||||
This compares the previous functional expand path with direct accumulation into
|
||||
the selected output slice. It is a synthetic kernel microbenchmark and does not
|
||||
load a diffusion model.
|
||||
|
||||
Example:
|
||||
python benchmarks/kernels/benchmark_diffusion_lora_expand.py \
|
||||
--m 1,16,128,512,4096,16384 \
|
||||
--in-dim 4096 --out-dim 4096 --rank 64 \
|
||||
--dtype bfloat16 --packed --warmup 30 \
|
||||
--small-iterations 500 --large-iterations 100
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import math
|
||||
import statistics
|
||||
from collections.abc import Callable
|
||||
|
||||
import torch
|
||||
|
||||
MiB = 1024**2
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--m", default="1,16,128,512,4096,16384")
|
||||
parser.add_argument("--in-dim", type=int, default=4096)
|
||||
parser.add_argument("--out-dim", type=int, default=4096)
|
||||
parser.add_argument("--rank", type=int, default=64)
|
||||
parser.add_argument("--dtype", choices=("bfloat16", "float16"), default="bfloat16")
|
||||
parser.add_argument(
|
||||
"--packed",
|
||||
action="store_true",
|
||||
help="Accumulate into the second half of a packed output tensor.",
|
||||
)
|
||||
parser.add_argument("--warmup", type=int, default=30)
|
||||
parser.add_argument("--small-iterations", type=int, default=500)
|
||||
parser.add_argument("--large-iterations", type=int, default=100)
|
||||
parser.add_argument("--large-m-threshold", type=int, default=1024)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def percentile(samples: list[float], probability: float) -> float:
|
||||
ordered = sorted(samples)
|
||||
position = probability * (len(ordered) - 1)
|
||||
lower = math.floor(position)
|
||||
upper = math.ceil(position)
|
||||
if lower == upper:
|
||||
return ordered[lower]
|
||||
weight = position - lower
|
||||
return ordered[lower] * (1 - weight) + ordered[upper] * weight
|
||||
|
||||
|
||||
def measure_latency_ms(operation: Callable[[], None], warmup: int, iterations: int) -> list[float]:
|
||||
for _ in range(warmup):
|
||||
operation()
|
||||
torch.accelerator.synchronize()
|
||||
|
||||
starts = [torch.cuda.Event(enable_timing=True) for _ in range(iterations)]
|
||||
ends = [torch.cuda.Event(enable_timing=True) for _ in range(iterations)]
|
||||
for start, end in zip(starts, ends, strict=True):
|
||||
start.record()
|
||||
operation()
|
||||
end.record()
|
||||
torch.accelerator.synchronize()
|
||||
return [start.elapsed_time(end) for start, end in zip(starts, ends, strict=True)]
|
||||
|
||||
|
||||
def measure_extra_peak_mib(operation: Callable[[], None]) -> float:
|
||||
torch.accelerator.synchronize()
|
||||
torch.accelerator.reset_peak_memory_stats()
|
||||
allocated = torch.accelerator.memory_allocated()
|
||||
operation()
|
||||
torch.accelerator.synchronize()
|
||||
peak = torch.accelerator.max_memory_allocated()
|
||||
return (peak - allocated) / MiB
|
||||
|
||||
|
||||
def print_header(args: argparse.Namespace, dtype: torch.dtype) -> None:
|
||||
device_name = torch.cuda.get_device_name()
|
||||
print(f"device={device_name} torch={torch.__version__} dtype={dtype}")
|
||||
print(f"in_dim={args.in_dim} out_dim={args.out_dim} rank={args.rank} packed={args.packed} warmup={args.warmup}")
|
||||
print("M iters old_p10 old_p50 old_p90 new_p10 new_p50 new_p90 speedup old_MiB new_MiB max_abs")
|
||||
|
||||
|
||||
def benchmark_shape(m: int, args: argparse.Namespace, dtype: torch.dtype) -> None:
|
||||
device = torch.device("cuda")
|
||||
output_width = args.out_dim * (2 if args.packed else 1)
|
||||
offset = args.out_dim if args.packed else 0
|
||||
output_slice = slice(offset, offset + args.out_dim)
|
||||
scale = 1 / math.sqrt(args.in_dim)
|
||||
|
||||
x = torch.randn(m, args.in_dim, device=device, dtype=dtype)
|
||||
lora_a = torch.randn(args.rank, args.in_dim, device=device, dtype=dtype) * scale
|
||||
lora_b = torch.randn(args.out_dim, args.rank, device=device, dtype=dtype) / math.sqrt(args.rank)
|
||||
initial_output = torch.randn(m, output_width, device=device, dtype=dtype)
|
||||
|
||||
def baseline(output: torch.Tensor) -> None:
|
||||
buffer = x @ lora_a.t()
|
||||
delta = buffer @ lora_b.t()
|
||||
selected = output[:, output_slice]
|
||||
selected[:] = selected + delta
|
||||
|
||||
def optimized(output: torch.Tensor) -> None:
|
||||
buffer = x @ lora_a.t()
|
||||
selected = output[:, output_slice]
|
||||
torch.addmm(selected, buffer, lora_b.t(), out=selected)
|
||||
|
||||
reference = initial_output.clone()
|
||||
candidate = initial_output.clone()
|
||||
baseline(reference)
|
||||
optimized(candidate)
|
||||
torch.testing.assert_close(candidate, reference, rtol=1e-2, atol=1e-2)
|
||||
max_abs = (candidate - reference).abs().max().item()
|
||||
|
||||
old_output = initial_output.clone()
|
||||
new_output = initial_output.clone()
|
||||
iterations = args.large_iterations if m >= args.large_m_threshold else args.small_iterations
|
||||
|
||||
old_peak = measure_extra_peak_mib(lambda: baseline(old_output))
|
||||
new_peak = measure_extra_peak_mib(lambda: optimized(new_output))
|
||||
old_samples = measure_latency_ms(lambda: baseline(old_output), args.warmup, iterations)
|
||||
new_samples = measure_latency_ms(lambda: optimized(new_output), args.warmup, iterations)
|
||||
|
||||
old_p50 = statistics.median(old_samples)
|
||||
new_p50 = statistics.median(new_samples)
|
||||
print(
|
||||
f"{m:<7} {iterations:<6} "
|
||||
f"{percentile(old_samples, 0.1):>8.5f} {old_p50:>8.5f} {percentile(old_samples, 0.9):>8.5f} "
|
||||
f"{percentile(new_samples, 0.1):>8.5f} {new_p50:>8.5f} {percentile(new_samples, 0.9):>8.5f} "
|
||||
f"{old_p50 / new_p50:>8.3f} {old_peak:>8.2f} {new_peak:>8.2f} {max_abs:>8.5f}"
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
if not torch.cuda.is_available():
|
||||
raise RuntimeError("This benchmark requires a CUDA GPU")
|
||||
if args.warmup < 0 or args.small_iterations <= 0 or args.large_iterations <= 0:
|
||||
raise ValueError("warmup must be non-negative and iteration counts must be positive")
|
||||
|
||||
dtype = getattr(torch, args.dtype)
|
||||
torch.manual_seed(0)
|
||||
print_header(args, dtype)
|
||||
with torch.inference_mode():
|
||||
for m in (int(value) for value in args.m.split(",")):
|
||||
if m <= 0:
|
||||
raise ValueError(f"M must be positive, got {m}")
|
||||
benchmark_shape(m, args, dtype)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+53
-24
@@ -2,7 +2,7 @@
|
||||
|
||||
A model-agnostic serving benchmark for TTS models in vllm-omni. One CLI
|
||||
(`bench_tts.py`) + one YAML registry (`model_configs.yaml`) drive perf and
|
||||
quality runs for every registered checkpoint: **Qwen3-TTS** (Base / CustomVoice)
|
||||
quality runs for every registered checkpoint: **Qwen3-TTS** (Base / CustomVoice / VoiceDesign)
|
||||
and **VoxCPM2** today, more to come.
|
||||
|
||||
The same three task types — `voice_clone`, `default_voice`, `voice_design` —
|
||||
@@ -21,6 +21,11 @@ The server auto-loads its Deploy YAML from `vllm_omni/deploy/qwen3_tts.yaml`
|
||||
(Pipeline + Deploy schema introduced in #2383). No explicit `--deploy-config`
|
||||
flag is needed for a registered model.
|
||||
|
||||
Start the checkpoint that matches the benchmark task below: `-Base` for
|
||||
`voice_clone`, `-CustomVoice` for `default_voice`, or `-VoiceDesign` for
|
||||
`voice_design`. Restart the server with the matching checkpoint when switching
|
||||
tasks; changing only the benchmark's `--model` does not reload the server.
|
||||
|
||||
### 2. Run the benchmark (`vllm bench serve --omni`)
|
||||
|
||||
The primary, directly-controllable path. Copy-paste one of these and tweak
|
||||
@@ -62,12 +67,12 @@ vllm bench serve --omni \
|
||||
--save-result --result-dir ./results
|
||||
```
|
||||
|
||||
#### voice_design (Qwen3-TTS-CustomVoice, bundled seed_tts_design)
|
||||
#### voice_design (Qwen3-TTS-VoiceDesign, bundled seed_tts_design)
|
||||
|
||||
```bash
|
||||
vllm bench serve --omni \
|
||||
--host 127.0.0.1 --port 8000 \
|
||||
--model Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice \
|
||||
--model Qwen/Qwen3-TTS-12Hz-1.7B-VoiceDesign \
|
||||
--backend openai-audio-speech \
|
||||
--endpoint /v1/audio/speech \
|
||||
--dataset-name seed-tts-design \
|
||||
@@ -80,6 +85,25 @@ vllm bench serve --omni \
|
||||
--save-result --result-dir ./results
|
||||
```
|
||||
|
||||
#### Set the sample rate for models that are not 24 kHz
|
||||
|
||||
Streamed PCM carries no header, so the harness derives `audio_duration` from the
|
||||
byte count and a **default 24000 Hz** assumption
|
||||
(`vllm_omni/metrics/definitions.py:DEFAULT_AUDIO_SAMPLE_RATE`). For a model that
|
||||
emits another rate, export the real one or every duration-derived metric is
|
||||
wrong by `real_sr / 24000`:
|
||||
|
||||
```bash
|
||||
# Audio8 TTS, Fish Speech S2 Pro, Ming-omni-tts, ... all decode at 44.1 kHz
|
||||
export VLLM_OMNI_BENCH_AUDIO_SAMPLE_RATE=44100
|
||||
```
|
||||
|
||||
Without it, a 44.1 kHz model reports `audio_duration` 1.84x too long, so
|
||||
`audio_rtf` looks 1.84x *better* than reality and `audio_underrun` /
|
||||
`Streaming continuity OK rate` are measured against inflated chunk budgets.
|
||||
A quick sanity check: compare `Mean AUDIO_DURATION (s)` against the actual
|
||||
length of a saved WAV for the same prompt.
|
||||
|
||||
#### Streaming continuity (`audio_underrun`)
|
||||
|
||||
`audio_underrun` (seconds) is the per-request worst-case buffer deficit
|
||||
@@ -180,11 +204,11 @@ Outputs TTFP / RTF / throughput curves (and a markdown table) for every
|
||||
|
||||
## Task types
|
||||
|
||||
| Task | Dataset | Request body | Checkpoints that support it |
|
||||
|-----------------|-------------------|-----------------------------------------------------|------------------------------------------|
|
||||
| `voice_clone` | `seed-tts` | `ref_audio` + `ref_text` + `task_type=Base` | `Qwen3-TTS-*-Base`, `VoxCPM2` |
|
||||
| `default_voice` | `seed-tts-text` | `voice=Vivian` + `task_type=CustomVoice` | `Qwen3-TTS-*-CustomVoice` |
|
||||
| `voice_design` | `seed-tts-design` | `instructions=<natural-language description>` + `task_type=VoiceDesign` | `Qwen3-TTS-*-CustomVoice` |
|
||||
| Task | Dataset | Request body | Checkpoints that support it |
|
||||
| --------------- | ----------------- | ----------------------------------------------------------------------- | ----------------------------- |
|
||||
| `voice_clone` | `seed-tts` | `ref_audio` + `ref_text` + `task_type=Base` | `Qwen3-TTS-*-Base`, `VoxCPM2` |
|
||||
| `default_voice` | `seed-tts-text` | `voice=Vivian` + `task_type=CustomVoice` | `Qwen3-TTS-*-CustomVoice` |
|
||||
| `voice_design` | `seed-tts-design` | `instructions=<natural-language description>` + `task_type=VoiceDesign` | `Qwen3-TTS-*-VoiceDesign` |
|
||||
|
||||
**`-CustomVoice` checkpoints do NOT ship `speaker_encoder` weights**, so
|
||||
voice_clone requests raise `ValueError` at model runtime. Use `-Base` for
|
||||
@@ -210,12 +234,12 @@ Then add the model's Deploy YAML under `vllm_omni/deploy/<model>.yaml`
|
||||
|
||||
## Datasets
|
||||
|
||||
| Dataset | Bundled? | Format | Source |
|
||||
|--------------------|----------|-------------------|----------------------------------------------------------------|
|
||||
| `seed-tts-design` | ✅ | 5-field meta.lst | `benchmarks/build_dataset/seed_tts_design/en/meta.lst` (20 prompts) |
|
||||
| `seed_tts_smoke` | ✅ | 4-field meta.lst | `benchmarks/build_dataset/seed_tts_smoke/en/meta.lst` (20 text-only) |
|
||||
| `seed-tts` | ❌ | 4-field meta.lst + WAVs | Google-Drive: [BytedanceSpeech/seed-tts-eval][seedtts] (~1.2 GB) |
|
||||
| `seed-tts-text` | ❌ | 4-field meta.lst | Same archive as `seed-tts` (wav column unused) |
|
||||
| Dataset | Bundled? | Format | Source |
|
||||
| ----------------- | -------- | ----------------------- | -------------------------------------------------------------------- |
|
||||
| `seed-tts-design` | ✅ | 5-field meta.lst | `benchmarks/build_dataset/seed_tts_design/en/meta.lst` (20 prompts) |
|
||||
| `seed_tts_smoke` | ✅ | 4-field meta.lst | `benchmarks/build_dataset/seed_tts_smoke/en/meta.lst` (20 text-only) |
|
||||
| `seed-tts` | ❌ | 4-field meta.lst + WAVs | Google-Drive: [BytedanceSpeech/seed-tts-eval][seedtts] (~1.2 GB) |
|
||||
| `seed-tts-text` | ❌ | 4-field meta.lst | Same archive as `seed-tts` (wav column unused) |
|
||||
|
||||
[seedtts]: https://github.com/BytedanceSpeech/seed-tts-eval
|
||||
|
||||
@@ -227,11 +251,11 @@ For manual voice_clone / default_voice runs against the full corpus, follow
|
||||
|
||||
`tests/dfx/perf/tests/test_tts.json` wires three perf regimes plus quality:
|
||||
|
||||
| eval_phase | concurrency | purpose | Baseline metrics |
|
||||
|---------------|-------------|---------------------------------------------------------|-----------------------------------------|
|
||||
| `latency` | 1 | Single-request TTFP / RTF SLO | `median_audio_ttfp_ms`, `median_audio_rtf` |
|
||||
| `throughput` | 8 | Codec-batching cliff sentinel (PDF #272 concurrency≥8) | `median_audio_ttfp_ms`, `median_audio_rtf` |
|
||||
| `quality` | 4 | WER / SIM / UTMOS regression (disabled in CI by default)| `mean_audio_rtf` |
|
||||
| eval_phase | concurrency | purpose | Baseline metrics |
|
||||
| ------------ | ----------- | -------------------------------------------------------- | ------------------------------------------ |
|
||||
| `latency` | 1 | Single-request TTFP / RTF SLO | `median_audio_ttfp_ms`, `median_audio_rtf` |
|
||||
| `throughput` | 8 | Codec-batching cliff sentinel (PDF #272 concurrency≥8) | `median_audio_ttfp_ms`, `median_audio_rtf` |
|
||||
| `quality` | 4 | WER / SIM / UTMOS regression (disabled in CI by default) | `mean_audio_rtf` |
|
||||
|
||||
Why `median_*` for latency/throughput and `mean_*` for quality: latency
|
||||
distributions have cold-start tails that drag the mean; quality aggregates
|
||||
@@ -245,10 +269,15 @@ PR #2558 — quality runs are manual / release-validation, not nightly).
|
||||
|
||||
Observed on H20-3e, Qwen3-TTS-1.7B (measured pre-merge on this branch):
|
||||
|
||||
| Task | Model | c=1 | c=4 | **c=8** | c=16 | c=32 |
|
||||
|---------------|---------------|--------|--------|------------|--------|--------|
|
||||
| voice_clone | 1.7B-Base | RTF 0.15 / TTFP 165ms | 0.28 / 412ms | **0.49 / 1701ms** | 0.72 / 3355ms | 0.77 / 3772ms |
|
||||
| voice_design | 1.7B-CustomVoice | RTF 0.08 / TTFP 53ms | 0.11 / 154ms | **0.21 / 872ms** | 0.33 / 1801ms | 0.38 / 1989ms |
|
||||
| Task | Model | c=1 | c=4 | **c=8** | c=16 | c=32 |
|
||||
| ------------ | ---------------- | --------------------- | ------------ | ----------------- | ------------- | ------------- |
|
||||
| voice_clone | 1.7B-Base | RTF 0.15 / TTFP 165ms | 0.28 / 412ms | **0.49 / 1701ms** | 0.72 / 3355ms | 0.77 / 3772ms |
|
||||
| voice_design | 1.7B-CustomVoice | RTF 0.08 / TTFP 53ms | 0.11 / 154ms | **0.21 / 872ms** | 0.33 / 1801ms | 0.38 / 1989ms |
|
||||
|
||||
The historical `voice_design` row above lists a CustomVoice checkpoint, which
|
||||
current task validation rejects for VoiceDesign requests. It does not establish
|
||||
a VoiceDesign checkpoint baseline; rerun with `Qwen3-TTS-12Hz-1.7B-VoiceDesign`
|
||||
before comparing performance.
|
||||
|
||||
Both models show a **4–6× TTFP jump from c=4 to c=8** while audio throughput
|
||||
saturates around c=4–8 — the codec-bs=1 bottleneck documented in
|
||||
@@ -257,7 +286,7 @@ sentinel for regressions in this area.
|
||||
|
||||
## File layout
|
||||
|
||||
```
|
||||
```text
|
||||
benchmarks/tts/
|
||||
├── README.md (this file)
|
||||
├── bench_tts.py CLI — serve-mode benchmark driver
|
||||
|
||||
@@ -17,12 +17,23 @@ models:
|
||||
extra_params:
|
||||
lang: en
|
||||
text_normalization: true
|
||||
# Audio8 TTS Preview: DualAR, 44.1 kHz. trust_remote_code stays false.
|
||||
# Bench at 44.1 kHz: export VLLM_OMNI_BENCH_AUDIO_SAMPLE_RATE=44100, otherwise
|
||||
# the streamed-PCM duration defaults to 24 kHz and RTF looks 1.84x too good.
|
||||
Audio8/Audio8-TTS-Preview-0.6b:
|
||||
supported_tasks: [default_voice, voice_clone]
|
||||
backend: openai-audio-speech
|
||||
endpoint: /v1/audio/speech
|
||||
trust_remote_code: false
|
||||
task_extra_body:
|
||||
default_voice: {}
|
||||
voice_clone: {}
|
||||
|
||||
# -CustomVoice checkpoints lack speaker_encoder weights, so voice_clone is
|
||||
# NOT supported (an attempt raises ValueError from _extract_speaker_embedding
|
||||
# at model runtime). Use -Base for voice_clone.
|
||||
Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice:
|
||||
supported_tasks: [default_voice, voice_design]
|
||||
supported_tasks: [default_voice]
|
||||
backend: openai-audio-speech
|
||||
endpoint: /v1/audio/speech
|
||||
trust_remote_code: true
|
||||
@@ -31,6 +42,13 @@ models:
|
||||
voice: Vivian
|
||||
language: English
|
||||
task_type: CustomVoice
|
||||
|
||||
Qwen/Qwen3-TTS-12Hz-1.7B-VoiceDesign:
|
||||
supported_tasks: [voice_design]
|
||||
backend: openai-audio-speech
|
||||
endpoint: /v1/audio/speech
|
||||
trust_remote_code: true
|
||||
task_extra_body:
|
||||
voice_design:
|
||||
task_type: VoiceDesign
|
||||
language: English
|
||||
@@ -132,3 +150,14 @@ models:
|
||||
endpoint: /v1/audio/speech
|
||||
task_extra_body:
|
||||
voice_design: {}
|
||||
|
||||
# OmniVoice serves plain text-to-speech over /v1/audio/speech; voice cloning
|
||||
# and voice design are offline-only for this model, so default_voice is the
|
||||
# only task the server can answer.
|
||||
k2-fsa/OmniVoice:
|
||||
supported_tasks: [default_voice]
|
||||
backend: openai-audio-speech
|
||||
endpoint: /v1/audio/speech
|
||||
trust_remote_code: true
|
||||
task_extra_body:
|
||||
default_voice: {}
|
||||
+6
-23
@@ -1,27 +1,10 @@
|
||||
# Target vllm version: v0.28.0 (tag v0.28.0 @ 2cf0a6915ce5).
|
||||
# Keep VLLM_BASE_TAG aligned with the target vLLM release on every rebase.
|
||||
#
|
||||
# Release-boundary form: there is no precompiled-wheel pin here, because the
|
||||
# base image already ships exactly the target vLLM. Everything the runtime
|
||||
# needs — torch, flashinfer, nvJitLink, numpy — comes from that image as one
|
||||
# self-consistent set.
|
||||
#
|
||||
# This block reappears only when the rebase targets a vLLM commit that is NOT a
|
||||
# released tag. During the 0.27 rebase, targeting v0.27.0rc1 on top of the
|
||||
# v0.26.0 base image required reinstalling vLLM from wheels.vllm.ai and then
|
||||
# hand-repairing the fallout: pinning torch ahead of flashinfer (installing it
|
||||
# after bound flashinfer's TVM FFI runtime to the old torch and aborted engine
|
||||
# core init), registering libnvJitLink.so.13 with ldconfig, and re-pinning numpy
|
||||
# under numba's ceiling. All of it was compensation for a base image and a vLLM
|
||||
# that disagreed. At a real release boundary they agree, so none of it is
|
||||
# needed. The 0.28 rebase repeated the cycle: the v0.28.0rc / final-tag wheels
|
||||
# rode on the v0.27.0 base image with the full reinstall block until the
|
||||
# official v0.28.0 image shipped, at which point this file dropped back to the
|
||||
# form you are reading. If you pin an untagged commit again, restore the
|
||||
# reinstall block — agent/tasks/20_update_wheel_url.sh fails the run rather
|
||||
# than letting the Dockerfile silently ship the wrong vLLM.
|
||||
# Target vLLM version: v0.29.0 (tag @ 98dff2a81d74).
|
||||
# Use the matching release image and its coherent CUDA/PyTorch/FlashInfer
|
||||
# runtime. Do not reinstall a commit wheel or repair dependencies on top of
|
||||
# an older base image at a release boundary. For a future unreleased target,
|
||||
# restore an explicit wheel pin and validate its complete runtime stack.
|
||||
ARG VLLM_BASE_IMAGE=vllm/vllm-openai
|
||||
ARG VLLM_BASE_TAG=v0.28.0
|
||||
ARG VLLM_BASE_TAG=v0.29.0
|
||||
|
||||
# We use registry-based layer caching for the dependencies,
|
||||
# where the tag is determined based on the hash of the contents
|
||||
|
||||
+4
-16
@@ -1,20 +1,7 @@
|
||||
ARG VLLM_ASCEND_IMAGE=quay.io/atlas-ci/vllm-ascend
|
||||
ARG VLLM_ASCEND_TAG=v0.28.0
|
||||
FROM ${VLLM_ASCEND_IMAGE}:${VLLM_ASCEND_TAG}
|
||||
|
||||
# WORKDIR /vllm-workspace/vllm
|
||||
# RUN git fetch origin --tags && git checkout v0.18.0
|
||||
|
||||
# WORKDIR /vllm-workspace/vllm-ascend
|
||||
# RUN git fetch origin releases/v0.18.0 && git checkout d781902ce9dbda8ab1e11bb0f2f0c1bc508fee7a
|
||||
# # Install vllm-ascend
|
||||
# # Append `libascend_hal.so` path (devlib) to LD_LIBRARY_PATH
|
||||
# RUN export PIP_EXTRA_INDEX_URL=https://mirrors.huaweicloud.com/ascend/repos/pypi && \
|
||||
# source /usr/local/Ascend/ascend-toolkit/set_env.sh && \
|
||||
# source /usr/local/Ascend/nnal/atb/set_env.sh && \
|
||||
# export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/Ascend/ascend-toolkit/latest/`uname -i`-linux/devlib && \
|
||||
# python3 -m pip install -v -e /vllm-workspace/vllm-ascend/ --extra-index https://download.pytorch.org/whl/cpu/ && \
|
||||
# python3 -m pip cache purge
|
||||
ARG VLLM_ASCEND_VERSION=v0.28.0
|
||||
ARG VLLM_ASCEND_DEVICE_SUFFIX=
|
||||
FROM ${VLLM_ASCEND_IMAGE}:${VLLM_ASCEND_VERSION}${VLLM_ASCEND_DEVICE_SUFFIX}
|
||||
|
||||
ARG APP_DIR=/vllm-workspace/vllm-omni
|
||||
WORKDIR ${APP_DIR}
|
||||
@@ -24,6 +11,7 @@ RUN export PIP_EXTRA_INDEX_URL=https://mirrors.huaweicloud.com/ascend/repos/pypi
|
||||
source /usr/local/Ascend/ascend-toolkit/set_env.sh && \
|
||||
source /usr/local/Ascend/nnal/atb/set_env.sh && \
|
||||
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/Ascend/ascend-toolkit/latest/`uname -i`-linux/devlib && \
|
||||
python3 -m pip install -v "mindiesd==3.1.0" && \
|
||||
python3 -m pip install -v -e /vllm-workspace/vllm-omni/ --no-build-isolation
|
||||
|
||||
RUN python3 -m pip install -v --no-deps \
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
ARG VLLM_ASCEND_IMAGE=quay.io/atlas-ci/vllm-ascend
|
||||
ARG VLLM_ASCEND_TAG=v0.28.0-a3
|
||||
FROM ${VLLM_ASCEND_IMAGE}:${VLLM_ASCEND_TAG}
|
||||
|
||||
# WORKDIR /vllm-workspace/vllm
|
||||
# RUN git fetch origin --tags && git checkout v0.18.0
|
||||
|
||||
# WORKDIR /vllm-workspace/vllm-ascend
|
||||
# RUN git fetch origin releases/v0.18.0 && git checkout d781902ce9dbda8ab1e11bb0f2f0c1bc508fee7a
|
||||
# # Install vllm-ascend
|
||||
# # Append `libascend_hal.so` path (devlib) to LD_LIBRARY_PATH
|
||||
# RUN export PIP_EXTRA_INDEX_URL=https://mirrors.huaweicloud.com/ascend/repos/pypi && \
|
||||
# source /usr/local/Ascend/ascend-toolkit/set_env.sh && \
|
||||
# source /usr/local/Ascend/nnal/atb/set_env.sh && \
|
||||
# export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/Ascend/ascend-toolkit/latest/`uname -i`-linux/devlib && \
|
||||
# python3 -m pip install -v -e /vllm-workspace/vllm-ascend/ --extra-index https://download.pytorch.org/whl/cpu/ && \
|
||||
# python3 -m pip cache purge
|
||||
|
||||
ARG APP_DIR=/vllm-workspace/vllm-omni
|
||||
WORKDIR ${APP_DIR}
|
||||
COPY . .
|
||||
|
||||
RUN export PIP_EXTRA_INDEX_URL=https://mirrors.huaweicloud.com/ascend/repos/pypi && \
|
||||
source /usr/local/Ascend/ascend-toolkit/set_env.sh && \
|
||||
source /usr/local/Ascend/nnal/atb/set_env.sh && \
|
||||
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/Ascend/ascend-toolkit/latest/`uname -i`-linux/devlib && \
|
||||
python3 -m pip install -v -e /vllm-workspace/vllm-omni/ --no-build-isolation
|
||||
|
||||
RUN python3 -m pip install -v --no-deps \
|
||||
"stepaudio2-minicpmo>=0.1.1" \
|
||||
"step-audio2>=1.0.0"
|
||||
|
||||
ENV VLLM_TARGET_DEVICE=npu
|
||||
ENV VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
|
||||
ENTRYPOINT []
|
||||
@@ -1,10 +1,10 @@
|
||||
ARG VLLM_ASCEND_IMAGE=quay.io/atlas-ci/vllm-ascend
|
||||
ARG VLLM_ASCEND_TAG=v0.28.0
|
||||
FROM ${VLLM_ASCEND_IMAGE}:${VLLM_ASCEND_TAG}
|
||||
ARG VLLM_ASCEND_VERSION=v0.28.0
|
||||
ARG VLLM_ASCEND_DEVICE_SUFFIX=
|
||||
FROM ${VLLM_ASCEND_IMAGE}:${VLLM_ASCEND_VERSION}${VLLM_ASCEND_DEVICE_SUFFIX}
|
||||
|
||||
ARG APP_DIR=/vllm-workspace/vllm-omni
|
||||
WORKDIR ${APP_DIR}
|
||||
COPY . .
|
||||
|
||||
RUN sed -i "s@^\(deb.*\)https\?://[a-z0-9.-]*\.ubuntu\.com@\1http://cache-service.nginx-pypi-cache.svc.cluster.local:8081@g" /etc/apt/sources.list
|
||||
|
||||
@@ -22,11 +22,9 @@ RUN apt-get update && \
|
||||
rm -rf /var/cache/apt/* && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN source /usr/local/Ascend/ascend-toolkit/set_env.sh && \
|
||||
source /usr/local/Ascend/nnal/atb/set_env.sh && \
|
||||
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/Ascend/ascend-toolkit/latest/`uname -i`-linux/devlib && \
|
||||
python3 -m pip install -U pip && \
|
||||
RUN python3 -m pip install -U pip && \
|
||||
python3 -m pip install -v \
|
||||
"mindiesd==3.1.0" \
|
||||
"pytest>=7.0.0" \
|
||||
"pytest-asyncio>=0.21.0" \
|
||||
"pytest-cov>=4.0.0" \
|
||||
@@ -48,7 +46,13 @@ RUN source /usr/local/Ascend/ascend-toolkit/set_env.sh && \
|
||||
"datasets>=3.0.0" \
|
||||
--index-url http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple \
|
||||
--trusted-host cache-service.nginx-pypi-cache.svc.cluster.local \
|
||||
--default-timeout=60 && \
|
||||
--default-timeout=60
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN source /usr/local/Ascend/ascend-toolkit/set_env.sh && \
|
||||
source /usr/local/Ascend/nnal/atb/set_env.sh && \
|
||||
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/Ascend/ascend-toolkit/latest/`uname -i`-linux/devlib && \
|
||||
python3 -m pip install -v -e /vllm-workspace/vllm-omni/ --no-build-isolation \
|
||||
--index-url http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple \
|
||||
--trusted-host cache-service.nginx-pypi-cache.svc.cluster.local \
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
ARG VLLM_ASCEND_IMAGE=quay.io/atlas-ci/vllm-ascend
|
||||
ARG VLLM_ASCEND_TAG=v0.28.0-a3
|
||||
FROM ${VLLM_ASCEND_IMAGE}:${VLLM_ASCEND_TAG}
|
||||
|
||||
ARG APP_DIR=/vllm-workspace/vllm-omni
|
||||
WORKDIR ${APP_DIR}
|
||||
COPY . .
|
||||
|
||||
RUN sed -i "s@^\(deb.*\)https\?://[a-z0-9.-]*\.ubuntu\.com@\1http://cache-service.nginx-pypi-cache.svc.cluster.local:8081@g" /etc/apt/sources.list
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
ffmpeg \
|
||||
libgl1 \
|
||||
libglib2.0-0 \
|
||||
libgomp1 \
|
||||
libsm6 \
|
||||
libxext6 \
|
||||
libxrender1 \
|
||||
libxcb1 \
|
||||
espeak-ng && \
|
||||
rm -rf /var/cache/apt/* && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN source /usr/local/Ascend/ascend-toolkit/set_env.sh && \
|
||||
source /usr/local/Ascend/nnal/atb/set_env.sh && \
|
||||
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/Ascend/ascend-toolkit/latest/`uname -i`-linux/devlib && \
|
||||
python3 -m pip install -U pip && \
|
||||
python3 -m pip install -v \
|
||||
"pytest>=7.0.0" \
|
||||
"pytest-asyncio>=0.21.0" \
|
||||
"pytest-cov>=4.0.0" \
|
||||
"pytest-mock>=3.10.0" \
|
||||
"openpyxl>=3.0.0" \
|
||||
"psutil>=7.2.0" \
|
||||
"soundfile>=0.13.1" \
|
||||
"pyttsx3>=2.99" \
|
||||
"opencc==1.4.1" \
|
||||
"opencv-python>=4.12.0.88" \
|
||||
"imageio[ffmpeg]>=0.6.0" \
|
||||
"jiwer>=3.0.0" \
|
||||
"zhon>=2.0.0" \
|
||||
"zhconv>=1.4.2" \
|
||||
"funasr>=1.0.0" \
|
||||
"hyperpyyaml>=1.2.0" \
|
||||
"onnx>=1.16.0" \
|
||||
"librosa>=0.10.0" \
|
||||
"torchmetrics>=1.9.0" \
|
||||
"datasets>=3.0.0" \
|
||||
--index-url http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple \
|
||||
--trusted-host cache-service.nginx-pypi-cache.svc.cluster.local \
|
||||
--default-timeout=60 && \
|
||||
python3 -m pip install -v -e /vllm-workspace/vllm-omni/ --no-build-isolation \
|
||||
--index-url http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple \
|
||||
--trusted-host cache-service.nginx-pypi-cache.svc.cluster.local \
|
||||
--default-timeout=60
|
||||
|
||||
# MiniCPM-o 4.5 Stage-2 vocoder assets: s3tokenizer plus flashcosyvoice, which
|
||||
# is shipped by step-audio2. --no-deps keeps pip from replacing the Ascend torch
|
||||
# build and from shadowing onnxruntime-cann with the vanilla onnxruntime wheel;
|
||||
# the deps these two actually need are installed above.
|
||||
RUN python3 -m pip install -v --no-deps \
|
||||
"s3tokenizer>=0.3.0" \
|
||||
"step-audio2>=1.0.0" \
|
||||
--index-url http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple \
|
||||
--trusted-host cache-service.nginx-pypi-cache.svc.cluster.local \
|
||||
--default-timeout=60
|
||||
|
||||
ENV VLLM_TARGET_DEVICE=npu
|
||||
ENV VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
|
||||
ENTRYPOINT []
|
||||
+21
-10
@@ -5,14 +5,23 @@ nav:
|
||||
- getting_started/quickstart.md
|
||||
- getting_started/installation/*
|
||||
- Serving:
|
||||
- OpenAI-Compatible API:
|
||||
- Chat Completions API: serving/chat_completions_api.md
|
||||
- Image Generation: serving/image_generation_api.md
|
||||
- Image Edit: serving/image_edit_api.md
|
||||
- Text to Speech: serving/speech_api.md
|
||||
- Videos: serving/videos_api.md
|
||||
- Audio Generation: serving/audio_generate_api.md
|
||||
- Streaming Video Input: serving/video_stream_api.md
|
||||
- API Server:
|
||||
- Overview: serving/README.md
|
||||
- Core APIs:
|
||||
- Chat Completions: serving/chat_completions_api.md
|
||||
- Text to Speech: serving/speech_api.md
|
||||
- Audio Generation: serving/audio_generate_api.md
|
||||
- Image Generation: serving/image_generation_api.md
|
||||
- Image Edit: serving/image_edit_api.md
|
||||
- Videos: serving/videos_api.md
|
||||
- Streaming and Realtime:
|
||||
- Realtime Audio: serving/realtime_api.md
|
||||
- Full Duplex: serving/full_duplex_api.md
|
||||
- Realtime Duplex Reference: serving/realtime_duplex_api.md
|
||||
- Streaming Video Input: serving/video_stream_api.md
|
||||
- Streaming Video Output: serving/streaming_video_output_api.md
|
||||
- OpenPI Robot Policy: serving/openpi_api.md
|
||||
- Standalone Experimental Servers: serving/standalone_servers.md
|
||||
- Examples:
|
||||
- examples/README.md
|
||||
- Offline Inference:
|
||||
@@ -57,13 +66,14 @@ nav:
|
||||
- ModelOpt: user_guide/quantization/modelopt.md
|
||||
- AutoRound: user_guide/quantization/autoround.md
|
||||
- msModelSlim: user_guide/quantization/msmodelslim.md
|
||||
- TorchAO: user_guide/quantization/torchao.md
|
||||
- Diffusion Acceleration:
|
||||
- Overview and Compatibility:
|
||||
- Overview: user_guide/diffusion_features.md
|
||||
- Feature Compatibility: user_guide/feature_compatibility.md
|
||||
- CPU Offloading:
|
||||
- Overview: user_guide/diffusion/cpu_offload.md
|
||||
- Model-Level: user_guide/diffusion/offloader/cpu_offload.md
|
||||
- Model-Level: user_guide/diffusion/offloader/module_offload.md
|
||||
- Layerwise: user_guide/diffusion/offloader/layerwise_offload.md
|
||||
- Distributed Layerwise: user_guide/diffusion/offloader/distributed_layerwise_offload.md
|
||||
- Cache Acceleration:
|
||||
@@ -117,6 +127,7 @@ nav:
|
||||
- Async Chunk: design/feature/async_chunk.md
|
||||
- Async Diffusion Output: design/feature/async_diffusion_output.md
|
||||
- Async Omni Output Materialization: design/feature/omni_async_output_materialization.md
|
||||
- Prefill/Decode Phase Contract: design/feature/preprocess_phase_contract.md
|
||||
- Automatic Prefix Caching: design/feature/prefix_caching.md
|
||||
- Realtime AR-Diffusion: design/feature/realtime_ar_diffusion.md
|
||||
- Communication:
|
||||
@@ -142,7 +153,7 @@ nav:
|
||||
- Skip-Softmax: design/feature/skip_softmax.md
|
||||
- CPU Offloading:
|
||||
- Overview: design/feature/offloader/README.md
|
||||
- Model-Level: design/feature/offloader/cpu_offload.md
|
||||
- Model-Level: design/feature/offloader/module_offload.md
|
||||
- Layerwise: design/feature/offloader/layerwise_offload.md
|
||||
- Distributed Layerwise: design/feature/offloader/distributed_layerwise_offload.md
|
||||
- Cache-DiT: design/feature/cache_dit.md
|
||||
|
||||
+4
-5
@@ -23,7 +23,6 @@ Easy, fast, and cheap omni-modality model serving for everyone
|
||||
<a class="github-button" href="https://github.com/vllm-project/vllm-omni/fork" data-show-count="true" data-icon="octicon-repo-forked" data-size="large" aria-label="Fork">Fork</a>
|
||||
</p>
|
||||
|
||||
|
||||
## About
|
||||
|
||||
[vLLM](https://github.com/vllm-project/vllm) was originally designed to support large language models for text-based autoregressive generation tasks. vLLM-Omni is a framework that extends its support for omni-modality model inference and serving:
|
||||
@@ -52,14 +51,14 @@ vLLM-Omni is flexible and easy to use with:
|
||||
- Tensor, pipeline, data and expert parallelism support for distributed inference
|
||||
- Streaming outputs
|
||||
- OpenAI-compatible API server
|
||||
- Full-duplex realtime serving with streaming audio input and output (experimental)
|
||||
- [Full-duplex realtime serving](serving/realtime_duplex_api.md) with streaming audio input and output
|
||||
|
||||
vLLM-Omni seamlessly supports most popular open-source models on HuggingFace, including:
|
||||
|
||||
- Omni-modality models (e.g. Qwen3-Omni, MiniCPM-o 4.5, Cosmos3, HunyuanImage, BAGEL)
|
||||
- TTS models (e.g. Qwen3-TTS, VoxCPM2, Ming-Omni-TTS, CosyVoice3)
|
||||
- Diffusion models — image, video, and audio generation (e.g. MiniMax H3, Qwen-Image, Wan2.2, FLUX)
|
||||
- Robot-policy and action models (e.g. GR00T-N1.7, DreamZero-DROID, InternVLA-A1, Cosmos3 action policy)
|
||||
- TTS models (e.g. Qwen3-TTS, IndexTTS 2.5, dots.tts, CosyVoice3)
|
||||
- Diffusion models — image, video, and audio generation (e.g. MiniMax H3, LTX-2.5, SANA-Video, Wan2.2)
|
||||
- Robot-policy and action models (e.g. π0, GR00T-N1.7, DreamZero-DROID, InternVLA-A1)
|
||||
|
||||
For more information, checkout the following:
|
||||
|
||||
|
||||
@@ -15,6 +15,36 @@ Main entry points for vLLM-Omni inference and serving.
|
||||
- [vllm_omni.entrypoints.omni_base.OmniBase][]
|
||||
- [vllm_omni.entrypoints.pd_utils.PDDisaggregationMixin][]
|
||||
|
||||
## Clients
|
||||
|
||||
Client libraries that talk to a running vLLM-Omni server (the full-duplex Realtime client and its per-model session presets).
|
||||
|
||||
- [vllm_omni.clients.duplex.AudioDelta][]
|
||||
- [vllm_omni.clients.duplex.AudioFormat][]
|
||||
- [vllm_omni.clients.duplex.ConnectionResumed][]
|
||||
- [vllm_omni.clients.duplex.DuplexClient][]
|
||||
- [vllm_omni.clients.duplex.DuplexClientError][]
|
||||
- [vllm_omni.clients.duplex.DuplexConnectionError][]
|
||||
- [vllm_omni.clients.duplex.DuplexEvent][]
|
||||
- [vllm_omni.clients.duplex.DuplexProtocolError][]
|
||||
- [vllm_omni.clients.duplex.DuplexSessionClosedError][]
|
||||
- [vllm_omni.clients.duplex.ErrorEvent][]
|
||||
- [vllm_omni.clients.duplex.EventCollector][]
|
||||
- [vllm_omni.clients.duplex.ListenDecision][]
|
||||
- [vllm_omni.clients.duplex.ReconnectPolicy][]
|
||||
- [vllm_omni.clients.duplex.ResponseCreated][]
|
||||
- [vllm_omni.clients.duplex.ResponseDone][]
|
||||
- [vllm_omni.clients.duplex.ResponseHandle][]
|
||||
- [vllm_omni.clients.duplex.SessionClosed][]
|
||||
- [vllm_omni.clients.duplex.SessionConfig][]
|
||||
- [vllm_omni.clients.duplex.SessionCreated][]
|
||||
- [vllm_omni.clients.duplex.SessionExpired][]
|
||||
- [vllm_omni.clients.duplex.SessionResumed][]
|
||||
- [vllm_omni.clients.duplex.SpeakDecision][]
|
||||
- [vllm_omni.clients.duplex.TextDelta][]
|
||||
- [vllm_omni.clients.duplex.TranscriptDelta][]
|
||||
- [vllm_omni.clients.duplex.WebSocketTransport][]
|
||||
|
||||
## Inputs
|
||||
|
||||
Input data structures for multi-modal inputs.
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 274 KiB After Width: | Height: | Size: 272 KiB |
+115
-7
@@ -1,11 +1,15 @@
|
||||
# vLLM-Omni Benchmark CLI Guide
|
||||
|
||||
The vllm bench command launches the vLLM-Omni benchmark to evaluate the performance of multimodal models.
|
||||
|
||||
## Notes
|
||||
|
||||
vLLM-Omni registers the `openai-chat-omni`, `openai-audio-speech`, `openai-image-edits-omni`, `daily-omni`, and `openai-realtime-duplex` serving benchmark backends. It also adds the `omniinteract` dataset.
|
||||
|
||||
## Basic Parameter Description
|
||||
|
||||
You can use `vllm bench serve --omni --help=all` to get descriptions of all parameters. The commonly used parameters are described below:
|
||||
|
||||
- `--omni`
|
||||
Enable Omni (multimodal) mode, supporting multimodal inputs and outputs such as images, videos, and audio.
|
||||
|
||||
@@ -77,8 +81,8 @@ Specify to save benchmark results to a json file
|
||||
- `--random-prefix-len`
|
||||
Number of fixed prefix tokens before the random context in a request.
|
||||
The total input length is the sum of random-prefix-len and a random
|
||||
context length sampled from [input_len * (1 - range_ratio),
|
||||
input_len * (1 + range_ratio)].Only the random and random-mm modes
|
||||
context length sampled from [input_len *(1 - range_ratio),
|
||||
input_len* (1 + range_ratio)].Only the random and random-mm modes
|
||||
support this parameter.
|
||||
|
||||
- `--random-input-len`
|
||||
@@ -91,7 +95,7 @@ Specify to save benchmark results to a json file
|
||||
Range ratio for sampling input/output length,
|
||||
used only for random sampling. Must be in the range [0, 1) to define
|
||||
a symmetric sampling range
|
||||
[length * (1 - range_ratio), length * (1 + range_ratio)].
|
||||
[length *(1 - range_ratio), length* (1 + range_ratio)].
|
||||
Only the random and random-mm modes support this parameter.
|
||||
|
||||
- `--random-mm-base-items-per-request`
|
||||
@@ -137,6 +141,7 @@ Specify to save benchmark results to a json file
|
||||
## Usage Examples
|
||||
|
||||
### Online Benchmark
|
||||
|
||||
<details class="admonition abstract" markdown="1">
|
||||
<summary>Show more</summary>
|
||||
|
||||
@@ -162,7 +167,9 @@ vllm bench serve \
|
||||
--dataset-path ShareGPT_V3_unfiltered_cleaned_split.json \
|
||||
--percentile-metrics ttft,tpot,itl,e2el
|
||||
```
|
||||
|
||||
If successful, you will see the following output:
|
||||
|
||||
```text
|
||||
============ Serving Benchmark Result ============
|
||||
Successful requests: 2
|
||||
@@ -264,6 +271,7 @@ Median AUDIO_RTF: 0.47
|
||||
P99 AUDIO_RTF: 0.48
|
||||
==================================================
|
||||
```
|
||||
|
||||
Notes:
|
||||
We use audio generation time / audio duration to calculate RTF.
|
||||
|
||||
@@ -301,12 +309,36 @@ options apply. Use `--omniinteract-require-response` only for functional E2E cas
|
||||
Each completed case writes `output.wav`, `wav_transcript.json`, `events.json`, `result.json`, and a final `.done` marker under
|
||||
`--omniinteract-output-dir`. The root also contains `batch_summary.json` and `official_eval_manifest.jsonl`; failed cases write
|
||||
`.failed.json`. Runs sharing one output root are serialized. Completion validates transport, response lifecycle, and artifacts,
|
||||
not answer accuracy. Transcript timestamps are serialized playback-queue times. Clipped or cancelled outputs are ineligible and
|
||||
omitted from the official manifest; `audio_clipped_bytes` records output beyond the rounded video horizon.
|
||||
not answer accuracy. Transcript timestamps are serialized playback-queue times. Playback ACKs report cumulative progress
|
||||
incrementally along that serialized clock, like a live listener; the first ack for a response goes out as soon as its audio
|
||||
arrives, checkpointing the response's history position so a later committed user input updates it in place. A residual
|
||||
`playback_ack_too_late` rejection is recorded as an artifact warning rather than failing the case. Clipped or cancelled
|
||||
outputs are ineligible and omitted from the official manifest; `audio_clipped_bytes` records output beyond the rounded video
|
||||
horizon.
|
||||
|
||||
TTFT, TTFP, and RTF start at client receipt of `response.created`. TPOT/ITL use engine stage-0 timing; ITL is emitted only when
|
||||
every token interval is present.
|
||||
|
||||
The checked-in local performance configuration measures four deterministic cases from each OmniInteract subset (12 videos
|
||||
total), with no benchmark warmups and a maximum concurrency of two. Each subset also sends one readiness request before its
|
||||
measured cases. Run it from the repository root with:
|
||||
|
||||
```bash
|
||||
export HF_HOME=/path/to/persistent/huggingface-cache
|
||||
export BENCHMARK_DIR=tests/dfx/perf/results
|
||||
bash tools/nightly/run_nightly_jobs.sh \
|
||||
--test-type local \
|
||||
--model-type omni \
|
||||
--label-substr minicpmo_4_5_omniinteract
|
||||
```
|
||||
|
||||
The first run downloads the pinned OmniInteract archive into `HF_HOME`; later runs reuse that cache.
|
||||
|
||||
It requires every case to commit its input, complete any emitted response lifecycles, and publish the expected WAV,
|
||||
transcript, event, and result artifacts without errors. A valid LISTEN-only case may have no response audio or transcript
|
||||
chunks. Official-manifest eligibility is reported separately because clipped or cancelled output is a benchmark-quality
|
||||
signal, not a transport failure. This local performance test does not score answer accuracy.
|
||||
|
||||
### Multi-Modal Benchmark
|
||||
|
||||
<details class="admonition abstract" markdown="1">
|
||||
@@ -319,8 +351,8 @@ Generate synthetic image、video、audio inputs alongside random text prompts to
|
||||
Notes:
|
||||
|
||||
- Works only with online benchmark via:
|
||||
- the OpenAI chat backend (`--backend openai-chat-omni`) and endpoint `/v1/chat/completions`.
|
||||
- the OpenAI edit image backend (`--backend openai-image-edits-omni`) and endpoint `/v1/images/edits`.
|
||||
- the OpenAI chat backend (`--backend openai-chat-omni`) and endpoint `/v1/chat/completions`.
|
||||
- the OpenAI edit image backend (`--backend openai-image-edits-omni`) and endpoint `/v1/images/edits`.
|
||||
|
||||
Start the server (example):
|
||||
|
||||
@@ -331,6 +363,7 @@ vllm serve Qwen/Qwen2.5-Omni-7B --omni
|
||||
It is recommended to use the flag `--ignore-eos` to simulate real responses. You can set the size of the output via the arg `random-output-len`.
|
||||
|
||||
Then run the benchmarking script:
|
||||
|
||||
```bash
|
||||
vllm bench serve \
|
||||
--omni \
|
||||
@@ -400,8 +433,79 @@ How sampling works:
|
||||
- If a modality (e.g., image) reaches its limit from `--random-mm-limit-mm-per-prompt`, all buckets of that modality are excluded and the remaining bucket probabilities are renormalized before continuing.
|
||||
This should be seen as an edge case, and if this behavior can be avoided by setting `--random-mm-limit-mm-per-prompt` to a large number. Note that this might result in errors due to engine config `--limit-mm-per-prompt`.
|
||||
- The resulting request contains synthetic image data in `multi_modal_data` (OpenAI Chat format). When `random-mm` is used with the OpenAI Chat backend, prompts remain text and MM content is attached via `multi_modal_data`.
|
||||
|
||||
</details>
|
||||
|
||||
### Image / video generation
|
||||
|
||||
For `/v1/images/generations`, `/v1/images/edits`, and `/v1/videos`, set `--endpoint` to that path and omit `--backend`. The endpoint path is registered as the bench request adapter, so a separate named backend is not required.
|
||||
|
||||
Example (`/v1/images/generations`):
|
||||
|
||||
```bash
|
||||
vllm bench serve --omni \
|
||||
--endpoint /v1/images/generations \
|
||||
--dataset-name random \
|
||||
--model ~/models/Qwen/Qwen-Image \
|
||||
--tokenizer ~/models/Qwen/Qwen-Image/tokenizer \
|
||||
--max-concurrency 1 \
|
||||
--num-warmups 2 \
|
||||
--num-prompts 10 \
|
||||
--random-input-len 64 \
|
||||
--random-output-len 1 \
|
||||
--ignore-eos \
|
||||
--percentile-metrics e2el \
|
||||
--extra-body '{
|
||||
"num_inference_steps": 20,
|
||||
"seed": 42,
|
||||
"true_cfg_scale": 4.0
|
||||
}'
|
||||
```
|
||||
|
||||
If successful, following output is like:
|
||||
|
||||
```text
|
||||
============ Serving Benchmark Result ============
|
||||
Successful requests: 3
|
||||
Failed requests: 0
|
||||
Maximum request concurrency: 1
|
||||
Benchmark duration (s): 8.42
|
||||
Request throughput (req/s): 0.36
|
||||
Peak concurrent requests: 2.00
|
||||
-------------------Peak Memory--------------------
|
||||
Mean PEAK_MEMORY_MB (MB): 58832.00
|
||||
Median PEAK_MEMORY_MB (MB): 58832.00
|
||||
P99 PEAK_MEMORY_MB (MB): 58832.00
|
||||
----------------End-to-end Latency----------------
|
||||
Mean E2EL (ms): 2798.16
|
||||
Median E2EL (ms): 2799.83
|
||||
P99 E2EL (ms): 2800.03
|
||||
================== Image Result ==================
|
||||
Total images generated: 3
|
||||
Image throughput (img/s): 0.36
|
||||
Average pixels per image: 1048576.00
|
||||
Mean denoise step latency (ms): 136.59
|
||||
---------------- Image Generation ----------------
|
||||
Mean IMAGE_GENERATION (ms): 2731.84
|
||||
Median IMAGE_GENERATION (ms): 2732.02
|
||||
P99 IMAGE_GENERATION (ms): 2734.65
|
||||
==================================================
|
||||
```
|
||||
|
||||
Use the same pattern with `--endpoint /v1/images/edits` or `--endpoint /v1/videos` (and model-specific `--extra-body` as needed). Pure image/video runs omit the Text Result section when there is no generated text.
|
||||
|
||||
`/v1/images/edits` defaults to **non-streaming JSON** (`stream=false`) so single-stage edit models are not rejected by the server. For multi-stage pipelines that need SSE (AR TTFT / image chunks), pass `"stream": true` in `--extra-body`.
|
||||
|
||||
`/v1/videos` is an async job API: the client creates a job, then polls until `completed`/`failed`. Measured **e2el** therefore includes client poll sleep and any overshoot after the job actually finishes. Tune polling via `--extra-body`:
|
||||
|
||||
- `poll_interval_s` (default `2.0`): sleep between status polls
|
||||
- `poll_timeout_s` (default `21600`, i.e. 6 hours): give up waiting for the job
|
||||
|
||||
**VIDEO_RTF** prefers server-reported generation time when available:
|
||||
|
||||
- `video_rtf = video_generation_time_ms / 1000 / video_duration`, where `video_generation_time_ms` comes from response `stage_durations` (or `inference_time_s` as fallback)
|
||||
- If generation time is missing, falls back to `e2el / video_duration` (this fallback *does* include poll overhead)
|
||||
|
||||
### Multi-Stage Benchmark
|
||||
|
||||
<details class="admonition abstract" markdown="1">
|
||||
@@ -414,6 +518,7 @@ vllm serve --omni --port 29999 --model ~/Qwen3-Omni-30B-A3B-Instruct
|
||||
```
|
||||
|
||||
Then run the benchmarking script (remember to add the flag `--print-stage`):
|
||||
|
||||
```bash
|
||||
vllm bench serve --omni \
|
||||
--dataset-name random \
|
||||
@@ -433,6 +538,7 @@ vllm bench serve --omni \
|
||||
```
|
||||
|
||||
Besides the "Serving Benchmark Result", there will be "Stage Benchmark Result" below:
|
||||
|
||||
```text
|
||||
============= Stage Benchmark Result =============
|
||||
=============== Stage 0 (thinker) ================
|
||||
@@ -489,7 +595,9 @@ Mean AUDIO_RTF: 0.24
|
||||
Median AUDIO_RTF: 0.26
|
||||
P99 AUDIO_RTF: 0.27
|
||||
```
|
||||
|
||||
Explanation:
|
||||
|
||||
- stage_gen_time: Time from submitting a request to a specific stage to that stage finishing generation.
|
||||
|
||||
- Serving TTFC (Time to First Chunk): Time from the HTTP request being accepted by the serving frontend to
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
This section lists the most common options for running vLLM-Omni.
|
||||
|
||||
For options within a vLLM Engine, please refer to the [vLLM 0.26 configuration guide](https://docs.vllm.ai/en/v0.26.0/configuration/index.html).
|
||||
For options within a vLLM Engine, please refer to the [vLLM 0.29 configuration guide](https://docs.vllm.ai/en/v0.29.0/configuration/index.html).
|
||||
|
||||
Each model defines fixed topology in a registered `PipelineConfig` and runtime
|
||||
overrides in a deploy YAML.
|
||||
|
||||
@@ -151,7 +151,7 @@ their keys only.
|
||||
## Inherited vLLM variables
|
||||
|
||||
vLLM-Omni also reads variables through its aligned vLLM dependency. Refer to
|
||||
the [vLLM 0.26 environment-variable reference](https://docs.vllm.ai/en/v0.26.0/configuration/env_vars.html)
|
||||
the [vLLM 0.29 environment-variable reference](https://docs.vllm.ai/en/v0.29.0/configuration/env_vars.html)
|
||||
for their definitions. This includes vLLM launch, cache, logging, plugin, ROCm,
|
||||
XPU, ModelScope, and FlashInfer workspace settings.
|
||||
|
||||
@@ -179,7 +179,7 @@ collection, examples, bug reports, or logs.
|
||||
|
||||
## Model-specific variables
|
||||
|
||||
The audit found 56 variables read by a single model or pipeline family. They are
|
||||
The audit found 58 variables read by a single model or pipeline family. They are
|
||||
not listed as public usage options here because doing so would turn implementation
|
||||
escape hatches into an accidental compatibility contract.
|
||||
|
||||
@@ -188,7 +188,7 @@ Every audited model-specific name has a migration disposition in the
|
||||
|
||||
| Disposition | Count | Required outcome |
|
||||
| --- | ---: | --- |
|
||||
| Promote | 33 | Move a stable setting into typed stage or model configuration. |
|
||||
| Promote | 35 | Move a stable setting into typed stage or model configuration. |
|
||||
| Request scope | 6 | Move request-varying behavior into a declared request-option schema. |
|
||||
| External | 0 | Retain only when a supported third-party library owns the contract. |
|
||||
| Internalize | 11 | Keep a debug or diagnostic switch out of public documentation and configuration. |
|
||||
|
||||
@@ -262,6 +262,28 @@ or serving, explain the required L2/L3 environment and provide the exact command
|
||||
|
||||
Repository skills guide the agent, but they do not replace contributor judgment, the accepted issue or RFC, required test evidence, or maintainer review. Review all generated changes before submitting them.
|
||||
|
||||
## AI Usage Policy
|
||||
|
||||
AI tools are welcome in vLLM-Omni contributions. Contributors, including maintainers, remain responsible for everything they submit. These requirements apply to pull requests, issues, discussions, and review comments:
|
||||
|
||||
- **Disclose AI assistance.** Name the tools used and describe their role in the submission, including assistance with code, tests, documentation, or writing.
|
||||
- **Understand your changes.** Review every generated change and be able to explain its behavior, design choices, and interaction with vLLM-Omni without relying on AI to answer for you.
|
||||
- **Validate before submitting.** Run relevant checks and report actual results. For model, kernel, or performance changes, include the hardware, model, configuration, and commands needed to reproduce accuracy or benchmark evidence. Clearly identify checks you could not run; generated claims are not test results.
|
||||
- **Keep communication useful.** Verify technical claims, search for existing reports, and edit AI-assisted text for accuracy and brevity. Submit only content you have personally reviewed, and take responsibility for responding to review feedback.
|
||||
- **Label generated media.** Model-generated images, video, and audio are valid examples or test artifacts for this multimodal project. Identify them as generated output and provide the model, inputs, and reproduction settings. Do not present them as ground-truth references or independent evidence of correctness.
|
||||
|
||||
For example, a PR description could include:
|
||||
|
||||
```text
|
||||
AI assistance: Used Codex to draft the regression test and PR description.
|
||||
I reviewed the changes and verified the test reproduces the reported failure.
|
||||
Validation: <commands, results, hardware, and any checks not run>.
|
||||
```
|
||||
|
||||
Maintainer review time is limited. Contributions that are unverified, misleading, or beyond the author's understanding may be closed. Repeated violations may lead to contribution restrictions. New contributors are welcome to ask for help and submit small, well-understood changes.
|
||||
|
||||
This policy is adapted from [Ghostty's AI Usage Policy](https://github.com/ghostty-org/ghostty/blob/main/AI_POLICY.md) for vLLM-Omni's contribution and multimodal validation workflows. The [coding-agent workflow](#using-repository-skills-with-coding-agents) provides practical guidance for applying it.
|
||||
|
||||
## Issues
|
||||
|
||||
If you encounter a bug or have a feature request, please search existing issues first to see if it has already been reported. If not, please file a new issue, providing as much relevant information as possible.
|
||||
|
||||
@@ -57,13 +57,11 @@ Canonical layout (prefer these paths for new changes):
|
||||
| Bootstrap vs test YAML | **Bootstrap** (`pipeline*.yml`) builds images and uploads **child** test pipelines. **Test** YAML (`test-*.yml`) lists pytest steps only. |
|
||||
| Register CI YAML in `skip_ci.py` | If you add a new whitelisted test pipeline file, update `L2_YAML_FILES`, `L3_YAML_FILES`, or `L45_YAML_FILES` in `.buildkite/common/scripts/skip_ci.py` so skip-ci paths stay correct. |
|
||||
|
||||
There are still **legacy copies** at `.buildkite/*.yaml` (without the `cuda/` prefix). Treat `.buildkite/cuda/*` as source of truth.
|
||||
|
||||
## Platform comparison
|
||||
|
||||
| Platform | Bootstrap entry | Test job files | Upload mechanism | Job hardware in YAML |
|
||||
| -------- | ---------------- | -------------- | ---------------- | -------------------- |
|
||||
| **CUDA** | `cuda/pipeline.yml` | `test-ready.yml`, `test-merge.yml`, `test-nightly.yml`, `test-weekly.yml` | `upload_pipeline.py --upload` (expands uploader-only keys) | `mirror_hardwares: <preset>` (string) |
|
||||
| **CUDA** | `cuda/pipeline.yml` | `test-ready.yml`, `test-merge.yml`, `test-nightly.yml`, `test-weekly.yml` | `upload_pipeline.py --upload` (expands uploader-only keys) | omit `mirror_hardwares` (from `-m`) / preset string + `MIRROR_HW` |
|
||||
| **NPU** | `npu/pipeline-npu.yml` | `test-npu-ready.yml`, `test-npu-nightly.yml` | `upload_pipeline.py --upload` | `mirror_hardwares: a2b3_npu_1` / `a2b3_npu_4` / `a3_npu_2` |
|
||||
| **AMD** | `amd/scripts/bootstrap-amd-omni.sh` | `test-amd-ready.yml`, `test-amd-merge.yml` | Jinja (`test-template-amd-omni.j2`) → `pipeline upload` | `agent_pool` + `mirror_hardwares: [amdproduction]` (array, template filter) |
|
||||
| **Intel** | `intel/scripts/bootstrap-intel-omni.sh` | `intel/pipeline-intel.yml` (steps inline) | Direct `pipeline upload` | Inline `agents.queue` on each step |
|
||||
@@ -92,7 +90,23 @@ There are still **legacy copies** at `.buildkite/*.yaml` (without the `cuda/` pr
|
||||
|
||||
**Upload:** `upload_pipeline.py --upload` expands uploader-only keys before Buildkite upload.
|
||||
|
||||
**Hardware in YAML:** `mirror_hardwares: <preset>` (string)—preset names in [`common/ci_mirror_hardwares.yml`](https://github.com/vllm-project/vllm-omni/blob/main/.buildkite/common/ci_mirror_hardwares.yml). Do **not** set `agents` / `plugins` on the same step.
|
||||
**Hardware in YAML:** omit `mirror_hardwares` to compose `{chip}_{n}` from pytest `-m` (SKU + `cards_n`). Several positive `cards_*` take the **max** n (`cards_2 and cards_3` → `{chip}_3`). `not cards_1` with no positive `cards_*` uses that chip's **highest existing** preset in `ci_mirror_hardwares.yml` (L4 → `l4_4`, H100 → `h100_4`, B200 → `b200_4`). Set a **preset string** to force a pool (marks are ignored). Do **not** set `agents` / `plugins` on the same step as `mirror_hardwares`. `MIRROR_HW=b200` remaps inferred jobs whose `-m` includes `B200`, and omits CUDA `h100_*` / `l4_*` strings. Jobs whose `-m` does not match (unset: no H100/L4; set: no B200) are **skipped**.
|
||||
|
||||
```yaml
|
||||
# inferred: H100+B200 + cards_2 → h100_2, or b200_2 when MIRROR_HW=b200
|
||||
commands:
|
||||
- pytest -sv tests/e2e -m "full_model and H100 and B200 and omni and cards_2"
|
||||
# no mirror_hardwares
|
||||
|
||||
# inferred: not cards_1 (no positive cards_*) → that chip's highest pool (l4_4 / h100_4 / b200_4)
|
||||
commands:
|
||||
- pytest -sv tests/e2e -m "full_model and diffusion and L4 and not cards_1"
|
||||
|
||||
# forced preset (ignores -m SKU/cards for pool selection)
|
||||
mirror_hardwares: h100_1
|
||||
```
|
||||
|
||||
Inferred form: unset/`empty` `MIRROR_HW` matches `H100` or `L4` in `-m` (both present → H100); no match skips the step. `MIRROR_HW=b200` must appear in `-m` or the step is skipped. The uploader does **not** rewrite pytest `-m`; B200 collection comes from the YAML expression and from tests that declare `B200` in `hardware_test` / `hardware_marks` (for example `res={"cuda": ["H100", "B200"]}`). `MIRROR_HW` must be empty or `b200` (case-insensitive); unknown values (for example `b20o`) **fail the upload**. A CUDA preset string such as `mirror_hardwares: h100_4` is skipped when `MIRROR_HW=b200`. Unset `MIRROR_HW` keeps string presets. NPU presets ignore `MIRROR_HW`. A `mirror_hardwares` name that is not a key in `ci_mirror_hardwares.yml` **fails the upload**.
|
||||
|
||||
**Conventions**
|
||||
|
||||
@@ -103,9 +117,9 @@ There are still **legacy copies** at `.buildkite/*.yaml` (without the `cuda/` pr
|
||||
|
||||
**Adding a job**
|
||||
|
||||
1. Pick the level file (ready / merge / nightly / weekly).
|
||||
1. Pick the level file (`test-ready.yml` / `test-merge.yml` / `test-nightly.yml` / `test-weekly.yml`).
|
||||
2. Add a step under the right **group** (usually **E2E Test** for model pytest).
|
||||
3. Set `label`, `commands`, `mirror_hardwares`, `depends_on: upload-<level>-pipeline`.
|
||||
3. Set `label`, `commands`, `mirror_hardwares`, `depends_on: upload-ready-pipeline` (or `upload-merge-pipeline` / `upload-nightly-pipeline` / `upload-weekly-pipeline`).
|
||||
4. For L2/L3 E2E, add `source_file_dependencies` (pytest + model + deploy YAML prefixes).
|
||||
5. Dry-run:
|
||||
|
||||
@@ -115,7 +129,7 @@ There are still **legacy copies** at `.buildkite/*.yaml` (without the `cuda/` pr
|
||||
|
||||
=== "NPU"
|
||||
|
||||
**Bootstrap:** [`npu/pipeline-npu.yml`](https://github.com/vllm-project/vllm-omni/blob/main/.buildkite/npu/pipeline-npu.yml) + [`npu/bootstrap-upload-steps.yml`](https://github.com/vllm-project/vllm-omni/blob/main/.buildkite/npu/bootstrap-upload-steps.yml)—same split as CUDA; builds A2/B3 and A3 CI images, then uploads child test pipelines.
|
||||
**Bootstrap:** [`npu/pipeline-npu.yml`](https://github.com/vllm-project/vllm-omni/blob/main/.buildkite/npu/pipeline-npu.yml) + [`npu/bootstrap-upload-steps.yml`](https://github.com/vllm-project/vllm-omni/blob/main/.buildkite/npu/bootstrap-upload-steps.yml)—same split as CUDA; builds A2/B3, A3, A5, and 310P CI images, then uploads child test pipelines.
|
||||
|
||||
**Test YAML:** `npu/test-npu-ready.yml` (L2), `test-npu-nightly.yml` (L4).
|
||||
|
||||
@@ -123,7 +137,12 @@ There are still **legacy copies** at `.buildkite/*.yaml` (without the `cuda/` pr
|
||||
|
||||
**Upload:** same as CUDA—`upload_pipeline.py --upload`.
|
||||
|
||||
**Hardware in YAML:** `mirror_hardwares` preset (string), expanded to `agents`, top-level `image`, and `plugins`. Presets: `a2b3_npu_1`, `a2b3_npu_4`, `a3_npu_2` in `common/ci_mirror_hardwares.yml`.
|
||||
**Hardware in YAML:** `mirror_hardwares` preset (string), expanded to `agents`, top-level `image`, and `plugins`. NPU presets in `common/ci_mirror_hardwares.yml` are:
|
||||
|
||||
- A2: `a2b3_npu_1`, `a2b3_npu_4`, `a2b3_npu_8`
|
||||
- A3: `a3_npu_2`, `a3_npu_4`, `a3_npu_8`, `a3_npu_16`
|
||||
- A5: `a5_npu_2`, `a5_npu_4`, `a5_npu_8`, `a5_npu_16`
|
||||
- 310P: `310p_npu_1`, `310p_npu_2`, `310p_npu_4`
|
||||
|
||||
**Conventions**
|
||||
|
||||
@@ -147,7 +166,9 @@ There are still **legacy copies** at `.buildkite/*.yaml` (without the `cuda/` pr
|
||||
|
||||
**Bootstrap:** [`amd/scripts/bootstrap-amd-omni.sh`](https://github.com/vllm-project/vllm-omni/blob/main/.buildkite/amd/scripts/bootstrap-amd-omni.sh)—skip-ci, diff filtering, Jinja render, then `buildkite-agent pipeline upload`.
|
||||
|
||||
**Test YAML (data):** `amd/test-amd-ready.yml` (L2 / PR), `test-amd-merge.yml` (L3 / main).
|
||||
**Test YAML (data):** `amd/test-amd-ready.yml` (L2 / `ready`), `test-amd-merge.yml` (L3 / `merge-test` and `main`). On a PR carrying both labels, AMD combines both suites behind one image build. PR builds without either tier label retain the legacy L2 fallback for compatibility with the `amd-test` trigger. `DEBUG_TEST_YAML` remains an explicit override. AMD has no L4 file yet, so `nightly-test` does not select an additional suite.
|
||||
|
||||
**Trigger boundary:** AMD label handling has two layers. First, the external `vllm-omni-amd-ci` Buildkite pipeline condition decides whether a GitHub label event creates a build. Only after that build starts does this repository's bootstrap inspect all current PR labels and select L2, L3, or both. Repository-side selection therefore cannot make a `merge-test` event start AMD CI by itself. The external condition must admit both `ready` and `merge-test`, while preserving the legacy `amd-test` trigger and non-PR `main` / scheduled builds. For PR builds without a `DEBUG_TEST_YAML` override, an unreadable label set fails the bootstrap instead of silently selecting the wrong tier. Debug overrides skip label lookup and are validated directly; invalid values, including whitespace-only values, fail the bootstrap. Keep `nightly-test` out of the AMD condition until an AMD L4 suite exists; otherwise it would start a build without providing nightly coverage.
|
||||
|
||||
**Rendering:** [`test-template-amd-omni.j2`](https://github.com/vllm-project/vllm-omni/blob/main/.buildkite/amd/test-template-amd-omni.j2) wraps data steps with `amd-build` image build and `amd_<agent_pool>` queues. Do **not** hand-edit generated `pipeline.yaml`.
|
||||
|
||||
@@ -414,10 +435,12 @@ settings in `pyproject.toml`: online tests launch the server with
|
||||
`subprocess.Popen` and stop it with SIGTERM, and without those settings the XML
|
||||
reflects only the pytest parent process, not the server's code paths.
|
||||
|
||||
The upload depends on `buildkite-agent` being callable inside the container. The
|
||||
`kubernetes` presets (`h100_*`, `*_npu_*`) provide it; the `docker` ones (`l4_*`)
|
||||
only do because they set `mount-buildkite-agent: true`. A new docker preset that
|
||||
runs a coverage job needs the same.
|
||||
The upload depends on `buildkite-agent` being callable inside the container.
|
||||
CUDA (`l4_*`, `h100_*`) and NPU (`*_npu_*`) presets all use the `kubernetes`
|
||||
plugin, which provides the agent. `l4_*` jobs run on the EKS `l4-k8s` queue
|
||||
(agent-stack-k8s); they no longer use the docker plugin or
|
||||
`mount-buildkite-agent`. A new **docker** preset that runs a coverage job
|
||||
still needs `mount-buildkite-agent: true`.
|
||||
|
||||
List both `run_cov_split.sh` and `pyproject.toml` in every opted-in job's
|
||||
`source_file_dependencies` — both change what the job measures, so without them a
|
||||
|
||||
@@ -136,7 +136,7 @@ Through five levels (L1-L5) and common (Common) specifications, the system clari
|
||||
<a href="test_writing_guide.md#l4-level-testing-full-functionality-performance-and-documentation-testing">L4</a><br>
|
||||
L4: Purpose, Test Content, Directory Location, Example
|
||||
</td>
|
||||
<td>Nightly</td>
|
||||
<td>Nightly / Days before Release<br>(additional GPU SKUs)</td>
|
||||
<td>GPU</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -175,6 +175,15 @@ Before entering specific testing levels, the project establishes two common spec
|
||||
|
||||
## Notes
|
||||
|
||||
### L4 cadence (nightly and pre-release)
|
||||
|
||||
CUDA **L4** (`full_model`) jobs are defined once in [`.buildkite/cuda/test-nightly.yml`](https://github.com/vllm-project/vllm-omni/blob/main/.buildkite/cuda/test-nightly.yml). That file is reused at two cadences:
|
||||
|
||||
- **Nightly:** scheduled `main` builds with `NIGHTLY=1` (and PR labels such as `nightly-test` / `omni-test`). Default fleet is typically H100.
|
||||
- **Pre-release:** the same L4 suite runs again on extra GPU SKUs before a release (for example B200 via `MIRROR_HW=b200`), so `full_model` coverage is not limited to the nightly machine type.
|
||||
|
||||
How count-form `mirror_hardwares` and `MIRROR_HW` pick a SKU: [CI Settings](./ci_settings.md).
|
||||
|
||||
### L2 / L3 diff-aware CI (CUDA)
|
||||
|
||||
On CUDA **L2** and **L3**, **E2E Test** Buildkite jobs may be omitted at pipeline upload when the PR diff does not touch their path prefixes; other groups still always upload. Full two-layer mechanics and YAML examples: [CI Settings — Diff-aware CI](./ci_settings.md#diff-aware-ci) ([step filtering](./ci_settings.md#step-filtering)).
|
||||
|
||||
@@ -48,6 +48,8 @@ Defined in `pyproject.toml`:
|
||||
| `MI325` | Tests that require MI325 GPU (AMD/ROCm) * |
|
||||
| `A2` | Tests that require A2 NPU * |
|
||||
| `A3` | Tests that require A3 NPU * |
|
||||
| `A5` | Tests that require A5 NPU * |
|
||||
| `310P` | Tests that require 310P NPU * |
|
||||
| `cards_{n}` | Tests that require *n* accelerator cards (`cards_1` … `cards_8`); auto-added from `num_cards` * |
|
||||
| `slow` | Slow tests (may skip in quick CI) |
|
||||
| `benchmark` | Benchmark tests (decorator on runner test functions; perf JSON uses `full_model` + type marker instead) |
|
||||
@@ -76,7 +78,7 @@ def test_video_to_audio()
|
||||
This decorator is intended to make hardware-aware, cross-platform test authoring easier and more robust for CI/CD environments. The `hardware_test` decorator in `vllm-omni/tests/helpers/mark.py` performs the following actions:
|
||||
|
||||
1. **Applies platform and resource markers**
|
||||
Adds the appropriate pytest markers for each specified hardware platform (e.g., `cuda`, `rocm`, `xpu`, `npu`) and resource type (e.g., `L4`, `H100`, `H200`, `B200`, `MI325`, `B60`, `A2`, `A3`).
|
||||
Adds the appropriate pytest markers for each specified hardware platform (e.g., `cuda`, `rocm`, `xpu`, `npu`) and resource type (e.g., `L4`, `H100`, `H200`, `B200`, `MI325`, `B60`, `A2`, `A3`, `A5`, `310P`).
|
||||
```python
|
||||
@pytest.mark.cuda
|
||||
@pytest.mark.L4
|
||||
|
||||
@@ -779,6 +779,31 @@ turning them on. For Qwen-Image-style serving examples, document
|
||||
`--step-execution` as the feature gate and `--max-num-seqs N` as the
|
||||
companion batching knob.
|
||||
|
||||
### Streaming Output and Mid-Stream Interactions
|
||||
|
||||
To add streaming generation with optional mid-stream prompt updates to a new diffusion pipeline (often a video generation pipeline):
|
||||
|
||||
0. **Model should internally support** chunked video emission and bounded VAE decode of a chunk.
|
||||
1. **Implement stepwise execution** so interaction RPCs can be consumed at chunk
|
||||
boundaries (see [Step Execution](#step-execution) above).
|
||||
2. **Mix in** ``InteractionMixin`` on the pipeline class.
|
||||
3. **Implement** ``peek_chunk_media(StepRequestState) -> ChunkMediaSpec`` to offer information
|
||||
about the next chunk to generate.
|
||||
This is called whenever one supported interaction modality needs frame-level calculation,
|
||||
e.g., per-frame camera trajectory. If only chunk-level calculation is involved, peeking is skipped.
|
||||
4. **Optionally implement** ``prepare_next_chunk(StepRequestState)`` if the
|
||||
pipeline must rebuild something (e.g., per-chunk state) after an interaction is applied.
|
||||
This function is called at chunk boundary.
|
||||
5. **Register** supported modalities and handlers in ``vllm_omni/diffusion/interaction/registry.py``
|
||||
(keyed by ``od_config.model_class_name`` → modality key as in ``req["multi_modal_data"]``).
|
||||
|
||||
Serve with ``--diffusion-streaming-output`` (auto-enables step execution). See
|
||||
[Diffusion Execution Modes](../../user_guide/diffusion/execution_modes.md#streaming-output)
|
||||
and the example script at `examples/online_serving/streaming_video_generation/`.
|
||||
|
||||
**Interaction payload contract:** Although ``OmniInteractionPrompt`` type annocation requires ``event_id``,
|
||||
the WebSocket API payload may omit ``event_id`` and let the API layer assigns one.
|
||||
|
||||
### Cache Acceleration
|
||||
|
||||
#### TeaCache
|
||||
|
||||
@@ -29,7 +29,7 @@ Each stage is implemented as a separate model class that can be configured indep
|
||||
|
||||
When adding a new model, you'll need to create the following structure:
|
||||
|
||||
```
|
||||
```text
|
||||
vllm_omni/model_executor/models/
|
||||
└── your_model_name/ # Model directory (e.g., qwen3_omni)
|
||||
├── __init__.py # Exports main model class
|
||||
@@ -222,6 +222,16 @@ __all__ = ["Qwen3OmniMoeForConditionalGeneration"]
|
||||
|
||||
## Key Components
|
||||
|
||||
### Preprocess phase metadata
|
||||
|
||||
Models with `has_preprocess=True`, including out-of-tree plugins, can rely on
|
||||
the runner's stable `_omni_is_prefill`, `_omni_prompt_len`, and
|
||||
`_omni_num_computed_tokens` fields. Use request progress to distinguish prompt
|
||||
prefill from decode: a one-token span can still be a prefill tail. See the
|
||||
[runner-to-model phase contract](../../design/feature/preprocess_phase_contract.md)
|
||||
for types, batched-hook semantics, MTP routing guarantees, and the correctness
|
||||
workaround for vLLM-Omni 0.20.x and earlier.
|
||||
|
||||
### 1. Model Interfaces
|
||||
|
||||
Your model should implement the appropriate interfaces:
|
||||
@@ -422,22 +432,25 @@ The stage transition process follows these steps:
|
||||
Understanding the data structures is crucial for implementing stage transitions:
|
||||
|
||||
**Input to your function:**
|
||||
|
||||
- `stage_list[source_stage_id].engine_outputs`: List of `EngineCoreOutput` objects
|
||||
- - Each contains `outputs`: List of `RequestOutput` objects
|
||||
- Each `RequestOutput` has:
|
||||
- - - `token_ids`: Generated token IDs
|
||||
- `multimodal_output`: Dict with keys like `"code_predictor_codes"`, etc.These are the hidden states or intermediate outputs from the model's forward pass
|
||||
- `prompt_token_ids`: Original prompt token IDs
|
||||
- Each contains `outputs`: List of `RequestOutput` objects
|
||||
- Each `RequestOutput` has:
|
||||
- `token_ids`: Generated token IDs
|
||||
- `multimodal_output`: Dict with keys like `"code_predictor_codes"`, etc. These are the hidden states or intermediate outputs from the model's forward pass
|
||||
- `prompt_token_ids`: Original prompt token IDs
|
||||
|
||||
**Output from your function:**
|
||||
|
||||
- Must return `list[OmniTokensPrompt]` where each `OmniTokensPrompt` contains:
|
||||
- - `prompt_token_ids`: List[int] - Token IDs for the next stage
|
||||
- `prompt_token_ids`: List[int] - Token IDs for the next stage
|
||||
- `additional_information`: Dict[str, Any] - Optional metadata (e.g., embeddings, hidden states)
|
||||
- `multi_modal_data`: Optional multimodal data if needed
|
||||
|
||||
### How Model Outputs Are Stored
|
||||
|
||||
The model's `forward()` method returns an `OmniOutput` object that contains:
|
||||
|
||||
- `text_hidden_states`: Final hidden states for text generation
|
||||
- `multimodal_outputs`: Dict containing intermediate outputs
|
||||
|
||||
@@ -464,6 +477,7 @@ def forward(self, ...):
|
||||
```
|
||||
|
||||
These keys are then accessible in your stage transition function:
|
||||
|
||||
```python
|
||||
# In stage_input_processors/qwen3_omni.py
|
||||
thinker_prefill_embeddings = output.multimodal_output["0"] # Access by key
|
||||
@@ -493,7 +507,7 @@ thinker_hidden_states = output.multimodal_output["24"]
|
||||
Create stage transition processors in `vllm_omni/model_executor/stage_input_processors/your_model_name.py`. Each inter-stage edge should provide a **coherent processor set** rather than a single monolithic function:
|
||||
|
||||
| Suffix | Role | Runs when |
|
||||
|--------|------|-----------|
|
||||
| -------- | ------ | ----------- |
|
||||
| `*_full_payload` | Worker-side payload producer | `async_chunk=false`; accumulates tensors and ships via connector |
|
||||
| `*_async_chunk` | Scheduler-side streaming producer | `async_chunk=true`; emits per-chunk payloads |
|
||||
| `*_token_only` | Orchestrator placeholder builder | `async_chunk=false`; allocates downstream prompt slots only |
|
||||
@@ -554,7 +568,16 @@ For comprehensive testing guidelines, please refer to the [Test Writing Guide](.
|
||||
|
||||
## Adding a Model Recipe
|
||||
|
||||
After implementing and testing your model, please add a model recipe to the [vllm-project/recipes](https://github.com/vllm-project/recipes) repository. This helps other users understand how to use your model with vLLM-Omni.
|
||||
After implementing and testing your model, add a recipe under this repository's
|
||||
`recipes/` directory. Follow the
|
||||
[`add-recipe` skill](https://github.com/vllm-project/vllm-omni/blob/main/.claude/skills/add-recipe/SKILL.md)
|
||||
and [`recipes/TEMPLATE.md`](https://github.com/vllm-project/vllm-omni/blob/main/recipes/TEMPLATE.md),
|
||||
then update the model row in
|
||||
[`recipes/README.md`](https://github.com/vllm-project/vllm-omni/blob/main/recipes/README.md).
|
||||
The external
|
||||
[vllm-project/recipes](https://github.com/vllm-project/recipes) repository can
|
||||
be used as a structural reference, but it is not the destination for a
|
||||
vLLM-Omni model recipe.
|
||||
|
||||
### What to Include
|
||||
|
||||
@@ -568,15 +591,10 @@ Your recipe should include:
|
||||
3. **Usage Examples**: Command-line examples demonstrating how to run the model
|
||||
4. **Configuration Details**: Important configuration parameters and their meanings
|
||||
|
||||
### Example
|
||||
|
||||
For reference, see the [LongCat recipe example](https://github.com/vllm-project/recipes/pull/179) which demonstrates the expected format and structure.
|
||||
|
||||
### Recipe Location
|
||||
|
||||
Create your recipe file in the appropriate directory structure:
|
||||
- For organization-specific models: `OrganizationName/ModelName.md`
|
||||
- For general models: `ModelName.md`
|
||||
Create `recipes/<vendor>/<model-family>.md` and keep commands, feature status,
|
||||
hardware evidence, and limitations specific to the model family.
|
||||
|
||||
The recipe should be a Markdown file that provides clear, reproducible instructions for users to get started with your model.
|
||||
|
||||
@@ -590,7 +608,7 @@ Adding a new model to vLLM-Omni involves:
|
||||
4. **Define pipeline topology and deploy defaults** in `pipeline.py` and `vllm_omni/deploy/`
|
||||
5. **Implement stage input processors** for stage transitions
|
||||
6. **Write tests** to verify functionality
|
||||
7. **Add model recipe** to the [vllm-project/recipes](https://github.com/vllm-project/recipes) repository (see [Adding a Model Recipe](#adding-a-model-recipe) section)
|
||||
7. **Add an in-repository model recipe** under `recipes/` and update its index (see [Adding a Model Recipe](#adding-a-model-recipe))
|
||||
|
||||
### Qwen3-Omni Reference Files
|
||||
|
||||
@@ -607,6 +625,7 @@ For a complete reference implementation, see:
|
||||
- **Testing**: `vllm_omni/tests/e2e/offline_inference/test_qwen3_omni.py`
|
||||
|
||||
For more information, see:
|
||||
|
||||
- [Architecture Overview](../../design/architecture_overview.md)
|
||||
- [Supported Models](../../models/supported_models.md)
|
||||
- [Pipeline and Deploy Configuration Guide](../../configuration/stage_configs.md)
|
||||
@@ -991,10 +991,13 @@ git push origin your-branch --force-with-lease
|
||||
|
||||
## Adding a Model Recipe
|
||||
|
||||
After implementing and testing your model, add a model recipe to the
|
||||
[vllm-project/recipes](https://github.com/vllm-project/recipes) repository so users can
|
||||
get started quickly. See [Adding an Omni-Modality Model](./adding_omni_model.md#adding-a-model-recipe)
|
||||
for the expected format.
|
||||
After implementing and testing your model, add a recipe under this repository's
|
||||
`recipes/` directory. Follow the
|
||||
[`add-recipe` skill](https://github.com/vllm-project/vllm-omni/blob/main/.claude/skills/add-recipe/SKILL.md)
|
||||
and [`recipes/TEMPLATE.md`](https://github.com/vllm-project/vllm-omni/blob/main/recipes/TEMPLATE.md),
|
||||
update the model row in
|
||||
[`recipes/README.md`](https://github.com/vllm-project/vllm-omni/blob/main/recipes/README.md),
|
||||
and synchronize the relevant TTS example and speech API documentation.
|
||||
|
||||
## Summary
|
||||
|
||||
@@ -1011,7 +1014,7 @@ Adding a TTS model to vLLM-Omni involves:
|
||||
8. **Online serving** - add all 5 integration points to `serving_speech.py` in one commit
|
||||
9. **Tests** - cover single request, batching, and streaming
|
||||
10. **Pre-commit + DCO** - run `pre-commit` before pushing; sign every commit with `git commit -s`
|
||||
11. **Model recipe** - add to [vllm-project/recipes](https://github.com/vllm-project/recipes)
|
||||
11. **Model recipe** - add under the in-repository `recipes/` directory and update its index
|
||||
12. **Invariants** - re-check I1–I5 (streaming contract, consumer hygiene, hot-loop discipline, validation pyramid, per-request state) at the end of every phase
|
||||
|
||||
### Qwen3-TTS Reference Files
|
||||
|
||||
@@ -22,11 +22,15 @@ A pipeline opts in by implementing `SupportsARDiffusionPipeline`. Its immutable
|
||||
- named fixed-length cross-attention KV, model-specific scratch geometry, and
|
||||
the pipeline's requested session-capacity upper bound.
|
||||
|
||||
The runner binds an `ARDiffusionKVState` only around one `forward()` call. The
|
||||
pipeline may use it during that context but must not retain it. The session
|
||||
survives subsequent requests with the same `session_id`. Request reset, explicit
|
||||
close, LRU eviction, and forward exceptions all release runner-owned KV and call
|
||||
the pipeline's reset/close lifecycle hook. Cross-attention KV is allocated lazily
|
||||
The runner binds an `ARDiffusionKVState` only around one runner invocation
|
||||
(`execute_model` or `execute_stepwise`). The pipeline may use it during that
|
||||
context but must not retain it. Scratch and committed pages stay on the
|
||||
runner-owned session object, so unbinding does not drop an in-flight stepwise
|
||||
chunk. Tick-mode sessions survive subsequent requests with the same
|
||||
`session_id`. On the stepwise path the session id is the request id and is
|
||||
released when that request finishes or fails. Request reset, explicit close,
|
||||
LRU eviction, and forward exceptions all release runner-owned KV and call the
|
||||
pipeline's reset/close lifecycle hook. Cross-attention KV is allocated lazily
|
||||
per session and logical KV branch, and is released on the same path. A pipeline
|
||||
publishes one named cache transactionally by yielding exactly one `(k, v)` pair
|
||||
per layer:
|
||||
@@ -48,11 +52,12 @@ successful forward. The runner also exposes `reset_session()` and
|
||||
|
||||
## Current execution limits
|
||||
|
||||
The experimental runner supports request-mode execution with
|
||||
`max_num_seqs=1`. It rejects step execution and request-batch execution at load
|
||||
time, and its inherited batch/step entry points also fail explicitly. Supporting
|
||||
those paths requires a batch-aware state-binding contract rather than silently
|
||||
bypassing `bind_ar_diffusion_state()`.
|
||||
The experimental runner supports `max_num_seqs=1`. Request-batch execution is
|
||||
still rejected. Step execution is allowed only when the loaded pipeline
|
||||
implements `SupportsStepExecution`; other AR-Diffusion pipelines still fail at
|
||||
load if `step_execution=True`. Capable pipelines bind KV around each
|
||||
`execute_stepwise` invocation instead of inheriting the generic runner entry
|
||||
point unwrapped.
|
||||
|
||||
The implementation may retain multiple sessions per model instance. It selects
|
||||
the largest feasible capacity up to the pipeline's `session_capacity`, based on
|
||||
|
||||
@@ -323,18 +323,24 @@ inputs, fields, and ownership rules are described below.
|
||||
```mermaid
|
||||
flowchart TB
|
||||
layer1["Layer 1 · Authoring inputs<br/>PipelineConfig + DeployConfig"]
|
||||
layer2["Layer 2 · Resolve once<br/>StageConfigFactory.create_from_model()<br/>VllmOmniConfig.from_pipeline_config()"]
|
||||
layer3["Layer 3 · Transport-safe control plane<br/>VllmOmniConfig"]
|
||||
layer2["Layer 2 · Production resolution boundary<br/>resolve_omni_config()<br/>(StageConfigFactory + VllmOmniConfig internally)"]
|
||||
layer3["Layer 3 · Startup hand-off<br/>OmniConfigResolution<br/>PipelineConfig + temporary OmegaConf stage bridge"]
|
||||
layer4["Layer 4 · Runtime launch planning<br/>StageRuntime"]
|
||||
layer5["Layer 5 · Engine materialization<br/>VllmConfig / OmniDiffusionConfig"]
|
||||
|
||||
layer1 --> layer2 --> layer3 --> layer4 --> layer5
|
||||
```
|
||||
|
||||
The single resolution boundary is implemented by
|
||||
`StageConfigFactory.create_from_model()` and
|
||||
`VllmOmniConfig.from_pipeline_config()` in
|
||||
The production resolution boundary is `resolve_omni_config()` in
|
||||
[`vllm_omni/config`](https://github.com/vllm-project/vllm-omni/tree/main/vllm_omni/config).
|
||||
It delegates typed construction to `StageConfigFactory.create_from_model()` and
|
||||
`VllmOmniConfig.from_pipeline_config()`, then returns an
|
||||
`OmniConfigResolution` consumed by both `AsyncOmniEngine` and headless startup.
|
||||
Until `StageRuntime` consumes typed stage configs directly, this envelope carries
|
||||
the effective `PipelineConfig` alongside OmegaConf-compatible `stage_configs` as
|
||||
a temporary runtime bridge; both views describe the same resolved topology,
|
||||
including injected stages.
|
||||
|
||||
The legacy `stage_args` YAML path has been removed. Model topology now resolves
|
||||
through `PipelineConfig`, with runtime overrides supplied by `DeployConfig`.
|
||||
|
||||
@@ -354,9 +360,12 @@ The important ownership rules are:
|
||||
deploy-time defaults; it does not redefine the model graph.
|
||||
3. CLI and Python overrides are applied at the resolution boundary, with
|
||||
per-stage overrides taking precedence over global values where supported.
|
||||
4. `StageRuntime` owns launch planning and replica lifecycle. `ReplicaInitPlan`
|
||||
4. `OmniConfigResolution` is the sole startup hand-off. Its `pipeline_config`
|
||||
and temporary `stage_configs` compatibility view must describe the same
|
||||
topology.
|
||||
5. `StageRuntime` owns launch planning and replica lifecycle. `ReplicaInitPlan`
|
||||
is runtime-private state, not a user configuration object.
|
||||
5. `VllmConfig` and the enriched `OmniDiffusionConfig` are materialized in the
|
||||
6. `VllmConfig` and the enriched `OmniDiffusionConfig` are materialized in the
|
||||
process that owns the corresponding engine.
|
||||
|
||||
## Main features
|
||||
@@ -418,7 +427,7 @@ The public interfaces map onto the same engine and stage boundaries:
|
||||
flowchart LR
|
||||
offline["Offline Python<br/>Omni.generate()"] --> engine["AsyncOmniEngine"]
|
||||
online["OpenAI-compatible serving<br/>vllm serve ... --omni"] --> engine
|
||||
duplex["Experimental duplex WebSocket<br/>/v1/duplex or realtime duplex"] --> engine
|
||||
duplex["Duplex WebSocket<br/>/v1/realtime?duplex=1 or /v1/duplex"] --> engine
|
||||
engine --> stages["Configured AR and diffusion stages"]
|
||||
stages --> result["Streaming or final multimodal output"]
|
||||
```
|
||||
|
||||
@@ -150,8 +150,17 @@ that do not belong in the shared contract should be stored in `state.extra`.
|
||||
Queueing and lifecycle metadata remain in the scheduler's request state.
|
||||
|
||||
Current native pipelines that explicitly enable step execution include
|
||||
Qwen-Image, HunyuanImage3, and Helios. Step execution alone does not imply
|
||||
continuous-batching support: Qwen-Image accepts batched step states.
|
||||
Qwen-Image, BAGEL, HunyuanImage3, and Helios. Step execution alone does not
|
||||
imply continuous-batching support: Qwen-Image accepts batched step states.
|
||||
BAGEL accepts batched step states for image generation with its default,
|
||||
think-mode, and single-stage deploy configs. Only the diffusion stage uses the
|
||||
step protocol in two-stage deployments; explicit single-stage text output
|
||||
retains complete-request execution. BAGEL requires at least two inference steps
|
||||
because it schedules `num_inference_steps - 1` denoising updates, and its step
|
||||
path does not currently support sequence parallelism or a diffusion cache
|
||||
backend. When a finished stepwise request is followed by an explicit text
|
||||
fallback wave, the runner retires its step state and paged diffusion-KV rows
|
||||
before the full forward dispatch.
|
||||
HunyuanImage3 accepts batched step states only when its resolved self-attention
|
||||
backend is `TORCH_SDPA`; otherwise it rejects groups larger than one request.
|
||||
Configure `DIFFUSION_ATTENTION_BACKEND=TORCH_SDPA` or
|
||||
|
||||
@@ -5,23 +5,27 @@ The three strategies have separate user and design pages:
|
||||
|
||||
| Strategy | User guide | Design contract |
|
||||
| --- | --- | --- |
|
||||
| Model-level | [Guide](../../../user_guide/diffusion/offloader/cpu_offload.md) | [Design](cpu_offload.md) |
|
||||
| Model-level | [Guide](../../../user_guide/diffusion/offloader/module_offload.md) | [Design](module_offload.md) |
|
||||
| Layerwise | [Guide](../../../user_guide/diffusion/offloader/layerwise_offload.md) | [Design](layerwise_offload.md) |
|
||||
| Distributed layerwise | [Guide](../../../user_guide/diffusion/offloader/distributed_layerwise_offload.md) | [Design](distributed_layerwise_offload.md) |
|
||||
|
||||
## Strategy selection
|
||||
|
||||
`OffloadConfig.from_od_config()` converts diffusion configuration into one
|
||||
`OffloadStrategy`. If callers set multiple flags, selection is deterministic:
|
||||
`OffloadStrategy`. The public config separates component selection from
|
||||
policy:
|
||||
|
||||
1. `DISTRIBUTED_LAYERWISE`;
|
||||
2. `LAYERWISE`;
|
||||
3. `MODEL_LEVEL`; and
|
||||
4. `NONE`.
|
||||
- `mode="module"` selects model-level offload;
|
||||
- `mode="layer"` with rank-local transfers selects ordinary layerwise
|
||||
offload; and
|
||||
- AllGather transfer or resident layers selects the distributed layerwise
|
||||
backend that implements those capabilities.
|
||||
|
||||
The factory logs when a higher-priority strategy disables another. It derives
|
||||
parallel and HSDP state from `DiffusionParallelConfig`; callers do not provide
|
||||
a separate offload group size.
|
||||
The compatibility boolean flags retain their historical priority (distributed
|
||||
layerwise, layerwise, then model-level). A compact config rejects a conflicting
|
||||
legacy strategy or non-default legacy DLO tuning. The factory derives parallel
|
||||
and HSDP state from `DiffusionParallelConfig`; callers do not provide a
|
||||
separate offload group size.
|
||||
|
||||
`get_offload_backend()` then validates platform offload support, resolves the
|
||||
device, and creates exactly one backend. Returning `None` means offloading is
|
||||
@@ -39,11 +43,21 @@ Every backend implements `OffloadBackend`:
|
||||
`disable()` does not promise to restore the pipeline's original device
|
||||
placement. The caller owns any subsequent rematerialization.
|
||||
|
||||
An enable failure must remove every partially installed hook. Rank-local
|
||||
backends restore ordinary tensors so the pipeline can be retried. A failed
|
||||
multi-rank AllGather startup must not enter unmatched recovery collectives;
|
||||
that worker is safely discarded after local resource cleanup.
|
||||
|
||||
Hooks are registered through `HookRegistry` and `ModelHook`; offload backends
|
||||
must use distinct hook names and remove only hooks they own.
|
||||
|
||||
## Discovery and topology
|
||||
|
||||
`resolve_offload_plan(pipeline, config)` is the single topology entry point.
|
||||
Every backend calls it once when `enable()` starts and then consumes the
|
||||
resolved components, block stacks, resident heads, and staged-residency flags
|
||||
it returns. Backends do not re-read model declarations.
|
||||
|
||||
Pipeline component discovery prefers `SupportsComponentDiscovery` declarations:
|
||||
|
||||
- `_dit_modules`;
|
||||
@@ -52,16 +66,26 @@ Pipeline component discovery prefers `SupportsComponentDiscovery` declarations:
|
||||
- `_resident_modules`.
|
||||
|
||||
Dotted paths are supported. Legacy pipelines may use the fallback scan of
|
||||
well-known attribute names, but new integrations should declare components
|
||||
explicitly.
|
||||
well-known attribute names; it now warns once per pipeline class, and new
|
||||
integrations must declare components explicitly.
|
||||
|
||||
Ordinary layerwise offload obtains topology from declared
|
||||
`_layerwise_offload_blocks_attrs` (with a deprecated singular-name
|
||||
compatibility path). Distributed layerwise offload may instead use a pipeline
|
||||
`OffloadPlan`, then falls back to declared block attributes and its documented
|
||||
compatibility discovery. Discovery metadata describes structure only; the
|
||||
backend remains responsible for transfer, synchronization, and storage
|
||||
ownership.
|
||||
Block topology comes from the pipeline `OffloadPlan` first: `block_attrs` maps
|
||||
each DiT path to its ordered block containers, `encoder_block_attrs` declares
|
||||
streamable encoder stacks, `on_demand_component_paths` marks pipeline-managed
|
||||
residency, `resident_dit_paths` marks the DiTs that may hold resident layers,
|
||||
and `encoder_dlo_weight_replication` marks the encoders whose loader-produced
|
||||
weights are safe for AllGather. DiTs absent from the plan fall back to
|
||||
`_layerwise_offload_blocks_attrs` (including the deprecated singular-name
|
||||
compatibility path).
|
||||
|
||||
The resolver is pure: it moves no tensor, installs no hook, writes no module
|
||||
attribute, and reads no process group — multi-rank facts come from
|
||||
`OffloadConfig`. It owns topology validation, so a configuration the model
|
||||
cannot serve fails before the first component is placed. Explicit component
|
||||
selection turns every declaration problem into an error, while the
|
||||
compatibility topology (no component selector) keeps the historical
|
||||
warn-and-skip behavior. Metadata still describes structure only; the backend
|
||||
remains responsible for transfer, synchronization, and storage ownership.
|
||||
|
||||
## Cross-strategy invariants
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
# Distributed Layerwise Offload
|
||||
|
||||
This document describes distributed layerwise offload (DLO) for diffusion
|
||||
models. DLO keeps only a small number of DiT blocks on the accelerator and
|
||||
streams the remaining blocks from host memory. The distributed backend can
|
||||
either shard those host-side weights across an existing parallel group or keep
|
||||
complete rank-local block sources and avoid an additional collective.
|
||||
models. DLO keeps only a small number of selected DiT or text-encoder blocks
|
||||
on the accelerator and streams the remaining blocks from host memory. The
|
||||
distributed backend can either shard those host-side weights across an
|
||||
existing parallel group or keep complete rank-local block sources and avoid
|
||||
an additional collective.
|
||||
|
||||
For user-facing commands, see the
|
||||
[distributed layerwise offloading guide](../../../user_guide/diffusion/offloader/distributed_layerwise_offload.md)
|
||||
@@ -38,10 +39,11 @@ detailed contracts and validation boundaries.
|
||||
|
||||
## Status
|
||||
|
||||
DLO is implemented for multi-device diffusion execution. The default
|
||||
AllGather path is the primary path for DP and SP deployments. The
|
||||
`--dlo-no-use-allgather` path streams complete blocks independently and adds no
|
||||
DLO weight collective.
|
||||
DLO is implemented for multi-device diffusion execution. The default public
|
||||
transfer is `rank-local`; setting a selected component's
|
||||
`layer_options.NAME.weight_transfer` to `allgather` enables weight sharding across a
|
||||
compatible DP or SP group. Transfers are configured independently for `dit`
|
||||
and `text_encoder`.
|
||||
|
||||
Host storage is selected separately from the transfer protocol. The loader can
|
||||
produce a direct-checkpoint mmap plan for a proven-compatible runtime layout;
|
||||
@@ -109,7 +111,7 @@ checkpoint adapter beside the ordinary loader.
|
||||
|
||||
### AllGather path
|
||||
|
||||
With the default `dlo_use_allgather=True`, each rank stores approximately
|
||||
For a component configured with `weight_transfer="allgather"`, each rank stores approximately
|
||||
`1 / group_size` of each streamable block in pinned host memory. The next
|
||||
block's shard is copied to a device buffer and reconstructed with
|
||||
`all_gather_into_tensor` on a communication stream while the current block is
|
||||
@@ -132,9 +134,8 @@ source used to prepare each rank's persistent shard. They can be closed after
|
||||
shard preparation. Across the AllGather group, those private shards total
|
||||
approximately one runtime model copy.
|
||||
|
||||
An effective DLO group size of one performs no collective, even when
|
||||
`dlo_use_allgather=True`; it follows the rank-local transfer path described
|
||||
below.
|
||||
An effective DLO group size of one performs no collective; it follows the
|
||||
rank-local transfer path described below.
|
||||
|
||||
When DP is greater than one, the engine can process one request per DP rank in
|
||||
the same denoising wave. Because AllGather is a collective, all participating
|
||||
@@ -142,9 +143,10 @@ requests must take the same execution path at every denoising step.
|
||||
|
||||
### Rank-local path without DLO AllGather
|
||||
|
||||
With `--dlo-no-use-allgather`, DLO forces its internal offload shard size to
|
||||
one and streams complete blocks using H2D copies only. The host backing may be
|
||||
either a loader-approved checkpoint mapping or ordinary runtime tensors.
|
||||
With `weight_transfer="rank-local"`, DLO forces the affected component's internal
|
||||
offload shard size to one and streams complete blocks using H2D copies only.
|
||||
The host backing may be either a loader-approved checkpoint mapping or
|
||||
ordinary runtime tensors.
|
||||
|
||||
For direct mmap, each process retains immutable safetensors views and uses two
|
||||
bounded pinned host staging slots. Processes on the same node that map the same
|
||||
@@ -204,7 +206,7 @@ adopt it.
|
||||
> optimizations without changing AllGather behavior.
|
||||
|
||||
Final-layout Host Weight Runtime (HWR) backing is opt-in and currently applies
|
||||
only to no-AllGather DLO. Enable it with
|
||||
only to a selected rank-local DiT. Enable it with
|
||||
`--host-weight-runtime-mode preferred` (or `required`) and
|
||||
`--host-weight-runtime-root <node-local-root>`.
|
||||
|
||||
@@ -422,8 +424,8 @@ failure.
|
||||
collective. When the model declares the final-layout contract, HWR keys the
|
||||
reusable artifact by TP rank/size and SP layout.
|
||||
- **HSDP + SP:** the general parallel configuration permits HSDP over SP, but
|
||||
DLO must use `--dlo-no-use-allgather`. HSDP remains responsible for weight
|
||||
materialization and synchronization.
|
||||
each selected component must use `weight_transfer="rank-local"`. HSDP remains
|
||||
responsible for weight materialization and synchronization.
|
||||
- **HSDP + DP or TP:** rejected independently by the diffusion parallel
|
||||
configuration.
|
||||
|
||||
@@ -447,8 +449,9 @@ recorded layouts (they all keep plain 1-byte dtypes over ordinary strided
|
||||
views: online INT8 has an int8 weight plus fp32 scale, online MXFP8 has an
|
||||
fp8 weight plus e8m0 block scale, each with a contiguous or transposed-view
|
||||
finalized tensor, all covered by the same physical-order packing as online
|
||||
FP8). Other online methods must use `--dlo-no-use-allgather` or disable
|
||||
online quantization until their runtime layouts are validated.
|
||||
FP8). Other online methods must set
|
||||
`layer_options.dit.weight_transfer="rank-local"` or disable online quantization
|
||||
until their runtime layouts are validated.
|
||||
|
||||
The Host Weight Runtime representation, publication, and no-AllGather consumer
|
||||
contracts are merged; see
|
||||
|
||||
@@ -36,12 +36,14 @@ dependencies, not incidental global synchronization, define correctness.
|
||||
## Topology contract
|
||||
|
||||
A DiT declares ordered block-container attributes using
|
||||
`_layerwise_offload_blocks_attrs` or an `OffloadPlan`. Each resolved item must
|
||||
be an executable `nn.Module`; ordering must match forward execution. Multiple
|
||||
containers are concatenated in execution order.
|
||||
`_layerwise_offload_blocks_attrs` or an `OffloadPlan`. A text encoder declares
|
||||
its block-list paths through `OffloadPlan.encoder_block_attrs`. Each resolved
|
||||
item must be an executable `nn.Module`; ordering must match forward execution.
|
||||
Multiple containers are streamed as separate hook rings.
|
||||
|
||||
Non-block DiT modules, encoders, VAEs, and declared resident modules are not
|
||||
streamed by this backend.
|
||||
Non-block state of a selected DiT or text encoder remains device resident.
|
||||
Unselected components, VAEs, and declared resident modules are not streamed by
|
||||
this backend.
|
||||
|
||||
## Invariants and limitations
|
||||
|
||||
@@ -50,7 +52,8 @@ streamed by this backend.
|
||||
- Blocks with heterogeneous shapes and dtypes use their recorded metadata;
|
||||
implementations must not assume identical blocks.
|
||||
- The ring is initialized before the first forward so block zero is available.
|
||||
- This backend is single-device; distributed weight reconstruction belongs to
|
||||
- This backend uses rank-local host weights. Multi-device compute may remain
|
||||
active, but cross-rank weight reconstruction belongs to
|
||||
[Distributed Layerwise Offload](distributed_layerwise_offload.md).
|
||||
|
||||
Shared selection and lifecycle behavior is defined in the
|
||||
|
||||
+11
-4
@@ -2,7 +2,7 @@
|
||||
|
||||
Model-level offload enforces mutual exclusion between pipeline component
|
||||
groups. For commands and model support, see the
|
||||
[model-level user guide](../../../user_guide/diffusion/offloader/cpu_offload.md).
|
||||
[model-level user guide](../../../user_guide/diffusion/offloader/module_offload.md).
|
||||
|
||||
## Generic hook path
|
||||
|
||||
@@ -10,10 +10,15 @@ groups. For commands and model support, see the
|
||||
makes encoders and VAEs device resident for initialization, then installs
|
||||
`SequentialOffloadHook` instances:
|
||||
|
||||
- each DiT hook offloads all encoders and other DiTs before loading itself;
|
||||
- each encoder hook offloads every DiT before loading itself; and
|
||||
- each DiT hook offloads selected encoders and other selected DiTs before
|
||||
loading itself;
|
||||
- each encoder hook offloads selected DiTs before loading itself; and
|
||||
- VAEs are not part of the mutual-exclusion hook set.
|
||||
|
||||
All execution-stage modules still receive a hook so an unselected resident
|
||||
stage can evict a selected component before it runs. Only components listed in
|
||||
`diffusion_offload_config.components` are eligible to move to CPU.
|
||||
|
||||
The hook moves parameters and buffers without recursively calling
|
||||
`module.to()`. This avoids recursion through modules that retain references to
|
||||
other transformer blocks. Host tensors are pinned when configured; DTensors
|
||||
@@ -36,7 +41,9 @@ contract:
|
||||
|
||||
- only the executing component is device resident;
|
||||
- enable and disable are idempotent;
|
||||
- transfers use the supplied device, pinning, and HSDP settings; and
|
||||
- transfers use the supplied device, pinning, and HSDP settings;
|
||||
- explicit component selection is either honored or rejected before partial
|
||||
setup; and
|
||||
- disabling removes all model-local offload hooks or contexts.
|
||||
|
||||
The delegation is currently duck typed. If another independent implementation
|
||||
@@ -0,0 +1,172 @@
|
||||
# Runner-to-model prefill/decode phase contract
|
||||
|
||||
## Overview
|
||||
|
||||
Autoregressive models with a custom `preprocess` hook need to distinguish
|
||||
prompt processing from decoding. A one-token span can be either a complete
|
||||
one-token prompt, the last chunk of a longer prompt, or a decode step. Using
|
||||
span length as the phase signal can replace a prompt embedding with a generated
|
||||
audio embedding and advance teacher-forced text too early without an exception.
|
||||
|
||||
[Issue #4382](https://github.com/vllm-project/vllm-omni/issues/4382) requests a
|
||||
stable contract for the metadata and routing guards introduced by
|
||||
[PR #3662](https://github.com/vllm-project/vllm-omni/pull/3662).
|
||||
The shared runner already implements those guards. This design documents the
|
||||
existing interface and adds runner-level regression coverage rather than
|
||||
introducing a second phase detector or changing scheduling policy.
|
||||
|
||||
## Scope and objectives
|
||||
|
||||
- Make the three existing phase keys safe to depend on in out-of-tree models.
|
||||
- Preserve prompt embeddings and model state on a one-token prefill tail.
|
||||
- Keep normal and batched decode routing consistent in mixed batches.
|
||||
- Explain the correctness workaround required on older releases.
|
||||
|
||||
This contract does not enable chunked prefill for models whose other state
|
||||
handling does not support it. It does not change the scheduler, introduce new
|
||||
kwargs, or standardize other `_omni_*` fields. The implementation adds
|
||||
documentation and regression coverage; no hot-path allocation is added.
|
||||
|
||||
## Design
|
||||
|
||||
### Ownership and data flow
|
||||
|
||||
```text
|
||||
Scheduler / current input batch
|
||||
prompt_token_ids + num_computed_tokens_cpu (before this step)
|
||||
|
|
||||
OmniGPUModelRunner._preprocess
|
||||
|
|
||||
per-request phase metadata
|
||||
/ \
|
||||
prefill / other spans single-token decode
|
||||
preprocess preprocess or preprocess_decode_batch
|
||||
| |
|
||||
prompt embedding runner-managed talker_mtp
|
||||
\ /
|
||||
model forward
|
||||
```
|
||||
|
||||
The producer is `OmniGPUModelRunner._preprocess` in
|
||||
`vllm_omni/worker/gpu_model_runner.py`. `GPUARModelRunner` and
|
||||
`GPUGenerationModelRunner` inherit it. The current `OmniNPUModelRunner` also
|
||||
inherits this method; device-specific execution still needs its own hardware
|
||||
validation. A plugin runner that overrides preprocessing is responsible for
|
||||
preserving this interface.
|
||||
|
||||
### Stable model-facing fields
|
||||
|
||||
The following names, types, and meanings are a supported interface for in-tree
|
||||
and out-of-tree model hooks despite the leading underscore. Models should treat
|
||||
them as read-only runner-owned values. Changes to their meanings or removal
|
||||
require an explicit compatibility/migration plan.
|
||||
|
||||
| Key | Python type | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `_omni_prompt_len` | `int` | Length of the current request state's `prompt_token_ids`. This is the stage's prompt length, not the current chunk size or raw text length. |
|
||||
| `_omni_num_computed_tokens` | `int` | The row's `input_batch.num_computed_tokens_cpu` value **before** processing the scheduled span. It includes already-computed/cached progress, not just tokens computed in the current call. |
|
||||
| `_omni_is_prefill` | `bool` | Exactly `_omni_num_computed_tokens < _omni_prompt_len`. Equality and greater progress mean decode. |
|
||||
|
||||
The runner refreshes the fields after current request state has been gathered,
|
||||
immediately before selecting the per-row hook. Existing values in intermediate
|
||||
model state cannot override the current phase. Row indices follow the current
|
||||
input batch (including any reordering), not request arrival order. A resumed
|
||||
or streaming request is classified from its **current** prompt/progress values;
|
||||
the phase is not latched for the lifetime of the request.
|
||||
|
||||
The normal hook receives these fields as keyword arguments:
|
||||
|
||||
```python
|
||||
def preprocess(self, input_ids, input_embeds, **info):
|
||||
if info["_omni_is_prefill"]:
|
||||
offset = info["_omni_num_computed_tokens"]
|
||||
# Select the scheduled slice of the model's prompt embeddings.
|
||||
...
|
||||
else:
|
||||
# Advance decode-only text/audio state.
|
||||
...
|
||||
```
|
||||
|
||||
For `preprocess_decode_batch(input_ids=..., req_infos=...)`, each `req_infos`
|
||||
entry contains the same fields, aligned with its corresponding input row.
|
||||
Every such row has `_omni_is_prefill == False`. The optional earlier
|
||||
`preprocess_batch` hook is a separate whole-batch preparation hook; these freshly
|
||||
computed per-row fields are not promised at that earlier boundary.
|
||||
|
||||
### Routing guarantee
|
||||
|
||||
With `has_preprocess=True`, the batched decode hook is eligible only when it is
|
||||
callable, the runner has `talker_mtp`, the scheduled span is one token, and
|
||||
`_omni_is_prefill` is false. Otherwise the normal `preprocess` hook runs.
|
||||
|
||||
Runner-managed MTP is likewise restricted to single-token decode rows. A prefill
|
||||
row, including a one-token tail, is never added to that MTP batch: the runner
|
||||
does not consume its `mtp_inputs`, generate codes for it, or overwrite its
|
||||
preprocessed embedding with an MTP embedding. Decode rows in the same step
|
||||
continue to receive their MTP outputs at their own token offsets. Models may
|
||||
still implement their own model-specific work inside `preprocess`.
|
||||
|
||||
| Prompt length | Computed before step | Scheduled span | Phase | Runner MTP eligible |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 5 | 0 | 4 | Prefill | No |
|
||||
| 5 | 4 | 1 | Prefill tail | No |
|
||||
| 5 | 5 | 1 | Decode | Yes |
|
||||
| 5 | 6 | 1 | Decode | Yes |
|
||||
| 1 | 0 | 1 | Prefill | No |
|
||||
| 5 | 5 | 2 | Decode | No (not a single-token span) |
|
||||
|
||||
### Compatibility
|
||||
|
||||
!!! warning "vLLM-Omni 0.20.x and earlier"
|
||||
AR talker models with custom preprocessing that infer phase from span length
|
||||
must use `enable_chunked_prefill: false` for correctness on these releases.
|
||||
They lack the phase metadata and the runner's prefill-tail MTP protection.
|
||||
A model-only heuristic cannot prevent the older runner from overwriting a
|
||||
one-token prompt embedding. Disabling chunking avoids split tails; it does
|
||||
not make span length a reliable phase detector for every possible prompt.
|
||||
|
||||
The mechanism landed in #3662 (v0.21.0rc2 / v0.22.0 and later); this document
|
||||
formalizes that existing behavior. Prefer upgrading the matching vLLM and
|
||||
vLLM-Omni release pair and consuming the explicit phase keys. Do not silently
|
||||
fall back to `input_ids.numel() > 1` when the model requires chunking correctness.
|
||||
For a custom or backported runtime, verify both metadata and MTP guards.
|
||||
|
||||
## Correctness and testing plan
|
||||
|
||||
Runner tests call the production `_preprocess` and `_talker_mtp_forward` methods
|
||||
with CPU buffers and a deterministic MTP model. They inspect kwargs at the model
|
||||
boundary, resulting embeddings, and per-request generated codes. Coverage must
|
||||
include a multi-step prompt split with a one-token tail followed by decode,
|
||||
mixed prefill/decode batches in different orders, normal/batched decode hooks,
|
||||
adjacent decode requests in the same hook call with exact per-request codes,
|
||||
single-token prompts, cached progress, and decode spans longer than one token.
|
||||
No test should pass merely because MTP was disabled for every row.
|
||||
|
||||
Real-weight validation uses Qwen3-TTS CustomVoice through the full talker and
|
||||
code2wav pipeline. Record the actual scheduled prompt progress, assert that at
|
||||
least one one-token prefill tail was observed, check its hook metadata and MTP
|
||||
exclusion, and require finite non-empty output audio. Compare to an unchunked
|
||||
run with the same real input, greedy sampling, and `VLLM_BATCH_INVARIANT=1`.
|
||||
Greedy sampling alone does not guarantee identical codes across different
|
||||
prefill shapes: floating-point reduction differences can change an argmax and
|
||||
diverge in subsequent autoregressive steps. Invariant kernels keep this exact
|
||||
comparison focused on the phase contract. The two engines must be initialized
|
||||
separately because their chunking settings and token budgets differ. GPU tests belong under
|
||||
`tests/e2e/features/`; existing model tests remain useful adjacent regression
|
||||
coverage. Functional audio checks do not establish throughput or all-model
|
||||
semantic parity; this change makes no performance claim.
|
||||
|
||||
## Open questions and discussions
|
||||
|
||||
There are no new field names or execution modes to choose. Future runner
|
||||
implementations, including Model Runner V2, should preserve these semantics or
|
||||
provide an explicit migration path. Hardware validation beyond the tested
|
||||
platform remains separate from the shared Python routing tests.
|
||||
|
||||
## References
|
||||
|
||||
- [Issue #4382](https://github.com/vllm-project/vllm-omni/issues/4382)
|
||||
- [Original metadata and guards, #3662](https://github.com/vllm-project/vllm-omni/pull/3662)
|
||||
- [Qwen2.5-Omni consumer fix, #5019](https://github.com/vllm-project/vllm-omni/pull/5019)
|
||||
- [Adding an Omni model](../../contributing/model/adding_omni_model.md)
|
||||
- [Test writing guide](../../contributing/ci/test_writing_guide.md)
|
||||
@@ -84,8 +84,8 @@ dictionary narrows this global scope through runtime layer-prefix routing.
|
||||
|
||||
The factory uses Omni-specific overrides for methods whose diffusion behavior
|
||||
or platform dispatch is not provided by the vLLM registry. The current override
|
||||
set includes INT8, BitsAndBytes, MXFP8, MXFP4, MXFP4 dual-scale, and INC/
|
||||
AutoRound. Other methods, such as FP8, GGUF, and ModelOpt, are resolved through
|
||||
set includes INT8, BitsAndBytes, MXFP8, MXFP4, MXFP4 dual-scale, INC/AutoRound,
|
||||
and TorchAO. Other methods, such as FP8, GGUF, and ModelOpt, are resolved through
|
||||
vLLM's registry when their configuration is compatible.
|
||||
|
||||
## Per-component routing
|
||||
|
||||
@@ -1,90 +1,197 @@
|
||||
# Skip-Softmax
|
||||
|
||||
Skip-Softmax is the sparse-attention mode of the `TRTLLM_ATTN` backend. Usage — the config keys
|
||||
and how to pick an operating point — is in
|
||||
[TRTLLM Attention](../../user_guide/diffusion/attention_backends/trtllm.md#skip-softmax).
|
||||
The shared selector contract is documented in
|
||||
[Diffusion Attention Backend Selection](attention_backend_selection.md). This
|
||||
page explains the algorithm.
|
||||
Skip-Softmax is the sparse-attention mode of the `TRTLLM_ATTN` backend. It implements
|
||||
[BLASST](https://arxiv.org/abs/2512.12087) (Dynamic BLocked Attention Sparsity via Softmax
|
||||
Thresholding), which adds a per-tile skip test to the FlashAttention main loop. This page describes the algorithm, how the user configuration is resolved
|
||||
into the value the kernel consumes, and how checkpoint calibration and timestep gating participate
|
||||
in that resolution. Configuration keys and operating-point guidance are covered in
|
||||
[TRTLLM Attention](../../user_guide/diffusion/attention_backends/trtllm.md#skip-softmax); the
|
||||
backend selection contract is covered in
|
||||
[Diffusion Attention Backend Selection](attention_backend_selection.md).
|
||||
|
||||
## Motivation
|
||||
|
||||
In a long attention row (a video DiT can have tens of thousands of keys), the softmax weight
|
||||
concentrates on a small fraction of the keys; the rest receive near-zero weight and barely move the
|
||||
output. Computing softmax and the value-weighted sum over those keys is wasted work. Skip-Softmax
|
||||
detects, per block of keys, when a block cannot matter and skips its softmax and its value
|
||||
multiply.
|
||||
detects, per tile of keys, when a tile cannot matter and skips its softmax and its value multiply.
|
||||
|
||||
It is approximate: a skipped block still carries a small non-zero contribution, so the mode is
|
||||
It is approximate: a skipped tile still carries a small non-zero contribution, so the mode is
|
||||
opt-in and off by default.
|
||||
|
||||
## The online-softmax pass
|
||||
## Algorithm
|
||||
|
||||
Attention is computed in a single streaming pass over the keys, in tiles of 128. Per query row the
|
||||

|
||||
|
||||
FlashAttention computes attention in a single streaming pass over KV tiles. Per query row the
|
||||
kernel maintains three running values:
|
||||
|
||||
- `m` — the largest score seen so far,
|
||||
- `l` — the running denominator `Σ exp(sⱼ − m)`,
|
||||
- `O` — the running numerator `Σ exp(sⱼ − m)·vⱼ`,
|
||||
|
||||
and returns `O / l` at the end. For each key tile it computes the tile's scores `Q · K_jᵀ · scale`,
|
||||
updates `m`, and accumulates that tile's contribution into `l` and `O`. Rescaling `l` and `O` when
|
||||
`m` grows keeps the single pass numerically exact.
|
||||
and returns `O / l` at the end. For each KV tile it computes the scores `QK_j^T`, updates `m`, and
|
||||
accumulates the tile's contribution into `l` and `O`. Rescaling `l` and `O` when `m` grows keeps the
|
||||
dense pass numerically exact.
|
||||
|
||||
## The skip test
|
||||
|
||||
Once a tile's scores are known, its largest score `tile_max` is compared against the running
|
||||
maximum:
|
||||
A tile is a block of query rows against a block of keys, and the kernel processes it as a unit.
|
||||
BLASST inserts one test between computing a tile's scores and accumulating them. For each query
|
||||
row `i` in the tile, let `tile_max[i]` be the row's largest score within the tile, `running_max[i]`
|
||||
the row's running maximum, and `λ` the effective threshold:
|
||||
|
||||
```text
|
||||
if exp(tile_max − running_max) < threshold:
|
||||
skip this tile # do not compute its softmax weights or its P·V accumulation
|
||||
if all(exp(tile_max[i] - running_max[i]) < λ for every row i in the tile):
|
||||
skip this tile # do not compute its Softmax or PV contribution
|
||||
```
|
||||
|
||||
`exp(tile_max − running_max)` is an upper bound on the softmax weight any key in the tile can
|
||||
receive: if even the tile's best key is far below the current maximum, every key in the tile is
|
||||
negligible, and both the softmax (the exponentials) and the `P·V` accumulation for that tile can be
|
||||
dropped. The tile's contribution to `l` and `O` is simply skipped.
|
||||
`exp(tile_max[i] − running_max[i])` is an upper bound on the softmax weight any key in the tile can
|
||||
receive in row `i`: if even the row's best key in this tile is far below the row's current maximum,
|
||||
the tile is unimportant for that row. Only when this holds for every row in the tile can the
|
||||
Softmax and `PV` work for the tile be skipped, in which case its contribution to `l` and `O` is
|
||||
simply omitted. A larger `λ` makes the test more aggressive. The figure states the same test in
|
||||
log space, `m̃ − m < ln(λ)`, which is how the kernel evaluates it.
|
||||
|
||||
## What this bounds
|
||||
The test needs the tile's scores, so `QK_j^T` always runs and only the Softmax and `PV` work can
|
||||
be skipped. Since the two matmuls have the same FLOP count, skipping every eligible tile removes at
|
||||
most roughly half of the attention arithmetic, and the kernel-level speedup stays well under 2×.
|
||||
|
||||
Two properties of the test shape the achievable speedup:
|
||||
## From configuration to the kernel threshold
|
||||
|
||||
- **`Q · K_jᵀ` always runs.** The test needs `tile_max`, which comes from the tile's scores, so the
|
||||
score matmul is never skipped — only the softmax and the `P·V` accumulation are. The score matmul
|
||||
and the value matmul are comparable in cost, so skipping every eligible tile removes at most
|
||||
roughly half the attention work; the kernel-level speedup is bounded well under 2×, not unbounded.
|
||||
|
||||
- **The decision is per tile, not per key.** A tile is skipped only when *all* of its keys are
|
||||
collectively negligible; a single important key keeps the whole tile. How many tiles actually
|
||||
qualify depends on the data and rounds down to tile granularity, so `target_sparsity` selects an
|
||||
operating point on a calibrated curve — it is not a promise that a fixed fraction of tiles is
|
||||
skipped.
|
||||
|
||||
## The threshold
|
||||
|
||||
The per-tile threshold is normalized by sequence length:
|
||||
The FlashInfer kernel does not take `λ` directly. It takes a `threshold_scale_factor` and divides
|
||||
it by the KV sequence length of the call:
|
||||
|
||||
```text
|
||||
threshold = factor / seqlen
|
||||
λ = threshold_scale_factor / max_kv_len
|
||||
```
|
||||
|
||||
`factor` comes from one of two sources:
|
||||
vLLM-Omni resolves the factor from one of two mutually exclusive user controls:
|
||||
|
||||
- **Calibrated** — `factor = a · exp(b · target_sparsity)`, where `a` and `b` are fit per model (and
|
||||
per expert, for a multi-expert model) so that a given `target_sparsity` lands near that fraction of
|
||||
skipped tiles on the calibration data.
|
||||
- **Direct** — `factor = skip_softmax.threshold · seqlen`, the calibration-free path; the user sets
|
||||
the threshold themselves.
|
||||
| Control | Factor passed to the kernel | Resulting `λ` |
|
||||
| --- | --- | --- |
|
||||
| `threshold` | `threshold * max_kv_len` | `threshold` |
|
||||
| `target_sparsity=s` | `a * exp(b * s)` | `a * exp(b * s) / max_kv_len` |
|
||||
|
||||
`threshold` is therefore `λ` itself and is independent of sequence length: the same value yields
|
||||
the same per-tile test at any resolution or frame count. `threshold=0` skips no tiles. Because the
|
||||
left-hand side of the skip test lies in `(0, 1]`, values in `(0, 1)` are the meaningful range; the
|
||||
schema accepts any finite non-negative value.
|
||||
|
||||
`target_sparsity` selects a point on a curve fitted per model by
|
||||
[NVIDIA ModelOpt](https://github.com/NVIDIA/Model-Optimizer/tree/main/examples/diffusers/sparsity),
|
||||
so that `s` lands near that fraction of skipped tiles on the calibration data. The coefficients
|
||||
`a` and `b` come from the checkpoint; the achieved sparsity on another prompt, shape, or layer can
|
||||
differ.
|
||||
|
||||
### Skip-Softmax calibration config
|
||||
|
||||
ModelOpt stores the calibration in each transformer component's `config.json` under
|
||||
`sparse_attention_config`. The following is taken from the `transformer/config.json` of
|
||||
[`nvidia/Wan2.2-T2V-A14B-Diffusers-FP8`](https://huggingface.co/nvidia/Wan2.2-T2V-A14B-Diffusers-FP8),
|
||||
with the `ignore` list abridged:
|
||||
|
||||
```json
|
||||
{
|
||||
"sparse_attention_config": {
|
||||
"config_groups": {
|
||||
"group_0": {
|
||||
"algorithm": "skip_softmax",
|
||||
"targets": ["WanAttention"],
|
||||
"ignore": ["blocks.0.attn1", "blocks.0.attn2", "blocks.1.attn1", "..."],
|
||||
"threshold_scale_factor": {
|
||||
"formula": "a * exp(b * target_sparsity)",
|
||||
"coefficients": {"a": 2822.6209952221557, "b": 4.388250623520793}
|
||||
},
|
||||
"target_sparsity": 0.7,
|
||||
"disabled_until_timestep": 0.93
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
A checkpoint with several transformer components carries one such config per component, each
|
||||
with its own coefficients; in this example, `transformer_2/config.json` holds the low-noise
|
||||
expert's curve.
|
||||
|
||||
vLLM-Omni consumes `formula`, `coefficients`, and `ignore`, and ignores `targets`,
|
||||
`target_sparsity`, and `disabled_until_timestep`:
|
||||
|
||||
- Only the formula `a * exp(b * target_sparsity)` is accepted; a checkpoint with any other formula
|
||||
is rejected at startup. If a spec requests `target_sparsity` and the checkpoint carries no
|
||||
calibration, startup fails and the error points to `threshold` as the calibration-free
|
||||
alternative.
|
||||
- Each component's curve is applied to that component's attention layers. If the config for a
|
||||
secondary component such as `transformer_2` cannot be read, that component stays dense and a
|
||||
warning is logged.
|
||||
- `ignore` holds fnmatch patterns matched against both the full module name and the name relative
|
||||
to the component, so `blocks.0.attn1` matches `transformer.blocks.0.attn1` as well as
|
||||
`transformer_2.blocks.0.attn1`. Matching layers receive no coefficients and therefore stay dense
|
||||
regardless of the user configuration.
|
||||
|
||||
At each attention call, a layer with a `threshold` uses it directly; a layer with
|
||||
`target_sparsity` and stamped coefficients evaluates the curve; a layer with `target_sparsity` but
|
||||
no coefficients runs dense. The timestep gate below is applied to the result before it reaches the
|
||||
kernel.
|
||||
|
||||
## Timestep gating
|
||||
|
||||
`disabled_until_timestep = D` keeps the mode off during the early, high-noise denoise steps and
|
||||
turns it on once the normalized timestep `t` drops to `t ≤ D` (`t` runs `1.0` → `0.0` over the
|
||||
schedule). The early steps set the global structure of the output and their errors propagate through
|
||||
every later step, so keeping them dense costs a few skipped-tile opportunities but protects fidelity.
|
||||
The early, high-noise denoising steps set the global structure of the output, and their errors
|
||||
propagate through every later step. Keeping just these steps dense markedly improves the quality
|
||||
of the generated video at a small cost in skipped work. `disabled_until_timestep` implements this:
|
||||
on each attention call the backend compares the current normalized timestep `t` against the
|
||||
configured cutoff and passes no skip factor to the kernel while `t` is above it. Skip-Softmax
|
||||
becomes active on the first step whose `t` is at or below the cutoff and stays active for the rest
|
||||
of the run, since `t` only decreases.
|
||||
|
||||
`t` is the scheduler's own timestep divided by `num_train_timesteps`, published by the pipeline via
|
||||
`DenoiseProgressMixin.record_denoise_step`. It is deliberately not derived from the step index,
|
||||
because schedulers space their steps non-uniformly. A pipeline that does not publish a timestep
|
||||
stays dense when `disabled_until_timestep` is set.
|
||||
The default `disabled_until_timestep = 0` is a sentinel rather than a cutoff: the gate is off, the
|
||||
timestep is never read, and Skip-Softmax runs on every step. Any positive value enables the gate.
|
||||
`disabled_until_timestep = 1.0` therefore also yields no dense steps on a pipeline that publishes
|
||||
`t`, but on a pipeline that does not publish it the backend stays dense and logs a warning once
|
||||
rather than guessing from the step index.
|
||||
|
||||
### Where `t` comes from
|
||||
|
||||
The pipeline publishes `t` once per denoising step before running the transformer. It is the
|
||||
scheduler's own position in `[0, 1]`, decreasing from near `1.0` (pure noise) to `0.0` (clean
|
||||
sample): scheduler-based pipelines publish the scheduler timestep divided by `num_train_timesteps`,
|
||||
and rectified-flow pipelines publish the current sigma. `t` is deliberately not the step index
|
||||
divided by the step count. Schedulers place their steps non-uniformly in `t`, so expressing the
|
||||
cutoff in `t` ties it to the same noise level across step counts and schedulers.
|
||||
|
||||
### Mapping the cutoff to denoising steps
|
||||
|
||||
For a run whose published sequence is `t[0], ..., t[N-1]`, the number of dense steps is
|
||||
|
||||
```text
|
||||
dense_steps = count(t[i] > disabled_until_timestep)
|
||||
skip_softmax_steps = N - dense_steps
|
||||
```
|
||||
|
||||
This count **depends on the schedule**, and schedules differ from model to model: the
|
||||
scheduler family, the number of steps, and any time shift all change where the steps land in `t`.
|
||||
The same cutoff can therefore gate a very different fraction of the run on two models. Take
|
||||
MiniMax-H3 as an example. Its video branch uses a flow-shifted rectified-flow schedule that applies
|
||||
a shift `s` to `N + 1` uniform positions `u` from `1` to `0`,
|
||||
|
||||
```text
|
||||
t = s * u / (1 + (s - 1) * u)
|
||||
```
|
||||
|
||||
and a large `s` pushes most positions toward `t ≈ 1`. With the default `s = 12` and
|
||||
`num_inference_steps = 50` (50 sigma points, `N = 49` denoiser forwards), the published sequence
|
||||
starts `1.000, 0.998, 0.996, 0.995, 0.993, ...` and stays above `0.9` for 28 steps, giving:
|
||||
|
||||
| `disabled_until_timestep` | Dense steps | Skip-Softmax steps |
|
||||
| :---: | ---: | ---: |
|
||||
| `1.00` | 0 | 49 |
|
||||
| `0.99` | 6 | 43 |
|
||||
| `0.97` | 14 | 35 |
|
||||
| `0.95` | 19 | 30 |
|
||||
| `0.90` | 28 | 21 |
|
||||
| `0.86` | 33 | 16 |
|
||||
|
||||
These numbers hold only for that schedule. A model with a smaller shift, a different step count,
|
||||
or a scheduler that is not rectified flow produces a different sequence and a different table; a
|
||||
request that overrides `flow_shift` or the step count changes it for MiniMax-H3 too.
|
||||
Choose the cutoff by counting against the schedule actually served, not by reusing a value from
|
||||
another model.
|
||||
@@ -0,0 +1,59 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="760" height="460" viewBox="0 0 760 460" role="img" aria-labelledby="title desc">
|
||||
<title id="title">MAGI-2 DLO one-step Pareto frontier</title>
|
||||
<desc id="desc">Warm user latency in seconds versus throughput per device. DP1SP4 is 5.735 seconds and 0.0436 outputs per second per device, DP2SP2 is 6.785 and 0.0737, and DP4 is 9.997 and 0.1000.</desc>
|
||||
<rect width="760" height="460" fill="#ffffff"/>
|
||||
<style>
|
||||
text { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #111827; }
|
||||
.title { font-size: 20px; font-weight: 600; }
|
||||
.axis-label { font-size: 14px; font-weight: 500; }
|
||||
.tick { font-size: 12px; fill: #4b5563; }
|
||||
.point-label { font-size: 13px; font-weight: 600; }
|
||||
.value-label { font-size: 11px; fill: #4b5563; }
|
||||
.grid { stroke: #e5e7eb; stroke-width: 1; }
|
||||
.axis { stroke: #6b7280; stroke-width: 1.5; }
|
||||
.frontier { fill: none; stroke: #2563eb; stroke-width: 3; }
|
||||
.point { fill: #2563eb; stroke: #ffffff; stroke-width: 2; }
|
||||
</style>
|
||||
|
||||
<text x="380" y="28" text-anchor="middle" class="title">One diffusion step</text>
|
||||
|
||||
<g aria-hidden="true">
|
||||
<line x1="85" y1="309" x2="700" y2="309" class="grid"/>
|
||||
<line x1="85" y1="226" x2="700" y2="226" class="grid"/>
|
||||
<line x1="85" y1="143" x2="700" y2="143" class="grid"/>
|
||||
<line x1="85" y1="61" x2="700" y2="61" class="grid"/>
|
||||
<line x1="85" y1="40" x2="85" y2="350" class="axis"/>
|
||||
<line x1="85" y1="350" x2="700" y2="350" class="axis"/>
|
||||
|
||||
<text x="73" y="313" text-anchor="end" class="tick">0.04</text>
|
||||
<text x="73" y="230" text-anchor="end" class="tick">0.06</text>
|
||||
<text x="73" y="147" text-anchor="end" class="tick">0.08</text>
|
||||
<text x="73" y="65" text-anchor="end" class="tick">0.10</text>
|
||||
|
||||
<text x="85" y="370" text-anchor="middle" class="tick">5</text>
|
||||
<text x="197" y="370" text-anchor="middle" class="tick">6</text>
|
||||
<text x="309" y="370" text-anchor="middle" class="tick">7</text>
|
||||
<text x="421" y="370" text-anchor="middle" class="tick">8</text>
|
||||
<text x="532" y="370" text-anchor="middle" class="tick">9</text>
|
||||
<text x="644" y="370" text-anchor="middle" class="tick">10</text>
|
||||
</g>
|
||||
|
||||
<text x="392" y="414" text-anchor="middle" class="axis-label">Warm user latency (seconds) — lower is better</text>
|
||||
<text x="20" y="195" transform="rotate(-90 20 195)" text-anchor="middle" class="axis-label">Throughput/device (outputs/s) — higher is better</text>
|
||||
|
||||
<polyline points="167,294 285,169 644,61" class="frontier"/>
|
||||
|
||||
<circle cx="167" cy="294" r="7" class="point"/>
|
||||
<text x="179" y="288" class="point-label">DP1SP4</text>
|
||||
<text x="179" y="305" class="value-label">5.735 s, 0.0436</text>
|
||||
|
||||
<circle cx="285" cy="169" r="7" class="point"/>
|
||||
<text x="297" y="163" class="point-label">DP2SP2</text>
|
||||
<text x="297" y="180" class="value-label">6.785 s, 0.0737</text>
|
||||
|
||||
<circle cx="644" cy="61" r="7" class="point"/>
|
||||
<text x="632" y="84" text-anchor="end" class="point-label">DP4</text>
|
||||
<text x="632" y="101" text-anchor="end" class="value-label">9.997 s, 0.1000</text>
|
||||
|
||||
<text x="700" y="444" text-anchor="end" class="tick">4x NVIDIA L20X · warm qualification run</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
@@ -0,0 +1,65 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="760" height="460" viewBox="0 0 760 460" role="img" aria-labelledby="title desc">
|
||||
<title id="title">MAGI-2 DLO four-step Pareto frontier</title>
|
||||
<desc id="desc">Warm user latency in seconds versus throughput per device. DP1SP4 is 8.647 seconds and 0.0289 outputs per second per device, DP2SP2 is 11.682 and 0.0428, and DP4 is 20.957 and 0.0477.</desc>
|
||||
<rect width="760" height="460" fill="#ffffff"/>
|
||||
<style>
|
||||
text { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #111827; }
|
||||
.title { font-size: 20px; font-weight: 600; }
|
||||
.axis-label { font-size: 14px; font-weight: 500; }
|
||||
.tick { font-size: 12px; fill: #4b5563; }
|
||||
.point-label { font-size: 13px; font-weight: 600; }
|
||||
.value-label { font-size: 11px; fill: #4b5563; }
|
||||
.grid { stroke: #e5e7eb; stroke-width: 1; }
|
||||
.axis { stroke: #6b7280; stroke-width: 1.5; }
|
||||
.frontier { fill: none; stroke: #2563eb; stroke-width: 3; }
|
||||
.point { fill: #2563eb; stroke: #ffffff; stroke-width: 2; }
|
||||
</style>
|
||||
|
||||
<text x="380" y="28" text-anchor="middle" class="title">Four diffusion steps</text>
|
||||
|
||||
<g aria-hidden="true">
|
||||
<line x1="85" y1="350" x2="700" y2="350" class="grid"/>
|
||||
<line x1="85" y1="288" x2="700" y2="288" class="grid"/>
|
||||
<line x1="85" y1="226" x2="700" y2="226" class="grid"/>
|
||||
<line x1="85" y1="164" x2="700" y2="164" class="grid"/>
|
||||
<line x1="85" y1="102" x2="700" y2="102" class="grid"/>
|
||||
<line x1="85" y1="40" x2="700" y2="40" class="grid"/>
|
||||
<line x1="85" y1="40" x2="85" y2="350" class="axis"/>
|
||||
<line x1="85" y1="350" x2="700" y2="350" class="axis"/>
|
||||
|
||||
<text x="73" y="354" text-anchor="end" class="tick">0.025</text>
|
||||
<text x="73" y="292" text-anchor="end" class="tick">0.030</text>
|
||||
<text x="73" y="230" text-anchor="end" class="tick">0.035</text>
|
||||
<text x="73" y="168" text-anchor="end" class="tick">0.040</text>
|
||||
<text x="73" y="106" text-anchor="end" class="tick">0.045</text>
|
||||
<text x="73" y="44" text-anchor="end" class="tick">0.050</text>
|
||||
|
||||
<text x="85" y="370" text-anchor="middle" class="tick">8</text>
|
||||
<text x="173" y="370" text-anchor="middle" class="tick">10</text>
|
||||
<text x="261" y="370" text-anchor="middle" class="tick">12</text>
|
||||
<text x="349" y="370" text-anchor="middle" class="tick">14</text>
|
||||
<text x="436" y="370" text-anchor="middle" class="tick">16</text>
|
||||
<text x="524" y="370" text-anchor="middle" class="tick">18</text>
|
||||
<text x="612" y="370" text-anchor="middle" class="tick">20</text>
|
||||
<text x="700" y="370" text-anchor="middle" class="tick">22</text>
|
||||
</g>
|
||||
|
||||
<text x="392" y="414" text-anchor="middle" class="axis-label">Warm user latency (seconds) — lower is better</text>
|
||||
<text x="20" y="195" transform="rotate(-90 20 195)" text-anchor="middle" class="axis-label">Throughput/device (outputs/s) — higher is better</text>
|
||||
|
||||
<polyline points="113,302 247,129 654,69" class="frontier"/>
|
||||
|
||||
<circle cx="113" cy="302" r="7" class="point"/>
|
||||
<text x="125" y="296" class="point-label">DP1SP4</text>
|
||||
<text x="125" y="313" class="value-label">8.647 s, 0.0289</text>
|
||||
|
||||
<circle cx="247" cy="129" r="7" class="point"/>
|
||||
<text x="259" y="123" class="point-label">DP2SP2</text>
|
||||
<text x="259" y="140" class="value-label">11.682 s, 0.0428</text>
|
||||
|
||||
<circle cx="654" cy="69" r="7" class="point"/>
|
||||
<text x="642" y="92" text-anchor="end" class="point-label">DP4</text>
|
||||
<text x="642" y="109" text-anchor="end" class="value-label">20.957 s, 0.0477</text>
|
||||
|
||||
<text x="700" y="444" text-anchor="end" class="tick">4x NVIDIA L20X · warm qualification run</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 562 KiB |
+29
-15
@@ -23,16 +23,18 @@ The target is the engine-native path:
|
||||
-> response.audio.delta + response.audio_transcript.delta
|
||||
```
|
||||
|
||||
The standalone `/api/chat` and `/v1/audio/duplex` server remains a compatibility
|
||||
surface. It is not evidence that the unified engine path works.
|
||||
The standalone `/api/chat` and `/v1/audio/duplex` server that accompanied the
|
||||
original PR was demo-only and has been removed from the tree; the unified
|
||||
engine path above is the only serving surface. It was never evidence that the
|
||||
unified engine path works.
|
||||
|
||||
## Why configuration-only enablement is invalid
|
||||
## Why configuration-only enablement was invalid
|
||||
|
||||
The current staged pipeline is explicitly turn based. Its Talker reads
|
||||
`pplex_user_codes`, `pplex_prefill_text`, and `pplex_silence_codes`, but no
|
||||
production staged input path writes those fields. The voice prompt, persona
|
||||
prefill, and streaming Mimi state live only in the standalone
|
||||
`PersonaPlexEngine`.
|
||||
The staged pipeline as shipped by the original PR was explicitly turn based.
|
||||
Its Talker read `pplex_user_codes`, `pplex_prefill_text`, and
|
||||
`pplex_silence_codes`, but no production staged input path wrote those fields.
|
||||
The voice prompt, persona prefill, and streaming Mimi state lived only in the
|
||||
standalone `PersonaPlexEngine` (demo-only, since removed).
|
||||
|
||||
Setting only the following fields would therefore advertise an endpoint whose
|
||||
model never receives the live microphone stream:
|
||||
@@ -56,7 +58,12 @@ The unified implementation supports:
|
||||
- continuous user input while assistant audio is generated or played;
|
||||
- bundled `.pt` voice prompts and a session persona;
|
||||
- greedy text and depformer sampling, matching the current PersonaPlex port;
|
||||
- `/v1/realtime?duplex=1`;
|
||||
- `/v1/realtime?duplex=1` (the wire vocabulary is catalogued in the
|
||||
[Realtime Duplex API](../serving/realtime_duplex_api.md) serving guide);
|
||||
- the public client preset
|
||||
`vllm_omni.clients.personaplex.create_duplex_session_config()` (24 kHz
|
||||
`pcm_f32le` input format, voice prompt, persona) as the canonical
|
||||
session-config source for `DuplexClient` consumers;
|
||||
- engine lease close, disconnect cleanup, reconnect after cleanup, and explicit
|
||||
response cancellation without cross-session state reuse.
|
||||
|
||||
@@ -189,10 +196,17 @@ model-neutral native results:
|
||||
```
|
||||
|
||||
It owns per-request audio and text cursors so a cumulative output cannot replay
|
||||
old audio. PersonaPlex keeps one visible response open while continuous output
|
||||
arrives. Session close or cancellation terminates that response through the
|
||||
generic Realtime lifecycle; a codec segment finishing is not a conversational
|
||||
turn boundary.
|
||||
old audio. The generic projector turns each native result into the Realtime
|
||||
`response.audio.delta` / `response.audio_transcript.delta` pair (and
|
||||
`response.output_text.delta` for text) under one `response_id`; the full
|
||||
mapping is the name map in the
|
||||
[Realtime Duplex API](../serving/realtime_duplex_api.md) serving guide.
|
||||
PersonaPlex keeps one visible response open while continuous output arrives.
|
||||
Session close or cancellation terminates that response through the generic
|
||||
Realtime lifecycle; a codec segment finishing is not a conversational turn
|
||||
boundary. PersonaPlex advertises `supports_barge_in=false` and
|
||||
`supports_session_resume=false`, so `barge_in`, `turn_detection.server_vad`,
|
||||
and `session.resume` are rejected on this model.
|
||||
|
||||
## Error and lifecycle contracts
|
||||
|
||||
@@ -210,8 +224,8 @@ turn boundary.
|
||||
- New sessions cannot observe the previous voice, persona, PCM tail, Mimi
|
||||
convolution state, or Talker delayed code frame.
|
||||
|
||||
The legacy `PersonaPlexDuplexRuntime.run()` and standalone server drain path
|
||||
must follow the same exception-safe rule while they remain in the tree.
|
||||
(The legacy `PersonaPlexDuplexRuntime.run()` and standalone server drain path,
|
||||
which followed the same exception-safe rule, have been removed from the tree.)
|
||||
|
||||
## Testing and acceptance
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# MiniCPM-o 4.5 Full-Duplex Runtime Architecture
|
||||
|
||||
> Relocation note: after the H20 validation recorded below, the full-duplex
|
||||
> implementation moved out of `vllm_omni/experimental/fullduplex/` into the
|
||||
> stable tree (`vllm_omni/engine/duplex/`, `vllm_omni/entrypoints/duplex/`,
|
||||
> `vllm_omni/model_executor/models/minicpmo_4_5/duplex/`, and companion
|
||||
> modules). Module paths in this document reflect the new layout; the runtime
|
||||
> evidence was produced on the pre-move tree, which is file-identical apart
|
||||
> from module paths.
|
||||
|
||||
## Purpose
|
||||
|
||||
This document records the runtime architecture implemented by PR #3907, the
|
||||
@@ -31,8 +39,8 @@ session was rejected and capacity returned after one session closed. The two
|
||||
accepted sessions independently completed their audio, transcript, response,
|
||||
playback, and close lifecycles. This is isolation and admission evidence for the
|
||||
tested two-session deployment, not a production fairness, arbitrary-capacity,
|
||||
or failure-recovery claim. The validated paths include the experimental runtime
|
||||
extension, typed resumable policy, extracted control plane and clients, request
|
||||
or failure-recovery claim. The validated paths include the MiniCPM duplex
|
||||
runtime extension, typed resumable policy, extracted control plane and clients, request
|
||||
preregistration, typed direct-output decision, engine-managed two-session
|
||||
capacity in the checked-in deploy profile, separate public/runtime configuration
|
||||
channels, private Session ledgers, ordered `session.update`, the MiniCPM runner
|
||||
@@ -58,7 +66,8 @@ The checkpoint keeps these verified contracts:
|
||||
- scheduler data-plane append over a resumable request;
|
||||
- stale epoch/turn/response fencing;
|
||||
- `/v1/duplex` and OpenAI Realtime projection;
|
||||
- optional OpenAI `server_vad` turn detection backed by per-session Silero VAD;
|
||||
- optional OpenAI `server_vad` turn detection backed by a process-shared Silero
|
||||
model and per-session VAD state;
|
||||
- existing JoyVL behavior.
|
||||
|
||||
The checkpoint does not claim:
|
||||
@@ -67,7 +76,9 @@ The checkpoint does not claim:
|
||||
- an acoustic-onset-to-cancel latency target across arbitrary client chunk sizes;
|
||||
- production multi-session admission, fairness, capacity, or failure recovery;
|
||||
- bounded long-session KV;
|
||||
- video input or audio/video synchronization.
|
||||
- audio/video synchronization beyond one camera frame per model unit (camera
|
||||
frames ride the audio appends as `video_frames`; the wire contract is in the
|
||||
[Realtime Duplex API](../serving/realtime_duplex_api.md) guide).
|
||||
|
||||
## Active Runtime Path
|
||||
|
||||
@@ -81,7 +92,7 @@ WebSocket
|
||||
-> AsyncOmni thin open/append/signal/close proxies
|
||||
-> DuplexControlClient over the engine-owned correlated RPC transport
|
||||
-> DuplexControlPlane + DuplexSessionRuntimeManager
|
||||
-> experimental DuplexRuntimeExtension + engine session/stage bindings
|
||||
-> MiniCPM DuplexRuntimeExtension + engine session/stage bindings
|
||||
-> StagePool
|
||||
-> resumable scheduler request
|
||||
-> MiniCPM Stage0
|
||||
@@ -177,21 +188,21 @@ ordering guarantee.
|
||||
|
||||
### Extracted control ownership
|
||||
|
||||
The generic engine mechanism and the experimental duplex plugin are split by
|
||||
The generic engine mechanism and the duplex plugin are split by
|
||||
responsibility:
|
||||
|
||||
| Module | Responsibility |
|
||||
| --- | --- |
|
||||
| `engine.messages` | ordinary engine queue messages and collective RPC envelopes |
|
||||
| `engine.rpc_result_router` | generic correlation routing through each result message's `rpc_correlation_key` |
|
||||
| `experimental.fullduplex.engine.contracts` | immutable duplex DTOs, extension/stage protocols, typed decisions, request-ID codec |
|
||||
| `experimental.fullduplex.engine.lease` | duplex lease activity, TTL/grace configuration, and expiry records |
|
||||
| `experimental.fullduplex.engine.messages` | identity fence and duplex control message/result envelopes |
|
||||
| `experimental.fullduplex.engine.duplex_runtime` | extension loading, validation, and compatibility exports |
|
||||
| `experimental.fullduplex.engine.duplex_session` | session resources, fences, append reservations, idempotency cache, binding cleanup |
|
||||
| `experimental.fullduplex.engine.duplex_control_plane` | control algorithms and the narrow `DuplexStagePort` used to submit and clean up stage requests |
|
||||
| `experimental.fullduplex.engine.duplex_control_client` | typed control-message construction and calls through the engine-owned correlated RPC transport |
|
||||
| `experimental.fullduplex.request_client` | deterministic request identity, client-state preregistration, acknowledgement validation, data-plane collection, rollback |
|
||||
| `engine.duplex.contracts` | immutable duplex DTOs, extension/stage protocols, typed decisions, request-ID codec |
|
||||
| `engine.duplex.lease` | duplex lease activity, TTL/grace configuration, and expiry records |
|
||||
| `engine.duplex.messages` | identity fence and duplex control message/result envelopes |
|
||||
| `engine.duplex.runtime` | extension loading, validation, and compatibility exports |
|
||||
| `engine.duplex.session` | session resources, fences, append reservations, idempotency cache, binding cleanup |
|
||||
| `engine.duplex.control_plane` | control algorithms and the narrow `DuplexStagePort` used to submit and clean up stage requests |
|
||||
| `engine.duplex.control_client` | typed control-message construction and calls through the engine-owned correlated RPC transport |
|
||||
| `entrypoints.duplex_request_client` | deterministic request identity, client-state preregistration, acknowledgement validation, data-plane collection, rollback |
|
||||
|
||||
`Orchestrator`, `AsyncOmniEngine`, and `AsyncOmni` retain explicit wiring and
|
||||
compatibility proxies. They no longer own the corresponding duplex algorithms.
|
||||
@@ -362,33 +373,34 @@ vllm_omni.engine.rpc_result_router # generic correlation routing
|
||||
vllm_omni.engine.async_engine_utils # ordinary engine lifecycle helpers
|
||||
```
|
||||
|
||||
The full-duplex implementation remains experimental:
|
||||
The full-duplex implementation lives in dedicated duplex modules:
|
||||
|
||||
```text
|
||||
vllm_omni.experimental.fullduplex.engine.duplex_runtime
|
||||
vllm_omni.engine.duplex.runtime
|
||||
# extension loading and validation
|
||||
vllm_omni.experimental.fullduplex.engine.contracts
|
||||
vllm_omni.engine.duplex.contracts
|
||||
# immutable DTOs and narrow protocols
|
||||
vllm_omni.experimental.fullduplex.engine.lease
|
||||
vllm_omni.engine.duplex.lease
|
||||
# duplex lease primitives
|
||||
vllm_omni.experimental.fullduplex.engine.messages
|
||||
vllm_omni.engine.duplex.messages
|
||||
# duplex identity and queue envelopes
|
||||
vllm_omni.experimental.fullduplex.engine.duplex_session
|
||||
vllm_omni.engine.duplex.session
|
||||
# session transaction implementation
|
||||
vllm_omni.experimental.fullduplex.engine.duplex_control_plane
|
||||
vllm_omni.engine.duplex.control_plane
|
||||
# engine-side control algorithms
|
||||
vllm_omni.experimental.fullduplex.engine.duplex_control_client
|
||||
vllm_omni.engine.duplex.control_client
|
||||
# correlated control client
|
||||
vllm_omni.experimental.fullduplex.request_client
|
||||
vllm_omni.entrypoints.duplex_request_client
|
||||
# entrypoint request/output lifecycle
|
||||
```
|
||||
|
||||
Stable engine and entrypoint modules import only the kernel at module load.
|
||||
They load the experimental ControlPlane, ControlClient, request client, and
|
||||
runtime extension inside duplex-only construction or call paths. Importing
|
||||
`Orchestrator`, `AsyncOmniEngine`, or `AsyncOmni` for an ordinary deployment
|
||||
therefore does not import `vllm_omni.experimental.fullduplex`. Compatibility
|
||||
exports remain lazy and are not part of ordinary startup.
|
||||
Stable engine and entrypoint modules import the duplex kernel eagerly at
|
||||
module load (top-of-file imports). Duplex behavior remains opt-in: the
|
||||
ControlPlane, ControlClient, and request client are *constructed* only on
|
||||
duplex-enabled deployments, and the model runtime extension still loads only
|
||||
through the dotted-string plugin path. The client-side package
|
||||
(`vllm_omni/clients/`, home of the public `DuplexClient`) is never imported
|
||||
by runtime code.
|
||||
|
||||
`PipelineConfig.duplex_control_enabled` enables the generic mechanism, while
|
||||
`PipelineConfig.duplex_runtime_extension` separately selects the model adapter.
|
||||
@@ -452,7 +464,7 @@ sampling generation unchanged.
|
||||
`extra_body` or session payloads:
|
||||
|
||||
| Field | Default | Owner | Lifecycle or resource effect |
|
||||
|---|---:|---|---|
|
||||
| --- | ---: | --- | --- |
|
||||
| `idle_ttl_s` | `300.0` | Engine lease manager | Expires an attached or detached session after inactivity; `null` disables only idle expiry. |
|
||||
| `disconnect_grace_s` | `30.0` | Engine lease manager and API attachment registry | Bounds how long a detached session may retain engine resources before cleanup. |
|
||||
| `reaper_interval_s` | `5.0` | Orchestrator | Sets the cadence for lease expiry and pending cleanup retries. |
|
||||
@@ -467,6 +479,30 @@ Deploy profiles may lower or raise these values to match scheduler capacity,
|
||||
but clients cannot override them. The engine remains authoritative for
|
||||
admission and cleanup ownership even when the API performs an early check.
|
||||
|
||||
### Client-side companion package
|
||||
|
||||
The public client surface ships under `vllm_omni/clients/` and stays on the
|
||||
consumer side of the boundary above: `vllm_omni.clients.duplex` provides the
|
||||
model-agnostic `DuplexClient` (typed events, per-response demux via
|
||||
`ResponseHandle`, `session.resume` with acknowledgement-based replay dedup)
|
||||
and the `EventCollector` helper used for timing and request-metric summaries.
|
||||
Model-specific session defaults live in thin per-model preset modules —
|
||||
`vllm_omni.clients.minicpmo_4_5.create_duplex_session_config()` and
|
||||
`vllm_omni.clients.personaplex.create_duplex_session_config()` — so the core
|
||||
client never imports model packages and the model duplex plugin packages
|
||||
never import the client (both directions are enforced by tests:
|
||||
`tests/engine/test_duplex_import_boundary.py`,
|
||||
`tests/entrypoints/openai_api/test_duplex_capability.py`, and the subprocess
|
||||
purity guards in `tests/clients/test_model_session_configs.py`).
|
||||
`examples/online_serving/barge_in_client.py` (with its flow companion
|
||||
`barge_in_client_flow.md`) is the runnable reference for the client library
|
||||
against a live duplex deployment. The wire vocabulary the client speaks —
|
||||
every `/v1/realtime?duplex=1` event with its OpenAI-compatibility tier and a
|
||||
JSON example, plus the native-to-Realtime name map — is catalogued in the
|
||||
[Realtime Duplex API](../serving/realtime_duplex_api.md) serving guide; that
|
||||
page derives from the normative contract table above and must be updated
|
||||
with it.
|
||||
|
||||
## Generic-Path Cleanup
|
||||
|
||||
Model-specific `MINICPMO45_PROFILE_LOGS` probes were removed from:
|
||||
@@ -508,14 +544,16 @@ The MiniCPM Stage1 input processor consumes that capability for token-faithful
|
||||
delta transcripts. The generic orchestrator no longer installs or names a
|
||||
MiniCPM-specific bridge-state key.
|
||||
|
||||
The stable API server inspects the deployment `session_mode` before importing or
|
||||
constructing the experimental duplex handler. Ordinary deployments do not load
|
||||
the full-duplex serving package or create its registries and background state.
|
||||
The stable API server imports the duplex serving package eagerly and inspects
|
||||
the deployment `session_mode` before constructing the duplex handler. Ordinary
|
||||
deployments do not create its registries and background state.
|
||||
For an enabled deployment, the client still selects the Realtime duplex route
|
||||
with `?duplex=1` (or an equivalent explicit true value). Model-name matching is
|
||||
not used for routing or native-runtime activation. MiniCPM clients explicitly
|
||||
set `extra_body.minicpmo45_native_duplex=true`; repository demos do so in their
|
||||
session payloads.
|
||||
set `extra_body.native_duplex=true`; the canonical way to do so is
|
||||
the `vllm_omni.clients.minicpmo_4_5.create_duplex_session_config()` preset,
|
||||
which the repository's barge-in example
|
||||
(`examples/online_serving/barge_in_client.py`) uses in its session payload.
|
||||
|
||||
`DuplexRequestClient` derives the data-plane request identity from the accepted
|
||||
fence and preregisters a resumable `ClientRequestState` before the append crosses
|
||||
@@ -615,7 +653,7 @@ EOS decisions advance the model conversation.
|
||||
|
||||
Explicit Realtime clients may still use `input_audio_buffer.commit` to create a
|
||||
conversation item. The translator validates that wire input before producing a
|
||||
commit carrying `realtime_item_id`. Native auto-response may already have
|
||||
commit carrying `item_id`. Native auto-response may already have
|
||||
streamed those PCM samples into the runtime, so the runner accepts a validated
|
||||
commit even when no runtime-side chunk remains. A truly empty explicit buffer
|
||||
continues to return `input_audio_buffer_empty`.
|
||||
@@ -623,7 +661,7 @@ continues to return `input_audio_buffer_empty`.
|
||||
The commit-to-response contract is:
|
||||
|
||||
| Session state at `input_audio_buffer.commit` | Runtime action | Required event outcome |
|
||||
|---|---|---|
|
||||
| --- | --- | --- |
|
||||
| Auto-response disabled, valid idle input | Commit and retain the input item. | Emit `input_audio_buffer.committed`; emit no response until an explicit `response.create`. |
|
||||
| Auto-response enabled, valid idle speech | Submit the final native append for the current model turn. | Emit `input_audio_buffer.committed` before model output. Create a response only on the first visible text/audio output; a model listen decision may end with `response.listen` and no response. |
|
||||
| Auto-response enabled, short overlap during an active response | Discard the short overlap as an acknowledgement. | Emit the committed/no-response acknowledgement; do not create or defer a response. |
|
||||
@@ -640,8 +678,7 @@ multi-turn runtime evidence.
|
||||
|
||||
During auto-response overlap, `preserve_realtime_input` distinguishes "do not
|
||||
append this silent chunk to the native buffer" from "clear the open Realtime
|
||||
item". Silent overlap no longer discards earlier user PCM. A separate explicit
|
||||
`server_vad` policy is documented in the Realtime contract above.
|
||||
item". Silent overlap no longer discards earlier user PCM.
|
||||
|
||||
The first chunk of one overlapping input item also reserves its target model
|
||||
turn. A later Realtime commit uses that reserved identity even if response EOS
|
||||
@@ -770,9 +807,12 @@ Graph capture. This is a model execution constraint, not an E2E-only override.
|
||||
|
||||
### Resource capabilities
|
||||
|
||||
The checked-in MiniCPM duplex deploy profile sets Stage0 and Stage1
|
||||
`max_num_seqs` to two and configures the engine runtime manager with
|
||||
`max_sessions=2`. The Realtime capability response therefore advertises
|
||||
At the time of this snapshot the checked-in MiniCPM duplex deploy profile set
|
||||
Stage0 and Stage1 `max_num_seqs` to two and configured the engine runtime
|
||||
manager with `max_sessions=2`; the duplex settings have since been folded into
|
||||
`vllm_omni/deploy/minicpmo_4_5.yaml`, which admits four sessions
|
||||
(`duplex_session.max_sessions: 4`, matching its stage `max_num_seqs`). The
|
||||
Realtime capability response advertises
|
||||
multi-session and same-replica multi-session support with
|
||||
`session_admission_mode="engine_managed"`. Client session fields cannot raise
|
||||
this server-owned limit. Fresh E2E evidence covers two concurrent sessions in
|
||||
@@ -1025,4 +1065,6 @@ Passing this checkpoint supports the statement:
|
||||
|
||||
It does not support claims for an acoustic-onset barge-in latency target across
|
||||
arbitrary client chunk sizes, multi-session production concurrency, bounded
|
||||
long-session KV, scheduler-native append, or video input.
|
||||
long-session KV, or scheduler-native append. Camera-frame input
|
||||
(`video_frames`) landed after this snapshot and is covered by its own E2E
|
||||
test rather than by the evidence above.
|
||||
@@ -17,11 +17,14 @@ implementation contract; it is not, by itself, a general support claim.
|
||||
|
||||
### Runtime and stage execution
|
||||
|
||||
- [Full-Duplex Runtime (MiniCPM-o 4.5)](fullduplex.md)
|
||||
- [Full-Duplex Runtime (PersonaPlex)](fullduplex-personaplex.md)
|
||||
- [Disaggregated Inference](feature/disaggregated_inference.md)
|
||||
- [Host Weight Runtime](feature/host_weight_runtime.md)
|
||||
- [Async Chunk](feature/async_chunk.md)
|
||||
- [Async Diffusion Output](feature/async_diffusion_output.md)
|
||||
- [Async Omni Output Materialization](feature/omni_async_output_materialization.md)
|
||||
- [Runner-to-model Prefill/Decode Phase Contract](feature/preprocess_phase_contract.md)
|
||||
- [Automatic Prefix Caching in Omni Models](feature/prefix_caching.md)
|
||||
- [Realtime AR-Diffusion Sessions](feature/realtime_ar_diffusion.md)
|
||||
|
||||
@@ -64,7 +67,7 @@ The design contracts separate selection mechanics from backend algorithms:
|
||||
#### CPU offloading
|
||||
|
||||
- [Overview and Shared Contracts](feature/offloader/README.md)
|
||||
- [Model-Level Offload](feature/offloader/cpu_offload.md)
|
||||
- [Model-Level Offload](feature/offloader/module_offload.md)
|
||||
- [Layerwise Offload](feature/offloader/layerwise_offload.md)
|
||||
- [Distributed Layerwise Offload](feature/offloader/distributed_layerwise_offload.md)
|
||||
|
||||
|
||||
@@ -8,22 +8,44 @@ owners:
|
||||
- "@SamitHuang"
|
||||
- "@fhfuih"
|
||||
primary_code_paths:
|
||||
- vllm_omni/diffusion/diffusion_engine.py
|
||||
- vllm_omni/diffusion/request.py
|
||||
- vllm_omni/diffusion/data.py
|
||||
- vllm_omni/diffusion/sched/**
|
||||
- vllm_omni/diffusion/executor/**
|
||||
- vllm_omni/diffusion/worker/**
|
||||
related_code_paths:
|
||||
- vllm_omni/diffusion/stage_diffusion_client.py
|
||||
- vllm_omni/diffusion/stage_diffusion_proc.py
|
||||
- vllm_omni/diffusion/inline_stage_diffusion_client.py
|
||||
- vllm_omni/diffusion/ipc.py
|
||||
- vllm_omni/diffusion/hooks/**
|
||||
- vllm_omni/diffusion/postprocess/**
|
||||
depends_on:
|
||||
- ../engine_orchestration.md
|
||||
- ../stage_runtime.md
|
||||
- ../input_output_modality_contracts.md
|
||||
- diffusion_model_integration.md
|
||||
validation_paths:
|
||||
- tests/diffusion/diffusion_backend/**
|
||||
- tests/diffusion/hooks/**
|
||||
- tests/diffusion/test_diffusion_engine.py
|
||||
- tests/diffusion/test_diffusion_engine_cleanup.py
|
||||
- tests/diffusion/test_diffusion_engine_dummy_run.py
|
||||
- tests/diffusion/test_diffusion_engine_rpc_routing.py
|
||||
- tests/diffusion/test_diffusion_scheduler.py
|
||||
- tests/diffusion/test_diffusion_model_runner.py
|
||||
- tests/diffusion/test_diffusion_worker.py
|
||||
- tests/diffusion/test_multiproc_engine_concurrency.py
|
||||
- tests/diffusion/test_uniproc_executor.py
|
||||
- tests/diffusion/test_result_pump.py
|
||||
- tests/diffusion/test_async_output_worker.py
|
||||
- tests/diffusion/test_diffusion_ipc.py
|
||||
- tests/diffusion/test_diffusion_streaming_output.py
|
||||
- tests/diffusion/test_stage_diffusion_proc.py
|
||||
- tests/diffusion/test_inline_stage_diffusion_client.py
|
||||
upstream_refs:
|
||||
- diffusers.DiffusionPipeline
|
||||
last_reviewed: 2026-07-16
|
||||
last_reviewed: 2026-09-02
|
||||
last_verified_commit: e51fe6ec1b9a9a0e14bb1fdb296d61b6593b93c6
|
||||
---
|
||||
|
||||
# Diffusion runtime
|
||||
@@ -31,6 +53,380 @@ last_reviewed: 2026-07-16
|
||||
The diffusion runtime owns request admission, scheduling, execution, progress,
|
||||
output, cancellation, and cleanup inside a diffusion stage.
|
||||
|
||||
It is easiest to think of the runtime as a small control loop:
|
||||
|
||||
1. the **scheduler** chooses ready requests;
|
||||
2. the **executor** sends that choice to the worker layer;
|
||||
3. each **worker** manages its device and delegates model work to a **runner**;
|
||||
4. the runner calls the model pipeline and returns per-request results; and
|
||||
5. the engine feeds those results back to the scheduler and output streams.
|
||||
|
||||
!!! note
|
||||
|
||||
This page describes the runtime plumbing. Model loading and pipeline
|
||||
contracts belong to [Diffusion model integration](diffusion_model_integration.md).
|
||||
Batch compatibility and step-state details belong to
|
||||
[Diffusion continuous batching](continuous_batching.md).
|
||||
|
||||
## Goals and non-goals
|
||||
|
||||
The runtime aims to keep request policy separate from device execution. It
|
||||
also keeps request identity stable from admission through the final output,
|
||||
including cancellation and failures.
|
||||
|
||||
This module does **not** choose the next Omni stage, place stage replicas, or
|
||||
define a model's denoising algorithm:
|
||||
|
||||
- cross-stage routing belongs to [Engine orchestration](../engine_orchestration.md);
|
||||
- process placement and replica lifecycle belong to
|
||||
[Stage runtime](../stage_runtime.md); and
|
||||
- model-specific work belongs to
|
||||
[Diffusion model integration](diffusion_model_integration.md).
|
||||
|
||||
## Runtime at a glance
|
||||
|
||||
The stage owns `DiffusionEngine` and a `DiffusionExecutor` backend. The inline
|
||||
stage client keeps that stack in the caller process; a process-backed stage
|
||||
runs it inside `StageDiffusionProc`.
|
||||
|
||||
The executor then chooses how the worker runs:
|
||||
|
||||
- **`uni`** (default when `num_gpus == 1`): `UniProcDiffusionExecutor` builds
|
||||
one in-process worker. There is no worker subprocess and no shared-memory
|
||||
RPC.
|
||||
- **`mp`** (default when `num_gpus > 1`, or when set explicitly):
|
||||
`MultiprocDiffusionExecutor` starts one `WorkerProc` per device.
|
||||
|
||||
Either way, each worker owns its device, distributed state,
|
||||
`DiffusionWorker`, `DiffusionModelRunner`, and pipeline instance.
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
client["Stage client"]
|
||||
|
||||
subgraph stage["Diffusion stage process"]
|
||||
stageProc["StageDiffusionProc<br/>stage transport and lifecycle"]
|
||||
engine["DiffusionEngine<br/>admission and control loop"]
|
||||
scheduler["RequestScheduler or StepScheduler<br/>request state and policy"]
|
||||
executor["DiffusionExecutor<br/>uni or mp backend"]
|
||||
streams["Per-request output streams"]
|
||||
|
||||
stageProc --> engine
|
||||
engine --> scheduler
|
||||
scheduler -->|"DiffusionSchedulerOutput"| engine
|
||||
engine --> executor
|
||||
engine --> streams
|
||||
end
|
||||
|
||||
subgraph workers["Worker layer"]
|
||||
workerProc["WorkerProc<br/>mp IPC loop only"]
|
||||
worker["DiffusionWorker<br/>device and distributed setup"]
|
||||
runner["DiffusionModelRunner<br/>model state and execution"]
|
||||
pipeline["Diffusion pipeline<br/>model-specific computation"]
|
||||
|
||||
workerProc --> worker
|
||||
worker --> runner --> pipeline
|
||||
end
|
||||
|
||||
client -->|"process-backed: ZMQ"| stageProc
|
||||
client -.->|"inline: direct call"| engine
|
||||
executor -->|"mp: broadcast / result queues"| workerProc
|
||||
executor -.->|"uni: in-process call"| worker
|
||||
streams -->|"process-backed"| stageProc
|
||||
stageProc -->|"ZMQ results"| client
|
||||
streams -.->|"inline"| client
|
||||
```
|
||||
|
||||
!!! info "Stage process vs worker process"
|
||||
|
||||
A deployed stage may already run in a `StageDiffusionProc` child process.
|
||||
That is separate from the optional `mp` worker processes. With `uni`, the
|
||||
worker lives in the same process as the engine.
|
||||
|
||||
## Component responsibilities
|
||||
|
||||
| Component | Owns | Does not own |
|
||||
| --- | --- | --- |
|
||||
| `DiffusionEngine` | Admission, the busy loop, scheduler/executor coordination, RPC ordering, output streams, warmup, abort routing | Batch policy details, device setup, model computation |
|
||||
| `BaseScheduler` | Request states, waiting/running sets, capacity, compatibility, terminal transitions | IPC, worker calls, output formatting |
|
||||
| `RequestScheduler` | Complete-request waves and optional admission delay for request-level batching | Denoising-step progress |
|
||||
| `StepScheduler` | Per-request denoising progress and step-wise completion | Pipeline tensors and model state |
|
||||
| `DiffusionExecutor` | Execution backend contract, worker RPC, health, shutdown | Admission and request-state transitions |
|
||||
| `UniProcDiffusionExecutor` | Single in-process worker for `num_gpus == 1`; no IPC or async output pump | Multi-GPU execution |
|
||||
| `MultiprocDiffusionExecutor` | Worker processes, shared-memory message queues, result dispatch, worker monitoring | In-process single-GPU path |
|
||||
| `WorkerProc` | One `mp` worker process's IPC loop and reply rules | Scheduling policy; unused by `uni` |
|
||||
| `DiffusionWorker` | Device/distributed setup, LoRA activation, profiling, sleep/wake, runner delegation | Request admission |
|
||||
| `DiffusionModelRunner` | Pipeline loading, request-local model state, cache/compile setup, request or step execution | Queueing and cross-stage routing |
|
||||
|
||||
The boundary between policy and execution is
|
||||
`DiffusionSchedulerOutput`. It contains newly admitted request payloads,
|
||||
IDs for requests whose runner state is already cached, finished IDs,
|
||||
optional KV-prefetch work, and per-request `diffusion_kv_metadata` on
|
||||
`NewRequestData` envelopes when paged Diffusion KV is enabled. Executors and
|
||||
workers consume this output; they do not decide what should run next.
|
||||
|
||||
`BaseScheduler.initialize()` sets `max_num_running_reqs` from
|
||||
`od_config.max_num_seqs` (default 1). The engine may override that capacity
|
||||
for distributed layerwise offload with AllGather DP concurrency.
|
||||
|
||||
## One scheduling tick
|
||||
|
||||
Async requests share one engine busy loop. Worker calls and control RPCs pass
|
||||
through that loop, so they cannot race on the executor transport.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Caller
|
||||
participant Engine as DiffusionEngine
|
||||
participant Scheduler
|
||||
participant Executor
|
||||
participant Worker
|
||||
participant Runner
|
||||
|
||||
Caller->>Engine: add request
|
||||
Engine->>Scheduler: add_request(request)
|
||||
Engine->>Scheduler: schedule()
|
||||
Scheduler-->>Engine: DiffusionSchedulerOutput
|
||||
Engine->>Executor: execute_batch() or execute_step()
|
||||
Executor->>Worker: run worker method
|
||||
Worker->>Runner: execute model or one step
|
||||
Runner-->>Worker: RunnerOutput(s)
|
||||
Worker-->>Executor: result
|
||||
Executor-->>Engine: BaseRunnerOutput
|
||||
Engine->>Scheduler: update_from_output(...)
|
||||
Scheduler-->>Engine: finished request IDs
|
||||
Engine-->>Caller: chunk or final output
|
||||
```
|
||||
|
||||
The engine catches request execution errors and turns them into per-request
|
||||
error outputs. A dead worker group is different: the executor marks itself
|
||||
failed, health checks raise `EngineDeadError`, and the owning stage client
|
||||
handles the stage-level failure.
|
||||
|
||||
## Execution modes
|
||||
|
||||
The engine resolves one mode at startup and binds the matching scheduler and
|
||||
executor call.
|
||||
|
||||
| Mode | Scheduler | Executor call | Runner path |
|
||||
| --- | --- | --- | --- |
|
||||
| Request batch | `RequestScheduler` | `execute_batch()` | `execute_model()` for one request, or `execute_model_batch()` for a fused batch |
|
||||
| Step batch | `StepScheduler` | `execute_step()` | `execute_stepwise()` |
|
||||
|
||||
**Request mode** runs a complete pipeline forward for each scheduled wave. A
|
||||
single-request wave is the conservative path. A multi-request wave usually
|
||||
uses fused `execute_model_batch()` and requires the pipeline to declare
|
||||
request-batch support.
|
||||
|
||||
**Distributed layerwise offload with AllGather (DLO DP concurrency)** is a
|
||||
separate multiproc dispatch path. It activates when
|
||||
`data_parallel_size > 1`, `enable_distributed_layerwise_offload` is set, and
|
||||
`dlo_use_allgather` is true. The engine then sets `dp_concurrent = True` and
|
||||
raises `scheduler.max_num_running_reqs` to `dp_size`, overriding the
|
||||
`max_num_seqs` value from `initialize()`. The scheduler still emits a
|
||||
multi-request `DiffusionSchedulerOutput`; the multiproc executor routes that
|
||||
wave through `execute_request()` instead of fused `execute_model_batch()`.
|
||||
|
||||
Optional admission coalescing can fill a `dp_size` wave: when
|
||||
`request_batch_max_wait_ms > 0`, `RequestScheduler` may wait briefly before
|
||||
the first schedule of a wave (under `dp_concurrent`, the stable window is
|
||||
`min(0.3s, wait/2)`). With the default `request_batch_max_wait_ms == 0`,
|
||||
there is no wait.
|
||||
|
||||
Before dispatch, `MultiprocDiffusionExecutor.execute_request()` rejects waves
|
||||
whose requests differ in sampling-parameter compatibility or `extra_args`
|
||||
(AllGather requires every DP rank to follow the same forward schedule). Each
|
||||
worker rank then picks one envelope from the wave:
|
||||
`req[dp_rank % len(req)]`, so every rank enters the same collective while
|
||||
computing different requests.
|
||||
|
||||
**Step mode** keeps `StepRequestState` in the runner. New requests run
|
||||
`prepare_encode()` once; every tick runs `denoise_step()` and
|
||||
`step_scheduler()`; completed requests run `post_decode()`. The scheduler keeps
|
||||
only lifecycle and progress metadata—it does not hold model tensors.
|
||||
|
||||
**Step mode is also the path for streaming diffusion output**. Enabling
|
||||
`streaming_output` turns on step execution if needed. Chunk-capable pipelines
|
||||
can emit intermediate outputs through the same request stream; final-only step
|
||||
pipelines still emit only the final result. If the pipeline does not implement
|
||||
step execution, initialization fails.
|
||||
|
||||
!!! tip
|
||||
|
||||
For user-facing flags and examples, see
|
||||
[Diffusion execution modes](../../../user_guide/diffusion/execution_modes.md).
|
||||
That guide explains how to select a mode; this page explains what happens
|
||||
after the choice is made.
|
||||
|
||||
## Request state and identity
|
||||
|
||||
The scheduler is the source of truth for an admitted request. Its normal state
|
||||
flow is:
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> WAITING: add_request
|
||||
WAITING --> RUNNING: schedule
|
||||
WAITING --> FINISHED_ABORTED: abort
|
||||
RUNNING --> FINISHED_ABORTED: abort
|
||||
RUNNING --> FINISHED_COMPLETED: successful output
|
||||
RUNNING --> FINISHED_ERROR: failed or missing output
|
||||
FINISHED_COMPLETED --> [*]: deliver and remove state
|
||||
FINISHED_ABORTED --> [*]: deliver and remove state
|
||||
FINISHED_ERROR --> [*]: deliver and remove state
|
||||
```
|
||||
|
||||
`PREEMPTED` and `BaseScheduler.preempt_request()` exist on the scheduler API,
|
||||
but the engine loop does not call them today—only scheduler unit tests do.
|
||||
|
||||
`request_id` ties together scheduler state, runner state, executor results, and
|
||||
the output queue. Batch positions are temporary and must never replace request
|
||||
identity when results are mapped back.
|
||||
|
||||
Only compatible requests enter the same running wave. The scheduler compares a
|
||||
mode-specific sampling-parameter key and stops at the first incompatible
|
||||
waiting request. This is intentionally conservative and can cause
|
||||
head-of-line blocking.
|
||||
|
||||
## Executor and IPC
|
||||
|
||||
`DiffusionExecutor` is the backend interface. Built-in backends are selected by
|
||||
`distributed_executor_backend`:
|
||||
|
||||
| Backend | When chosen | Worker layout |
|
||||
| --- | --- | --- |
|
||||
| `uni` | Default for `num_gpus == 1`, or set explicitly | One in-process worker |
|
||||
| `mp` | Default for `num_gpus > 1`, or set explicitly | One worker process per device |
|
||||
|
||||
Ray and external-launcher diffusion backends are not implemented. A custom
|
||||
`DiffusionExecutor` subclass or import path is also accepted.
|
||||
|
||||
### Uniproc backend
|
||||
|
||||
`UniProcDiffusionExecutor` constructs `WorkerWrapperBase` in the engine process
|
||||
and calls worker methods directly. It avoids a second model load, MessageQueue
|
||||
rings, ZMQ IPC sockets, and `/dev/shm` tensor packing. RPC timeouts are
|
||||
accepted for interface parity but are not enforced: a hung worker blocks the
|
||||
calling thread. Sticky accelerator faults mark the executor dead through
|
||||
failure callbacks; ordinary request errors stay per-request.
|
||||
|
||||
### Multiproc backend
|
||||
|
||||
`MultiprocDiffusionExecutor`:
|
||||
|
||||
1. creates a broadcast queue shared by all workers;
|
||||
2. spawns one worker process per configured device;
|
||||
3. waits until every worker reports ready;
|
||||
4. sends generation and control calls as RPC messages;
|
||||
5. applies rank-aware reply rules so only expected ranks respond; and
|
||||
6. monitors worker process sentinels and fails the executor if a worker dies.
|
||||
|
||||
In request mode on this backend, each async output produces two correlated
|
||||
messages keyed by `async_output_id`:
|
||||
|
||||
- `COMPUTE_DONE` — the forward finished and the device can start the next
|
||||
request.
|
||||
- `OUTPUT_READY` — background D2H/SHM packing finished and the final output
|
||||
is ready.
|
||||
|
||||
The worker queues background packing work before it enqueues `COMPUTE_DONE`,
|
||||
but either message may reach the executor first. The result pump and
|
||||
`execute_batch()` must handle both orderings. Step mode keeps the synchronous
|
||||
result path and does not start those pumps.
|
||||
|
||||
See [Async diffusion output](../../feature/async_diffusion_output.md) for the
|
||||
multiproc request-mode timeline.
|
||||
|
||||
## Worker and runner boundary
|
||||
|
||||
On the `mp` path, `WorkerProc` receives messages and calls methods through
|
||||
`WorkerWrapperBase`. On the `uni` path, the executor owns that wrapper
|
||||
directly. In both cases, `DiffusionWorker` handles the parts tied to a device:
|
||||
distributed initialization, model-runner construction, LoRA activation,
|
||||
profiling, and memory sleep/wake. It delegates actual model work to
|
||||
`DiffusionModelRunner`.
|
||||
|
||||
The runner owns long-lived model-side state:
|
||||
|
||||
- the loaded pipeline and compilation setup;
|
||||
- cache and offload integration;
|
||||
- random-generator setup;
|
||||
- request batches for complete forwards; and
|
||||
- `StepRequestState` plus `InputBatch` for step execution.
|
||||
|
||||
This split keeps infrastructure out of model pipelines. A pipeline receives a
|
||||
request batch or step state and performs model work; it should not inspect
|
||||
engine queues or change scheduler state.
|
||||
|
||||
## Lifecycle and cleanup
|
||||
|
||||
### Startup
|
||||
|
||||
`DiffusionEngine.make_engine()` resolves the engine class, constructs the
|
||||
executor and scheduler, then normally runs a small dummy request to warm up the
|
||||
model.
|
||||
|
||||
Warmup is skipped only when distributed layerwise offload with AllGather
|
||||
is enabled (via `enable_distributed_layerwise_offload` and `dlo_use_allgather`)
|
||||
and `max(data_parallel_size, sequence_parallel_size) > 1`, because
|
||||
the dummy run sends one request while AllGather needs every shard rank to
|
||||
enter the same collective. If initialization or warmup fails, the runtime
|
||||
closes scheduler state and worker resources before returning the error.
|
||||
|
||||
### Cancellation
|
||||
|
||||
`abort()` places request IDs on an abort queue. The busy loop marks those
|
||||
requests `FINISHED_ABORTED`; finalization removes scheduler state and emits an
|
||||
aborted output when a consumer still exists. The runner removes cached
|
||||
step-mode state when it receives a scheduler output that reports the finished
|
||||
request ID.
|
||||
|
||||
On the multiproc request-mode async path, abort or consumer drop while output
|
||||
is still materializing should release the associated async-output bookkeeping
|
||||
so late results are not retained after the request ends
|
||||
*(pending #6253/#6439/#6580)*. Today, an unconsumed `OUTPUT_READY` is cached
|
||||
by design until `wait_output_ready()` or executor teardown. Dropping an
|
||||
output-stream consumer removes that consumer's queue. It does not take
|
||||
ownership of scheduler cleanup.
|
||||
|
||||
### Shutdown and worker failure
|
||||
|
||||
`DiffusionEngine.close()` is idempotent. It stops the busy loop, wakes pending
|
||||
streams with an error, closes scheduler state, and shuts down the executor.
|
||||
|
||||
- **`mp`**: the executor asks workers to stop, waits for them, then terminates
|
||||
workers that miss the grace period. Shutdown marks unfinished RPC and
|
||||
async-output futures as failed, then clears them. Completed async outputs
|
||||
cached for later `wait_output_ready()` calls are dropped with the executor
|
||||
object.
|
||||
- **`uni`**: the executor shuts down the in-process worker, drops the final
|
||||
model reference, and empties the accelerator cache so a later engine can reuse
|
||||
the device.
|
||||
|
||||
!!! warning
|
||||
|
||||
Do not continue using a worker group after a fatal collective timeout or
|
||||
unexpected worker exit. Distributed state may be incomplete. The
|
||||
multiprocess executor deliberately fails closed so the stage can restart.
|
||||
The uniproc executor fails closed when the accelerator context itself is
|
||||
poisoned.
|
||||
|
||||
## Extension boundaries
|
||||
|
||||
- A custom engine may set `default_diffusion_model_runner_cls`; an explicit
|
||||
`diffusion_model_runner_cls` configuration still wins.
|
||||
- Tests and custom engine integrations may inject a `BaseScheduler` subclass.
|
||||
`SchedulerInterface` remains only as a deprecated compatibility name.
|
||||
- `distributed_executor_backend` may be `"uni"`, `"mp"`, a custom `DiffusionExecutor`
|
||||
subclass, or an import path. A backend must preserve scheduler output,
|
||||
request identity, health, and cleanup contracts. Use `"mp"` when you need
|
||||
process isolation or multi-GPU; `"uni"` is the single-GPU default.
|
||||
- Worker extensions go through `WorkerWrapperBase`. They add worker methods;
|
||||
they do not become a second scheduler or request lifecycle.
|
||||
|
||||
These are advanced Python integration points, not stable end-user CLI
|
||||
customization surfaces.
|
||||
|
||||
## Candidate invariants
|
||||
|
||||
### DIFF-RUNTIME-INV-001: One lifecycle owner
|
||||
@@ -41,19 +437,50 @@ until completion, cancellation, or failure.
|
||||
### DIFF-RUNTIME-INV-002: Execution follows scheduler output
|
||||
|
||||
**Rule:** Executors and workers MUST execute scheduler decisions without
|
||||
admitting, reordering, or forwarding requests independently.
|
||||
admitting, reordering, or forwarding requests independently. Distributed
|
||||
layerwise offload with AllGather is an exception at dispatch time: the
|
||||
scheduler may schedule a multi-request wave, but each DP rank executes one
|
||||
envelope from that wave so every rank enters the same collective schedule.
|
||||
|
||||
### DIFF-RUNTIME-INV-003: Terminal cleanup is complete
|
||||
|
||||
**Rule:** Every terminal path MUST release request state, temporary tensors,
|
||||
hooks, and runtime-owned resources.
|
||||
hooks, and runtime-owned resources. Async-output bookkeeping on abort or
|
||||
consumer drop before consumption is also required *(pending #6253/#6439/#6580)*.
|
||||
|
||||
### DIFF-RUNTIME-INV-004: Optional features use runtime hooks
|
||||
|
||||
**Rule:** Cache, profiling, offload, and parallel features SHOULD integrate at
|
||||
defined hooks instead of creating another request lifecycle.
|
||||
|
||||
### DIFF-RUNTIME-INV-005: Results keep request identity
|
||||
|
||||
**Rule:** Batched and asynchronous results MUST be mapped by stable request ID,
|
||||
not by assumed completion order.
|
||||
|
||||
## Safe-change guide
|
||||
|
||||
Test success, cancellation, failure, shutdown, multiple requests, and resource
|
||||
cleanup.
|
||||
Test the smallest affected slice, then cover its neighboring boundary:
|
||||
|
||||
| Change area | Minimum evidence |
|
||||
| --- | --- |
|
||||
| Admission or state transitions | `test_diffusion_scheduler.py`, including duplicate IDs, compatibility, abort, and missing output |
|
||||
| Engine loop or output delivery | `test_diffusion_engine.py`, `test_diffusion_engine_cleanup.py`, `test_diffusion_engine_rpc_routing.py` |
|
||||
| Executor or IPC | `test_multiproc_engine_concurrency.py`, `test_uniproc_executor.py`, `test_result_pump.py`, `test_diffusion_ipc.py`, `test_async_output_worker.py` |
|
||||
| Worker or runner | `test_diffusion_worker.py`, `test_diffusion_model_runner.py` |
|
||||
| Stage boundary | `test_stage_diffusion_proc.py`, `test_inline_stage_diffusion_client.py` |
|
||||
| Warmup or streaming | `test_diffusion_engine_dummy_run.py`, `test_diffusion_streaming_output.py` |
|
||||
|
||||
Always exercise success, cancellation, per-request failure, fatal worker
|
||||
failure, repeated shutdown, one request, and multiple compatible requests.
|
||||
For step-mode changes, also test partial progress and cleanup of runner state.
|
||||
|
||||
## Related documents
|
||||
|
||||
- [Diffusion module overview](index.md)
|
||||
- [Diffusion model integration](diffusion_model_integration.md)
|
||||
- [Diffusion continuous batching](continuous_batching.md)
|
||||
- [Diffusion execution modes](../../../user_guide/diffusion/execution_modes.md)
|
||||
- [Async diffusion output](../../feature/async_diffusion_output.md)
|
||||
- [Engine orchestration](../engine_orchestration.md)
|
||||
- [Stage runtime](../stage_runtime.md)
|
||||
@@ -28,6 +28,9 @@ related_code_paths:
|
||||
- vllm_omni/engine/stage_pool.py
|
||||
- vllm_omni/outputs/output_processor.py
|
||||
- vllm_omni/distributed/omni_coordinator/**
|
||||
# Duplex kernel design is owned by ../fullduplex.md; listed here because the
|
||||
# orchestrator hosts the duplex control plane.
|
||||
- vllm_omni/engine/duplex/**
|
||||
depends_on:
|
||||
- input_output_modality_contracts.md
|
||||
- error_contracts.md
|
||||
@@ -44,7 +47,8 @@ validation_paths:
|
||||
- tests/engine/test_orchestrator_stage_input_bridge.py
|
||||
- tests/engine/test_cfg_companion_lifecycle.py
|
||||
- tests/engine/test_rpc_result_router.py
|
||||
- tests/e2e/features/fullduplex/engine/**
|
||||
- tests/engine/duplex/**
|
||||
- tests/engine/test_duplex_import_boundary.py
|
||||
upstream_refs:
|
||||
- vllm.v1.engine.EngineCoreRequest
|
||||
- vllm.v1.engine.EngineCoreOutputs
|
||||
|
||||
@@ -26,9 +26,18 @@ primary_code_paths:
|
||||
- vllm_omni/entrypoints/client_request_state.py
|
||||
- vllm_omni/entrypoints/stage_utils.py
|
||||
- vllm_omni/entrypoints/utils.py
|
||||
- vllm_omni/entrypoints/duplex/**
|
||||
- vllm_omni/entrypoints/duplex_request_client.py
|
||||
- vllm_omni/clients/**
|
||||
primary_path_exceptions:
|
||||
- path: vllm_omni/entrypoints/openai/errors.py
|
||||
owner: error_contracts.md
|
||||
- path: vllm_omni/entrypoints/duplex/**
|
||||
owner: ../fullduplex.md
|
||||
- path: vllm_omni/entrypoints/duplex_request_client.py
|
||||
owner: ../fullduplex.md
|
||||
- path: vllm_omni/clients/**
|
||||
owner: ../fullduplex.md
|
||||
related_code_paths:
|
||||
- vllm_omni/errors.py
|
||||
- vllm_omni/inputs/**
|
||||
|
||||
@@ -410,8 +410,8 @@ Notes:
|
||||
- `stages[].max_num_seqs` controls stage-level batching.
|
||||
- Thinker/Talker commonly use `enforce_eager: false` for CUDA Graph paths.
|
||||
- Code2Wav often remains eager (`enforce_eager: true`) depending on runtime behavior.
|
||||
- Qwen3-Omni defaults `VLLM_USE_FLASHINFER_MOE_FP16=0`. The Triton has been more stable & faster
|
||||
than the FlashInfer CUTLASS unquantized MoE backend on recent vLLM rebases.
|
||||
- The Qwen3-Omni deploy config pins `moe_backend: triton` for the Thinker and Talker stages. Triton has been more
|
||||
stable and faster than the FlashInfer CUTLASS unquantized MoE backend on recent vLLM rebases.
|
||||
|
||||
#### 2) Enable async chunk
|
||||
|
||||
|
||||
@@ -3,4 +3,6 @@
|
||||
vLLM-Omni's examples are split into two categories:
|
||||
|
||||
- If you are using vLLM-Omni from within Python code, see the *Offline Inference* section.
|
||||
- If you are using vLLM-Omni from an HTTP application or client, see the *Online Serving* section.
|
||||
- If you are using vLLM-Omni from an HTTP application or client, start with
|
||||
the [API Server guide](../serving/README.md) to choose an endpoint, then use
|
||||
the *Online Serving* section for model-specific examples.
|
||||
@@ -1,7 +1,7 @@
|
||||
# Installation
|
||||
|
||||
!!! important
|
||||
vLLM-Omni is released against the matching upstream vLLM major/minor version. vLLM-Omni 0.28.x requires vLLM 0.28.x; the stable v0.28.0 instructions pin vLLM 0.28.0.
|
||||
vLLM-Omni is released against the matching upstream vLLM major/minor version. The 0.29 development line requires vLLM 0.29.x; the source-install instructions pin vLLM 0.29.0. Published vLLM-Omni 0.28.0 wheels and images still require vLLM 0.28.x.
|
||||
|
||||
vLLM-Omni supports the following hardware platforms:
|
||||
|
||||
|
||||
@@ -34,7 +34,9 @@ vLLM-Omni is a Python library that supports the following GPU variants. The libr
|
||||
|
||||
### Pre-built wheels
|
||||
|
||||
Note: Pre-built wheels are currently available for vLLM-Omni 0.11.0rc1, 0.12.0rc1, 0.14.0rc1, 0.14.0, 0.16.0, 0.18.0, 0.20.0, 0.21.0, 0.22.0, 0.23.0, 0.24.0, 0.25.0, and 0.26.0; the 0.28.0 wheels are published together with the v0.28.0 release this branch produces. If you need a newer unreleased revision, please [build from source](https://docs.vllm.ai/projects/vllm-omni/en/latest/getting_started/installation/gpu/#build-wheel-from-source).
|
||||
The source-install instructions target vLLM 0.29.0. Until vLLM-Omni 0.29.0 artifacts are published, the CUDA/ROCm pre-built examples below remain on the matching 0.28.0 release pair.
|
||||
|
||||
Note: Pre-built wheels are currently available for vLLM-Omni 0.11.0rc1, 0.12.0rc1, 0.14.0rc1, 0.14.0, 0.16.0, 0.18.0, 0.20.0, 0.21.0, 0.22.0, 0.23.0, 0.24.0, 0.25.0, 0.26.0, and 0.28.0. If you need a newer unreleased revision, please [build from source](https://docs.vllm.ai/projects/vllm-omni/en/latest/getting_started/installation/gpu/#build-wheel-from-source).
|
||||
|
||||
=== "NVIDIA CUDA"
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
# --8<-- [end:requirements]
|
||||
# --8<-- [start:set-up-using-python]
|
||||
|
||||
vLLM-Omni depends on the matching major/minor release of vLLM. The vLLM-Omni 0.26.x release line uses vLLM 0.26.x.
|
||||
vLLM-Omni depends on the matching major/minor release of vLLM. The 0.29 development line uses vLLM 0.29.x. Published 0.28.0 wheels and images use vLLM 0.28.x.
|
||||
|
||||
!!! note
|
||||
PyTorch installed via `conda` will statically link `NCCL` library, which can cause issues when vLLM tries to use `NCCL`. See <gh-issue:8420> for more details.
|
||||
@@ -18,6 +18,8 @@ Therefore, it is recommended to install vLLM and vLLM-Omni with a **fresh new**
|
||||
|
||||
#### Installation of vLLM
|
||||
|
||||
These pre-built wheel instructions install the published vLLM-Omni 0.28.0 release. For the 0.29 development line, use the source-install instructions below.
|
||||
|
||||
vLLM-Omni is built based on vLLM. Please install it with command below.
|
||||
```bash
|
||||
uv pip install vllm==0.28.0 --torch-backend=auto
|
||||
@@ -26,12 +28,12 @@ uv pip install vllm==0.28.0 --torch-backend=auto
|
||||
#### Installation of vLLM-Omni
|
||||
|
||||
```bash
|
||||
uv pip install vllm-omni
|
||||
uv pip install vllm-omni==0.28.0
|
||||
```
|
||||
|
||||
To run Gradio demos, also install the optional extras:
|
||||
```bash
|
||||
uv pip install 'vllm-omni[demo]'
|
||||
uv pip install 'vllm-omni[demo]==0.28.0'
|
||||
```
|
||||
|
||||
# --8<-- [end:pre-built-wheels]
|
||||
@@ -39,13 +41,13 @@ uv pip install 'vllm-omni[demo]'
|
||||
# --8<-- [start:build-wheel-from-source]
|
||||
|
||||
#### Installation of vLLM
|
||||
If you do not need to modify source code of vLLM, you can directly install the stable 0.28.0 release version of the library
|
||||
If you do not need to modify source code of vLLM, you can directly install the stable 0.29.0 release version of the library
|
||||
|
||||
```bash
|
||||
uv pip install vllm==0.28.0 --torch-backend=auto
|
||||
uv pip install vllm==0.29.0 --torch-backend=auto
|
||||
```
|
||||
|
||||
The 0.28.0 release of vLLM ships CUDA 13.0-compatible binaries by default. If you need a different CUDA variant or want to reuse an existing PyTorch installation, build vLLM from source instead.
|
||||
The 0.29.0 release of vLLM ships CUDA 13.0-compatible binaries by default. If you need a different CUDA variant or want to reuse an existing PyTorch installation, build vLLM from source instead.
|
||||
|
||||
#### Installation of vLLM-Omni
|
||||
Since vllm-omni is rapidly evolving, it's recommended to install it from source
|
||||
@@ -66,12 +68,12 @@ If you want to check, modify or debug with source code of vLLM, install the libr
|
||||
```bash
|
||||
git clone https://github.com/vllm-project/vllm.git
|
||||
cd vllm
|
||||
git checkout v0.28.0
|
||||
git checkout v0.29.0
|
||||
```
|
||||
Set up environment variables to get pre-built wheels. If there are internet problems, just download the whl file manually. And set `VLLM_PRECOMPILED_WHEEL_LOCATION` as your local absolute path of whl file.
|
||||
```bash
|
||||
#For CUDA 13.0 (the default for v0.28.0; the wheel filename has no `+cu130` suffix)
|
||||
export VLLM_PRECOMPILED_WHEEL_LOCATION=https://github.com/vllm-project/vllm/releases/download/v0.28.0/vllm-0.28.0-cp38-abi3-manylinux_2_28_x86_64.whl
|
||||
#For CUDA 13.0 (the default for v0.29.0; the wheel filename has no `+cu130` suffix)
|
||||
export VLLM_PRECOMPILED_WHEEL_LOCATION=https://github.com/vllm-project/vllm/releases/download/v0.29.0/vllm-0.29.0-cp38-abi3-manylinux_2_28_x86_64.whl
|
||||
```
|
||||
Install vllm with command below (If you have no existing PyTorch).
|
||||
```bash
|
||||
@@ -95,6 +97,13 @@ uv pip install --no-build-isolation --editable .
|
||||
|
||||
vLLM-Omni offers an official docker image for deployment. These images are built on top of vLLM docker images and available on Docker Hub as [vllm/vllm-omni](https://hub.docker.com/r/vllm/vllm-omni/tags). The version of vLLM-Omni indicates which release of vLLM it is based on.
|
||||
|
||||
Nightly CUDA images built from the latest `main` are also available:
|
||||
|
||||
- [`vllm/vllm-omni:nightly`](https://hub.docker.com/r/vllm/vllm-omni/tags?name=nightly) is the rolling multi-architecture tag.
|
||||
- [`vllm/vllm-omni:nightly-<commit>`](https://hub.docker.com/r/vllm/vllm-omni/tags?name=nightly-) provides commit-pinned multi-architecture tags. The newest 14 are retained.
|
||||
|
||||
Use a commit-pinned tag when you need a reproducible nightly environment.
|
||||
|
||||
Here's an example deployment command that has been verified on 2 x H100's:
|
||||
```bash
|
||||
docker run --runtime nvidia --gpus 2 \
|
||||
@@ -124,7 +133,7 @@ If you want to specify the base vLLM version:
|
||||
```bash
|
||||
DOCKER_BUILDKIT=1 docker build \
|
||||
-f docker/Dockerfile.cuda \
|
||||
--build-arg BASE_IMAGE=vllm/vllm-openai:v0.28.0 \
|
||||
--build-arg BASE_IMAGE=vllm/vllm-openai:v0.29.0 \
|
||||
-t vllm-omni-cuda .
|
||||
```
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ vLLM-Omni current recommends the steps in under setup through Docker Images.
|
||||
|
||||
#### Installation of vLLM
|
||||
|
||||
These pre-built wheel instructions install the published vLLM-Omni 0.28.0 release. For the 0.29 development line, use the source-install instructions below.
|
||||
|
||||
vLLM-Omni is built based on vLLM. Please install it with command below.
|
||||
```bash
|
||||
uv pip install vllm==0.28.0+rocm723 --extra-index-url https://wheels.vllm.ai/rocm/0.28.0/rocm723
|
||||
@@ -22,7 +24,7 @@ uv pip install vllm==0.28.0+rocm723 --extra-index-url https://wheels.vllm.ai/roc
|
||||
# we need to add --no-build-isolation as the torch
|
||||
# is not obtained from pypi, we have to install using the
|
||||
# torch installed in our environment
|
||||
uv pip install vllm-omni
|
||||
uv pip install vllm-omni==0.28.0
|
||||
|
||||
# Optional if want to run Qwen3 TTS
|
||||
uv pip uninstall onnxruntime # should be removed before we can install onnxruntime-rocm
|
||||
@@ -34,13 +36,13 @@ uv pip install onnxruntime-rocm
|
||||
# --8<-- [start:build-wheel-from-source]
|
||||
|
||||
#### Installation of vLLM
|
||||
If you do not need to modify source code of vLLM, you can directly install the stable 0.28.0 release version of the library
|
||||
If you do not need to modify source code of vLLM, you can directly install the stable 0.29.0 release version of the library
|
||||
|
||||
```bash
|
||||
uv pip install vllm==0.28.0+rocm723 --extra-index-url https://wheels.vllm.ai/rocm/0.28.0/rocm723
|
||||
uv pip install vllm==0.29.0+rocm723 --extra-index-url https://wheels.vllm.ai/rocm/0.29.0/rocm723
|
||||
```
|
||||
|
||||
The pre-built 0.28.0 vLLM wheel targets ROCm 7.2.3. If you need a different ROCm stack or want to reuse an existing PyTorch installation, build vLLM from source instead.
|
||||
The pre-built 0.29.0 vLLM wheel targets ROCm 7.2.3. If you need a different ROCm stack or want to reuse an existing PyTorch installation, build vLLM from source instead.
|
||||
|
||||
#### Installation of vLLM-Omni
|
||||
Since vllm-omni is rapidly evolving, it's recommended to install it from source
|
||||
@@ -58,7 +60,7 @@ If you want to check, modify or debug with source code of vLLM, install the libr
|
||||
```bash
|
||||
git clone https://github.com/vllm-project/vllm.git
|
||||
cd vllm
|
||||
git checkout v0.28.0
|
||||
git checkout v0.29.0
|
||||
python3 -m pip install -r requirements/rocm.txt
|
||||
python3 setup.py develop
|
||||
```
|
||||
|
||||
@@ -7,13 +7,34 @@ For detailed hardware and software requirements, please refer to the [vLLM-Ascen
|
||||
|
||||
The recommended way to use vLLM-Omni on NPU is through the vLLM-Ascend pre-built Docker images:
|
||||
|
||||
=== "A2"
|
||||
|
||||
```bash
|
||||
export IMAGE=quay.io/atlas-ci/vllm-ascend:v0.28.0
|
||||
```
|
||||
|
||||
=== "A3"
|
||||
|
||||
```bash
|
||||
export IMAGE=quay.io/atlas-ci/vllm-ascend:v0.28.0-a3
|
||||
```
|
||||
|
||||
=== "A5"
|
||||
|
||||
```bash
|
||||
export IMAGE=quay.io/atlas-ci/vllm-ascend:v0.28.0-a5
|
||||
```
|
||||
|
||||
=== "310P (Experimental)"
|
||||
|
||||
```bash
|
||||
export IMAGE=quay.io/atlas-ci/vllm-ascend:v0.28.0-310p
|
||||
```
|
||||
|
||||
```bash
|
||||
# vLLM-Ascend image
|
||||
# Atlas A2: quay.io/atlas-ci/vllm-ascend:v0.26.0
|
||||
# Atlas A3: quay.io/atlas-ci/vllm-ascend:v0.26.0-a3
|
||||
docker run --rm \
|
||||
--name vllm-omni-npu \
|
||||
--shm-size=1g \
|
||||
--shm-size=64g \
|
||||
--device /dev/davinci0 \
|
||||
--device /dev/davinci1 \
|
||||
--device /dev/davinci2 \
|
||||
@@ -27,12 +48,12 @@ docker run --rm \
|
||||
-v /usr/local/Ascend/driver/version.info:/usr/local/Ascend/driver/version.info \
|
||||
-v /etc/ascend_install.info:/etc/ascend_install.info \
|
||||
-v /root/.cache:/root/.cache \
|
||||
-p 8000:8000 \
|
||||
-it quay.io/atlas-ci/vllm-ascend:v0.26.0-a3 bash
|
||||
-p 8091:8091 \
|
||||
-it "$IMAGE" bash
|
||||
|
||||
# Inside the container, install vLLM-Omni from source
|
||||
cd /vllm-workspace
|
||||
git clone -b v0.26.0 https://github.com/vllm-project/vllm-omni.git
|
||||
git clone -b v0.28.0 https://github.com/vllm-project/vllm-omni.git
|
||||
cd vllm-omni
|
||||
pip install -v -e . --no-build-isolation
|
||||
# or VLLM_OMNI_TARGET_DEVICE=npu pip install -v -e .
|
||||
@@ -50,21 +71,11 @@ We are keeping [issue #886](https://github.com/vllm-project/vllm-omni/issues/886
|
||||
|
||||
# --8<-- [start:installation-main]
|
||||
|
||||
You can also build vLLM-Omni from the latest main branch if you want to use the latest features or bug fixes. (But sometimes it will break for a while. You can check [issue #886](https://github.com/vllm-project/vllm-omni/issues/886) for the status of the latest commit of vLLM-Omni main branch on NPU.)
|
||||
You can also install vLLM-Omni from the latest main branch if you want to use the latest features or bug fixes. Use the aligned vLLM-Ascend v0.28.0 image described above, and check [issue #886](https://github.com/vllm-project/vllm-omni/issues/886) for the status of the latest vLLM-Omni commit on NPU.
|
||||
|
||||
```bash
|
||||
# Pin vLLM and vLLM-Ascend to the v0.26 release line
|
||||
git clone -b v0.26.0 https://github.com/vllm-project/vllm.git
|
||||
cd vllm
|
||||
VLLM_TARGET_DEVICE=empty pip install -v -e .
|
||||
cd ..
|
||||
|
||||
git clone -b releases/v0.26.0rc https://github.com/vllm-project/vllm-ascend.git
|
||||
cd vllm-ascend
|
||||
pip install -v -e .
|
||||
cd ..
|
||||
|
||||
# Install vLLM-Omni from the latest main branch
|
||||
# Inside the vLLM-Ascend v0.28.0 container, install vLLM-Omni from main
|
||||
cd /vllm-workspace
|
||||
git clone https://github.com/vllm-project/vllm-omni.git
|
||||
cd vllm-omni
|
||||
pip install -v -e . --no-build-isolation
|
||||
@@ -82,53 +93,146 @@ Supported images as following.
|
||||
|
||||
| image name | Hardware | OS |
|
||||
|-|-|-|
|
||||
| image-tag | Atlas A2 | Ubuntu |
|
||||
| image-tag-a3 | Atlas A3 | Ubuntu |
|
||||
| image-tag | A2 | Ubuntu |
|
||||
| image-tag-a3 | A3 | Ubuntu |
|
||||
| image-tag-a5 | A5 | Ubuntu |
|
||||
| image-tag-310p | 310P (experimental) | Ubuntu |
|
||||
|
||||
Here's an example deployment command that has been verified on 4 x NPUs:
|
||||
|
||||
```bash
|
||||
# Atlas A2: quay.io/ascend/vllm-omni:v0.26.0
|
||||
# Atlas A3: quay.io/ascend/vllm-omni:v0.26.0-a3
|
||||
docker run --rm \
|
||||
--name vllm-omni-a3 \
|
||||
--shm-size=4g \
|
||||
--device /dev/davinci0 \
|
||||
--device /dev/davinci1 \
|
||||
--device /dev/davinci2 \
|
||||
--device /dev/davinci3 \
|
||||
--device /dev/davinci_manager \
|
||||
--device /dev/devmm_svm \
|
||||
--device /dev/hisi_hdc \
|
||||
-v /usr/local/dcmi:/usr/local/dcmi \
|
||||
-v /usr/local/bin/npu-smi:/usr/local/bin/npu-smi \
|
||||
-v /usr/local/Ascend/driver/lib64/:/usr/local/Ascend/driver/lib64/ \
|
||||
-v /usr/local/Ascend/driver/version.info:/usr/local/Ascend/driver/version.info \
|
||||
--device /dev/davinci_manager \
|
||||
--device /dev/devmm_svm \
|
||||
--device /dev/hisi_hdc \
|
||||
-v /usr/local/dcmi:/usr/local/dcmi \
|
||||
-v /usr/local/bin/npu-smi:/usr/local/bin/npu-smi \
|
||||
-v /usr/local/Ascend/driver/lib64/:/usr/local/Ascend/driver/lib64/ \
|
||||
-v /usr/local/Ascend/driver/version.info:/usr/local/Ascend/driver/version.info \
|
||||
-v /etc/ascend_install.info:/etc/ascend_install.info \
|
||||
-v ~/.cache:/root/.cache \
|
||||
-p 8091:8091 \
|
||||
-it quay.io/ascend/vllm-omni:v0.26.0-a3 bash
|
||||
```
|
||||
=== "A2"
|
||||
|
||||
```bash
|
||||
export IMAGE=quay.io/ascend/vllm-omni:v0.28.0
|
||||
docker run --rm \
|
||||
--name vllm-omni-a2 \
|
||||
--shm-size=64g \
|
||||
--device /dev/davinci0 \
|
||||
--device /dev/davinci1 \
|
||||
--device /dev/davinci2 \
|
||||
--device /dev/davinci3 \
|
||||
--device /dev/davinci_manager \
|
||||
--device /dev/devmm_svm \
|
||||
--device /dev/hisi_hdc \
|
||||
-v /usr/local/dcmi:/usr/local/dcmi \
|
||||
-v /usr/local/bin/npu-smi:/usr/local/bin/npu-smi \
|
||||
-v /usr/local/Ascend/driver/lib64/:/usr/local/Ascend/driver/lib64/ \
|
||||
-v /usr/local/Ascend/driver/version.info:/usr/local/Ascend/driver/version.info \
|
||||
-v /etc/ascend_install.info:/etc/ascend_install.info \
|
||||
-v ~/.cache:/root/.cache \
|
||||
-p 8091:8091 \
|
||||
-it "$IMAGE" bash
|
||||
```
|
||||
|
||||
=== "A3"
|
||||
|
||||
```bash
|
||||
export IMAGE=quay.io/ascend/vllm-omni:v0.28.0-a3
|
||||
docker run --rm \
|
||||
--name vllm-omni-a3 \
|
||||
--shm-size=64g \
|
||||
--device /dev/davinci0 \
|
||||
--device /dev/davinci1 \
|
||||
--device /dev/davinci2 \
|
||||
--device /dev/davinci3 \
|
||||
--device /dev/davinci_manager \
|
||||
--device /dev/devmm_svm \
|
||||
--device /dev/hisi_hdc \
|
||||
-v /usr/local/dcmi:/usr/local/dcmi \
|
||||
-v /usr/local/bin/npu-smi:/usr/local/bin/npu-smi \
|
||||
-v /usr/local/Ascend/driver/lib64/:/usr/local/Ascend/driver/lib64/ \
|
||||
-v /usr/local/Ascend/driver/version.info:/usr/local/Ascend/driver/version.info \
|
||||
-v /etc/ascend_install.info:/etc/ascend_install.info \
|
||||
-v ~/.cache:/root/.cache \
|
||||
-p 8091:8091 \
|
||||
-it "$IMAGE" bash
|
||||
```
|
||||
|
||||
=== "A5"
|
||||
|
||||
```bash
|
||||
export IMAGE=quay.io/ascend/vllm-omni:v0.28.0-a5
|
||||
docker run --rm \
|
||||
--name vllm-omni-a5 \
|
||||
--shm-size=64g \
|
||||
--device /dev/davinci0 \
|
||||
--device /dev/davinci1 \
|
||||
--device /dev/davinci2 \
|
||||
--device /dev/davinci3 \
|
||||
--device /dev/davinci_manager \
|
||||
--device /dev/devmm_svm \
|
||||
--device /dev/hisi_hdc \
|
||||
-v /usr/local/dcmi:/usr/local/dcmi \
|
||||
-v /usr/local/bin/npu-smi:/usr/local/bin/npu-smi \
|
||||
-v /usr/local/Ascend/driver/lib64/:/usr/local/Ascend/driver/lib64/ \
|
||||
-v /usr/local/Ascend/driver/version.info:/usr/local/Ascend/driver/version.info \
|
||||
-v /etc/ascend_install.info:/etc/ascend_install.info \
|
||||
-v ~/.cache:/root/.cache \
|
||||
-p 8091:8091 \
|
||||
-it "$IMAGE" bash
|
||||
```
|
||||
|
||||
=== "310P (Experimental)"
|
||||
|
||||
```bash
|
||||
export IMAGE=quay.io/ascend/vllm-omni:v0.28.0-310p
|
||||
docker run --rm \
|
||||
--name vllm-omni-310p \
|
||||
--shm-size=64g \
|
||||
--device /dev/davinci0 \
|
||||
--device /dev/davinci1 \
|
||||
--device /dev/davinci2 \
|
||||
--device /dev/davinci3 \
|
||||
--device /dev/davinci_manager \
|
||||
--device /dev/devmm_svm \
|
||||
--device /dev/hisi_hdc \
|
||||
-v /usr/local/dcmi:/usr/local/dcmi \
|
||||
-v /usr/local/bin/npu-smi:/usr/local/bin/npu-smi \
|
||||
-v /usr/local/Ascend/driver/lib64/:/usr/local/Ascend/driver/lib64/ \
|
||||
-v /usr/local/Ascend/driver/version.info:/usr/local/Ascend/driver/version.info \
|
||||
-v /etc/ascend_install.info:/etc/ascend_install.info \
|
||||
-v ~/.cache:/root/.cache \
|
||||
-p 8091:8091 \
|
||||
-it "$IMAGE" bash
|
||||
```
|
||||
|
||||
!!! tip
|
||||
You can use this docker image to serve models the same way you would with in vLLM! To do so, make sure you overwrite the default entrypoint (`vllm serve --omni`) which works only for models supported in the vLLM-Omni project.
|
||||
|
||||
Or build IMAGE from **source code**:
|
||||
Or build the image from **source code**:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/vllm-project/vllm-omni.git
|
||||
cd vllm-omni
|
||||
# A2
|
||||
# docker build -t vllm-omni-dev-image:latest -f ./docker/Dockerfile.npu .
|
||||
# A3
|
||||
docker build -t vllm-omni-dev-image:latest -f ./docker/Dockerfile.npu.a3 .
|
||||
```
|
||||
|
||||
All hardware variants use `docker/Dockerfile.npu`. Choose the device suffix for your hardware:
|
||||
|
||||
=== "A2"
|
||||
|
||||
```bash
|
||||
docker build -t vllm-omni-dev-image:latest -f docker/Dockerfile.npu .
|
||||
```
|
||||
|
||||
=== "A3"
|
||||
|
||||
```bash
|
||||
docker build -t vllm-omni-dev-image:latest -f docker/Dockerfile.npu \
|
||||
--build-arg VLLM_ASCEND_DEVICE_SUFFIX=-a3 .
|
||||
```
|
||||
|
||||
=== "A5"
|
||||
|
||||
```bash
|
||||
docker build -t vllm-omni-dev-image:latest -f docker/Dockerfile.npu \
|
||||
--build-arg VLLM_ASCEND_DEVICE_SUFFIX=-a5 .
|
||||
```
|
||||
|
||||
=== "310P (Experimental)"
|
||||
|
||||
```bash
|
||||
docker build -t vllm-omni-dev-image:latest -f docker/Dockerfile.npu \
|
||||
--build-arg VLLM_ASCEND_DEVICE_SUFFIX=-310p .
|
||||
```
|
||||
|
||||
# --8<-- [end:pre-built-images]
|
||||
@@ -19,10 +19,10 @@ uv venv --python 3.12 --seed
|
||||
source .venv/bin/activate
|
||||
|
||||
# On CUDA
|
||||
uv pip install vllm==0.28.0 --torch-backend=auto
|
||||
uv pip install vllm==0.29.0 --torch-backend=auto
|
||||
|
||||
# On ROCm
|
||||
uv pip install vllm==0.28.0+rocm723 --extra-index-url https://wheels.vllm.ai/rocm/0.28.0/rocm723
|
||||
uv pip install vllm==0.29.0+rocm723 --extra-index-url https://wheels.vllm.ai/rocm/0.29.0/rocm723
|
||||
|
||||
git clone https://github.com/vllm-project/vllm-omni.git
|
||||
cd vllm-omni
|
||||
@@ -34,7 +34,7 @@ For additional installation methods — please see the [installation guide](inst
|
||||
!!! note
|
||||
It is important to install the same major & minor version of vLLM and vLLM Omni, otherwise things may not work as expected. If the versions are misaligned, you will see a warning when you import vLLM Omni.
|
||||
|
||||
If you are seeing strange behavior with the `vllm` command not handling the `--omni` flag correctly, you most likely have a version mismatch with vLLM < `0.28.0` and vLLM Omni `0.28.0`, as vLLM Omni no longer hijacks the vLLM entrypoint. Updating vLLM should resolve this issue.
|
||||
If you are seeing strange behavior with the `vllm` command not handling the `--omni` flag correctly, you most likely have a version mismatch with vLLM < `0.29.0` and vLLM Omni `0.29.0`, as vLLM Omni no longer hijacks the vLLM entrypoint. Updating vLLM should resolve this issue.
|
||||
|
||||
## Offline Inference
|
||||
|
||||
@@ -91,7 +91,7 @@ if __name__ == "__main__":
|
||||
|
||||
For more usages, please refer to [offline inference](../user_guide/examples/offline_inference/qwen2_5_omni.md)
|
||||
|
||||
## Online Serving with OpenAI-Completions API
|
||||
## Online Serving with the API Server
|
||||
|
||||
Text-to-image generation quickstart with vLLM-Omni:
|
||||
|
||||
@@ -100,20 +100,16 @@ vllm serve Tongyi-MAI/Z-Image-Turbo --omni --port 8091
|
||||
```
|
||||
|
||||
```bash
|
||||
curl -s http://localhost:8091/v1/chat/completions \
|
||||
curl -s http://localhost:8091/v1/images/generations \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"messages": [
|
||||
{"role": "user", "content": "a cup of coffee on the table"}
|
||||
],
|
||||
"extra_body": {
|
||||
"height": 1024,
|
||||
"width": 1024,
|
||||
"num_inference_steps": 50,
|
||||
"guidance_scale": 4.0,
|
||||
"seed": 42
|
||||
}
|
||||
}' | jq -r '.choices[0].message.content[0].image_url.url' | cut -d',' -f2 | base64 -d > coffee.png
|
||||
"prompt": "a cup of coffee on the table",
|
||||
"size": "1024x1024",
|
||||
"response_format": "b64_json",
|
||||
"seed": 42
|
||||
}' | jq -r '.data[0].b64_json' | base64 -d > coffee.png
|
||||
```
|
||||
|
||||
For more details, please refer to [online serving](../user_guide/examples/online_serving/text_to_image.md).
|
||||
See the [API Server guide](../serving/README.md) to choose an endpoint. For
|
||||
model-specific details, refer to the [text-to-image online serving
|
||||
example](../user_guide/examples/online_serving/text_to_image.md).
|
||||
@@ -22,6 +22,13 @@ CATEGORIES = {
|
||||
"name": "Entry Points",
|
||||
"description": "Main entry points for vLLM-Omni inference and serving.",
|
||||
},
|
||||
"clients": {
|
||||
"name": "Clients",
|
||||
"description": (
|
||||
"Client libraries that talk to a running vLLM-Omni server "
|
||||
"(the full-duplex Realtime client and its per-model session presets)."
|
||||
),
|
||||
},
|
||||
"inputs": {
|
||||
"name": "Inputs",
|
||||
"description": "Input data structures for multi-modal inputs.",
|
||||
|
||||
@@ -124,6 +124,7 @@ def extract_omni_serve_subparser_init():
|
||||
"make_arg_parser": lambda parser: parser, # no-op for doc
|
||||
"_ensure_vllm_platform": lambda: None, # no-op for doc
|
||||
"_nonneg_finite_float": float,
|
||||
"_parse_stage_overrides": __import__("json").loads,
|
||||
"VLLM_SUBCMD_PARSER_EPILOG": "",
|
||||
"logger": logger,
|
||||
"DummySubparsers": DummySubparsers,
|
||||
|
||||
@@ -18,10 +18,10 @@ th {
|
||||
</style>
|
||||
|
||||
| Architecture | Models | Example HF Models | NVIDIA GPU | AMD GPU | Ascend NPU | Intel GPU | Recipe |
|
||||
|--------------|--------|-------------------|------------|---------|-----|-----------|--------|
|
||||
| -------------- | -------- | ------------------- | ------------ | --------- | ----- | ----------- | -------- |
|
||||
| `Qwen3OmniMoeForConditionalGeneration` | Qwen3-Omni | `Qwen/Qwen3-Omni-30B-A3B-Instruct` | ✅︎ | | | | [Repository](https://github.com/vllm-project/vllm-omni/blob/main/recipes/Qwen/Qwen3-Omni.md) |
|
||||
| `Qwen2_5OmniForConditionalGeneration` | Qwen2.5-Omni | `Qwen/Qwen2.5-Omni-7B`, `Qwen/Qwen2.5-Omni-3B` | ✅︎ | ✅︎ | ✅︎ | ✅︎ | — |
|
||||
| `MingFlashOmniForConditionalGeneration` + `MingImagePipeline` | Ming-flash-omni-2.0 (omni-speech + imagegen<sup>1</sup>) | `Jonathan1909/Ming-flash-omni-2.0` | ✅︎ | | | | — |
|
||||
| `MingFlashOmniForConditionalGeneration` + `MingImagePipeline` | Ming-flash-omni-2.0 (omni-speech + imagegen<sup>1</sup>) | `Jonathan1909/Ming-flash-omni-2.0` | ✅︎ | | | | — |
|
||||
| `BagelForConditionalGeneration` | BAGEL (DiT-only) | `ByteDance-Seed/BAGEL-7B-MoT` | ✅︎ | ✅︎ | | ✅︎ | — |
|
||||
| `InternVLAA1Pipeline` | InternVLA-A1 | `InternRobotics/InternVLA-A1-3B` | ✅︎ | ✅︎ | | | — |
|
||||
| `Gr00tN1d7Pipeline` | GR00T N1.7 | `nvidia/GR00T-N1.7-3B` | ✅︎ | | | | — |
|
||||
@@ -42,7 +42,7 @@ th {
|
||||
| `SanaWmPipeline` | SANA-WM | `BBBBruce/SANA-WM_bidirectional-stage1-diffusers` | ✅︎ | | | | [Repository](https://github.com/vllm-project/vllm-omni/blob/main/recipes/NVIDIA/SANA-WM.md) |
|
||||
| `Wan22S2VPipeline` | Wan2.2-S2V | `Wan-AI/Wan2.2-S2V-14B` | ✅︎ | ✅︎ | ✅︎ | ✅︎ | — |
|
||||
| `Wan22VACEPipeline` | Wan2.1-VACE | `Wan-AI/Wan2.1-VACE-1.3B-diffusers`, `Wan-AI/Wan2.1-VACE-14B-diffusers` | ✅︎ | ✅︎ | ✅︎ | ✅︎ | — |
|
||||
| `Wan22VACEPipeline` | Wan2.2-VACE | `Pyros13/Wan2.2-VACE-Fun-A14B-Diffusers` | ✅︎ | | | ✅︎ | — |
|
||||
| `Wan22VACEPipeline` | Wan2.2-VACE | `Pyros13/Wan2.2-VACE-Fun-A14B-Diffusers` | ✅︎ | | | ✅︎ | — |
|
||||
| `LTX2Pipeline` | LTX-2 / LTX-2.3 one-stage T2V and I2V | `Lightricks/LTX-2`, `diffusers/LTX-2.3-Diffusers` | ✅︎ | ✅︎ | | | — |
|
||||
| `LTX2TwoStagePipeline` | LTX-2 / LTX-2.3 ordinary two-stage T2V and I2V | `Lightricks/LTX-2`, `diffusers/LTX-2.3-Diffusers` + matching Lightricks LoRA and upsampler | ✅︎ | ✅︎ | | | — |
|
||||
| `LTX2DistilledOneStagePipeline` | LTX-2 / LTX-2.3 merged-distilled one-stage T2V and I2V | `rootonchair/LTX-2-19b-distilled`, `diffusers/LTX-2.3-Distilled-Diffusers` | ✅︎ | ✅︎ | | | — |
|
||||
@@ -55,6 +55,7 @@ th {
|
||||
| `SanaVideoPipeline` | SANA-Video-2B T2V (native 480p/720p; Diffusers adapter validated at both resolutions) | `Efficient-Large-Model/SANA-Video_2B_480p_diffusers`, `Efficient-Large-Model/SANA-Video_2B_720p_diffusers` | ✅︎ | | | | [Repository](https://github.com/vllm-project/vllm-omni/blob/main/recipes/NVIDIA/SANA-Video-2B.md) |
|
||||
| `SanaImageToVideoPipeline` | SANA-Video-2B I2V (native and Diffusers adapter validated at 480p/720p) | `Efficient-Large-Model/SANA-Video_2B_480p_diffusers`, `Efficient-Large-Model/SANA-Video_2B_720p_diffusers` | ✅︎ | | | | [Repository](https://github.com/vllm-project/vllm-omni/blob/main/recipes/NVIDIA/SANA-Video-2B.md) |
|
||||
| `MiniMaxH3Pipeline` | MiniMax H3 T2VA, first/last-keyframe FL2VA, and mixed-reference Ref2VA | `MiniMaxAI/MiniMax-H3` | ✅︎ | ✅︎ | | | [Published](https://recipes.vllm.ai/MiniMaxAI/MiniMax-H3) |
|
||||
| `Magi2Pipeline` | MAGI-2 native Preview-stage T2VA and I2VA (272p/540p) | `sand-ai/MAGI-2-preview` | ✅︎ | | | | [Repository](https://github.com/vllm-project/vllm-omni/blob/main/recipes/SandAI/MAGI-2-preview-L20X.md) |
|
||||
| `DreamZeroPipeline` | DreamZero-DROID | `GEAR-Dreams/DreamZero-DROID` | ✅︎ | ✅︎ | ✅︎ | ✅︎ | — |
|
||||
| `Pi0Pipeline` | π0 (Pi-Zero) VLA | `lerobot/pi0_base` | ✅︎ | | | | [Repository](https://github.com/vllm-project/vllm-omni/blob/main/recipes/lerobot/Pi0.md) |
|
||||
| `HeliosPipeline`, `HeliosPyramidPipeline` | Helios | `BestWishYsh/Helios-Base`, `BestWishYsh/Helios-Mid`, `BestWishYsh/Helios-Distilled` | ✅︎ | ✅︎ | ✅︎ | | — |
|
||||
@@ -62,13 +63,14 @@ th {
|
||||
| `LongcatImagePipeline` | LongCat-Image | `meituan-longcat/LongCat-Image` | ✅︎ | ✅︎ | ✅︎ | ✅︎ | — |
|
||||
| `LongCatImageEditPipeline` | LongCat-Image-Edit | `meituan-longcat/LongCat-Image-Edit` | ✅︎ | ✅︎ | ✅︎ | ✅︎ | — |
|
||||
| `LongCatVideoAvatarPipeline` | LongCat-Video-Avatar-1.5 A2V/AI2V (native single-speaker and multi-speaker AI2V/AVC) | `meituan-longcat/LongCat-Video-Avatar-1.5` | ✅︎ | | | | [Repository](https://github.com/vllm-project/vllm-omni/blob/main/recipes/meituan-longcat/LongCat-Video-Avatar-1.5.md) |
|
||||
| `BooguImagePipeline` | Boogu-Image | `Boogu/Boogu-Image-0.1-Base`, `Boogu/Boogu-Image-0.1-Edit` | ✅︎ | | | ✅︎ | — |
|
||||
| `BooguImagePipeline` | Boogu-Image | `Boogu/Boogu-Image-0.1-Base`, `Boogu/Boogu-Image-0.1-Edit`, `Boogu/Boogu-Image-0.1-Edit-Turbo` | ✅︎ | | | ✅︎ | [Repository](https://github.com/vllm-project/vllm-omni/blob/main/recipes/Boogu/Boogu-Image.md) |
|
||||
| `BooguImageTurboPipeline` | Boogu-Image Turbo distilled T2I | `Boogu/Boogu-Image-0.1-Turbo` | ✅︎ | | | | [Repository](https://github.com/vllm-project/vllm-omni/blob/main/recipes/Boogu/Boogu-Image.md) |
|
||||
| `StableDiffusionXLPipeline` | Stable-Diffusion-XL | `stabilityai/stable-diffusion-xl-base-1.0` | ✅︎ | ✅︎ | ✅︎ | ✅︎ | — |
|
||||
| `StableDiffusion3Pipeline` | Stable-Diffusion-3 | `stabilityai/stable-diffusion-3.5-medium` | ✅︎ | ✅︎ | | ✅︎ | — |
|
||||
| `CosyVoice3Model` | CosyVoice3 | `FunAudioLLM/Fun-CosyVoice3-0.5B-2512` | ✅︎ | ✅︎ | | ✅︎ | — |
|
||||
| `OmniVoiceModel` | OmniVoice | `k2-fsa/OmniVoice` | ✅︎ | | | ✅︎ | — |
|
||||
| `VoxCPM2TalkerForConditionalGeneration` | VoxCPM2 | `openbmb/VoxCPM2` | ✅︎ | | | ✅︎ | — |
|
||||
| `DotsTTSForConditionalGeneration` | dots.tts | `rednote-hilab/dots.tts-soar` | ✅︎ | | | | — |
|
||||
| `DotsTTSForConditionalGeneration` | dots.tts | `dots-studio/dots.tts-soar` | ✅︎ | | | | — |
|
||||
| `MammothModa2ForConditionalGeneration` | MammothModa2-Preview | `bytedance-research/MammothModa2-Preview` | ✅︎ | ✅︎ | | | — |
|
||||
| `MammothModa2ForConditionalGeneration` | MammothModa2-Dev (AR-only image understanding) | `bytedance-research/MammothModa2-Dev` | ✅︎ | | | | — |
|
||||
| `Flux2KleinPipeline` | FLUX.2-klein | `black-forest-labs/FLUX.2-klein-4B`, `black-forest-labs/FLUX.2-klein-9B` | ✅︎ | ✅︎ | ✅︎ | ✅︎ | — |
|
||||
@@ -95,17 +97,18 @@ th {
|
||||
| `MiMoV2ASRForCausalLM` | MiMo-V2.5-ASR | `XiaomiMiMo/MiMo-V2.5-ASR` | ✅︎ | ✅︎ | | | — |
|
||||
| `Flux2Pipeline` | FLUX.2-dev | `black-forest-labs/FLUX.2-dev` | ✅︎ | ✅︎ | | | — |
|
||||
| `FishSpeechSlowARForConditionalGeneration` | Fish Speech S2 Pro | `fishaudio/s2-pro` | ✅︎ | ✅︎ | | ✅︎ | — |
|
||||
| `SenseNovaU1Pipeline` | SenseNova-U1 (DiT-only) | `SenseNova/SenseNova-U1-8B-MoT` | ✅︎ | | | | — |
|
||||
| `Audio8TTSSlowARForConditionalGeneration` | Audio8 TTS Preview | `Audio8/Audio8-TTS-Preview-0.6b` | ✅︎ | | | | — |
|
||||
| `SenseNovaU1Pipeline` | SenseNova-U1 (DiT-only), SenseNova-U1.5 | `SenseNova/SenseNova-U1-8B-MoT`, `sensenova/SenseNova-U1.5-8B-MoT` | ✅︎ | | | | [Repository](https://github.com/vllm-project/vllm-omni/blob/main/recipes/SenseNova/SenseNova-U1.5.md) |
|
||||
| `LancePipeline` | Lance | `bytedance-research/Lance` | ✅︎ | | | | — |
|
||||
| `HunyuanVideo15Pipeline` | HunyuanVideo-1.5-T2V | `hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-480p_t2v`, `hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-720p_t2v` | ✅︎ | ✅︎ | | | — |
|
||||
| `HunyuanVideo15ImageToVideoPipeline` | HunyuanVideo-1.5-I2V | `hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-480p_i2v`, `hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-720p_i2v` | ✅︎ | ✅︎ | | | — |
|
||||
| `VoxtralTTSForConditionalGeneration` | Voxtral TTS | `mistralai/Voxtral-4B-TTS-2603` | ✅︎ | ✅︎ | | | — |
|
||||
| `CovoAudioForConditionalGeneration` | Covo-Audio-Chat | `tencent/Covo-Audio-Chat` | ✅︎ | | | | — |
|
||||
|`DyninOmniForConditionalGeneration` | Dynin-Omni | `snu-aidas/Dynin-Omni` | ✅︎ | | | ✅︎ | — |
|
||||
| `DyninOmniForConditionalGeneration` | Dynin-Omni | `snu-aidas/Dynin-Omni` | ✅︎ | | | ✅︎ | — |
|
||||
| `MiniCPMO45OmniForConditionalGeneration` | MiniCPM-o 4.5 | `openbmb/MiniCPM-o-4_5` | ✅︎ | | ✅︎ | | — |
|
||||
| `ErnieImagePipeline` | ERNIE-Image | `baidu/ERNIE-Image`, `baidu/ERNIE-Image-Turbo` | ✅︎ | ✅︎ | ✅︎ | ✅︎ | — |
|
||||
| `GepardTalkerForConditionalGeneration` | Gepard-1.0 | `nineninesix/gepard-1.0` | ✅︎ | | | ✅︎ | — |
|
||||
|`HiDreamImagePipeline` | HiDream-I1-Full | `HiDream-ai/HiDream-I1-Full` | ✅︎ | ✅︎ | | | — |
|
||||
| `HiDreamImagePipeline` | HiDream-I1-Full | `HiDream-ai/HiDream-I1-Full` | ✅︎ | ✅︎ | | | — |
|
||||
| `HiDreamO1ImagePipeline` | HiDream-O1-Image | `HiDream-ai/HiDream-O1-Image` | ✅︎ | | | | — |
|
||||
|
||||
The `Recipe` column links the corresponding deployment recipe. `Published` is
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
# API Server
|
||||
|
||||
vLLM-Omni exposes OpenAI-compatible HTTP APIs plus vLLM-Omni extensions for
|
||||
image, audio, video, realtime, and robot-policy workloads. Use this page to
|
||||
choose an endpoint; follow the linked reference page for model-specific fields,
|
||||
wire protocols, and response details.
|
||||
|
||||
Each server instance hosts one model. An endpoint is usable only when the
|
||||
loaded model supports that task.
|
||||
|
||||
This guide focuses on vLLM-Omni's primary task endpoints. Compatible models
|
||||
may also expose standard routes inherited from vLLM, such as
|
||||
`POST /v1/completions`; those routes remain model- and task-dependent.
|
||||
|
||||
## Start and Verify the Server
|
||||
|
||||
Start a model with the shared serving command:
|
||||
|
||||
```bash
|
||||
vllm serve <model> --omni --port 8091
|
||||
```
|
||||
|
||||
Some models require additional flags or a deployment configuration. Use the
|
||||
model-specific guide when one is provided.
|
||||
|
||||
After the server starts, check its health and served model name:
|
||||
|
||||
```bash
|
||||
export VLLM_OMNI_BASE_URL=http://localhost:8091
|
||||
|
||||
curl "$VLLM_OMNI_BASE_URL/health"
|
||||
curl "$VLLM_OMNI_BASE_URL/v1/models" | jq .
|
||||
```
|
||||
|
||||
Use `http://localhost:8091/v1` as the `base_url` for an OpenAI SDK client. If
|
||||
the server was started with `--api-key`, include an `Authorization` header with
|
||||
`Bearer <api-key>` in requests.
|
||||
|
||||
## Choose a Core HTTP Endpoint
|
||||
|
||||
Prefer a task-specific endpoint when one matches your workload. Use Chat
|
||||
Completions for conversational or heterogeneous omni pipelines, rather than as
|
||||
the default wrapper for every generation task.
|
||||
|
||||
| Task | Endpoint | Request | Response | Details |
|
||||
| ------ | ---------- | --------- | ---------- | --------- |
|
||||
| Conversation or multimodal understanding/generation | `POST /v1/chat/completions` | JSON | OpenAI-style JSON or SSE | [Chat Completions](chat_completions_api.md) |
|
||||
| Text-to-speech | `POST /v1/audio/speech` | JSON | Audio bytes or SSE | [Speech](speech_api.md) |
|
||||
| Sound, music, or ambient audio generation | `POST /v1/audio/generate` | JSON | Audio bytes | [Audio Generation](audio_generate_api.md) |
|
||||
| Text-to-image | `POST /v1/images/generations` | JSON | Base64 JSON or an image file | [Image Generation](image_generation_api.md) |
|
||||
| Image editing | `POST /v1/images/edits` | Multipart form | Base64 JSON or SSE | [Image Edit](image_edit_api.md) |
|
||||
| Video generation (recommended) | `POST /v1/videos` | Multipart form | Asynchronous job JSON | [Videos](videos_api.md) |
|
||||
| Video generation (blocking) | `POST /v1/videos/sync` | Multipart form | Video bytes | [Videos](videos_api.md#synchronous-response) |
|
||||
|
||||
`POST /v1/videos/sync` is intended for tests and simple scripts. Use the
|
||||
asynchronous job API for production requests so clients can poll, download,
|
||||
and delete generated videos independently.
|
||||
|
||||
## Minimal Requests
|
||||
|
||||
The examples below assume a compatible model is already running. TTS voices,
|
||||
media inputs, generation controls, and output capabilities vary by model.
|
||||
|
||||
=== "Chat"
|
||||
|
||||
```bash
|
||||
curl "$VLLM_OMNI_BASE_URL/v1/chat/completions" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"messages": [{"role": "user", "content": "Describe vLLM-Omni briefly."}],
|
||||
"modalities": ["text"]
|
||||
}'
|
||||
```
|
||||
|
||||
=== "Speech"
|
||||
|
||||
```bash
|
||||
curl "$VLLM_OMNI_BASE_URL/v1/audio/speech" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"input": "Hello from vLLM-Omni.",
|
||||
"voice": "vivian"
|
||||
}' \
|
||||
--output speech.wav
|
||||
```
|
||||
|
||||
=== "Audio"
|
||||
|
||||
```bash
|
||||
curl "$VLLM_OMNI_BASE_URL/v1/audio/generate" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"input": "Ocean waves on a quiet beach",
|
||||
"audio_length": 5.0
|
||||
}' \
|
||||
--output audio.wav
|
||||
```
|
||||
|
||||
=== "Image"
|
||||
|
||||
```bash
|
||||
curl "$VLLM_OMNI_BASE_URL/v1/images/generations" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"prompt": "A small robot reading beside a window",
|
||||
"size": "1024x1024",
|
||||
"response_format": "file"
|
||||
}' \
|
||||
--output image.png
|
||||
```
|
||||
|
||||
=== "Image edit"
|
||||
|
||||
```bash
|
||||
curl "$VLLM_OMNI_BASE_URL/v1/images/edits" \
|
||||
-F "image=@input.png" \
|
||||
-F "prompt=Turn the background into a snowy mountain" \
|
||||
| jq -r '.data[0].b64_json' \
|
||||
| base64 --decode > edited.png
|
||||
```
|
||||
|
||||
=== "Video"
|
||||
|
||||
```bash
|
||||
curl "$VLLM_OMNI_BASE_URL/v1/videos" \
|
||||
-F "prompt=A cinematic tracking shot of a mountain lake at sunrise" \
|
||||
-F "size=1280x720" \
|
||||
-F "seconds=5"
|
||||
```
|
||||
|
||||
The response contains a video job `id`. Poll and download it with:
|
||||
|
||||
```bash
|
||||
curl "$VLLM_OMNI_BASE_URL/v1/videos/<video_id>"
|
||||
curl -L "$VLLM_OMNI_BASE_URL/v1/videos/<video_id>/content" --output video.mp4
|
||||
```
|
||||
|
||||
## Choose a Streaming or Realtime Endpoint
|
||||
|
||||
These WebSocket APIs have different event schemas and cannot be used
|
||||
interchangeably.
|
||||
|
||||
| Workload | Endpoint | Interaction model | Details |
|
||||
| ---------- | ---------- | ------------------- | --------- |
|
||||
| Incremental text input for speech synthesis | `WS /v1/audio/speech/stream` | Send text events and receive audio | [Streaming Text to Speech](speech_api.md#streaming-text-input-websocket) |
|
||||
| Live video understanding | `WS /v1/video/chat/stream` | Send video frames and receive text/audio | [Streaming Video Input](video_stream_api.md) |
|
||||
| Turn-based realtime audio | `WS /v1/realtime` | Stream one audio input and receive transcript/audio events | [Realtime Audio](realtime_api.md) |
|
||||
| Continuous speech-to-speech interaction | `WS /v1/realtime?duplex=1` or `WS /v1/duplex` | Listen and speak concurrently with session control | [Full Duplex](full_duplex_api.md) |
|
||||
| Generated video chunks | `WS /v1/realtime/video` | Start a diffusion request and receive fragmented MP4 | [Streaming Video Output](streaming_video_output_api.md) |
|
||||
| Robot policy inference | `WS /v1/realtime/robot/openpi` | Send MessagePack observations and receive action arrays | [OpenPI Robot Policy](openpi_api.md) |
|
||||
|
||||
All six routes are model- or configuration-dependent. In particular,
|
||||
`/v1/realtime` is not full duplex unless the client sets `duplex=1` and the
|
||||
deployment explicitly enables duplex sessions. Clients should also verify the
|
||||
duplex capability payload because the query-parameter form falls back to the
|
||||
ordinary realtime handler when duplex is unavailable.
|
||||
|
||||
## Related Endpoints
|
||||
|
||||
| Purpose | Endpoints | Reference |
|
||||
| --------- | ----------- | ----------- |
|
||||
| Discovery and readiness | `GET /health`, `GET /v1/models` | This page |
|
||||
| Batched conversations | `POST /v1/chat/completions/batch` | [Batch requests](chat_completions_api.md#batch-requests) |
|
||||
| Batched speech | `POST /v1/audio/speech/batch` | [Batch speech generation](speech_api.md#batch-speech-generation) |
|
||||
| TTS voice management | `GET/POST /v1/audio/voices`, `DELETE /v1/audio/voices/{name}` | [Voices](speech_api.md#voices-endpoint) |
|
||||
| Video job lifecycle | `GET /v1/videos`, `GET/DELETE /v1/videos/{video_id}`, `GET /v1/videos/{video_id}/content` | [Video endpoints](videos_api.md#endpoints) |
|
||||
| Release and restore stage memory | `POST /v1/omni/sleep`, `POST /v1/omni/wakeup` | [Sleep Mode](../features/sleep_mode.md) |
|
||||
|
||||
## Standalone Experimental Servers
|
||||
|
||||
JoyVL provides a stateful interaction orchestrator in front of another model
|
||||
server. It is a separate process with its own routes; they are not additional
|
||||
paths on every `vllm serve --omni` instance. See [Standalone Experimental
|
||||
Servers](standalone_servers.md) before deploying it.
|
||||
|
||||
## Compatibility Notes
|
||||
|
||||
- A registered path does not mean every model supports it. Unsupported tasks
|
||||
return an OpenAI-style error or a WebSocket `error` event.
|
||||
- Use JSON for Chat, Speech, Audio Generation, and Image Generation. Use
|
||||
`multipart/form-data` when uploading images, videos, or other files.
|
||||
- Generation can take minutes for large diffusion models. Configure client
|
||||
timeouts accordingly; prefer asynchronous video jobs over long blocking
|
||||
requests.
|
||||
- Model-specific parameters are vLLM-Omni extensions. Check the endpoint
|
||||
reference before assuming an OpenAI SDK exposes them directly.
|
||||
@@ -4,14 +4,14 @@ vLLM-Omni provides an API for text-to-audio generation using diffusion-based mod
|
||||
|
||||
Unlike the [Speech API](speech_api.md) which targets text-to-speech synthesis, the Audio Generate API is designed for general-purpose audio generation from text descriptions (sound effects, music, ambient soundscapes, etc.).
|
||||
|
||||
Each server instance runs a single model (specified at startup via `vllm-omni serve <model> --omni`).
|
||||
Each server instance runs a single model (specified at startup via `vllm serve <model> --omni`).
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Start the Server
|
||||
|
||||
```bash
|
||||
vllm-omni serve stabilityai/stable-audio-open-1.0 \
|
||||
vllm serve stabilityai/stable-audio-open-1.0 \
|
||||
--host 0.0.0.0 \
|
||||
--port 8091 \
|
||||
--gpu-memory-utilization 0.9 \
|
||||
@@ -55,7 +55,7 @@ with open("cat.wav", "wb") as f:
|
||||
|
||||
### Endpoint
|
||||
|
||||
```
|
||||
```text
|
||||
POST /v1/audio/generate
|
||||
Content-Type: application/json
|
||||
```
|
||||
@@ -63,7 +63,7 @@ Content-Type: application/json
|
||||
### Request Parameters
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| ----------- | ------ | --------- | ------------- |
|
||||
| `input` | string | **required** | Text prompt describing the audio to generate |
|
||||
| `model` | string | server's model | Model to use (optional, should match server if specified) |
|
||||
| `response_format` | string | "wav" | Audio format: wav, mp3, flac, pcm, opus |
|
||||
@@ -72,7 +72,7 @@ Content-Type: application/json
|
||||
#### Diffusion Parameters
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| ----------- | ------ | --------- | ------------- |
|
||||
| `audio_length` | float | null | Audio duration in seconds (default value is the max ~47s for `stable-audio-open-1.0`) |
|
||||
| `audio_start` | float | 0.0 | Audio start time in seconds |
|
||||
| `negative_prompt` | string | null | Text describing what to avoid in generation |
|
||||
@@ -85,7 +85,7 @@ Content-Type: application/json
|
||||
Returns binary audio data with the appropriate `Content-Type` header:
|
||||
|
||||
| `response_format` | Content-Type |
|
||||
|--------------------|--------------|
|
||||
| -------------------- | -------------- |
|
||||
| `wav` | `audio/wav` |
|
||||
| `mp3` | `audio/mpeg` |
|
||||
| `flac` | `audio/flac` |
|
||||
@@ -210,7 +210,7 @@ with open("thunder.wav", "wb") as f:
|
||||
Controls how closely the generated audio follows the text prompt.
|
||||
|
||||
| Range | Behaviour |
|
||||
|-------|-----------|
|
||||
| ------- | ----------- |
|
||||
| 3 - 5 | More creative / varied output |
|
||||
| 7 (default) | Balanced adherence |
|
||||
| 10+ | Strict adherence to the prompt |
|
||||
@@ -220,7 +220,7 @@ Controls how closely the generated audio follows the text prompt.
|
||||
Controls the number of denoising steps in the diffusion process.
|
||||
|
||||
| Steps | Quality | Speed | Use Case |
|
||||
|-------|---------|-------|----------|
|
||||
| ------- | --------- | ------- | ---------- |
|
||||
| 50 | Good | Fast | Quick previews |
|
||||
| 100 | Very Good | Medium | General purpose |
|
||||
| 150+ | Excellent | Slow | Final / critical audio |
|
||||
@@ -240,7 +240,7 @@ Describes characteristics to avoid. Common negative prompts include:
|
||||
## Supported Models
|
||||
|
||||
| Model | Description |
|
||||
|-------|-------------|
|
||||
| ----- | ----------- |
|
||||
| `stabilityai/stable-audio-open-1.0` | Open-source audio generation model, up to ~47 seconds, 44.1 kHz stereo |
|
||||
|
||||
## Error Responses
|
||||
@@ -326,7 +326,7 @@ curl http://localhost:8091/health
|
||||
Enable debug logging:
|
||||
|
||||
```bash
|
||||
vllm-omni serve stabilityai/stable-audio-open-1.0 \
|
||||
vllm serve stabilityai/stable-audio-open-1.0 \
|
||||
--host 0.0.0.0 \
|
||||
--port 8091 \
|
||||
--gpu-memory-utilization 0.9 \
|
||||
|
||||
@@ -1,78 +1,110 @@
|
||||
# Chat Completions API
|
||||
|
||||
vLLM-Omni supports generating and editing images via the `/v1/chat/completions`
|
||||
endpoint using diffusion models. This page explains how to pass generation
|
||||
parameters (such as `num_inference_steps`, `height`, `width`) to diffusion
|
||||
models through this endpoint.
|
||||
Use `/v1/chat/completions` for conversational and multimodal pipelines that
|
||||
follow the OpenAI Chat API schema. Depending on the loaded model, a request can
|
||||
contain text, image, audio, or video input and can return text, audio, images,
|
||||
or other model-specific output.
|
||||
|
||||
!!! tip
|
||||
For dedicated endpoints that accept generation parameters as top-level
|
||||
fields, see [Image Generation API](image_generation_api.md) and
|
||||
[Image Edit API](image_edit_api.md).
|
||||
Some diffusion pipelines also support image generation and editing through
|
||||
Chat Completions. Prefer the dedicated [Image Generation
|
||||
API](image_generation_api.md) or [Image Edit API](image_edit_api.md) for
|
||||
task-specific clients; their request fields and response contracts are more
|
||||
direct.
|
||||
|
||||
## Passing Generation Parameters
|
||||
|
||||
The `/v1/chat/completions` endpoint follows the OpenAI Chat API schema, which
|
||||
does not natively include diffusion-specific fields like `num_inference_steps`
|
||||
or `height`. How you pass these extra fields depends on your client.
|
||||
|
||||
### curl / Python `requests`
|
||||
|
||||
Wrap generation parameters inside an `"extra_body"` key in the JSON body:
|
||||
## Basic Request
|
||||
|
||||
```bash
|
||||
curl -s http://localhost:8091/v1/chat/completions \
|
||||
curl http://localhost:8091/v1/chat/completions \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"messages": [
|
||||
{"role": "user", "content": "A beautiful landscape painting"}
|
||||
{"role": "user", "content": "Describe vLLM-Omni briefly."}
|
||||
],
|
||||
"extra_body": {
|
||||
"num_inference_steps": 50,
|
||||
"seed": 42
|
||||
}
|
||||
"modalities": ["text"]
|
||||
}'
|
||||
```
|
||||
|
||||
### OpenAI Python SDK
|
||||
Use `stream: true` for Server-Sent Events. Input media syntax, supported output
|
||||
`modalities`, and response choices depend on the model; see the
|
||||
[model-specific online serving examples](../user_guide/examples/online_serving/qwen3_omni.md).
|
||||
|
||||
Use the `extra_body` **keyword argument**. The SDK automatically merges these
|
||||
fields into the top-level request body:
|
||||
!!! tip
|
||||
Each server hosts one model. Query `GET /v1/models` for its served model
|
||||
name when your client requires the `model` field.
|
||||
|
||||
```python
|
||||
response = client.chat.completions.create(
|
||||
model="Qwen/Qwen-Image",
|
||||
messages=[{"role": "user", "content": "A beautiful landscape painting"}],
|
||||
extra_body={
|
||||
"num_inference_steps": 50,
|
||||
"seed": 42,
|
||||
},
|
||||
)
|
||||
## vLLM Extension Parameters
|
||||
|
||||
Send standard and vLLM-specific Chat parameters as top-level fields in a
|
||||
direct HTTP request. When using the OpenAI Python SDK, pass vLLM-specific
|
||||
fields through the SDK's `extra_body` keyword; the SDK merges them into the
|
||||
top-level JSON sent to the server.
|
||||
|
||||
=== "curl"
|
||||
|
||||
```bash
|
||||
curl http://localhost:8091/v1/chat/completions \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"messages": [{"role": "user", "content": "Write a short haiku."}],
|
||||
"top_k": 40
|
||||
}'
|
||||
```
|
||||
|
||||
=== "OpenAI Python SDK"
|
||||
|
||||
```python
|
||||
from openai import OpenAI
|
||||
|
||||
client = OpenAI(base_url="http://localhost:8091/v1", api_key="none")
|
||||
|
||||
response = client.chat.completions.create(
|
||||
model="your-served-model",
|
||||
messages=[{"role": "user", "content": "Write a short haiku."}],
|
||||
extra_body={"top_k": 40},
|
||||
)
|
||||
```
|
||||
|
||||
??? note "Diffusion compatibility"
|
||||
|
||||
Diffusion models exposed through Chat Completions can accept supported
|
||||
fields such as `num_inference_steps`, `seed`, `height`, and `width`. Send
|
||||
them at the top level with direct HTTP, or in the SDK's `extra_body`
|
||||
keyword argument.
|
||||
|
||||
A literal nested `"extra_body"` JSON object is accepted for compatibility,
|
||||
but it is not recommended for new direct HTTP clients. Do not provide the
|
||||
same parameter in multiple locations; the server returns a `400` error for
|
||||
duplicate diffusion parameters. Prefer the dedicated [Image Generation
|
||||
API](image_generation_api.md) or [Image Edit API](image_edit_api.md) when
|
||||
either endpoint matches the task.
|
||||
|
||||
## Batch Requests
|
||||
|
||||
`POST /v1/chat/completions/batch` accepts the same shared generation fields,
|
||||
but `messages` is a list of conversations. The response contains one choice
|
||||
per conversation in input order.
|
||||
|
||||
```bash
|
||||
curl http://localhost:8091/v1/chat/completions/batch \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"messages": [
|
||||
[{"role": "user", "content": "Summarize vLLM in one sentence."}],
|
||||
[{"role": "user", "content": "Summarize vLLM-Omni in one sentence."}]
|
||||
],
|
||||
"max_tokens": 64
|
||||
}'
|
||||
```
|
||||
|
||||
!!! note "SDK `extra_body` vs. JSON `extra_body`"
|
||||
These two `extra_body` usages look similar but work differently under the
|
||||
hood. The SDK flattens the dict into the top-level request JSON, while the
|
||||
curl/requests approach sends it as a nested `"extra_body"` key. Both are
|
||||
handled correctly by the server.
|
||||
|
||||
!!! note "About the `ignored fields` warning"
|
||||
You may see a log message like:
|
||||
|
||||
```
|
||||
WARNING: The following fields were present in the request but ignored: {'height', 'width', ...}
|
||||
```
|
||||
|
||||
This is **harmless**. It is emitted by vLLM's request validation layer
|
||||
because these fields are not part of the standard OpenAI
|
||||
`ChatCompletionRequest` schema. The fields are still stored internally
|
||||
and correctly forwarded to the diffusion pipeline.
|
||||
Batch Chat Completions does not support streaming, tools, beam search, or
|
||||
`n > 1`.
|
||||
|
||||
## Model-Specific Examples
|
||||
|
||||
For complete examples with full request/response details, see the model-specific
|
||||
guides:
|
||||
For complete examples with model-specific inputs and outputs, see:
|
||||
|
||||
- [Qwen3-Omni](../user_guide/examples/online_serving/qwen3_omni.md)
|
||||
- [Qwen2.5-Omni](../user_guide/examples/online_serving/qwen2_5_omni.md)
|
||||
- [Text-to-Image (Qwen-Image)](../user_guide/examples/online_serving/text_to_image.md)
|
||||
- [Image-to-Image (Qwen-Image-Edit, Qwen-Image-Layered)](../user_guide/examples/online_serving/image_to_image.md)
|
||||
- [GLM-Image](../user_guide/examples/online_serving/glm_image.md)
|
||||
@@ -0,0 +1,122 @@
|
||||
# Full-Duplex WebSocket API
|
||||
|
||||
vLLM-Omni provides a full-duplex runtime for models that can
|
||||
continue receiving speech while producing speech. It adds persistent session
|
||||
state, model-specific turn policy, overlap handling, playback acknowledgement,
|
||||
and optional session resume.
|
||||
|
||||
Full duplex is distinct from the turn-based [Realtime Audio API](realtime_api.md).
|
||||
|
||||
This page is the endpoint overview. For the complete wire contract, the
|
||||
`vllm_omni.clients.duplex.DuplexClient` Python library, and the per-model
|
||||
capability gates, see the [Realtime Duplex API guide](realtime_duplex_api.md).
|
||||
|
||||
## Choose an Endpoint
|
||||
|
||||
| Endpoint | Protocol | Recommended use |
|
||||
| --- | --- | --- |
|
||||
| `WS /v1/realtime?duplex=1` | OpenAI Realtime-style event projection | Applications and browser clients |
|
||||
| `WS /v1/duplex` | Native vLLM-Omni duplex events | Runtime integration and low-level testing |
|
||||
|
||||
Both endpoints use the same duplex engine and require the same model-side
|
||||
adapter. Prefer the Realtime projection unless the native lifecycle events are
|
||||
specifically required.
|
||||
|
||||
## Enable Full Duplex
|
||||
|
||||
The route becomes usable only when the deployment configuration explicitly
|
||||
sets:
|
||||
|
||||
```yaml
|
||||
session_mode: duplex
|
||||
```
|
||||
|
||||
The selected model pipeline must also provide a duplex serving adapter.
|
||||
Model-native deployments configure the corresponding engine runtime extension
|
||||
and control plane as part of their registered pipeline.
|
||||
|
||||
!!! warning
|
||||
|
||||
`WS /v1/duplex` fails with `Duplex API is not available` when duplex is not
|
||||
enabled. By contrast, `/v1/realtime?duplex=1` falls back to the ordinary
|
||||
turn-based realtime handler when no duplex handler exists. Confirm that
|
||||
`session.created.session.capabilities` is present before treating the
|
||||
connection as full duplex.
|
||||
|
||||
The current unified-runtime integrations are:
|
||||
|
||||
- MiniCPM-o 4.5, using `vllm_omni/deploy/minicpmo_4_5.yaml`
|
||||
(`session_mode: duplex`);
|
||||
- PersonaPlex, whose default `vllm_omni/deploy/personaplex.yaml` enables duplex;
|
||||
- Nemotron VoiceChat, via its registered duplex plugin package.
|
||||
|
||||
JoyVL is a separate HTTP interaction orchestrator and does not use these
|
||||
WebSocket endpoints. See [Standalone Experimental Servers](standalone_servers.md).
|
||||
|
||||
## MiniCPM-o Quick Start
|
||||
|
||||
Start the duplex deployment:
|
||||
|
||||
```bash
|
||||
vllm serve openbmb/MiniCPM-o-4_5 --omni \
|
||||
--deploy-config vllm_omni/deploy/minicpmo_4_5.yaml \
|
||||
--trust-remote-code \
|
||||
--port 8091
|
||||
```
|
||||
|
||||
Stream a mono, PCM16, 16 kHz WAV file with the provided client:
|
||||
|
||||
```bash
|
||||
python examples/online_serving/minicpmo/realtime_duplex_demo.py \
|
||||
--url 'ws://localhost:8091/v1/realtime?duplex=1' \
|
||||
--model openbmb/MiniCPM-o-4_5 \
|
||||
--input-wav input_16k_mono.wav \
|
||||
--ref-audio reference_voice.wav \
|
||||
--output-dir /tmp/minicpmo-duplex
|
||||
```
|
||||
|
||||
## Realtime Event Lifecycle
|
||||
|
||||
A typical `/v1/realtime?duplex=1` session follows this lifecycle:
|
||||
|
||||
1. Send `session.update` with the model, modalities, audio formats, and session
|
||||
options.
|
||||
2. Wait for `session.created`; inspect `session.capabilities` instead of
|
||||
assuming every model supports the same controls.
|
||||
3. Send `input_audio_buffer.append` events while microphone audio arrives.
|
||||
4. Send `input_audio_buffer.commit` at a user-turn boundary when required by
|
||||
the model policy.
|
||||
5. Consume `response.created`, transcript deltas, `response.audio.delta`, and
|
||||
`response.done` or `response.listen` events.
|
||||
6. Send `playback.ack` after audio has been played when the session advertises
|
||||
playback acknowledgement support.
|
||||
7. Send `session.close` and wait for `session.closed`.
|
||||
|
||||
Unlike the turn-based realtime endpoint, input may continue while a response
|
||||
is active. The server can emit `overlap.decision` to describe whether input was
|
||||
deferred, treated as a short acknowledgement, or used to interrupt output.
|
||||
|
||||
## Capabilities and Model Differences
|
||||
|
||||
The `session.created` payload includes capability fields such as
|
||||
`supports_barge_in`, `supports_playback_ack`, `supports_multi_session`,
|
||||
`supports_session_resume`, and `chunk_period_ms`. Treat this payload as the
|
||||
runtime contract.
|
||||
|
||||
For example, PersonaPlex supports native overlapping speech but currently
|
||||
advertises `supports_barge_in=false`; destructive output interruption and
|
||||
model-state rewind have not been validated for that integration. Capacity and
|
||||
session-resume behavior also depend on the selected deployment configuration.
|
||||
|
||||
## Native Protocol
|
||||
|
||||
`WS /v1/duplex` exposes lower-level lifecycle names including
|
||||
`session.create`, `input_audio_buffer.append`, `turn.signal`, `playback.ack`,
|
||||
and `session.close`. It returns native session, input, response, overlap, and
|
||||
error events. This protocol is experimental and may evolve with the runtime;
|
||||
applications should use the provided Realtime client where possible.
|
||||
|
||||
See the [MiniCPM-o example](https://github.com/vllm-project/vllm-omni/tree/main/examples/online_serving/minicpmo),
|
||||
[PersonaPlex example](https://github.com/vllm-project/vllm-omni/tree/main/examples/online_serving/personaplex),
|
||||
and the [full-duplex runtime design](../design/fullduplex.md)
|
||||
for model-specific validation and architecture details.
|
||||
@@ -30,7 +30,6 @@ curl -s -D >(grep -i x-request-id >&2) \
|
||||
-F "output_format=png"
|
||||
```
|
||||
|
||||
|
||||
**Using OpenAI SDK:**
|
||||
|
||||
```python
|
||||
@@ -72,7 +71,7 @@ with open("edit_out_http.jpeg", "wb") as f:
|
||||
|
||||
### Endpoint
|
||||
|
||||
```
|
||||
```text
|
||||
POST /v1/images/edits
|
||||
Content-Type: multipart/form-data
|
||||
```
|
||||
@@ -82,7 +81,7 @@ Content-Type: multipart/form-data
|
||||
#### OpenAI Standard Parameters
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| ----------- | ------ | --------- | ------------- |
|
||||
| `prompt` | string | **required** | A text description of the desired image |
|
||||
| `model` | string | server's model | Model to use (optional, should match server if specified) |
|
||||
| `image` | string or array | **required** | The image(s) to edit. |
|
||||
@@ -98,7 +97,7 @@ Content-Type: multipart/form-data
|
||||
#### vllm-omni Extension Parameters
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| ----------- | ------ | --------- | ------------- |
|
||||
| `url` | string or array | None | The image(s) to edit. |
|
||||
| `negative_prompt` | string | null | Text describing what to avoid in the image |
|
||||
| `num_inference_steps` | integer | model defaults | Number of diffusion steps |
|
||||
@@ -212,7 +211,6 @@ Use the `ar_delta` events for progressive display of the AR-generated
|
||||
recaption. Decode the `b64_json` field from the final `image` event to get the
|
||||
edited image.
|
||||
|
||||
|
||||
## Parameter Handling
|
||||
|
||||
The API passes parameters directly to the diffusion pipeline without model-specific transformation:
|
||||
@@ -266,6 +264,7 @@ curl http://localhost:8000/v1/images/edits \
|
||||
### Out of Memory
|
||||
|
||||
If you encounter OOM errors:
|
||||
|
||||
1. Reduce image size: `"size": "512x512"`
|
||||
2. Reduce inference steps: `"num_inference_steps": 25`
|
||||
|
||||
|
||||
@@ -45,7 +45,6 @@ curl -o dragon.png -X POST http://localhost:8000/v1/images/generations \
|
||||
}'
|
||||
```
|
||||
|
||||
|
||||
**Using Python:**
|
||||
|
||||
```python
|
||||
@@ -123,7 +122,7 @@ response = client.images.generate(
|
||||
|
||||
### Endpoint
|
||||
|
||||
```
|
||||
```text
|
||||
POST /v1/images/generations
|
||||
Content-Type: application/json
|
||||
```
|
||||
@@ -133,7 +132,7 @@ Content-Type: application/json
|
||||
#### OpenAI Standard Parameters
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| ----------- | ------ | --------- | ------------- |
|
||||
| `prompt` | string | **required** | Text description of the desired image |
|
||||
| `model` | string | server's model | Model to use (optional, should match server if specified) |
|
||||
| `n` | integer | 1 | Number of images to generate (1-10) |
|
||||
@@ -144,7 +143,7 @@ Content-Type: application/json
|
||||
#### vllm-omni Extension Parameters
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| ----------- | ------ | --------- | ------------- |
|
||||
| `negative_prompt` | string | null | Text describing what to avoid in the image |
|
||||
| `num_inference_steps` | integer | model defaults | Number of diffusion steps |
|
||||
| `guidance_scale` | float | model defaults | Classifier-free guidance scale (typically 0.0-20.0) |
|
||||
@@ -267,6 +266,7 @@ curl http://localhost:8000/v1/images/generations \
|
||||
### Out of Memory
|
||||
|
||||
If you encounter OOM errors:
|
||||
|
||||
1. Reduce image size: `"size": "512x512"`
|
||||
2. Reduce inference steps: `"num_inference_steps": 25`
|
||||
3. Generate fewer images: `"n": 1`
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
# OpenPI Robot Policy WebSocket API
|
||||
|
||||
Use `WS /v1/realtime/robot/openpi` for low-latency robot-policy inference with
|
||||
OpenPI-compatible clients. A persistent WebSocket carries observations to the
|
||||
loaded policy model and returns action arrays.
|
||||
|
||||
Despite the `/realtime` prefix, this endpoint does not use the OpenAI Realtime
|
||||
event schema. It uses binary MessagePack frames with NumPy extensions.
|
||||
|
||||
## Availability
|
||||
|
||||
The endpoint is available only when the loaded diffusion policy configuration
|
||||
contains `policy_server_config`. The server sends that model-specific
|
||||
configuration to the client as the first binary frame.
|
||||
|
||||
For example, DreamZero declares:
|
||||
|
||||
```yaml
|
||||
policy_server_config:
|
||||
image_resolution: [180, 320]
|
||||
n_external_cameras: 2
|
||||
needs_wrist_camera: true
|
||||
needs_stereo_camera: false
|
||||
needs_session_id: true
|
||||
action_space: joint_position
|
||||
```
|
||||
|
||||
Without this configuration, the WebSocket returns `Robot policy not available`
|
||||
and closes.
|
||||
|
||||
## DreamZero Quick Start
|
||||
|
||||
Start the bundled DreamZero deployment:
|
||||
|
||||
```bash
|
||||
vllm serve GEAR-Dreams/DreamZero-DROID --omni --port 8091 \
|
||||
--served-model-name dreamzero-droid \
|
||||
--deploy-config vllm_omni/deploy/dreamzero_tp1_cfg2.yaml \
|
||||
--enforce-eager --disable-log-stats
|
||||
```
|
||||
|
||||
Install the optional client dependencies, download the sample camera inputs,
|
||||
and run the client:
|
||||
|
||||
```bash
|
||||
pip install openpi-client websockets opencv-python
|
||||
|
||||
hf download YangshenDeng/vllm-omni-dreamzero-assets \
|
||||
--repo-type dataset \
|
||||
--local-dir outputs/dreamzero/assets
|
||||
|
||||
python examples/online_serving/dreamzero/openpi_client.py \
|
||||
--host 127.0.0.1 \
|
||||
--port 8091 \
|
||||
--video-dir outputs/dreamzero/assets
|
||||
```
|
||||
|
||||
## Protocol
|
||||
|
||||
The connection is request-response after an initial server handshake:
|
||||
|
||||
```text
|
||||
connect
|
||||
<- msgpack(policy_server_config)
|
||||
|
||||
infer
|
||||
-> msgpack({"endpoint": "infer", "session_id": "...", "seed": <optional int>, ...observation})
|
||||
<- msgpack(ndarray | dict[str, ndarray])
|
||||
|
||||
reset
|
||||
-> msgpack({"endpoint": "reset"})
|
||||
<- msgpack({"status": "reset successful"})
|
||||
```
|
||||
|
||||
If `endpoint` is omitted, the server treats the message as `infer`. Each
|
||||
inference message produces one action response after the engine request
|
||||
completes; action tokens or intermediate tensors are not streamed.
|
||||
|
||||
NumPy arrays use the marker format implemented by `openpi-client`. The server
|
||||
also accepts the legacy vLLM NumPy marker representation on input. JSON text
|
||||
frames are not observation messages.
|
||||
|
||||
## Sessions and Reset
|
||||
|
||||
- The API layer's current-session and first-call counters are scoped to each
|
||||
WebSocket connection.
|
||||
- `session_id` identifies model-side state across observations on that
|
||||
connection. If omitted, it defaults to `default`.
|
||||
- The first inference for a session is sent to the model with `reset=true`.
|
||||
- Changing `session_id`, or sending the `reset` command, causes the next
|
||||
inference to start with `reset=true`.
|
||||
- The policy pipeline owns observation transforms and persistent model state,
|
||||
normally keyed by `session_id`; the API layer forwards the raw observation
|
||||
dictionary.
|
||||
- An optional integer `seed` in an inference message becomes the engine
|
||||
request's `sampling_params.seed`. A policy whose sampling consumes the
|
||||
request generator (GR00T-N1.7) returns the same action chunk for the same
|
||||
observation and seed. If omitted, the engine assigns a random per-request
|
||||
seed, so unseeded inferences are independent of each other. The API layer
|
||||
consumes `seed`; it is not forwarded inside the observation.
|
||||
|
||||
## Limits and Errors
|
||||
|
||||
- Maximum inbound payload size is 64 MiB.
|
||||
- The server closes an idle connection after 30 seconds.
|
||||
- Invalid binary input returns a MessagePack
|
||||
`{"type":"error","message":"Invalid request payload"}` response.
|
||||
- Inference failures return a generic `Internal inference error` without
|
||||
exposing an internal traceback.
|
||||
- Unsupported NumPy object, structured, and complex dtypes are rejected.
|
||||
|
||||
Observation keys, camera layout, state tensors, and action shapes are defined
|
||||
by the loaded policy rather than by this transport. Read the handshake before
|
||||
constructing observations. See the [DreamZero example](https://github.com/vllm-project/vllm-omni/tree/main/examples/online_serving/dreamzero)
|
||||
for a complete OpenPI client and DROID simulation loop.
|
||||
@@ -0,0 +1,82 @@
|
||||
# Realtime Audio WebSocket API
|
||||
|
||||
Use `WS /v1/realtime` to stream PCM audio into a compatible omni model and
|
||||
receive incremental transcription and synthesized response audio. This is a
|
||||
turn-based streaming API: it starts one generation over an audio stream and
|
||||
finishes after the client closes that input stream.
|
||||
|
||||
For a model that listens while it is already speaking, use the
|
||||
[Full-Duplex API](full_duplex_api.md) instead.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Start a compatible model. The currently documented example uses Qwen3-Omni:
|
||||
|
||||
```bash
|
||||
vllm serve Qwen/Qwen3-Omni-30B-A3B-Instruct --omni --port 8091
|
||||
```
|
||||
|
||||
Run the provided client with a mono, 16-bit PCM, 16 kHz WAV file:
|
||||
|
||||
```bash
|
||||
python examples/online_serving/qwen3_omni/openai_realtime_client.py \
|
||||
--url ws://localhost:8091/v1/realtime \
|
||||
--model Qwen/Qwen3-Omni-30B-A3B-Instruct \
|
||||
--input-wav input_16k_mono.wav \
|
||||
--output-wav response.wav \
|
||||
--output-text response.txt
|
||||
```
|
||||
|
||||
The client dependency is `websockets`.
|
||||
|
||||
## Protocol
|
||||
|
||||
Messages in both directions are JSON text frames. Audio carried inside an
|
||||
event is base64-encoded raw PCM16.
|
||||
|
||||
| Direction | Event | Purpose |
|
||||
| ----------- | ------- | --------- |
|
||||
| Server to client | `session.created` | Confirms that the WebSocket connection is ready |
|
||||
| Client to server | `session.update` | Selects and validates the served model |
|
||||
| Client to server | `input_audio_buffer.commit` with `final: false` | Starts generation over the incoming stream |
|
||||
| Client to server | `input_audio_buffer.append` | Appends base64 PCM16 audio |
|
||||
| Client to server | `input_audio_buffer.commit` with `final: true` | Marks the end of input |
|
||||
| Server to client | `transcription.delta` | Carries incremental response text |
|
||||
| Server to client | `transcription.done` | Carries final text and token usage |
|
||||
| Server to client | `response.audio.delta` | Carries incremental PCM16 response audio |
|
||||
| Server to client | `response.audio.done` | Marks the end of response audio |
|
||||
| Server to client | `error` | Reports an invalid event, model, or audio payload |
|
||||
|
||||
A minimal client sends events in this order:
|
||||
|
||||
```json
|
||||
{"type":"session.update","model":"Qwen/Qwen3-Omni-30B-A3B-Instruct"}
|
||||
{"type":"input_audio_buffer.commit","final":false}
|
||||
{"type":"input_audio_buffer.append","audio":"<base64-pcm16>"}
|
||||
{"type":"input_audio_buffer.commit","final":true}
|
||||
```
|
||||
|
||||
The initial non-final commit intentionally starts generation before all audio
|
||||
has arrived. Continue sending `input_audio_buffer.append` events while the
|
||||
engine is consuming the stream.
|
||||
|
||||
## Audio Handling
|
||||
|
||||
- Input is mono PCM16 at 16 kHz for the Qwen3-Omni example.
|
||||
- `response.audio.delta.audio` contains base64-encoded PCM16 bytes.
|
||||
- Read `sample_rate_hz` from each audio event instead of assuming an output
|
||||
rate. Qwen3-Omni output is typically 24 kHz.
|
||||
- Concatenate audio deltas in receive order to construct the output waveform.
|
||||
- Set `input_audio_buffer.commit.final` to `true` after the last input chunk so
|
||||
the server can terminate the streaming request.
|
||||
|
||||
## Availability and Limitations
|
||||
|
||||
The path is registered on the unified API server, but it is usable only when
|
||||
the loaded pipeline implements realtime audio input and produces compatible
|
||||
audio output. Unsupported deployments return an `error` event. The endpoint
|
||||
does not provide duplex session resume, playback acknowledgement, overlap
|
||||
policy, or barge-in controls.
|
||||
|
||||
See the [Qwen3-Omni online serving example](https://github.com/vllm-project/vllm-omni/tree/main/examples/online_serving/qwen3_omni)
|
||||
for concurrency options, chunk pacing, and per-delta audio debugging.
|
||||
File diff suppressed because it is too large.
Load diff
+91
-42
@@ -2,7 +2,7 @@
|
||||
|
||||
vLLM-Omni provides an OpenAI-compatible API for text-to-speech (TTS) generation. Supported TTS models include:
|
||||
|
||||
- **Qwen3-TTS** (`Qwen/Qwen3-TTS-12Hz-*`) -- Qwen3-based TTS with CustomVoice, VoiceDesign, and Base (voice cloning) task types. Output: 24 kHz.
|
||||
- **Qwen3-TTS** (`Qwen/Qwen3-TTS-12Hz-*`) -- Qwen3-based TTS with CustomVoice, VoiceDesign, and Base (voice cloning) task types. Native output: 24 kHz; API output can be resampled to 8 kHz.
|
||||
- **Fish Speech S2 Pro** (`fishaudio/s2-pro`) -- Dual-AR TTS with DAC codec. Supports text-to-speech and voice cloning via reference audio. Output: 44.1 kHz.
|
||||
- **Voxtral TTS** (`mistralai/Voxtral-4B-TTS-2603`) -- AR + FlowMatching TTS with preset voices. Output: 24 kHz.
|
||||
- **CosyVoice3** (`FunAudioLLM/Fun-CosyVoice3-0.5B-2512`) -- 2-stage talker + flow-matching code2wav. Voice cloning via `ref_audio` + `ref_text` (no presets). Output: 24 kHz.
|
||||
@@ -95,7 +95,7 @@ response.stream_to_file("output.wav")
|
||||
|
||||
### Endpoint
|
||||
|
||||
```
|
||||
```text
|
||||
POST /v1/audio/speech
|
||||
Content-Type: application/json
|
||||
```
|
||||
@@ -105,7 +105,7 @@ Content-Type: application/json
|
||||
#### OpenAI Standard Parameters
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| ----------- | ------ | --------- | ------------- |
|
||||
| `input` | string | **required** | The text to synthesize into speech |
|
||||
| `model` | string | server's model | Model to use (optional, should match server if specified) |
|
||||
| `voice` | string | "vivian" | Speaker name (e.g., vivian, ryan, aiden) |
|
||||
@@ -115,8 +115,9 @@ Content-Type: application/json
|
||||
#### vLLM-Omni Extension Parameters
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `task_type` | string | "CustomVoice" | TTS task type: CustomVoice, VoiceDesign, or Base |
|
||||
| ----------- | ------ | --------- | ------------- |
|
||||
| `task_type` | string | null (inferred) | TTS task type: CustomVoice, VoiceDesign, or Base. For Qwen3-TTS, `ref_audio` or `ref_text` infers Base when this field is omitted, while an uploaded/precomputed voice always selects Base. Other omitted values select CustomVoice; VoiceDesign must be specified explicitly. |
|
||||
| `sample_rate` | integer | model native | Target output sample rate. Qwen3-TTS supports 8000 or 24000 Hz; the model remains native 24 kHz internally and is resampled before encoding. |
|
||||
| `language` | string | "Auto" | Language (see supported languages below) |
|
||||
| `instructions` | string | "" | Voice style/emotion instructions |
|
||||
| `max_new_tokens` | integer | 2048 | Maximum tokens to generate |
|
||||
@@ -130,7 +131,7 @@ Content-Type: application/json
|
||||
#### Voice Clone Parameters (Base task)
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| ----------- | ------ | --------- | ------------- |
|
||||
| `ref_audio` | string | null | Reference audio (HTTP URL, base64 data URL, or `file://` URI with `--allowed-local-media-path`). Local files fold `mtime_ns` and `size` into cache keys to automatically reload on-disk edits; HTTP URLs and base64 URIs remain cached by string locator. |
|
||||
| `ref_text` | string | null | Transcript of reference audio |
|
||||
| `x_vector_only_mode` | bool | null | Use speaker embedding only (no ICL) |
|
||||
@@ -204,7 +205,7 @@ The `usage` object on `speech.audio.done` is the same shape returned per item by
|
||||
|
||||
### Voices Endpoint
|
||||
|
||||
```
|
||||
```text
|
||||
GET /v1/audio/voices
|
||||
```
|
||||
|
||||
@@ -229,7 +230,7 @@ Lists available voices for the loaded model.
|
||||
|
||||
`uploaded_voices` is always present (empty list when no custom voices have been uploaded). Fields `ref_text` and `speaker_description` are omitted per-entry when not provided at upload time.
|
||||
|
||||
```
|
||||
```text
|
||||
POST /v1/audio/voices
|
||||
Content-Type: multipart/form-data
|
||||
```
|
||||
@@ -239,7 +240,7 @@ Upload a new voice sample for voice cloning in Base task TTS requests.
|
||||
**Form Parameters:**
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| ----------- | ------ | ---------- | ------------- |
|
||||
| `audio_sample` | file | Yes | Audio file (max 10MB, supported formats: wav, mp3, flac, ogg, aac, webm, mp4) |
|
||||
| `consent` | string | Yes | Consent recording ID |
|
||||
| `name` | string | Yes | Name for the new voice |
|
||||
@@ -278,17 +279,22 @@ curl -X POST http://localhost:8091/v1/audio/voices \
|
||||
|
||||
## Streaming Text Input (WebSocket)
|
||||
|
||||
The `/v1/audio/speech/stream` WebSocket endpoint accepts text incrementally and generates audio per sentence as boundaries are detected.
|
||||
The `/v1/audio/speech/stream` WebSocket endpoint accepts text incrementally.
|
||||
By default (`split_granularity=none`) it buffers until `input.done` and
|
||||
synthesizes that flush as **one** TTS request, which keeps long-form timbre
|
||||
stable. Set `split_granularity` to `sentence` or `clause` to emit a request
|
||||
at each detected boundary (lower time-to-first-audio for STT/LLM pipelines).
|
||||
|
||||
> Note: text input is always streamed incrementally. Audio output remains sentence-scoped:
|
||||
> use `stream_audio=false` for one binary frame per sentence, or `stream_audio=true` for one or more PCM chunks per sentence.
|
||||
> Note: `stream_audio` only changes how **audio bytes** are framed (one WAV/PCM
|
||||
> payload vs chunked PCM). Text segmentation is controlled separately by
|
||||
> `split_granularity`.
|
||||
|
||||
### WebSocket Protocol
|
||||
|
||||
Client -> Server:
|
||||
|
||||
| Message | Description |
|
||||
|---------|-------------|
|
||||
| --------- | ------------- |
|
||||
| `{"type": "session.config", ...}` | Session configuration (first message; may be resent between utterances to change it) |
|
||||
| `{"type": "input.text", "text": "..."}` | Text chunk |
|
||||
| `{"type": "input.done"}` | End of utterance: flushes the buffer and keeps the connection open |
|
||||
@@ -297,7 +303,7 @@ Client -> Server:
|
||||
Server -> Client:
|
||||
|
||||
| Message | Description |
|
||||
|---------|-------------|
|
||||
| --------- | ------------- |
|
||||
| `{"type": "audio.start", "utterance_index": 0, "sentence_index": 0, "sentence_text": "...", "format": "pcm", "sample_rate": 24000}` | Audio generation starting for the buffered input |
|
||||
| Binary frame | Raw audio bytes (one or more PCM chunks when `stream_audio=true`) |
|
||||
| `{"type": "audio.done", "utterance_index": 0, "sentence_index": 0, "total_bytes": 96000, "error": false}` | Audio complete for the buffered input |
|
||||
@@ -311,17 +317,19 @@ text, emits `session.done`, and then waits on the same connection for the next
|
||||
utterance, so a client that speaks repeatedly (for example one driven by an
|
||||
upstream LLM) pays the WebSocket handshake once instead of once per utterance.
|
||||
|
||||
* The session config is sticky. Send `input.text` again straight after
|
||||
- The session config is sticky. Send `input.text` again straight after
|
||||
`session.done` to reuse it, or send another `session.config` first to change
|
||||
voice, format, or reference audio. A `session.config` sent while text is
|
||||
still buffered is rejected so no pending input is silently dropped.
|
||||
* An utterance is the flush unit, not a linguistic one: it is whatever text was
|
||||
buffered when `input.done` arrived, of any length, synthesized as one request.
|
||||
`utterance_index` counts those flushes across the connection, so it tells you
|
||||
which `input.done` a frame belongs to. `sentence_index` counts within one
|
||||
flush and so pairs with `total_sentences`, which means every utterance reports
|
||||
`sentence_index: 0` of `total_sentences: 1` (or `0` for an empty buffer).
|
||||
* End the connection with `session.close`, or by closing the socket. An idle
|
||||
voice, format, or reference audio. A `session.config` sent in the middle of
|
||||
an utterance is rejected so no pending input is silently dropped and so a
|
||||
split utterance cannot end up half in one voice and half in another.
|
||||
- An utterance is the flush unit, not a linguistic one: it is one `input.done`
|
||||
cycle. `utterance_index` counts those flushes across the connection, so it
|
||||
tells you which `input.done` a frame belongs to. `sentence_index` counts the
|
||||
TTS requests inside one flush and so pairs with `total_sentences`: with the
|
||||
default `split_granularity=none` that is always `sentence_index: 0` of
|
||||
`total_sentences: 1` (or `0` for an empty buffer), while `sentence` or
|
||||
`clause` counts the linguistic units actually synthesized.
|
||||
- End the connection with `session.close`, or by closing the socket. An idle
|
||||
connection is still closed after the server's idle timeout, which now also
|
||||
applies to the gap between utterances.
|
||||
|
||||
@@ -330,9 +338,16 @@ upstream LLM) pays the WebSocket handshake once instead of once per utterance.
|
||||
All REST API parameters are supported, plus:
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `stream_audio` | bool | false | Stream one or more PCM chunks for the buffered input over WebSocket |
|
||||
| ----------- | ------ | --------- | ------------- |
|
||||
| `stream_audio` | bool | false | Stream one or more PCM chunks for each TTS request over WebSocket |
|
||||
| `split_granularity` | string | `"none"` | `"none"`: one request per `input.done`. `"sentence"`: split on `.!?` plus CJK `。!?…`, Indic danda `।॥`, and Arabic `؟`. `"clause"`: also split on `,;,;،؛`. |
|
||||
| `seed` | integer | null | Forwarded to the speech engine for this session |
|
||||
|
||||
ASCII punctuation only ends a unit when whitespace or `input.done` follows it,
|
||||
and decimals (`3.14`), thousands separators (`1,000`), abbreviations (`Dr.`,
|
||||
`e.g.`) and initials (`J. R.`) are not treated as boundaries. A punctuation run
|
||||
and any closing quote or bracket stay with the unit they close, so `Wait...`
|
||||
and `He said "Hello."` are one request each.
|
||||
|
||||
```bash
|
||||
DELETE /v1/audio/voices/{name}
|
||||
@@ -343,7 +358,7 @@ Delete an uploaded voice sample.
|
||||
**Path Parameters:**
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| ----------- | ------ | ---------- | ------------- |
|
||||
| `name` | string | Yes | Name of the voice to delete |
|
||||
|
||||
**Response Example:**
|
||||
@@ -433,6 +448,7 @@ curl -X POST http://localhost:8091/v1/audio/speech \
|
||||
### Upload Voice
|
||||
|
||||
Upload voice (speaker embedding only):
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8091/v1/audio/voices \
|
||||
-F "audio_sample=@/path/to/voice_sample.wav" \
|
||||
@@ -441,6 +457,7 @@ curl -X POST http://localhost:8091/v1/audio/voices \
|
||||
```
|
||||
|
||||
Upload voice with transcript (in-context cloning, higher quality):
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8091/v1/audio/voices \
|
||||
-F "audio_sample=@/path/to/voice_sample.wav" \
|
||||
@@ -450,6 +467,7 @@ curl -X POST http://localhost:8091/v1/audio/voices \
|
||||
```
|
||||
|
||||
### Use Uploaded Voice
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8091/v1/audio/speech \
|
||||
-H "Content-Type: application/json" \
|
||||
@@ -459,6 +477,11 @@ curl -X POST http://localhost:8091/v1/audio/speech \
|
||||
}' --output cloned.wav
|
||||
```
|
||||
|
||||
For Qwen3-TTS, an uploaded voice is a Base voice-cloning input. The server
|
||||
infers `task_type="Base"` when `voice` names an uploaded entry, so the request
|
||||
must be sent to a Base checkpoint. Built-in presets such as `vivian` and
|
||||
`ryan` remain CustomVoice speakers and require a CustomVoice checkpoint.
|
||||
|
||||
### Voice Storage & Caching
|
||||
|
||||
Uploaded voices are persisted to disk as a single `.safetensors` file per voice
|
||||
@@ -478,6 +501,9 @@ once.
|
||||
### Precomputed Custom Voices
|
||||
|
||||
Qwen3-TTS Base and VoxCPM2 can load offline-precomputed voices at startup.
|
||||
Qwen3-TTS precomputed voices follow the same Base task and checkpoint-matching
|
||||
rules as uploaded voices.
|
||||
|
||||
Generate a directory containing `custom_voice_manifest.json` plus one
|
||||
`.safetensors` file per voice, then set the pipeline-wide deploy config field:
|
||||
|
||||
@@ -516,7 +542,7 @@ by `GET /v1/audio/voices`. Valid precomputed voices can be used in
|
||||
**Configuration (environment variables):**
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| ---------- | --------- | ------------- |
|
||||
| `SPEAKER_SAMPLES_DIR` | `~/.cache/vllm-omni/speakers` | Directory for persisted uploaded speakers (`.safetensors` files). |
|
||||
| `SPEAKER_MAX_UPLOADED` | `1000` | Maximum number of uploaded speakers kept on disk. Upload requests past the cap return 400. |
|
||||
|
||||
@@ -528,7 +554,7 @@ The batch endpoint synthesizes multiple texts in a single request, returning all
|
||||
|
||||
### Endpoint
|
||||
|
||||
```
|
||||
```text
|
||||
POST /v1/audio/speech/batch
|
||||
Content-Type: application/json
|
||||
```
|
||||
@@ -536,7 +562,7 @@ Content-Type: application/json
|
||||
### Request Parameters
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| ----------- | ------ | --------- | ------------- |
|
||||
| `items` | array | **required** | List of items to synthesize (1–32) |
|
||||
| `model` | string | server's model | Model to use |
|
||||
| `voice` | string | null | Default voice for all items |
|
||||
@@ -553,7 +579,7 @@ Content-Type: application/json
|
||||
Each item in the `items` array requires only `input` (the text). All other fields are optional and override the batch-level defaults when set:
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| ------- | ------ | ------------- |
|
||||
| `input` | string | **required** — text to synthesize |
|
||||
| `voice` | string | Override voice for this item |
|
||||
| `response_format` | string | Override format for this item |
|
||||
@@ -685,7 +711,7 @@ for result in response.json()["results"]:
|
||||
### Configuration
|
||||
|
||||
| Parameter | Source | Default | Description |
|
||||
|-----------|--------|---------|-------------|
|
||||
| ----------- | -------- | --------- | ------------- |
|
||||
| `tts_batch_max_items` | engine kwarg | 32 | Maximum number of items per batch request |
|
||||
|
||||
All items are fanned out to `generate()` concurrently. The engine's stage worker automatically batches them up to the configured `max_num_seqs` and queues the rest — no client-side throttling needed.
|
||||
@@ -708,17 +734,17 @@ The bundled config also sets `initial_codec_chunk_frames: 1`. This emits only th
|
||||
### Qwen3-TTS
|
||||
|
||||
| Model | Task Type | Description |
|
||||
|-------|-----------|-------------|
|
||||
| ------- | ----------- | ------------- |
|
||||
| `Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice` | CustomVoice | Predefined speaker voices with optional style control |
|
||||
| `Qwen/Qwen3-TTS-12Hz-1.7B-VoiceDesign` | VoiceDesign | Natural language voice style description |
|
||||
| `Qwen/Qwen3-TTS-12Hz-1.7B-Base` | Base | Voice cloning from reference audio |
|
||||
| `Qwen/Qwen3-TTS-12Hz-1.7B-Base` | Base | Voice cloning from reference audio or an uploaded/precomputed voice |
|
||||
| `Qwen/Qwen3-TTS-12Hz-0.6B-CustomVoice` | CustomVoice | Smaller/faster variant |
|
||||
| `Qwen/Qwen3-TTS-12Hz-0.6B-Base` | Base | Smaller/faster variant for voice cloning |
|
||||
| `Qwen/Qwen3-TTS-12Hz-0.6B-Base` | Base | Smaller/faster Base variant for voice cloning, including uploaded/precomputed voices |
|
||||
|
||||
### Fish Speech S2 Pro
|
||||
|
||||
| Model | Description |
|
||||
|-------|-------------|
|
||||
| ------- | ------------- |
|
||||
| `fishaudio/s2-pro` | 4B dual-AR TTS with DAC codec (44.1 kHz). Supports text-to-speech and voice cloning. |
|
||||
|
||||
Fish Speech uses `ref_audio` and `ref_text` for voice cloning (no `task_type` needed). The `voice` field should be set to `"default"`. See the [Fish Speech section of the online TTS hub](../user_guide/examples/online_serving/text_to_speech.md#fish-speech-s2-pro) for details.
|
||||
@@ -726,31 +752,35 @@ Fish Speech uses `ref_audio` and `ref_text` for voice cloning (no `task_type` ne
|
||||
### Voxtral TTS
|
||||
|
||||
| Model | Description |
|
||||
|-------|-------------|
|
||||
| ------- | ------------- |
|
||||
| `mistralai/Voxtral-4B-TTS-2603` | 3B AR + FlowMatching TTS. Supports text-to-speech with preset voices. |
|
||||
|
||||
### CosyVoice3
|
||||
|
||||
| Model | Description |
|
||||
|-------|-------------|
|
||||
| ------- | ------------- |
|
||||
| `FunAudioLLM/Fun-CosyVoice3-0.5B-2512` | Voice cloning from `ref_audio` + `ref_text`. No built-in voice presets — upload a voice or pass `ref_audio`/`ref_text` per request. |
|
||||
|
||||
### OmniVoice
|
||||
|
||||
| Model | Description |
|
||||
|-------|-------------|
|
||||
| ------- | ------------- |
|
||||
| `k2-fsa/OmniVoice` | Pure-diffusion TTS. Supports voice cloning via `ref_audio` (with optional `ref_text`); no built-in voice presets. |
|
||||
|
||||
OmniVoice uses packed variable-length attention for batched generator execution. The attention operator accepts FP16 and BF16 inputs, so its query,
|
||||
key, and value tensors are evaluated in BF16 even when the stage is configured with `dtype: float32`; the attention output is converted back to the model's
|
||||
hidden-state dtype before the output projection. Consequently, a float32 stage configuration does not imply FP32 attention arithmetic.
|
||||
|
||||
### VoxCPM2
|
||||
|
||||
| Model | Description |
|
||||
|-------|-------------|
|
||||
| ------- | ------------- |
|
||||
| `openbmb/VoxCPM2` | TTS + voice cloning with built-in speaker presets and uploaded-voice support. Accepts `voice` (preset or uploaded) or `ref_audio` + optional `ref_text`. |
|
||||
|
||||
### MOSS-TTS-Nano
|
||||
|
||||
| Model | Description |
|
||||
|-------|-------------|
|
||||
| ------- | ------------- |
|
||||
| `OpenMOSS-Team/MOSS-TTS-Nano` | Voice cloning only. Requires `ref_audio` (or an uploaded `voice`); no built-in voice presets. `ref_text` is accepted but ignored — upstream's `voice_clone` mode does not consume a transcript. |
|
||||
|
||||
## Error Responses
|
||||
@@ -770,6 +800,19 @@ Invalid parameters:
|
||||
}
|
||||
```
|
||||
|
||||
Qwen3-TTS task/checkpoint mismatch:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"message": "Qwen3-TTS CustomVoice checkpoint does not support task_type='Base'. Use task_type='CustomVoice' or load the matching Base checkpoint.",
|
||||
"type": "BadRequestError",
|
||||
"param": null,
|
||||
"code": 400
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 404 Not Found
|
||||
|
||||
Model not found:
|
||||
@@ -790,10 +833,15 @@ Model not found:
|
||||
### "TTS model did not produce audio output"
|
||||
|
||||
Ensure you're using the correct model variant for your task type:
|
||||
|
||||
- CustomVoice task → CustomVoice model
|
||||
- VoiceDesign task → VoiceDesign model
|
||||
- Base task → Base model
|
||||
|
||||
Uploaded and precomputed Qwen3-TTS voices select the Base task, even when the
|
||||
client supplies a different `task_type`; serve them with a Base checkpoint.
|
||||
Built-in Qwen3-TTS presets remain CustomVoice voices.
|
||||
|
||||
### Server Not Running
|
||||
|
||||
```bash
|
||||
@@ -804,6 +852,7 @@ curl http://localhost:8091/v1/audio/voices
|
||||
### Out of Memory
|
||||
|
||||
If you encounter OOM errors:
|
||||
|
||||
1. Use smaller model variant: `Qwen/Qwen3-TTS-12Hz-0.6B-CustomVoice`
|
||||
2. Reduce `--gpu-memory-utilization`
|
||||
|
||||
@@ -822,7 +871,7 @@ final-output drain to a condition-variable wakeup at the same time.
|
||||
**Configuration (environment variables):**
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| --- | --- | --- |
|
||||
| `VLLM_OMNI_EVENT_DRIVEN_ORCH` | `0` (off) | Switches the orchestration loop and the final-output drain from the legacy 1 ms poll to event-driven wakeups. Enabled by `1`, `true`, `yes`, or `on`, matched case-insensitively after surrounding whitespace is stripped; any other value leaves it off. |
|
||||
|
||||
Set it on the process that runs the orchestrator (stage 0 of an omni
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# Standalone Experimental Servers
|
||||
|
||||
Most vLLM-Omni endpoints run on the unified server started with
|
||||
`vllm serve <model> --omni`. The experimental package also contains one
|
||||
separate user-facing server process; its routes are not added to the unified
|
||||
API server.
|
||||
|
||||
| Server | Transport | Role |
|
||||
| --- | --- | --- |
|
||||
| JoyVL interaction server | HTTP | Adds state, memory, proactive decisions, and delegation in front of a separate OpenAI-compatible model backend |
|
||||
|
||||
PersonaPlex no longer ships a standalone compatibility server: it is served
|
||||
through the unified duplex path (`vllm_omni/deploy/personaplex.yaml`) over
|
||||
[`WS /v1/realtime?duplex=1`](full_duplex_api.md); see the
|
||||
[PersonaPlex example](https://github.com/vllm-project/vllm-omni/tree/main/examples/online_serving/personaplex)
|
||||
for its clients.
|
||||
|
||||
## JoyVL Interaction Server
|
||||
|
||||
JoyVL is an orchestration layer, not a model-serving engine. It calls a
|
||||
separate OpenAI-compatible backend and adds per-session frame history, memory,
|
||||
persona policy, and optional delegation.
|
||||
|
||||
Start the model backend and orchestrator separately:
|
||||
|
||||
```bash
|
||||
vllm serve jdopensource/JoyAI-VL-Interaction-Preview \
|
||||
--served-model-name JoyAI-VL-Interaction-Preview \
|
||||
--port 8092 \
|
||||
--max-model-len 131072 \
|
||||
--enable-prefix-caching \
|
||||
--limit-mm-per-prompt '{"image":256,"video":1}'
|
||||
|
||||
python -m vllm_omni.experimental.fullduplex.joyvl.serving.server \
|
||||
--port 8091 \
|
||||
--main-backend-url http://127.0.0.1:8092/v1 \
|
||||
--main-model JoyAI-VL-Interaction-Preview
|
||||
```
|
||||
|
||||
| Method and route | Purpose |
|
||||
| ------------------ | --------- |
|
||||
| `GET /health` | Readiness check |
|
||||
| `GET /v1/models` | Reports the configured interaction model |
|
||||
| `POST /v1/chat/completions` | Processes one frame or interaction tick |
|
||||
| `POST /reset` | Resets a session |
|
||||
| `POST /v1/streaming/reset` | Alias for `/reset` |
|
||||
| `POST /v1/streaming/persona` | Changes the session persona |
|
||||
|
||||
Send one multimodal Chat Completions request per video frame, normally around
|
||||
one frame per second, and identify the session with `x-session-id`. Responses
|
||||
include an `interaction` block whose action is `silence`, `response`, or
|
||||
`delegate`.
|
||||
|
||||
```bash
|
||||
curl http://127.0.0.1:8091/v1/chat/completions \
|
||||
-H 'content-type: application/json' \
|
||||
-H 'x-session-id: demo' \
|
||||
-d '{
|
||||
"messages": [{"role": "user", "content": [
|
||||
{"type": "text", "text": "Alert me if a fire appears"},
|
||||
{"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,..."}}
|
||||
]}]
|
||||
}'
|
||||
```
|
||||
|
||||
See the [JoyVL recipe](https://github.com/vllm-project/vllm-omni/blob/main/recipes/JD/JoyAI-VL-Interaction.md)
|
||||
for memory, personas, delegation backends, and the browser UI.
|
||||
@@ -0,0 +1,92 @@
|
||||
# Streaming Video Output WebSocket API
|
||||
|
||||
Use `WS /v1/realtime/video` to receive fragmented MP4 chunks while a compatible
|
||||
diffusion video model is generating. This endpoint streams generated video
|
||||
output; it is unrelated to [`WS /v1/video/chat/stream`](video_stream_api.md),
|
||||
which streams live video input into a conversational model.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Start a model that supports step execution and enable diffusion streaming
|
||||
output:
|
||||
|
||||
```bash
|
||||
vllm serve BestWishYsh/Helios-Distilled --omni \
|
||||
--diffusion-streaming-output \
|
||||
--port 8091
|
||||
```
|
||||
|
||||
Install the client dependency and run the provided client:
|
||||
|
||||
```bash
|
||||
pip install av websockets
|
||||
|
||||
python examples/online_serving/streaming_video_generation/streaming_video_client.py \
|
||||
--host 127.0.0.1 \
|
||||
--port 8091 \
|
||||
--model BestWishYsh/Helios-Distilled \
|
||||
--prompt "A serene lakeside sunrise with mist over the water" \
|
||||
--output output.mp4
|
||||
```
|
||||
|
||||
## Protocol
|
||||
|
||||
Control events are JSON text frames. Each `video.chunk_metadata` event is
|
||||
immediately followed by one binary WebSocket frame containing fragmented MP4
|
||||
bytes.
|
||||
|
||||
| Direction | Event or frame | Purpose |
|
||||
| ----------- | ---------------- | --------- |
|
||||
| Client to server | `session.start` | Starts generation with a prompt and video parameters |
|
||||
| Server to client | `video.start` | Confirms the request ID, format, and accepted configuration |
|
||||
| Server to client | `video.chunk_metadata` | Describes the next binary media frame |
|
||||
| Server to client | Binary frame | Carries one fragmented MP4 chunk |
|
||||
| Client to server | `session.interaction` | Queues an optional prompt update during generation |
|
||||
| Server to client | `session.interaction.queued` | Confirms that an interaction was queued |
|
||||
| Client to server | `session.ping` | Refreshes the stall clock |
|
||||
| Server to client | `session.pong` | Acknowledges a ping |
|
||||
| Client to server | `session.stop` | Cancels the active generation |
|
||||
| Server to client | `session.done` | Reports completion or cancellation and the chunk count |
|
||||
| Server to client | `error` | Reports validation, generation, or timeout failures |
|
||||
|
||||
Start a minimal session with:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "session.start",
|
||||
"model": "BestWishYsh/Helios-Distilled",
|
||||
"prompt": "A serene lakeside sunrise",
|
||||
"format": "m4s",
|
||||
"width": 640,
|
||||
"height": 384,
|
||||
"fps": 16,
|
||||
"num_frames": 99
|
||||
}
|
||||
```
|
||||
|
||||
Only `m4s` output is currently accepted. Concatenate the binary frames in
|
||||
receive order, then remux the fragmented stream if the target player requires
|
||||
a progressive MP4 file.
|
||||
|
||||
## Mid-Generation Interaction
|
||||
|
||||
Models that implement interaction updates can change the active prompt:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "session.interaction",
|
||||
"interaction": {
|
||||
"event_id": "sunlight-update",
|
||||
"event": {"prompt": "Sunlight breaks through the mist"},
|
||||
"transition_chunks": 2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This feature is pipeline-dependent. An enabled endpoint does not imply that
|
||||
the loaded model supports midway prompt updates.
|
||||
|
||||
The default start timeout is 10 seconds and the server reports a stall after
|
||||
about 60 seconds without generation progress or a `session.ping`. See the
|
||||
[streaming video generation example](https://github.com/vllm-project/vllm-omni/tree/main/examples/online_serving/streaming_video_generation)
|
||||
for prompt-update scheduling and browser playback.
|
||||
Loaded 100 of 1097 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user