Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cde57c7a81 | ||
|
|
b3d396a05e | ||
|
|
7a310d8547 | ||
|
|
55e4370075 | ||
|
|
0f1b522ee7 |
No files matched your search
@@ -1,20 +0,0 @@
|
||||
steps:
|
||||
- label: "Build and upload wheel"
|
||||
key: "build-wheel"
|
||||
agents:
|
||||
queue: cpu_queue_release
|
||||
commands:
|
||||
- "curl -LsSf https://astral.sh/uv/install.sh | sh"
|
||||
- 'export PATH="$HOME/.local/bin:$PATH"'
|
||||
- "uv venv --python=3.12 && source .venv/bin/activate"
|
||||
- "uv pip install --upgrade build"
|
||||
- "python3 -m build"
|
||||
- "bash .buildkite/scripts/upload-nightly-wheels.sh"
|
||||
|
||||
- label: "Generate and upload wheel indices"
|
||||
depends_on: "build-wheel"
|
||||
allow_dependency_failure: true
|
||||
agents:
|
||||
queue: small_cpu_queue_release
|
||||
commands:
|
||||
- "bash .buildkite/scripts/generate-and-upload-nightly-index.sh"
|
||||
@@ -0,0 +1,92 @@
|
||||
steps:
|
||||
- label: ":buildkit: Build and Push NPU Test Image"
|
||||
key: image-build
|
||||
agents:
|
||||
queue: "ascend-a2b3"
|
||||
resource_class: "npu-2"
|
||||
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
|
||||
env:
|
||||
VLLM_IMAGE_TAG: "${BUILDKITE_COMMIT}"
|
||||
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}"
|
||||
|
||||
git config --global url."https://gh-proxy.test.osinfra.cn/https://github.com/".insteadOf "https://github.com/"
|
||||
|
||||
if ! command -v buildctl &> /dev/null; then
|
||||
echo "Installing buildctl..."
|
||||
mkdir -p /tmp/buildkit
|
||||
BUILDKIT_VERSION="v0.29.0"
|
||||
wget -q "https://gh-proxy.test.osinfra.cn/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 \
|
||||
--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: "Upload Ready Pipeline"
|
||||
depends_on: image-build
|
||||
key: upload-ready-pipeline
|
||||
if: build.branch != "main" && build.pull_request.labels includes "ready"
|
||||
agents:
|
||||
queue: "ascend-a2b3"
|
||||
resource_class: "npu-2"
|
||||
command: |
|
||||
buildkite-agent pipeline upload .buildkite/test-npu-ready.yaml
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -ex
|
||||
|
||||
# Generate and upload wheel indices for all vllm-omni wheels in the commit directory.
|
||||
# This script should run once after all wheels have been built and uploaded.
|
||||
# All paths are under the omni/ prefix in the vllm-wheels S3 bucket.
|
||||
|
||||
# ======== setup ========
|
||||
|
||||
BUCKET="vllm-wheels"
|
||||
INDICES_OUTPUT_DIR="indices"
|
||||
PYTHON="${PYTHON_PROG:-python3}"
|
||||
SUBPATH="omni/$BUILDKITE_COMMIT"
|
||||
S3_COMMIT_PREFIX="s3://$BUCKET/$SUBPATH/"
|
||||
|
||||
# detect if python3.12+ is available
|
||||
has_new_python=$($PYTHON -c "print(1 if __import__('sys').version_info >= (3,12) else 0)")
|
||||
if [[ "$has_new_python" -eq 0 ]]; then
|
||||
# use new python from docker
|
||||
docker pull python:3-slim
|
||||
PYTHON="docker run --rm -v $(pwd):/app -w /app python:3-slim python3"
|
||||
fi
|
||||
|
||||
echo "Using python interpreter: $PYTHON"
|
||||
echo "Python version: $($PYTHON --version)"
|
||||
|
||||
# ======== generate and upload indices ========
|
||||
|
||||
# list all wheels in the commit directory
|
||||
echo "Existing wheels on S3:"
|
||||
aws s3 ls "$S3_COMMIT_PREFIX" || echo "(no objects found)"
|
||||
obj_json="objects.json"
|
||||
aws s3api list-objects-v2 --bucket "$BUCKET" --prefix "$SUBPATH/" --delimiter / --output json > "$obj_json"
|
||||
mkdir -p "$INDICES_OUTPUT_DIR"
|
||||
|
||||
# HACK: we do not need regex module here, but it is required by pre-commit hook
|
||||
# To avoid any external dependency, we simply replace it back to the stdlib re module
|
||||
sed -i 's/import regex as re/import re/g' .buildkite/scripts/generate-nightly-index.py
|
||||
|
||||
# Generate indices -- the version is just the commit hash (not omni/{commit})
|
||||
# because relative paths are computed between the index and wheel directories,
|
||||
# both of which live under the omni/ prefix in S3.
|
||||
$PYTHON .buildkite/scripts/generate-nightly-index.py \
|
||||
--version "$BUILDKITE_COMMIT" \
|
||||
--current-objects "$obj_json" \
|
||||
--output-dir "$INDICES_OUTPUT_DIR" \
|
||||
--comment "commit $BUILDKITE_COMMIT"
|
||||
|
||||
# copy indices to /omni/{commit}/ unconditionally
|
||||
echo "Uploading indices to $S3_COMMIT_PREFIX"
|
||||
aws s3 cp --recursive "$INDICES_OUTPUT_DIR/" "$S3_COMMIT_PREFIX"
|
||||
|
||||
# copy to /omni/nightly/ when NIGHTLY=1
|
||||
if [[ "${NIGHTLY:-}" == "1" ]]; then
|
||||
echo "Uploading indices to overwrite /omni/nightly/"
|
||||
aws s3 cp --recursive "$INDICES_OUTPUT_DIR/" "s3://$BUCKET/omni/nightly/"
|
||||
fi
|
||||
|
||||
# detect version from any wheel in the commit directory
|
||||
first_wheel_key=$($PYTHON -c "import json; obj=json.load(open('$obj_json')); print(next((c['Key'] for c in obj.get('Contents', []) if c['Key'].endswith('.whl')), ''))")
|
||||
if [[ -z "$first_wheel_key" ]]; then
|
||||
echo "Error: No wheels found in $S3_COMMIT_PREFIX"
|
||||
exit 1
|
||||
fi
|
||||
first_wheel=$(basename "$first_wheel_key")
|
||||
aws s3 cp "s3://$BUCKET/${first_wheel_key}" "/tmp/${first_wheel}"
|
||||
version=$(unzip -p "/tmp/${first_wheel}" '**/METADATA' | grep '^Version: ' | cut -d' ' -f2)
|
||||
rm -f "/tmp/${first_wheel}"
|
||||
echo "Version in wheel: $version"
|
||||
pure_version="${version%%+*}"
|
||||
echo "Pure version (without variant): $pure_version"
|
||||
|
||||
# re-generate and copy to /omni/{version}/ only if it does not have "dev" in the version
|
||||
if [[ "$version" != *"dev"* ]]; then
|
||||
echo "Re-generating indices for /omni/$pure_version/"
|
||||
rm -rf "${INDICES_OUTPUT_DIR:?}"
|
||||
mkdir -p "$INDICES_OUTPUT_DIR"
|
||||
# wheel-dir is overridden to be the commit directory, so that the indices point to the correct wheel path
|
||||
$PYTHON .buildkite/scripts/generate-nightly-index.py \
|
||||
--version "$pure_version" \
|
||||
--wheel-dir "$BUILDKITE_COMMIT" \
|
||||
--current-objects "$obj_json" \
|
||||
--output-dir "$INDICES_OUTPUT_DIR" \
|
||||
--comment "version $pure_version"
|
||||
aws s3 cp --recursive "$INDICES_OUTPUT_DIR/" "s3://$BUCKET/omni/$pure_version/"
|
||||
fi
|
||||
@@ -1,193 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from dataclasses import asdict, dataclass
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from urllib.parse import quote
|
||||
|
||||
import regex as re
|
||||
|
||||
|
||||
def normalize_package_name(name: str) -> str:
|
||||
"""Normalize package name per PEP 503."""
|
||||
return re.sub(r"[-_.]+", "-", name).lower()
|
||||
|
||||
|
||||
if not sys.version_info >= (3, 12):
|
||||
raise RuntimeError("This script requires Python 3.12 or higher.")
|
||||
|
||||
INDEX_HTML_TEMPLATE = """<!DOCTYPE html>
|
||||
<html>
|
||||
<!-- {comment} -->
|
||||
<meta name="pypi:repository-version" content="1.0">
|
||||
<body>
|
||||
{items}
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
@dataclass
|
||||
class WheelFileInfo:
|
||||
package_name: str
|
||||
version: str
|
||||
build_tag: str | None
|
||||
python_tag: str
|
||||
abi_tag: str
|
||||
platform_tag: str
|
||||
filename: str
|
||||
|
||||
|
||||
def parse_from_filename(file: str) -> WheelFileInfo:
|
||||
"""
|
||||
Parse wheel filename per PEP 427:
|
||||
{package_name}-{version}(-{build_tag})?-{python_tag}-{abi_tag}-{platform_tag}.whl
|
||||
"""
|
||||
wheel_file_re = re.compile(
|
||||
r"^(?P<package_name>.+)-(?P<version>[^-]+?)(-(?P<build_tag>[^-]+))?-(?P<python_tag>[^-]+)-(?P<abi_tag>[^-]+)-(?P<platform_tag>[^-]+)\.whl$"
|
||||
)
|
||||
match = wheel_file_re.match(file)
|
||||
if not match:
|
||||
raise ValueError(f"Invalid wheel file name: {file}")
|
||||
|
||||
return WheelFileInfo(
|
||||
package_name=match.group("package_name"),
|
||||
version=match.group("version"),
|
||||
build_tag=match.group("build_tag"),
|
||||
python_tag=match.group("python_tag"),
|
||||
abi_tag=match.group("abi_tag"),
|
||||
platform_tag=match.group("platform_tag"),
|
||||
filename=file,
|
||||
)
|
||||
|
||||
|
||||
def generate_project_list(package_names: list[str], comment: str = "") -> str:
|
||||
"""Generate top-level PEP 503 project list HTML."""
|
||||
href_tags = []
|
||||
for name in sorted(package_names):
|
||||
href_tags.append(f' <a href="{name}/">{name}/</a><br/>')
|
||||
return INDEX_HTML_TEMPLATE.format(items="\n".join(href_tags), comment=comment)
|
||||
|
||||
|
||||
def generate_package_index(
|
||||
wheel_files: list[WheelFileInfo],
|
||||
wheel_base_dir: Path,
|
||||
index_base_dir: Path,
|
||||
comment: str = "",
|
||||
) -> tuple[str, str]:
|
||||
"""Generate package index HTML and metadata JSON linking to wheel files."""
|
||||
href_tags = []
|
||||
metadata = []
|
||||
for file in sorted(wheel_files, key=lambda x: x.filename):
|
||||
relative_path = wheel_base_dir.relative_to(index_base_dir, walk_up=True) / file.filename
|
||||
# handle '+' in URL; avoid double-encoding '/' and '%2B' (AWS S3 behavior)
|
||||
file_path_quoted = quote(relative_path.as_posix(), safe=":%/")
|
||||
href_tags.append(f' <a href="{file_path_quoted}">{file.filename}</a><br/>')
|
||||
file_meta = asdict(file)
|
||||
file_meta["path"] = file_path_quoted
|
||||
metadata.append(file_meta)
|
||||
index_str = INDEX_HTML_TEMPLATE.format(items="\n".join(href_tags), comment=comment)
|
||||
metadata_str = json.dumps(metadata, indent=2)
|
||||
return index_str, metadata_str
|
||||
|
||||
|
||||
def generate_index(
|
||||
whl_files: list[str],
|
||||
wheel_base_dir: Path,
|
||||
index_base_dir: Path,
|
||||
comment: str = "",
|
||||
):
|
||||
"""
|
||||
Generate PEP 503 index for all wheel files.
|
||||
|
||||
Output structure:
|
||||
index_base_dir/
|
||||
index.html # project list linking to vllm-omni/
|
||||
vllm-omni/
|
||||
index.html # package index linking to wheel files
|
||||
metadata.json # machine-readable metadata
|
||||
"""
|
||||
parsed_files = [parse_from_filename(f) for f in whl_files]
|
||||
|
||||
if not parsed_files:
|
||||
print("No wheel files found, skipping index generation.")
|
||||
return
|
||||
|
||||
comment_str = f" ({comment})" if comment else ""
|
||||
comment_tmpl = f"Generated on {datetime.now().isoformat()}{comment_str}"
|
||||
|
||||
# Group by normalized package name
|
||||
packages: dict[str, list[WheelFileInfo]] = {}
|
||||
for file in parsed_files:
|
||||
name = normalize_package_name(file.package_name)
|
||||
packages.setdefault(name, []).append(file)
|
||||
|
||||
print(f"Found packages: {list(packages.keys())}")
|
||||
|
||||
# Generate per-package index
|
||||
for package, files in packages.items():
|
||||
package_dir = index_base_dir / package
|
||||
package_dir.mkdir(parents=True, exist_ok=True)
|
||||
index_str, metadata_str = generate_package_index(files, wheel_base_dir, package_dir, comment)
|
||||
with open(package_dir / "index.html", "w") as f:
|
||||
f.write(index_str)
|
||||
with open(package_dir / "metadata.json", "w") as f:
|
||||
f.write(metadata_str)
|
||||
|
||||
# Generate top-level project list
|
||||
project_list_str = generate_project_list(sorted(packages.keys()), comment_tmpl)
|
||||
with open(index_base_dir / "index.html", "w") as f:
|
||||
f.write(project_list_str)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Generate PEP 503 wheel index from S3 object listing.")
|
||||
parser.add_argument("--version", type=str, required=True, help="Version string (e.g., commit hash)")
|
||||
parser.add_argument("--current-objects", type=str, required=True, help="Path to JSON from S3 list-objects-v2")
|
||||
parser.add_argument("--output-dir", type=str, required=True, help="Directory to write index files")
|
||||
parser.add_argument("--wheel-dir", type=str, default=None, help="Wheel directory (defaults to --version)")
|
||||
parser.add_argument("--comment", type=str, default="", help="Comment for generated HTML")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
version = args.version
|
||||
if "\\" in version or "/" in version:
|
||||
raise ValueError("Version string must not contain slashes or backslashes.")
|
||||
|
||||
output_dir = Path(args.output_dir)
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with open(args.current_objects) as f:
|
||||
current_objects: dict[str, list[dict[str, Any]]] = json.load(f)
|
||||
|
||||
wheel_files = [
|
||||
item["Key"].split("/")[-1] for item in current_objects.get("Contents", []) if item["Key"].endswith(".whl")
|
||||
]
|
||||
|
||||
print(f"Found {len(wheel_files)} wheel files for version {version}: {wheel_files}")
|
||||
|
||||
# For release versions, filter to only matching non-dev wheels
|
||||
PY_VERSION_RE = re.compile(r"^\d+\.\d+\.\d+([a-zA-Z0-9.+-]*)?$")
|
||||
if PY_VERSION_RE.match(version):
|
||||
wheel_files = [f for f in wheel_files if version in f and "dev" not in f]
|
||||
print(f"Non-nightly version detected, wheel files used: {wheel_files}")
|
||||
else:
|
||||
print("Nightly version detected, keeping all wheel files.")
|
||||
|
||||
wheel_dir = (args.wheel_dir or version).strip().rstrip("/")
|
||||
wheel_base_dir = Path(output_dir).parent / wheel_dir
|
||||
index_base_dir = Path(output_dir)
|
||||
|
||||
generate_index(
|
||||
whl_files=wheel_files,
|
||||
wheel_base_dir=wheel_base_dir,
|
||||
index_base_dir=index_base_dir,
|
||||
comment=args.comment.strip(),
|
||||
)
|
||||
print(f"Successfully generated index in {output_dir}")
|
||||
@@ -1,33 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -ex
|
||||
|
||||
# Upload a single wheel to S3 under the omni/ prefix.
|
||||
# Index generation is handled separately by generate-and-upload-nightly-index.sh.
|
||||
|
||||
BUCKET="vllm-wheels"
|
||||
SUBPATH="omni/$BUILDKITE_COMMIT"
|
||||
S3_COMMIT_PREFIX="s3://$BUCKET/$SUBPATH/"
|
||||
|
||||
# ========= collect & upload the wheel ==========
|
||||
|
||||
# python3 -m build outputs to dist/ by default
|
||||
wheel_files=(dist/*.whl)
|
||||
|
||||
# Check that exactly one wheel is found
|
||||
if [[ ${#wheel_files[@]} -ne 1 ]]; then
|
||||
echo "Error: Expected exactly one wheel file in dist/, but found ${#wheel_files[@]}"
|
||||
exit 1
|
||||
fi
|
||||
wheel="${wheel_files[0]}"
|
||||
|
||||
echo "Uploading wheel: $wheel"
|
||||
|
||||
# Extract the version from the wheel
|
||||
version=$(unzip -p "$wheel" '**/METADATA' | grep '^Version: ' | cut -d' ' -f2)
|
||||
echo "Version in wheel: $version"
|
||||
|
||||
# Upload wheel to S3
|
||||
aws s3 cp "$wheel" "$S3_COMMIT_PREFIX"
|
||||
|
||||
echo "Wheel uploaded to $S3_COMMIT_PREFIX. Index generation is handled by a separate step."
|
||||
@@ -134,33 +134,15 @@ steps:
|
||||
- timeout 10m pytest -s -v tests/e2e/offline_inference/test_qwen3_omni.py
|
||||
- timeout 20m pytest -s -v tests/e2e/online_serving/test_qwen3_omni.py -m "advanced_model" --run-level "advanced_model"
|
||||
|
||||
- label: "Qwen3-TTS CustomVoice E2E Test"
|
||||
- label: "Qwen3-TTS E2E Test"
|
||||
agent_pool: mi325_2
|
||||
depends_on: amd-build
|
||||
mirror_hardwares: [amdproduction]
|
||||
grade: Blocking
|
||||
commands:
|
||||
- |
|
||||
timeout 20m 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_customvoice.py -m "advanced_model" --run-level "advanced_model" && pytest -s -v tests/e2e/offline_inference/test_qwen3_tts_customvoice.py
|
||||
'
|
||||
|
||||
- label: "Qwen3-TTS Base E2E Test"
|
||||
agent_pool: mi325_2
|
||||
depends_on: amd-build
|
||||
mirror_hardwares: [amdproduction]
|
||||
grade: Blocking
|
||||
commands:
|
||||
- |
|
||||
timeout 20m 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 -m "advanced_model" --run-level "advanced_model" && pytest -s -v tests/e2e/offline_inference/test_qwen3_tts_base.py
|
||||
'
|
||||
- export VLLM_LOGGING_LEVEL=DEBUG
|
||||
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
- timeout 20m pytest -s -v tests/e2e/online_serving/test_qwen3_tts.py
|
||||
|
||||
- label: "Diffusion Image Edit Test"
|
||||
agent_pool: mi325_1
|
||||
|
||||
@@ -124,8 +124,7 @@ steps:
|
||||
commands:
|
||||
- export VLLM_LOGGING_LEVEL=DEBUG
|
||||
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
- export VLLM_ALLOW_LONG_MAX_MODEL_LEN="1"
|
||||
- timeout 20m pytest -s -v tests/e2e/online_serving/test_qwen3_tts_customvoice.py -m "core_model" --run-level "core_model"
|
||||
- timeout 20m pytest -s -v tests/e2e/online_serving/test_qwen3_tts.py
|
||||
|
||||
- label: "Diffusion Image Edit Test"
|
||||
agent_pool: mi325_1
|
||||
|
||||
@@ -208,52 +208,6 @@ steps:
|
||||
volumes:
|
||||
- "/fsx/hf_cache:/fsx/hf_cache"
|
||||
|
||||
- label: "Qwen3-TTS CustomVoice E2E Test"
|
||||
depends_on: upload-merge-pipeline
|
||||
commands:
|
||||
- |
|
||||
timeout 20m 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_customvoice.py -m "advanced_model" --run-level "advanced_model" && pytest -s -v tests/e2e/offline_inference/test_qwen3_tts_customvoice.py
|
||||
'
|
||||
agents:
|
||||
queue: "gpu_1_queue"
|
||||
plugins:
|
||||
- docker#v5.2.0:
|
||||
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
|
||||
always-pull: true
|
||||
propagate-environment: true
|
||||
shm-size: "8gb"
|
||||
environment:
|
||||
- "HF_HOME=/fsx/hf_cache"
|
||||
volumes:
|
||||
- "/fsx/hf_cache:/fsx/hf_cache"
|
||||
|
||||
- label: "Qwen3-TTS Base E2E Test"
|
||||
depends_on: upload-merge-pipeline
|
||||
commands:
|
||||
- |
|
||||
timeout 20m 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 -m "advanced_model" --run-level "advanced_model" && pytest -s -v tests/e2e/offline_inference/test_qwen3_tts_base.py
|
||||
'
|
||||
agents:
|
||||
queue: "gpu_1_queue"
|
||||
plugins:
|
||||
- docker#v5.2.0:
|
||||
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
|
||||
always-pull: true
|
||||
propagate-environment: true
|
||||
shm-size: "8gb"
|
||||
environment:
|
||||
- "HF_HOME=/fsx/hf_cache"
|
||||
volumes:
|
||||
- "/fsx/hf_cache:/fsx/hf_cache"
|
||||
|
||||
- label: "Omni Model Test with H100"
|
||||
timeout_in_minutes: 30
|
||||
depends_on: upload-merge-pipeline
|
||||
|
||||
@@ -50,15 +50,13 @@ steps:
|
||||
path: /mnt/hf-cache
|
||||
type: DirectoryOrCreate
|
||||
|
||||
- label: ":full_moon: Omni Model Test with L4"
|
||||
timeout_in_minutes: 90
|
||||
- label: ":full_moon: Omni Model Test"
|
||||
timeout_in_minutes: 60
|
||||
depends_on: upload-nightly-pipeline
|
||||
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test"
|
||||
commands:
|
||||
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
- export VLLM_ALLOW_LONG_MAX_MODEL_LEN="1"
|
||||
- pytest -s -v tests/examples/ -m "advanced_model and L4 and omni" --run-level "advanced_model"
|
||||
- pytest -s -v tests/e2e/online_serving/test_*_expansion.py -m "advanced_model and L4 and omni" --run-level "advanced_model"
|
||||
- pytest -s -v tests/examples/online_serving/test_qwen2_5_omni.py -m "advanced_model" --run-level "advanced_model"
|
||||
agents:
|
||||
queue: "gpu_4_queue" # g6.12xlarge instance on AWS, has 4 L4 GPU
|
||||
plugins:
|
||||
@@ -219,6 +217,29 @@ steps:
|
||||
path: /mnt/hf-cache
|
||||
type: DirectoryOrCreate
|
||||
|
||||
- label: ":full_moon: Qwen3-TTS Non-Async-Chunk E2E Test"
|
||||
timeout_in_minutes: 30
|
||||
depends_on: upload-nightly-pipeline
|
||||
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test"
|
||||
commands:
|
||||
- |
|
||||
huggingface-cli download Qwen/Qwen3-TTS-12Hz-0.6B-CustomVoice
|
||||
export VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
pytest -s -v tests/e2e/online_serving/test_qwen3_tts.py::TestQwen3TTSNoAsyncChunk
|
||||
agents:
|
||||
queue: "gpu_4_queue"
|
||||
plugins:
|
||||
- docker#v5.2.0:
|
||||
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
|
||||
always-pull: true
|
||||
propagate-environment: true
|
||||
shm-size: "8gb"
|
||||
environment:
|
||||
- "HF_HOME=/fsx/hf_cache"
|
||||
- "HF_TOKEN"
|
||||
volumes:
|
||||
- "/fsx/hf_cache:/fsx/hf_cache"
|
||||
|
||||
- label: ":full_moon: Omni Model Perf Test & Test Case Statistics"
|
||||
key: nightly-omni-performance
|
||||
timeout_in_minutes: 180
|
||||
@@ -227,7 +248,6 @@ steps:
|
||||
commands:
|
||||
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
- export BENCHMARK_DIR=tests/dfx/perf/results
|
||||
- export VLLM_ALLOW_LONG_MAX_MODEL_LEN="1"
|
||||
- pytest -s -v tests/dfx/perf/scripts/run_benchmark.py
|
||||
- buildkite-agent artifact upload "tests/dfx/perf/results/*.json"
|
||||
- python tools/nightly/buildkite_testcase_statistics.py -o tests/dfx/perf/results/buildkite_testcase_statistics.html
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
steps:
|
||||
- label: "Simple Unit Test"
|
||||
depends_on: upload-ready-pipeline
|
||||
agents:
|
||||
queue: "ascend-a2b3"
|
||||
resource_class: "npu-2"
|
||||
image: "swr.cn-southwest-2.myhuaweicloud.com/modelfoundry/vllm-omni-ci-npu:${BUILDKITE_COMMIT}"
|
||||
plugins:
|
||||
- kubernetes:
|
||||
podSpecPatch:
|
||||
imagePullSecrets:
|
||||
- name: swr-secret
|
||||
commands:
|
||||
- "timeout 20m pytest -v -s -m 'core_model and cpu' --cov=vllm_omni --cov-branch --cov-report=term-missing --cov-report=html --cov-report=xml"
|
||||
@@ -303,13 +303,13 @@ steps:
|
||||
commands:
|
||||
- |
|
||||
timeout 20m bash -c '
|
||||
huggingface-cli download Qwen/Qwen3-TTS-12Hz-0.6B-CustomVoice
|
||||
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_customvoice.py -m "core_model" --run-level "core_model"
|
||||
pytest -s -v tests/e2e/online_serving/test_qwen3_tts.py -k "not NoAsyncChunk"
|
||||
'
|
||||
agents:
|
||||
queue: "gpu_1_queue"
|
||||
queue: "gpu_4_queue"
|
||||
plugins:
|
||||
- docker#v5.2.0:
|
||||
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
|
||||
|
||||
+15
-7
@@ -5,26 +5,34 @@ FROM ${VLLM_ASCEND_IMAGE}:${VLLM_ASCEND_TAG}
|
||||
WORKDIR /vllm-workspace/vllm
|
||||
RUN git fetch origin --tags && git checkout v0.18.0
|
||||
|
||||
RUN git config --global url."https://gh-proxy.test.osinfra.cn/https://github.com/".insteadOf "https://github.com/"
|
||||
|
||||
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 && \
|
||||
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-ascend/ --extra-index https://download.pytorch.org/whl/cpu/ && \
|
||||
python3 -m pip install -U pip && \
|
||||
python3 -m pip install -v -e /vllm-workspace/vllm-ascend/ \
|
||||
--index-url http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple \
|
||||
--extra-index https://download.pytorch.org/whl/cpu/ \
|
||||
--trusted-host cache-service.nginx-pypi-cache.svc.cluster.local \
|
||||
--default-timeout=60
|
||||
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 && \
|
||||
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
|
||||
python3 -m pip install -U pip && \
|
||||
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
|
||||
|
||||
ENV VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
|
||||
|
||||
@@ -5,26 +5,34 @@ FROM ${VLLM_ASCEND_IMAGE}:${VLLM_ASCEND_TAG}
|
||||
WORKDIR /vllm-workspace/vllm
|
||||
RUN git fetch origin --tags && git checkout v0.18.0
|
||||
|
||||
RUN git config --global url."https://gh-proxy.test.osinfra.cn/https://github.com/".insteadOf "https://github.com/"
|
||||
|
||||
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 && \
|
||||
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-ascend/ --extra-index https://download.pytorch.org/whl/cpu/ && \
|
||||
python3 -m pip install -U pip && \
|
||||
python3 -m pip install -v -e /vllm-workspace/vllm-ascend/ \
|
||||
--index-url http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple \
|
||||
--extra-index https://download.pytorch.org/whl/cpu/ \
|
||||
--trusted-host cache-service.nginx-pypi-cache.svc.cluster.local \
|
||||
--default-timeout=60
|
||||
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 && \
|
||||
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
|
||||
python3 -m pip install -U pip && \
|
||||
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
|
||||
|
||||
ENV VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
|
||||
|
||||
+6
-16
@@ -54,28 +54,20 @@ nav:
|
||||
- Features:
|
||||
- Sleep Mode: features/sleep_mode.md
|
||||
- Diffusion Features:
|
||||
- Overview: user_guide/diffusion_features.md
|
||||
- Feature Compatibility: user_guide/feature_compatibility.md
|
||||
- Cache Acceleration:
|
||||
- TeaCache: user_guide/diffusion/cache_acceleration/teacache.md
|
||||
- Cache-DiT: user_guide/diffusion/cache_acceleration/cache_dit.md
|
||||
- Acceleration Overview: user_guide/diffusion_acceleration.md
|
||||
- TeaCache: user_guide/diffusion/teacache.md
|
||||
- Cache-DiT: user_guide/diffusion/cache_dit_acceleration.md
|
||||
- Quantization:
|
||||
- Overview: user_guide/diffusion/quantization/overview.md
|
||||
- FP8: user_guide/diffusion/quantization/fp8.md
|
||||
- Int8: user_guide/diffusion/quantization/int8.md
|
||||
- GGUF: user_guide/diffusion/quantization/gguf.md
|
||||
- Parallelism:
|
||||
- Overview: user_guide/diffusion/parallelism/overview.md
|
||||
- CFG Parallel: user_guide/diffusion/parallelism/cfg_parallel.md
|
||||
- Expert Parallel: user_guide/diffusion/parallelism/expert_parallel.md
|
||||
- Hybrid Sharded Data Parallel: user_guide/diffusion/parallelism/hsdp.md
|
||||
- Sequence Parallel: user_guide/diffusion/parallelism/sequence_parallel.md
|
||||
- Tensor Parallel: user_guide/diffusion/parallelism/tensor_parallel.md
|
||||
- VAE Patch Parallel: user_guide/diffusion/parallelism/vae_patch_parallel.md
|
||||
- Step Execution: user_guide/diffusion/step_execution.md
|
||||
- Parallelism Acceleration: user_guide/diffusion/parallelism_acceleration.md
|
||||
- CPU Offloading: user_guide/diffusion/cpu_offload_diffusion.md
|
||||
- LoRA: user_guide/diffusion/lora.md
|
||||
- Hybrid Sharded Data Parallel: design/feature/hsdp.md
|
||||
- Custom Pipeline: features/custom_pipeline.md
|
||||
- Step Execution: user_guide/diffusion/step_execution.md
|
||||
- ComfyUI: features/comfyui.md
|
||||
- Developer Guide:
|
||||
- General:
|
||||
@@ -100,8 +92,6 @@ nav:
|
||||
- design/feature/cfg_parallel.md
|
||||
- design/feature/sequence_parallel.md
|
||||
- design/feature/tensor_parallel.md
|
||||
- design/feature/vae_parallel.md
|
||||
- design/feature/hsdp.md
|
||||
- design/feature/cache_dit.md
|
||||
- design/feature/teacache.md
|
||||
- design/feature/async_chunk_design.md
|
||||
|
||||
@@ -20,4 +20,7 @@ For introduction, please check [Introduction for stage config](./stage_configs.m
|
||||
|
||||
## Optimization Features
|
||||
|
||||
- **[Diffusion Features Overview](../user_guide/diffusion_features.md)** - Complete overview of all diffusion model features and supported models
|
||||
- **[TeaCache Configuration](../user_guide/diffusion/teacache.md)** - Enable TeaCache adaptive caching for DiT models to achieve 1.5x-2.0x speedup with minimal quality loss
|
||||
- **[Cache-DiT Configuration](../user_guide/diffusion/cache_dit_acceleration.md)** - Enable Cache-DiT as cache acceleration backends for DiT models
|
||||
- **[Parallelism Configuration](../user_guide/diffusion/parallelism_acceleration.md)** - Enable parallelism (e.g., sequence parallelism) for for DiT models
|
||||
- **[CPU Offloading](../user_guide/diffusion/cpu_offload_diffusion.md)** - Enable CPU offloading (model-level and layerwise) for for DiT models
|
||||
@@ -548,19 +548,103 @@ L4 level testing is a comprehensive quality audit before a version release. It e
|
||||
- ***Execution Environment***: ***GPU*** server clusters to meet the resource demands of performance testing.
|
||||
- ***Script Example***:
|
||||
|
||||
??? example "Test Examples: Documentation Example Tests"
|
||||
???+ example "Test Examples"
|
||||
|
||||
--8<-- "docs/contributing/ci/test_examples/l4_doc_example_tests.inc.md"
|
||||
When adding L4-level ***documentation example Tests***, please pay attention to the following guides.
|
||||
|
||||
??? example "Test Examples: Performance Tests"
|
||||
--8<-- "docs/contributing/ci/test_examples/doc_example_tests.inc.md"
|
||||
|
||||
--8<-- "docs/contributing/ci/test_examples/l4_performance_tests.inc.md"
|
||||
When you want to add L4-level ***performance test*** cases, you can refer to the following format for case addition in tests/dfx/perf/tests/test.json:
|
||||
|
||||
??? example "Test Examples: Functionality Tests"
|
||||
```JSON
|
||||
{
|
||||
"test_name": "test_qwen3_omni",
|
||||
"server_params": {
|
||||
"model": "Qwen/Qwen3-Omni-30B-A3B-Instruct",
|
||||
"stage_config_name": "qwen3_omni.yaml"
|
||||
},
|
||||
"benchmark_params": [
|
||||
{
|
||||
"dataset_name": "random",
|
||||
"num_prompts": [10, 20],
|
||||
"max_concurrency": [1, 4],
|
||||
"random_input_len": 2500,
|
||||
"random_output_len": 900,
|
||||
"ignore_eos": true,
|
||||
"percentile-metrics": "ttft,tpot,itl,e2el,audio_rtf,audio_ttfp,audio_duration",
|
||||
"baseline": {
|
||||
"mean_ttft_ms": [500, 800],
|
||||
"mean_audio_ttfp_ms": [2000, 3500],
|
||||
"mean_audio_rtf": [0.25, 0.35]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
--8<-- "docs/contributing/ci/test_examples/l4_functionality_tests.inc.md"
|
||||
**Parameter Explanation**
|
||||
|
||||
- ***Run Command***: (Specific commands would depend on the performance testing tool and configuration defined in `nightly.json`).
|
||||
*Overview*
|
||||
|
||||
| Field | Required | Description |
|
||||
| ---------------- | -------- | --------------------------------------------------------------- |
|
||||
| test_name | Yes | Unique identifier for the test case |
|
||||
| server_params | Yes | Server-side configuration parameters |
|
||||
| benchmark_params | Yes | Benchmark running parameters (supports multiple configurations) |
|
||||
|
||||
**server_params Configuration**
|
||||
|
||||
*Basic Parameters*
|
||||
|
||||
| Parameter | Required | Example | Description |
|
||||
| ----------------- | -------- | ---------------------------------- | ----------------------------- |
|
||||
| model | Yes | "Qwen/Qwen3-Omni-30B-A3B-Instruct" | Model name or path |
|
||||
| stage_config_name | Yes | "qwen3_omni.yaml" | Stage configuration file name |
|
||||
|
||||
*Dynamic Configuration (update/delete)*
|
||||
|
||||
Supports incremental modifications based on the basic configuration:
|
||||
|
||||
| Operation | Description |
|
||||
| --------- | ------------------------------------ |
|
||||
| update | Update or add configuration items |
|
||||
| delete | Delete specified configuration items |
|
||||
|
||||
***Example***:
|
||||
|
||||
```
|
||||
"update": {
|
||||
"async_chunk": true, // Enable asynchronous chunk processing
|
||||
"stage_args": {
|
||||
"0": {
|
||||
"engine_args.custom_process_next_stage_input_func": "vllm_omni.model_executor.stage_input_processors.qwen3_omni.thinker2talker_async_chunk"
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"stage_args": {
|
||||
"2": ["custom_process_input_func"] // Delete this configuration for stage 2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**benchmark_params Configuration**
|
||||
|
||||
You can add any benchmark running parameters you need here. For all optional parameters, refer to the [benchmark documentation](https://github.com/vllm-project/vllm-omni/blob/main/docs/cli/bench/serve.md). General modifications are as follows:
|
||||
|
||||
1. Change the ---xxx-xx-xx running parameters to xxx_xx_xx format and fill them as keys in the JSON file.
|
||||
2. For boolean variables in the running parameters, modify them to forms such as ignore_eos: true/false and fill them into the JSON file.
|
||||
3. Optionally add a `baseline` object (see **Baseline thresholds** below). If you omit `baseline` or leave it empty, the performance test still runs but does not assert metric thresholds from this field.
|
||||
4. The qps and concurrency modes are mutually exclusive. For detailed explanations, see the table below:
|
||||
|
||||
| Parameter | Type | Required | Example/Values | Description |
|
||||
| --------------- | ----------- | -------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| num_prompts | int / array | Yes | 10,[10, 20, 30] | Number of requests. Supports single values or arrays. If a single value is used, it will be automatically expanded to match the number of qps or max_concurrency, e.g., [10,10,10]. If an array is used, its length must match the number of qps or max_concurrency. |
|
||||
| request_rate | int / array | No | 1, [1, 2, 3] | Queries per second. Supports single values or arrays. If a single value is used, it will be automatically expanded to match the number of num_prompts, e.g., [1,1,1]. If an array is used, its length must match the number of num_prompts. |
|
||||
| max_concurrency | int / array | No | 1, [1, 2, 3] | Maximum concurrent in-flight requests. Same array / expansion rules as `request_rate` (mutually exclusive with QPS mode). |
|
||||
| baseline | object | No | see above | Optional per-metric thresholds; keys must match benchmark output fields. Scalar, list (per sweep step), or object (keyed by concurrency or QPS string). |
|
||||
|
||||
- - ***Run Command***: (Specific commands would depend on the performance testing tool and configuration defined in `nightly.json`).
|
||||
|
||||
## Chapter 4: L5 Level Testing - Stability and Reliability Testing
|
||||
|
||||
|
||||
File renamed without changes.
@@ -1,46 +0,0 @@
|
||||
**Scope**
|
||||
|
||||
For diffusion models, the L4 functionality test covers all or common *diffusion features* that are supported by this model, including several [parallelism acceleration methods](https://docs.vllm.ai/projects/vllm-omni/en/latest/user_guide/diffusion/parallelism_acceleration/), [CPU offloading](https://docs.vllm.ai/projects/vllm-omni/en/latest/user_guide/diffusion/cpu_offload_diffusion/), [TeaCache](https://docs.vllm.ai/projects/vllm-omni/en/latest/user_guide/diffusion/teacache/) and [Cache-DiT](https://docs.vllm.ai/projects/vllm-omni/en/latest/user_guide/diffusion/cache_dit_acceleration/) cache backends, [quantization methods](https://docs.vllm.ai/projects/vllm-omni/en/latest/user_guide/diffusion/quantization/overview/).
|
||||
|
||||
**Test Case Design**
|
||||
|
||||
For a *high priority* model (currently listed in [issue #1832](https://github.com/vllm-project/vllm-omni/issues/1832)), we use several test cases, each with multiple features turned on, so that each supported feature is tested in at least one test case. This is to relieve the GPU workload on the CI machine. The suggested test case combination is as follows:
|
||||
|
||||
- If the model can fit into 4 L4 GPU (with quantization and tensor parallel always on) (20GB RAM each)
|
||||
- (1 GPU) TeaCache + Layerwise CPU offloading + GGUF
|
||||
- (4 GPUs) CacheDiT + Ulysses=2 + TP=2 + VAE=2 + FP8
|
||||
- (4 GPUs) CacheDiT + Ring=2 + HSDP=2 + VAE=2 + GGUF
|
||||
- (4 GPUs) TeaCache + CFG=2 + TP=2 + VAE=2 + FP8
|
||||
- Otherwise, consider 2 H100 GPU environment (80GB RAM each) with the following tests
|
||||
- (1 GPU) TeaCache + Layerwise CPU offloading + GGUF
|
||||
- (2 GPUs) CacheDiT + Ulysses=2 + FP8
|
||||
- (2 GPUs) CacheDiT + Ring=2 + GGUF
|
||||
- (2 GPUs) TeaCache + CFG=2 + FP8
|
||||
- (2 GPUs) CacheDiT + TP=2 + VAE=2 + FP8
|
||||
- (2 GPUs) CacheDiT + HSDP=2 + VAE=2 + GGUF
|
||||
- If 2 H100 GPU cannot handle the model either (e.g., HunyuanImage 3.0)
|
||||
- Still design tests and feature combinations that can best fit real-world scenario.
|
||||
- Do not include it in CI (or exclude it from the CI's filtering criteria). Instead, relevant PR authors are suggested to run these tests locally.
|
||||
- Fallback plan
|
||||
- If the model does not support layerwise CPU offloading, replace the corresponding test case with module-wise offloading
|
||||
- If the model only supports specific or no caching feature, use this option or remove this option in all test cases.
|
||||
- If the model only supports specific or no quantization feature, use this option or remove this option in all test cases.
|
||||
- If the model does not support certain other features, remove this option from that test case. If, consequently, the coverage of this modified test case completely overlaps with others, remove this test case.
|
||||
|
||||
For a *normal priority* model, further reduce the number of test cases.
|
||||
|
||||
- Only write one or two test cases for the most common feature combinations.
|
||||
- The author can explore themselves to see which feature combination balances output quality and performance. Alternatively, the author can refer to any example code in the PR that adds the model, or the example code in the PR that adds a feature (if the code involves this model of interest).
|
||||
|
||||
Currently all the features are available in online serving mode. Hence, only need to add `tests/e2e/online_serving/test_{model}_expansion.py`.
|
||||
|
||||
**Code Style**
|
||||
|
||||
- Validation: test that the multimodal output files of your model have the correct shapes. `OpenAIClientHandler.send_diffusion_request` should have taken care of this.
|
||||
- Test marks: always add `advanced_model` and `diffusion`. Add GPU-related marks if needed. Ref: [Markers for Tests](https://docs.vllm.ai/projects/vllm-omni/en/latest/contributing/ci/tests_markers/).
|
||||
- To maximize code reuse, you may refer to
|
||||
- `tests/conftest.py` for `omni_server` (running server in subprocess) and `openai_client` fixtures (sending requests and validating output), `generate_synthetic_image` and `assert_XXX_valid` helper.
|
||||
- `tests/utils.py` for `@hardware_test(...)` and `hardware_marks`.
|
||||
- [Parametrizing tests (pytest doc)](https://docs.pytest.org/en/stable/example/parametrize.html) to reuse test function implementation for different cases.
|
||||
- Doc: add a concise docstring for each test function.
|
||||
- Reference L4 test implementation: [tests/e2e/online_serving/test_qwen_image_edit_expansion.py](https://github.com/vllm-project/vllm-omni/blob/main/tests/e2e/online_serving/test_qwen_image_edit_expansion.py).
|
||||
@@ -1,89 +0,0 @@
|
||||
When you want to add L4-level ***performance test*** cases, you can refer to the following format for case addition in tests/dfx/perf/tests/test.json:
|
||||
|
||||
```JSON
|
||||
{
|
||||
"test_name": "test_qwen3_omni",
|
||||
"server_params": {
|
||||
"model": "Qwen/Qwen3-Omni-30B-A3B-Instruct",
|
||||
"stage_config_name": "qwen3_omni.yaml"
|
||||
},
|
||||
"benchmark_params": [
|
||||
{
|
||||
"dataset_name": "random",
|
||||
"num_prompts": [10, 20],
|
||||
"max_concurrency": [1, 4],
|
||||
"random_input_len": 2500,
|
||||
"random_output_len": 900,
|
||||
"ignore_eos": true,
|
||||
"percentile-metrics": "ttft,tpot,itl,e2el,audio_rtf,audio_ttfp,audio_duration",
|
||||
"baseline": {
|
||||
"mean_ttft_ms": [500, 800],
|
||||
"mean_audio_ttfp_ms": [2000, 3500],
|
||||
"mean_audio_rtf": [0.25, 0.35]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Parameter Explanation**
|
||||
|
||||
*Overview*
|
||||
|
||||
| Field | Required | Description |
|
||||
| ---------------- | -------- | --------------------------------------------------------------- |
|
||||
| test_name | Yes | Unique identifier for the test case |
|
||||
| server_params | Yes | Server-side configuration parameters |
|
||||
| benchmark_params | Yes | Benchmark running parameters (supports multiple configurations) |
|
||||
|
||||
**`server_params` Configuration**
|
||||
|
||||
*Basic Parameters*
|
||||
|
||||
| Parameter | Required | Example | Description |
|
||||
| ----------------- | -------- | ---------------------------------- | ----------------------------- |
|
||||
| model | Yes | "Qwen/Qwen3-Omni-30B-A3B-Instruct" | Model name or path |
|
||||
| stage_config_name | Yes | "qwen3_omni.yaml" | Stage configuration file name |
|
||||
|
||||
*Dynamic Configuration (update/delete)*
|
||||
|
||||
Supports incremental modifications based on the basic configuration:
|
||||
|
||||
| Operation | Description |
|
||||
| --------- | ------------------------------------ |
|
||||
| update | Update or add configuration items |
|
||||
| delete | Delete specified configuration items |
|
||||
|
||||
**Example**:
|
||||
|
||||
```
|
||||
"update": {
|
||||
"async_chunk": true, // Enable asynchronous chunk processing
|
||||
"stage_args": {
|
||||
"0": {
|
||||
"engine_args.custom_process_next_stage_input_func": "vllm_omni.model_executor.stage_input_processors.qwen3_omni.thinker2talker_async_chunk"
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"stage_args": {
|
||||
"2": ["custom_process_input_func"] // Delete this configuration for stage 2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**`benchmark_params` Configuration**
|
||||
|
||||
You can add any benchmark running parameters you need here. For all optional parameters, refer to the [benchmark documentation](https://github.com/vllm-project/vllm-omni/blob/main/docs/cli/bench/serve.md). General modifications are as follows:
|
||||
|
||||
1. Change the --xxx-xx-xx running parameters to xxx_xx_xx format and fill them as keys in the JSON file.
|
||||
2. For boolean variables in the running parameters, modify them to forms such as ignore_eos: true/false and fill them into the JSON file.
|
||||
3. Optionally add a `baseline` object (see **Baseline thresholds** below). If you omit `baseline` or leave it empty, the performance test still runs but does not assert metric thresholds from this field.
|
||||
4. The qps and concurrency modes are recommended to be mutually exclusive. For detailed explanations, see the table below:
|
||||
|
||||
| Parameter | Type | Required | Example/Values | Description |
|
||||
| --------------- | ----------- | -------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| num_prompts | int / array | Yes | 10,[10, 20, 30] | Number of requests. Supports single values or arrays. If a single value is used, it will be automatically expanded to match the number of qps or max_concurrency, e.g., [10,10,10]. If an array is used, its length must match the number of qps or max_concurrency. |
|
||||
| request_rate | float / array | No | 0.5, [0.5, 1, inf] | Queries per second. Supports single values or arrays. If a single value is used, it will be automatically expanded to match the number of num_prompts, e.g., [1,1,1]. If an array is used, its length must match the number of num_prompts. |
|
||||
| max_concurrency | int / array | No | 1, [1, 2, 3] | Maximum concurrent in-flight requests. Same array / expansion rules as `request_rate` (mutually exclusive with QPS mode). |
|
||||
| baseline | object | No | see above | Optional per-metric thresholds; keys must match benchmark output fields. Scalar, list (per sweep step), or object (keyed by concurrency or QPS string).
|
||||
@@ -653,7 +653,25 @@ For a fair comparison, keep the same **prompt**, **seed**, **resolution**, **num
|
||||
|
||||
To ensure project maintainability and sustainable development, please submit test code (unit tests, system tests, or end-to-end tests) alongside their code changes.
|
||||
|
||||
For comprehensive testing guidelines and the definition of test levels (L1-L5), please refer to the [Multi-Level Automated Testing System Documentation](../ci/CI_5levels.md). You are at least required to add an L4 *functionality* test described in that document.
|
||||
For comprehensive testing guidelines and the definition of test levels (L1-L5), please refer to the [Test File Structure and Style Guide](../ci/tests_style.md).
|
||||
The following tests are required to add:
|
||||
|
||||
- L4 test of the model's full *functionality* (i.e., all the *diffusion features* that are supported by this model), including several [parallelism acceleration methods](https://docs.vllm.ai/projects/vllm-omni/en/latest/user_guide/diffusion/parallelism_acceleration/), [CPU offloading](https://docs.vllm.ai/projects/vllm-omni/en/latest/user_guide/diffusion/cpu_offload_diffusion/), [TeaCache](https://docs.vllm.ai/projects/vllm-omni/en/latest/user_guide/diffusion/teacache/) and [Cache-DiT](https://docs.vllm.ai/projects/vllm-omni/en/latest/user_guide/diffusion/cache_dit_acceleration/) cache backends, [quantization methods](https://docs.vllm.ai/projects/vllm-omni/en/latest/user_guide/diffusion/quantization/overview/).
|
||||
- Test cases: Currently all the features are available in online serving mode. Hence, only need to add `tests/e2e/online_serving/test_{model}_expansion.py`. The following test cases shall cover all features:
|
||||
- 1 GPU: TeaCache & GGUF (or fallback to FP8, or disable it) & Layer-wise CPU offloading (or fallback to Module-wise)
|
||||
- 2 GPUs: Cache-DiT & FP8 (or fallback to GGUF, or disable it) & Ulysses = 2
|
||||
- 2 GPUs: Cache-DiT & GGUF (or fallback to FP8, or disable it) & Ring = 2
|
||||
- 2 GPUs: TeaCache & FP8 (or fallback to GGUF, or disable it) & CFG Parallel = 2
|
||||
- 2 GPUs: Cache-DiT & FP8 (or fallback to GGUF, or disable it) & Tensor Parallel = 2 & VAE Patch Parallel = 2
|
||||
- 2 GPUs: Cache-DiT & GGUF (or fallback to FP8, or disable it) & HSDP = 2 & VAE Patch Parallel = 2
|
||||
- Validation: test that the multimodal output files of your model have the correct shapes.
|
||||
- Test marks: always add `advanced_model` and `diffusion`. Add `parallel` and GPU-related marks if needed. Ref: [Markers for Tests](https://docs.vllm.ai/projects/vllm-omni/en/latest/contributing/ci/tests_markers/)
|
||||
- To maximize code reuse, you may refer to
|
||||
- `tests/conftest.py` for `omni_server` and `openai_client` fixtures, `generate_synthetic_image` and `assert_XXX_valid` helper.
|
||||
- `tests/utils.py` for `@hardware_test(...)` and `hardware_marks`.
|
||||
- [Parametrizing tests (pytest doc)](https://docs.pytest.org/en/stable/example/parametrize.html) to reuse test function implementation for different cases.
|
||||
- Doc: add a concise dostring for each test function.
|
||||
- Reference L4 test implementation: [tests/e2e/online_serving/test_qwen_image_edit_expansion.py](https://github.com/vllm-project/vllm-omni/blob/main/tests/e2e/online_serving/test_qwen_image_edit_expansion.py).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -256,7 +256,7 @@ cache_config={
|
||||
}
|
||||
```
|
||||
|
||||
Check the [user guide for cache_dit](../../user_guide/diffusion/cache_acceleration/cache_dit.md) for more adjustable parameters.
|
||||
Check the [user guide for cache_dit](../../user_guide/diffusion/cache_dit_acceleration.md) for more adjustable parameters.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ vLLM-omni provides `CFGParallelMixin` that encapsulates all CFG parallel logic.
|
||||
| Method | Purpose | Automatic Behavior |
|
||||
|--------|---------|-------------------|
|
||||
| [`predict_noise_maybe_with_cfg()`](https://docs.vllm.ai/projects/vllm-omni/en/latest/api/vllm_omni/diffusion/distributed/cfg_parallel/) | Predict noise with CFG | Detects parallel mode, distributes computation, gathers results |
|
||||
| [`scheduler_step_maybe_with_cfg()`](https://docs.vllm.ai/projects/vllm-omni/en/latest/api/vllm_omni/diffusion/distributed/cfg_parallel/) | Step scheduler | All ranks step locally (no broadcast needed) |
|
||||
| [`scheduler_step_maybe_with_cfg()`](https://docs.vllm.ai/projects/vllm-omni/en/latest/api/vllm_omni/diffusion/distributed/cfg_parallel/) | Step scheduler with sync | Rank 0 steps, broadcasts latents to all ranks |
|
||||
| [`combine_cfg_noise()`](https://docs.vllm.ai/projects/vllm-omni/en/latest/api/vllm_omni/diffusion/distributed/cfg_parallel/) | Combine positive/negative | Applies CFG formula with optional normalization |
|
||||
| [`predict_noise()`](https://docs.vllm.ai/projects/vllm-omni/en/latest/api/vllm_omni/diffusion/distributed/cfg_parallel/) | Forward pass wrapper | Override for custom transformer calls |
|
||||
| [`cfg_normalize_function()`](https://docs.vllm.ai/projects/vllm-omni/en/latest/api/vllm_omni/diffusion/distributed/cfg_parallel/) | Normalize CFG output | Override for custom normalization |
|
||||
@@ -47,7 +47,7 @@ vLLM-omni provides `CFGParallelMixin` that encapsulates all CFG parallel logic.
|
||||
- Rank 0 computes positive prompt prediction
|
||||
- Rank 1 computes negative prompt prediction
|
||||
- Results are gathered via `all_gather()`
|
||||
- All ranks compute CFG combine locally (deterministic, identical results)
|
||||
- Combined on rank 0 using CFG formula
|
||||
|
||||
- **Sequential mode** (when `cfg_world_size == 1`):
|
||||
- Single rank computes both positive and negative predictions
|
||||
@@ -55,7 +55,14 @@ vLLM-omni provides `CFGParallelMixin` that encapsulates all CFG parallel logic.
|
||||
|
||||
`scheduler_step_maybe_with_cfg()` ensures consistent latent states across all ranks:
|
||||
|
||||
- All ranks compute the scheduler step locally — no broadcast needed because `predict_noise_maybe_with_cfg` already ensures all ranks have identical noise predictions after `all_gather` + local combine.
|
||||
- **CFG-Parallel mode**:
|
||||
- Only rank 0 performs the scheduler step (applies noise prediction to update latents)
|
||||
- Updated latents are broadcast to all other ranks via `broadcast()`
|
||||
- All ranks maintain synchronized latent states for the next iteration
|
||||
|
||||
- **Sequential mode**:
|
||||
- Single rank directly performs the scheduler step
|
||||
- No synchronization needed
|
||||
|
||||
---
|
||||
|
||||
@@ -170,70 +177,6 @@ class LongCatImagePipeline(nn.Module, CFGParallelMixin):
|
||||
# return noise_pred
|
||||
```
|
||||
|
||||
|
||||
### Override `combine_cfg_noise()` for Multi-Output Models
|
||||
|
||||
When `predict_noise()` returns a tuple (e.g., video + audio), the default `combine_cfg_noise()` applies CFG to every element. Override it to apply different logic per element — for example, CFG on video but positive-only on audio:
|
||||
|
||||
```python
|
||||
class MyVideoAudioPipeline(nn.Module, CFGParallelMixin):
|
||||
def combine_cfg_noise(self, positive_noise_pred, negative_noise_pred, scale, normalize):
|
||||
(video_pos, audio_pos) = positive_noise_pred
|
||||
(video_neg, audio_neg) = negative_noise_pred
|
||||
video_combined = super().combine_cfg_noise(video_pos, video_neg, scale, normalize)
|
||||
return (video_combined, audio_pos) # audio: positive only, no CFG
|
||||
```
|
||||
|
||||
This also requires `predict_noise()` to return a tuple (see [Override predict_noise](#override-predict_noise-for-custom-transformer-calls) above).
|
||||
|
||||
### Implement a Composite Scheduler for Multi-Output Models
|
||||
|
||||
When each output has its own denoising schedule, implement a composite scheduler that dispatches to per-output schedulers. Assign it to `self.scheduler` so the default `scheduler_step()` works without override.
|
||||
|
||||
**Complete example (video + audio with separate schedulers and diffuse loop):**
|
||||
|
||||
```python
|
||||
class VideoAudioScheduler:
|
||||
"""Composite scheduler dispatching to video and audio schedulers."""
|
||||
def __init__(self, video_scheduler, audio_scheduler):
|
||||
self.video_scheduler = video_scheduler
|
||||
self.audio_scheduler = audio_scheduler
|
||||
|
||||
def step(self, noise_pred, t, latents, return_dict=False, generator=None):
|
||||
video_out = self.video_scheduler.step(noise_pred[0], t[0], latents[0], return_dict=False, generator=generator)[0]
|
||||
audio_out = self.audio_scheduler.step(noise_pred[1], t[1], latents[1], return_dict=False, generator=generator)[0]
|
||||
return ((video_out, audio_out),)
|
||||
|
||||
class MyVideoAudioPipeline(nn.Module, CFGParallelMixin):
|
||||
def __init__(self, ...):
|
||||
self.scheduler = VideoAudioScheduler(video_sched, audio_sched)
|
||||
|
||||
def predict_noise(self, **kwargs):
|
||||
video_pred, audio_pred = self.transformer(**kwargs)
|
||||
return (video_pred, audio_pred)
|
||||
|
||||
def combine_cfg_noise(self, positive_noise_pred, negative_noise_pred, scale, normalize):
|
||||
# ... (as above)
|
||||
|
||||
def diffuse(self, video_latents, audio_latents, timesteps_video, timesteps_audio, ...):
|
||||
for t_v, t_a in zip(timesteps_video, timesteps_audio):
|
||||
positive_kwargs = {...}
|
||||
negative_kwargs = {...} if do_true_cfg else None
|
||||
|
||||
video_pred, audio_pred = self.predict_noise_maybe_with_cfg(
|
||||
do_true_cfg=do_true_cfg, true_cfg_scale=self.guidance_scale,
|
||||
positive_kwargs=positive_kwargs, negative_kwargs=negative_kwargs,
|
||||
)
|
||||
video_latents, audio_latents = self.scheduler_step_maybe_with_cfg(
|
||||
(video_pred, audio_pred), (t_v, t_a),
|
||||
(video_latents, audio_latents), do_true_cfg=do_true_cfg,
|
||||
generator=generator,
|
||||
)
|
||||
return video_latents, audio_latents
|
||||
```
|
||||
|
||||
> **Note:** If you use a non-deterministic scheduler, e.g., DDPM, please set `self.scheduler_step_maybe_with_cfg(..., generator=torch.Generator(device).manual_seed(seed))` explicitly to control the randomness of scheduler step among ranks.
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
@@ -369,7 +369,7 @@ images = omni.generate(
|
||||
2. **Compare performance** - Measure speedup vs baseline (expect 1.5x-2.0x)
|
||||
3. **Verify output quality** - Visually compare cached vs uncached outputs (should be nearly identical)
|
||||
|
||||
See more detailed examples in [user guide for teacache](../../user_guide/diffusion/cache_acceleration/teacache.md).
|
||||
See more detailed examples in [user guide for teacache](../../user_guide/diffusion/teacache.md).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,285 +0,0 @@
|
||||
# Cache-DiT Guide
|
||||
|
||||
|
||||
## Table of Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Quick Start](#quick-start)
|
||||
- [Example Script](#example-script)
|
||||
- [Acceleration Methods](#acceleration-methods)
|
||||
- [Configuration Parameters](#configuration-parameters)
|
||||
- [Best Practices](#best-practices)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Summary](#summary)
|
||||
- [Additional Resources](#additional-resources)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Cache-DiT accelerates diffusion transformer models through intelligent caching mechanisms, providing significant speedup with minimal quality loss. It supports multiple acceleration techniques that can be combined for optimal performance:
|
||||
|
||||
- **DBCache**: Dual Block Cache for reducing redundant computations
|
||||
- **TaylorSeer**: Taylor expansion-based forecasting for faster inference
|
||||
- **SCM**: Step Computation Masking for selective step computation
|
||||
|
||||
See supported models list in [Supported Models](../../diffusion_features.md#supported-models).
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Basic Usage
|
||||
|
||||
Enable cache-dit acceleration by simply setting `cache_backend="cache_dit"`:
|
||||
|
||||
```python
|
||||
from vllm_omni import Omni
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
|
||||
omni = Omni(
|
||||
model="Qwen/Qwen-Image",
|
||||
cache_backend="cache_dit", # Enable Cache-DiT with defaults
|
||||
)
|
||||
|
||||
outputs = omni.generate(
|
||||
"a beautiful landscape",
|
||||
OmniDiffusionSamplingParams(num_inference_steps=50),
|
||||
)
|
||||
```
|
||||
|
||||
**Note**: When `cache_config` is not provided, Cache-DiT uses optimized default values. See the [Configuration Parameters](#configuration-parameters) section for details.
|
||||
|
||||
### Custom Configuration
|
||||
|
||||
To customize cache-dit settings, provide a `cache_config` dictionary, for example:
|
||||
|
||||
```python
|
||||
omni = Omni(
|
||||
model="Qwen/Qwen-Image",
|
||||
cache_backend="cache_dit",
|
||||
cache_config={
|
||||
"Fn_compute_blocks": 1,
|
||||
"Bn_compute_blocks": 0,
|
||||
"max_warmup_steps": 4,
|
||||
"residual_diff_threshold": 0.12,
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example Script
|
||||
|
||||
### Offline Inference
|
||||
|
||||
Use the example script under `examples/offline_inference/text_to_image`:
|
||||
|
||||
```bash
|
||||
cd examples/offline_inference/text_to_image
|
||||
python text_to_image.py \
|
||||
--model Qwen/Qwen-Image \
|
||||
--prompt "a cup of coffee on the table" \
|
||||
--cache-backend cache_dit \
|
||||
--num-inference-steps 50
|
||||
```
|
||||
|
||||
See the [text_to_image.py](https://github.com/vllm-project/vllm-omni/blob/main/examples/offline_inference/text_to_image/text_to_image.py) for detailed configuration options.
|
||||
|
||||
The script uses cache-dit acceleration with a hybrid configuration combining DBCache, SCM, and TaylorSeer:
|
||||
|
||||
```python
|
||||
omni = Omni(
|
||||
model="Qwen/Qwen-Image",
|
||||
cache_backend="cache_dit",
|
||||
cache_config={
|
||||
# Scheme: Hybrid DBCache + SCM + TaylorSeer
|
||||
"Fn_compute_blocks": 1, # Optimized for single-transformer models
|
||||
"Bn_compute_blocks": 0, # Number of backward compute blocks
|
||||
"max_warmup_steps": 4, # Maximum warmup steps (works for few-step models)
|
||||
"residual_diff_threshold": 0.24, # Higher threshold for more aggressive caching
|
||||
"max_continuous_cached_steps": 3, # Limit to prevent precision degradation
|
||||
# TaylorSeer parameters [cache-dit only]
|
||||
"enable_taylorseer": False, # Disabled by default (not suitable for few-step models)
|
||||
"taylorseer_order": 1, # TaylorSeer polynomial order
|
||||
# SCM (Step Computation Masking) parameters [cache-dit only]
|
||||
"scm_steps_mask_policy": None, # SCM mask policy: None (disabled), "slow", "medium", "fast", "ultra"
|
||||
"scm_steps_policy": "dynamic", # SCM steps policy: "dynamic" or "static"
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
You can customize the configuration by modifying the `cache_config` dictionary to use only specific methods (e.g., DBCache only, DBCache + SCM, etc.) based on your quality and speed requirements.
|
||||
|
||||
For image-to-image tasks, use the example script under `examples/offline_inference/image_to_image`:
|
||||
|
||||
```bash
|
||||
cd examples/offline_inference/image_to_image
|
||||
python image_edit.py \
|
||||
--model Qwen/Qwen-Image-Edit \
|
||||
--prompt "make the sky more colorful" \
|
||||
--image path/to/input/image.jpg \
|
||||
--cache-backend cache_dit \
|
||||
--num-inference-steps 50 \
|
||||
--cache-dit-max-continuous-cached-steps 3 \
|
||||
--cache-dit-residual-diff-threshold 0.24 \
|
||||
--cache-dit-enable-taylorseer
|
||||
```
|
||||
|
||||
See the [image_edit.py](https://github.com/vllm-project/vllm-omni/blob/main/examples/offline_inference/image_to_image/image_edit.py) for detailed configuration options.
|
||||
|
||||
### Online Serving
|
||||
|
||||
```bash
|
||||
# Default configuration (recommended)
|
||||
vllm serve Qwen/Qwen-Image --omni --port 8091 --cache-backend cache_dit
|
||||
|
||||
# Custom configuration
|
||||
vllm serve Qwen/Qwen-Image --omni --port 8091 \
|
||||
--cache-backend cache_dit \
|
||||
--cache-config '{"Fn_compute_blocks": 1, "residual_diff_threshold": 0.12}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Acceleration Methods
|
||||
|
||||
For comprehensive illustration, please view Cache-DiT [User Guide](https://cache-dit.readthedocs.io/en/latest/user_guide/OVERVIEWS/).
|
||||
|
||||
### 1. DBCache (Dual Block Cache)
|
||||
|
||||
DBCache intelligently caches intermediate transformer block outputs when the residual differences between consecutive steps are small, reducing redundant computations without sacrificing quality.
|
||||
|
||||
**Example Configuration**:
|
||||
|
||||
```python
|
||||
cache_config={
|
||||
"Fn_compute_blocks": 8, # Use first 8 blocks for difference computation
|
||||
"Bn_compute_blocks": 0, # No additional fusion blocks
|
||||
"max_warmup_steps": 8, # Cache after 8 warmup steps
|
||||
"residual_diff_threshold": 0.12, # Lower threshold for faster inference
|
||||
"max_cached_steps": -1, # No limit on cached steps
|
||||
}
|
||||
```
|
||||
|
||||
**Performance Tips**:
|
||||
|
||||
- Default `Fn_compute_blocks=1` works well for most cases. Some models (e.g., [FLUX.2-klein](https://github.com/wtomin/vllm-omni/blob/main/vllm_omni/diffusion/cache/cache_dit_backend.py#L363)) use a larger value for `Fn_compute_blocks` for a balanced performance.
|
||||
- Increase `residual_diff_threshold` (e.g., 0.12-0.15) for faster inference with slight quality trade-off, or decrease from default 0.24 for higher quality.
|
||||
- Default `max_warmup_steps=4` is optimized for few-step models. Increase to 6-8 for more steps if needed.
|
||||
|
||||
### 2. TaylorSeer
|
||||
|
||||
TaylorSeer uses Taylor expansion to forecast future hidden states, allowing the model to skip some computation steps while maintaining quality.
|
||||
|
||||
**Example Configuration**:
|
||||
|
||||
```python
|
||||
cache_config={
|
||||
"enable_taylorseer": True,
|
||||
"taylorseer_order": 1, # First-order Taylor expansion
|
||||
}
|
||||
```
|
||||
|
||||
**Performance Tips**:
|
||||
|
||||
- TaylorSeer is **not suitable for few-step distilled models**.
|
||||
- Use `taylorseer_order=1` for most cases (good balance of speed and quality).
|
||||
- Combine with DBCache for maximum acceleration.
|
||||
- Higher orders (2-3) may improve quality but reduce speed gains.
|
||||
|
||||
### 3. SCM (Step Computation Masking)
|
||||
|
||||
SCM allows you to specify which steps must be computed and which can use cached results, similar to LeMiCa/EasyCache style acceleration.
|
||||
|
||||
`scm_steps_mask_policy` options (number of compute steps out of 28):
|
||||
|
||||
| Policy | Compute Steps | Speed | Quality |
|
||||
|--------|--------------|-------|---------|
|
||||
| `None` (default) | All | Baseline | Best |
|
||||
| `"slow"` | 18 / 28 | Moderate | High |
|
||||
| `"medium"` | 15 / 28 | Balanced | Good |
|
||||
| `"fast"` | 11 / 28 | Fast | Moderate |
|
||||
| `"ultra"` | 8 / 28 | Fastest | Lower |
|
||||
|
||||
**Example Configuration**:
|
||||
|
||||
```python
|
||||
cache_config={
|
||||
"scm_steps_mask_policy": "medium", # Balanced speed/quality
|
||||
"scm_steps_policy": "dynamic", # Use dynamic cache
|
||||
}
|
||||
```
|
||||
|
||||
**Performance Tips**:
|
||||
|
||||
- SCM is disabled by default. Enable it by setting a policy value if you need additional acceleration.
|
||||
- Start with `"medium"` policy and adjust based on quality requirements.
|
||||
- Use `"fast"` or `"ultra"` for maximum speed when quality can be slightly compromised.
|
||||
- `"dynamic"` policy generally provides better quality than `"static"`.
|
||||
- SCM mask is automatically regenerated when `num_inference_steps` changes during inference.
|
||||
|
||||
---
|
||||
|
||||
## Configuration Parameters
|
||||
|
||||
In `cache_config` passed to `Omni` constructor, it accepts the arguments of `DBCacheConfig` ([Cache-DiT API Reference](https://cache-dit.readthedocs.io/en/latest/user_guide/CACHE_API/)). Key parameters are listed below:
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `Fn_compute_blocks` | int | 1 | First n blocks for difference computation (optimized for single-transformer models) |
|
||||
| `Bn_compute_blocks` | int | 0 | Last n blocks for fusion |
|
||||
| `max_warmup_steps` | int | 4 | Steps before caching starts (optimized for few-step distilled models) |
|
||||
| `max_cached_steps` | int | -1 | Max cached steps (-1 = unlimited) |
|
||||
| `max_continuous_cached_steps` | int | 3 | Max consecutive cached steps (prevents precision degradation) |
|
||||
| `residual_diff_threshold` | float | 0.24 | Residual difference threshold (higher for more aggressive caching) |
|
||||
| `num_inference_steps` | int \| None | None | Initial inference steps for SCM mask generation (optional, auto-refreshed during inference) |
|
||||
| `enable_taylorseer` | bool | False | Enable TaylorSeer acceleration (not suitable for few-step distilled models) |
|
||||
| `taylorseer_order` | int | 1 | Taylor expansion order |
|
||||
| `scm_steps_mask_policy` | str \| None | None | SCM mask policy (None, "slow", "medium", "fast", "ultra") |
|
||||
| `scm_steps_policy` | str | "dynamic" | SCM computation policy ("dynamic" or "static") |
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### When to Use
|
||||
|
||||
**Good for:**
|
||||
|
||||
- Production deployments requiring fast inference
|
||||
- Diffusion transformer models (DiT architecture)
|
||||
- Scenarios where 1.5x-3x speedup is valuable
|
||||
|
||||
**Not for:**
|
||||
|
||||
- Non-DiT architectures (use model-specific acceleration instead)
|
||||
- Models already using few-step distillation (< 10 steps)
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issue 1: Quality Degradation
|
||||
|
||||
**Symptoms**: Generated images have visible artifacts or lower quality
|
||||
|
||||
**Solution**:
|
||||
```python
|
||||
# Reduce aggressiveness - use more conservative settings
|
||||
cache_config={
|
||||
"residual_diff_threshold": 0.20, # Lower threshold (closer to default 0.24)
|
||||
"Fn_compute_blocks": 8, # Use more blocks for better decisions
|
||||
"max_warmup_steps": 6, # Longer warmup
|
||||
"scm_steps_mask_policy": "slow", # More compute steps
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Using Cache-DiT acceleration:
|
||||
|
||||
1. ✅ **Enable Cache-DiT** - Set `cache_backend="cache_dit"` to get 1.5x-3x speedup with optimized defaults
|
||||
2. ✅ **(Optional) Customize** - Adjust `cache_config` parameters for specific speed/quality trade-offs
|
||||
@@ -1,194 +0,0 @@
|
||||
# TeaCache Guide
|
||||
|
||||
|
||||
## Table of Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Quick Start](#quick-start)
|
||||
- [Example Script](#example-script)
|
||||
- [Configuration Parameters](#configuration-parameters)
|
||||
- [Best Practices](#best-practices)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Summary](#summary)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
TeaCache accelerates diffusion model inference by caching transformer computations when consecutive timesteps are similar, providing **1.5x-2.0x speedup** with minimal quality loss. It dynamically decides whether to reuse cached outputs based on input similarity, making it ideal for production deployments where inference speed matters without sacrificing generation quality.
|
||||
|
||||
See supported models list in [Supported Models](../../diffusion_features.md#supported-models).
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
|
||||
|
||||
### Basic Usage
|
||||
|
||||
|
||||
```python
|
||||
from vllm_omni import Omni
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
|
||||
omni = Omni(
|
||||
model="Qwen/Qwen-Image",
|
||||
cache_backend="tea_cache",
|
||||
)
|
||||
|
||||
outputs = omni.generate(
|
||||
"A cat sitting on a windowsill",
|
||||
OmniDiffusionSamplingParams(num_inference_steps=50),
|
||||
)
|
||||
```
|
||||
|
||||
### Custom Configuration
|
||||
|
||||
```python
|
||||
omni = Omni(
|
||||
model="Qwen/Qwen-Image",
|
||||
cache_backend="tea_cache",
|
||||
cache_config={
|
||||
"rel_l1_thresh": 0.2, # Controls speed/quality tradeoff
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
### Using Environment Variable
|
||||
|
||||
You can also enable TeaCache via environment variable:
|
||||
|
||||
```bash
|
||||
export DIFFUSION_CACHE_BACKEND=tea_cache
|
||||
```
|
||||
|
||||
Then initialize without explicitly setting `cache_backend`:
|
||||
|
||||
```python
|
||||
from vllm_omni import Omni
|
||||
|
||||
omni = Omni(
|
||||
model="Qwen/Qwen-Image",
|
||||
cache_config={"rel_l1_thresh": 0.2}
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example Script
|
||||
|
||||
### Offline Inference
|
||||
|
||||
Use python script under `examples/offline_inference/text_to_image/` or `examples/offline_inference/image_to_image/` with CLI:
|
||||
|
||||
```bash
|
||||
# Text-to-image example
|
||||
python examples/offline_inference/text_to_image/text_to_image.py \
|
||||
--model Qwen/Qwen-Image \
|
||||
--cache-backend tea_cache
|
||||
|
||||
# Image-to-image example
|
||||
python examples/offline_inference/image_to_image/image_edit.py \
|
||||
--model Qwen/Qwen-Image-Edit \
|
||||
--image input.png \
|
||||
--prompt "Edit description" \
|
||||
--cache-backend tea_cache \
|
||||
--tea-cache-rel-l1-thresh 0.25
|
||||
```
|
||||
|
||||
See the [text_to_image.py](https://github.com/vllm-project/vllm-omni/blob/main/examples/offline_inference/text_to_image/text_to_image.py) or [image_edit.py](https://github.com/vllm-project/vllm-omni/blob/main/examples/offline_inference/image_to_image/image_edit.py) for detailed configuration options.
|
||||
|
||||
### Online Serving
|
||||
|
||||
```bash
|
||||
# Default configuration
|
||||
vllm serve Qwen/Qwen-Image --omni --port 8091 --cache-backend tea_cache
|
||||
|
||||
# Custom configuration
|
||||
vllm serve Qwen/Qwen-Image --omni --port 8091 \
|
||||
--cache-backend tea_cache \
|
||||
--cache-config '{"rel_l1_thresh": 0.2}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration Parameters
|
||||
|
||||
In `OmniDiffusionConfig`
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `rel_l1_thresh` | float | `0.2` | Similarity threshold for cache reuse. Lower values prioritize quality (less caching), higher values prioritize speed (more caching). Suggested range: 0.1-0.8 |
|
||||
| `coefficients` | list[float] \| None | `None` | Polynomial coefficients for rescaling L1 distance. Must contain exactly 5 elements if provided. If `None`, uses model-specific defaults based on transformer type. |
|
||||
|
||||
Users can find the default model coefficients in [`vllm_omni/diffusion/cache/teacache/config.py`](https://github.com/vllm-project/vllm-omni/blob/main/vllm_omni/diffusion/cache/teacache/config.py), for example:
|
||||
|
||||
```python
|
||||
_MODEL_COEFFICIENTS = {
|
||||
# Qwen-Image transformer coefficients from ComfyUI-TeaCache
|
||||
# Tuned specifically for Qwen's dual-stream transformer architecture
|
||||
# Used for all Qwen-Image Family pipelines, in general
|
||||
"QwenImageTransformer2DModel": [
|
||||
-4.50000000e02,
|
||||
2.80000000e02,
|
||||
-4.50000000e01,
|
||||
3.20000000e00,
|
||||
-2.00000000e-02,
|
||||
],
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### When to Use
|
||||
|
||||
**Good for:**
|
||||
|
||||
- Production deployments requiring faster inference, tolerant of minimal quality loss
|
||||
- Scenarios where 1.5-2x speedup is valuable
|
||||
- Useful for single-card acceleration
|
||||
|
||||
**Not for:**
|
||||
|
||||
- Maximum quality requirements where no degradation is acceptable
|
||||
- Very short inference runs (< 20 steps) where caching overhead may outweigh benefits
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issue 1: Quality Degradation
|
||||
|
||||
**Symptoms**: Generated images show artifacts, reduced detail, or inconsistent quality compared to non-cached results
|
||||
|
||||
**Solution**:
|
||||
|
||||
```python
|
||||
# Lower the threshold for more conservative caching
|
||||
cache_config={"rel_l1_thresh": 0.1}
|
||||
```
|
||||
|
||||
### Common Issue 2: Limited Speedup
|
||||
|
||||
**Symptoms**: Actual speedup is less than expected (< 1.3x)
|
||||
|
||||
**Solutions**:
|
||||
1. Increase the threshold to enable more aggressive caching:
|
||||
```python
|
||||
cache_config={"rel_l1_thresh": 0.8}
|
||||
```
|
||||
2. Ensure you're using sufficient inference steps (35+ recommended)
|
||||
3. Check that your model architecture is supported (see Supported Models section)
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Summary
|
||||
|
||||
1. ✅ **Enable TeaCache** - Set `cache_backend="tea_cache"` to get 1.5x-2.0x speedup with optimized defaults
|
||||
2. ✅ **(Optional) Customize** - Adjust thresholds and polynomial coefficients for specific speed/quality trade-offs
|
||||
@@ -0,0 +1,228 @@
|
||||
# Cache-DiT Acceleration Guide
|
||||
|
||||
This guide explains how to use cache-dit acceleration in vLLM-Omni to speed up diffusion model inference.
|
||||
|
||||
## Overview
|
||||
|
||||
Cache-dit is a library that accelerates diffusion transformer models through intelligent caching mechanisms. It supports multiple acceleration techniques that can be combined for optimal performance:
|
||||
|
||||
- **DBCache**: Dual Block Cache for reducing redundant computations
|
||||
- **TaylorSeer**: Taylor expansion-based forecasting for faster inference
|
||||
- **SCM**: Step Computation Masking for selective step computation
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Basic Usage
|
||||
|
||||
Enable cache-dit acceleration by simply setting `cache_backend="cache_dit"`. Cache-dit will use its recommended default parameters:
|
||||
|
||||
```python
|
||||
from vllm_omni.entrypoints.omni import Omni
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
|
||||
# Simplest way: just enable cache-dit with default parameters
|
||||
omni = Omni(
|
||||
model="Qwen/Qwen-Image",
|
||||
cache_backend="cache_dit",
|
||||
)
|
||||
|
||||
images = omni.generate(
|
||||
"a beautiful landscape",
|
||||
OmniDiffusionSamplingParams(num_inference_steps=50),
|
||||
)
|
||||
```
|
||||
|
||||
**Default Parameters**: When `cache_config` is not provided, cache-dit uses optimized default values. See the [Configuration Reference](#configuration-reference) section for a complete list of all parameters and their default values.
|
||||
|
||||
### Custom Configuration
|
||||
|
||||
To customize cache-dit settings, provide a `cache_config` dictionary, for example:
|
||||
|
||||
```python
|
||||
omni = Omni(
|
||||
model="Qwen/Qwen-Image",
|
||||
cache_backend="cache_dit",
|
||||
cache_config={
|
||||
"Fn_compute_blocks": 1,
|
||||
"Bn_compute_blocks": 0,
|
||||
"max_warmup_steps": 4,
|
||||
"residual_diff_threshold": 0.12,
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
## Online Serving (OpenAI-Compatible)
|
||||
|
||||
Enable Cache-DiT for online serving by passing `--cache-backend cache_dit` when starting the server:
|
||||
|
||||
```bash
|
||||
# Use Cache-DiT default (recommended) parameters
|
||||
vllm serve Qwen/Qwen-Image --omni --port 8091 --cache-backend cache_dit
|
||||
```
|
||||
|
||||
To customize Cache-DiT settings for online serving, pass a JSON string via `--cache-config`:
|
||||
|
||||
```bash
|
||||
vllm serve Qwen/Qwen-Image --omni --port 8091 \
|
||||
--cache-backend cache_dit \
|
||||
--cache-config '{"Fn_compute_blocks": 1, "Bn_compute_blocks": 0, "max_warmup_steps": 4, "residual_diff_threshold": 0.12}'
|
||||
```
|
||||
|
||||
## Acceleration Methods
|
||||
|
||||
For comprehensive illustration, please view cache-dit [User_Guide](https://cache-dit.readthedocs.io/en/latest/user_guide/OVERVIEWS/)
|
||||
|
||||
### 1. DBCache (Dual Block Cache)
|
||||
|
||||
DBCache intelligently caches intermediate transformer block outputs when the residual differences between consecutive steps are small, reducing redundant computations without sacrificing quality.
|
||||
|
||||
**Key Parameters**:
|
||||
|
||||
- `Fn_compute_blocks` (int, default: 1): Number of **first n** transformer blocks used to compute stable feature differences. Higher values provide more accurate caching decisions but increase computation.
|
||||
- `Bn_compute_blocks` (int, default: 0): Number of **last n** transformer blocks used for additional fusion. These blocks act as an auto-scaler for approximate hidden states.
|
||||
- `max_warmup_steps` (int, default: 4): Number of initial steps where caching is disabled to ensure the model learns sufficient features before caching begins. Optimized for few-step distilled models.
|
||||
- `residual_diff_threshold` (float, default: 0.24): Threshold for residual difference. Higher values lead to faster performance but may reduce precision. Default uses a relatively higher threshold for more aggressive caching.
|
||||
- `max_cached_steps` (int, default: -1): Maximum number of cached steps. Set to -1 for unlimited caching.
|
||||
- `max_continuous_cached_steps` (int, default: 3): Maximum number of consecutive cached steps. Limits consecutive caching to prevent precision degradation.
|
||||
|
||||
**Example Configuration**:
|
||||
|
||||
```python
|
||||
cache_config={
|
||||
"Fn_compute_blocks": 8, # Use first 8 blocks for difference computation
|
||||
"Bn_compute_blocks": 0, # No additional fusion blocks
|
||||
"max_warmup_steps": 8, # Cache after 8 warmup steps
|
||||
"residual_diff_threshold": 0.12, # Higher threshold for faster inference
|
||||
"max_cached_steps": -1, # No limit on cached steps
|
||||
}
|
||||
```
|
||||
|
||||
**Performance Tips**:
|
||||
|
||||
- Default `Fn_compute_blocks=1` works well for most cases. Increase to 8-12 for larger models or when more accuracy is needed
|
||||
- Increase `residual_diff_threshold` (e.g., 0.12-0.15) for faster inference with slight quality trade-off, or decrease from default 0.24 for higher quality
|
||||
- Default `max_warmup_steps=4` is optimized for few-step models. Increase to 6-8 for more steps if needed
|
||||
|
||||
### 2. TaylorSeer
|
||||
|
||||
TaylorSeer uses Taylor expansion to forecast future hidden states, allowing the model to skip some computation steps while maintaining quality.
|
||||
|
||||
**Key Parameters**:
|
||||
|
||||
- `enable_taylorseer` (bool, default: False): Enable TaylorSeer acceleration
|
||||
- `taylorseer_order` (int, default: 1): Order of Taylor expansion. Higher orders provide better accuracy but require more computation.
|
||||
|
||||
**Example Configuration**:
|
||||
|
||||
```python
|
||||
cache_config={
|
||||
"enable_taylorseer": True,
|
||||
"taylorseer_order": 1, # First-order Taylor expansion
|
||||
}
|
||||
```
|
||||
|
||||
**Performance Tips**:
|
||||
|
||||
- Use `taylorseer_order=1` for most cases (good balance of speed and quality)
|
||||
- Combine with DBCache for maximum acceleration
|
||||
- Higher orders (2-3) may improve quality but reduce speed gains
|
||||
|
||||
### 3. SCM (Step Computation Masking)
|
||||
|
||||
SCM allows you to specify which steps must be computed and which can use cached results, similar to LeMiCa/EasyCache style acceleration.
|
||||
|
||||
**Key Parameters**:
|
||||
|
||||
- `scm_steps_mask_policy` (str | None, default: None): Predefined mask policy. Options:
|
||||
- `None`: SCM disabled (default)
|
||||
- `"slow"`: More compute steps, higher quality (18 compute steps out of 28)
|
||||
- `"medium"`: Balanced (15 compute steps out of 28)
|
||||
- `"fast"`: More cache steps, faster inference (11 compute steps out of 28)
|
||||
- `"ultra"`: Maximum speed (8 compute steps out of 28)
|
||||
- `scm_steps_policy` (str, default: "dynamic"): Policy for cached steps:
|
||||
- `"dynamic"`: Use dynamic cache for masked steps (recommended)
|
||||
- `"static"`: Use static cache for masked steps
|
||||
|
||||
**Example Configuration**:
|
||||
|
||||
```python
|
||||
cache_config={
|
||||
"scm_steps_mask_policy": "medium", # Balanced speed/quality
|
||||
"scm_steps_policy": "dynamic", # Use dynamic cache
|
||||
}
|
||||
```
|
||||
|
||||
**Performance Tips**:
|
||||
|
||||
- SCM is disabled by default (`scm_steps_mask_policy=None`). Enable it by setting a policy value if you need additional acceleration
|
||||
- Start with `"medium"` policy and adjust based on quality requirements
|
||||
- Use `"fast"` or `"ultra"` for maximum speed when quality can be slightly compromised
|
||||
- `"dynamic"` policy generally provides better quality than `"static"`
|
||||
- SCM mask is automatically regenerated when `num_inference_steps` changes during inference
|
||||
|
||||
## Configuration Reference
|
||||
|
||||
### DiffusionCacheConfig Parameters
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `Fn_compute_blocks` | int | 1 | First n blocks for difference computation (optimized for single-transformer models) |
|
||||
| `Bn_compute_blocks` | int | 0 | Last n blocks for fusion |
|
||||
| `max_warmup_steps` | int | 4 | Steps before caching starts (optimized for few-step distilled models) |
|
||||
| `max_cached_steps` | int | -1 | Max cached steps (-1 = unlimited) |
|
||||
| `max_continuous_cached_steps` | int | 3 | Max consecutive cached steps (prevents precision degradation) |
|
||||
| `residual_diff_threshold` | float | 0.24 | Residual difference threshold (higher for more aggressive caching) |
|
||||
| `num_inference_steps` | int \| None | None | Initial inference steps for SCM mask generation (optional, auto-refreshed during inference) |
|
||||
| `enable_taylorseer` | bool | False | Enable TaylorSeer acceleration (not suitable for few-step distilled models) |
|
||||
| `taylorseer_order` | int | 1 | Taylor expansion order |
|
||||
| `scm_steps_mask_policy` | str \| None | None | SCM mask policy (None, "slow", "medium", "fast", "ultra") |
|
||||
| `scm_steps_policy` | str | "dynamic" | SCM computation policy ("dynamic" or "static") |
|
||||
|
||||
## Example: Accelerate Text-to-Image Generation with CacheDiT
|
||||
|
||||
See `examples/offline_inference/text_to_image/text_to_image.py` for a complete working example with cache-dit acceleration.
|
||||
|
||||
```bash
|
||||
# Enable cache-dit with hybrid acceleration
|
||||
cd examples/offline_inference/text_to_image
|
||||
python text_to_image.py \
|
||||
--model Qwen/Qwen-Image \
|
||||
--prompt "a cup of coffee on the table" \
|
||||
--cache-backend cache_dit \
|
||||
--num-inference-steps 50
|
||||
```
|
||||
|
||||
|
||||
The script uses cache-dit acceleration with a hybrid configuration combining DBCache, SCM, and TaylorSeer:
|
||||
|
||||
```python
|
||||
omni = Omni(
|
||||
model="Qwen/Qwen-Image",
|
||||
cache_backend="cache_dit",
|
||||
cache_config={
|
||||
# Scheme: Hybrid DBCache + SCM + TaylorSeer
|
||||
# DBCache
|
||||
"Fn_compute_blocks": 8,
|
||||
"Bn_compute_blocks": 0,
|
||||
"max_warmup_steps": 4,
|
||||
"residual_diff_threshold": 0.12,
|
||||
# TaylorSeer
|
||||
"enable_taylorseer": True,
|
||||
"taylorseer_order": 1,
|
||||
# SCM
|
||||
"scm_steps_mask_policy": "fast", # Set to None to disable SCM
|
||||
"scm_steps_policy": "dynamic",
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
You can customize the configuration by modifying the `cache_config` dictionary to use only specific methods (e.g., DBCache only, DBCache + SCM, etc.) based on your quality and speed requirements.
|
||||
|
||||
To test another model, you can modify `--model` with the target model identifier like `Tongyi-MAI/Z-Image-Turbo` and update `cache_config` according the model architecture (e.g., number of transformer blocks).
|
||||
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Cache-DiT User Guide](https://cache-dit.readthedocs.io/en/latest/user_guide/OVERVIEWS/)
|
||||
- [Cache-DiT Benchmark](https://cache-dit.readthedocs.io/en/latest/benchmark/HYBRID_CACHE/)
|
||||
- [DBCache Technical Details](https://cache-dit.readthedocs.io/en/latest/user_guide/CACHE_API/)
|
||||
@@ -1,169 +0,0 @@
|
||||
# CFG-Parallel Guide
|
||||
|
||||
|
||||
## Table of Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Quick Start](#quick-start)
|
||||
- [Example Script](#example-script)
|
||||
- [Configuration Parameters](#configuration-parameters)
|
||||
- [Best Practices](#best-practices)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Summary](#summary)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
CFG-Parallel accelerates diffusion models by distributing positive and negative classifier-free guidance (CFG) passes across different GPUs, providing ~1.8x speedup when CFG is enabled. It's ideal for image editing tasks that require guidance scales greater than 1.0.
|
||||
|
||||
See supported models list in [Supported Models](../../diffusion_features.md#supported-models).
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Basic Usage
|
||||
|
||||
Simplest working example:
|
||||
|
||||
```python
|
||||
from vllm_omni import Omni
|
||||
from vllm_omni.diffusion.data import DiffusionParallelConfig
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
from PIL import Image
|
||||
|
||||
omni = Omni(
|
||||
model="Qwen/Qwen-Image-Edit",
|
||||
parallel_config=DiffusionParallelConfig(cfg_parallel_size=2), # Enable CFG-Parallel
|
||||
)
|
||||
|
||||
input_image = Image.open("input.png").convert("RGB")
|
||||
outputs = omni.generate(
|
||||
{
|
||||
"prompt": "turn this cat to a dog",
|
||||
"negative_prompt": "low quality, blurry",
|
||||
"multi_modal_data": {"image": input_image},
|
||||
},
|
||||
OmniDiffusionSamplingParams(
|
||||
true_cfg_scale=4.0,
|
||||
num_inference_steps=50,
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example Script
|
||||
|
||||
### Offline Inference
|
||||
|
||||
Use python script under `examples/offline_inference/image_to_image/image_edit.py`:
|
||||
|
||||
```bash
|
||||
cd examples/offline_inference/image_to_image/
|
||||
python image_edit.py \
|
||||
--model "Qwen/Qwen-Image-Edit" \
|
||||
--image "input.png" \
|
||||
--prompt "turn this cat to a dog" \
|
||||
--negative-prompt "low quality, blurry" \
|
||||
--cfg-scale 4.0 \
|
||||
--output "edited_image.png" \
|
||||
--cfg-parallel-size 2
|
||||
```
|
||||
|
||||
### Online Serving
|
||||
|
||||
Enable CFG-Parallel in online serving:
|
||||
|
||||
```bash
|
||||
# Default configuration
|
||||
vllm serve Qwen/Qwen-Image-Edit --omni --port 8091 --cfg-parallel-size 2
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration Parameters
|
||||
|
||||
In `DiffusionParallelConfig`
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `cfg_parallel_size` | int | 1 | Number of GPUs for CFG parallelism. Set to 2 to enable CFG-Parallel (rank 0 for positive, rank 1 for negative branch) |
|
||||
|
||||
|
||||
!!! info
|
||||
Most models support `cfg_parallel_size=2` (positive branch on rank 0, negative branch on rank 1). **Bagel** is an exception: it supports `cfg_parallel_size=3`, which adds a third branch on rank 2 for full three-way CFG parallelism.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### When to Use
|
||||
|
||||
**Good for:**
|
||||
|
||||
- Tasks requiring classifier-free guidance
|
||||
- Multi-GPU setups (at least 2 GPUs available)
|
||||
- Combining with other parallelism methods (sequence/tensor parallel)
|
||||
|
||||
**Not for:**
|
||||
|
||||
- Single GPU setups
|
||||
- Models that don't support CFG-Parallel (check [supported models](../../diffusion_features.md#supported-models))
|
||||
- Workloads without negative prompts or classifier-free guidance
|
||||
- Very short inference runs (< 10 steps) where parallelism overhead may outweigh benefits
|
||||
|
||||
### Expected Performance
|
||||
|
||||
| Configuration | Speedup | Quality | Use Case |
|
||||
|--------------|---------|---------|----------|
|
||||
| CFG-Parallel (2 GPUs) | 1.5~1.8x | No degradation | Large model, VRAM limited |
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issue 1: No Speedup with CFG-Parallel
|
||||
|
||||
**Symptoms**: CFG-Parallel enabled but no performance improvement
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. **Ensure CFG scale is set correctly:**
|
||||
```python
|
||||
# Bad: No CFG effect
|
||||
sampling_params = OmniDiffusionSamplingParams(num_inference_steps=50)
|
||||
|
||||
# Good: CFG-Parallel will work
|
||||
sampling_params = OmniDiffusionSamplingParams(
|
||||
num_inference_steps=50,
|
||||
true_cfg_scale=4.0 # Must be > 1.0
|
||||
)
|
||||
```
|
||||
|
||||
2. **Add negative prompt:**
|
||||
```python
|
||||
outputs = omni.generate(
|
||||
{
|
||||
"prompt": "beautiful landscape",
|
||||
"negative_prompt": "low quality, blurry", # Required for best results
|
||||
"multi_modal_data": {"image": input_image}
|
||||
},
|
||||
sampling_params
|
||||
)
|
||||
```
|
||||
|
||||
3. **Check model support:**
|
||||
- Verify your model in [supported models](../../diffusion_features.md#supported-models)
|
||||
- Some models don't support CFG-Parallel
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
1. ✅ **Enable CFG-Parallel** - Set `cfg_parallel_size=2` in `DiffusionParallelConfig` to get speedup when using CFG
|
||||
2. ✅ **Set CFG Scale** - Ensure `true_cfg_scale > 1.0` in `OmniDiffusionSamplingParams` for CFG-Parallel to take effect
|
||||
3. ✅ **Check Model Support** - Verify your model supports CFG-Parallel in [supported models](../../diffusion_features.md#supported-models)
|
||||
@@ -1,87 +0,0 @@
|
||||
# Expert Parallelism Guide
|
||||
|
||||
|
||||
## Table of Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Quick Start](#quick-start)
|
||||
- [Configuration Parameters](#configuration-parameters)
|
||||
- [Best Practices](#best-practices)
|
||||
- [Summary](#summary)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Unlike Tensor Parallelism which shards every layer's weights, Expert Parallelism (EP) only shards the MoE expert MLP blocks. This significantly reduces the memory footprint of MoE models (e.g., HunyuanImage3.0) while maintaining constant dense-equivalent compute efficiency.
|
||||
|
||||
During the forward pass, a gating mechanism routes tokens to their designated experts, requiring all-to-all communication to dispatch tokens to the correct ranks and combine results.
|
||||
|
||||
See supported models list in [Supported Models](../../diffusion_features.md#supported-models).
|
||||
|
||||
!!! note "EP Size Constraint"
|
||||
The effective EP size equals `tp × sp × cfg × dp`. At least one of TP/SP/CFG/DP must be set when EP is enabled.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```python
|
||||
from vllm_omni import Omni
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
from vllm_omni.diffusion.data import DiffusionParallelConfig
|
||||
|
||||
omni = Omni(
|
||||
model="tencent/HunyuanImage-3.0",
|
||||
parallel_config=DiffusionParallelConfig(
|
||||
tensor_parallel_size=8,
|
||||
enable_expert_parallel=True,
|
||||
),
|
||||
)
|
||||
|
||||
outputs = omni.generate(
|
||||
"A brown and white dog is running on the grass",
|
||||
OmniDiffusionSamplingParams(
|
||||
num_inference_steps=50,
|
||||
width=1024,
|
||||
height=1024,
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration Parameters
|
||||
|
||||
In `DiffusionParallelConfig`:
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `enable_expert_parallel` | bool | False | Enable Expert Parallelism for MoE models |
|
||||
|
||||
EP size is derived automatically as `tp × sp × cfg × dp` — configure at least one of those to set the EP degree.
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### When to Use
|
||||
|
||||
**Good for:**
|
||||
|
||||
- MoE models (e.g., HunyuanImage3.0) with numbers of experts
|
||||
- Memory-constrained multi-GPU setups where only expert blocks need sharding
|
||||
|
||||
**Not for:**
|
||||
|
||||
- Dense models (no MoE layers) — EP has no effect
|
||||
- Single GPU setups
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
1. ✅ **Enable EP** - Set `enable_expert_parallel=True` in `DiffusionParallelConfig` for MoE models
|
||||
2. ✅ **Set parallelism degree** - At least one of `tensor_parallel_size` / `ulysses_degree` / `cfg_parallel_size` must be > 1 to define the EP size
|
||||
@@ -1,149 +0,0 @@
|
||||
# HSDP Guide
|
||||
|
||||
|
||||
## Table of Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Quick Start](#quick-start)
|
||||
- [Example Script](#example-script)
|
||||
- [Configuration Parameters](#configuration-parameters)
|
||||
- [Best Practices](#best-practices)
|
||||
- [Summary](#summary)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
HSDP (Hybrid Sharded Data Parallel) shards model weights across GPUs to reduce per-GPU memory usage. This enables inference of large models (e.g., Wan2.2 14B) on GPUs with limited memory.
|
||||
|
||||
Unlike Tensor Parallelism which splits computation, HSDP uses PyTorch's FSDP2 to shard and redistribute weights at runtime. Each GPU only holds a fraction of the model weights, and weights are gathered on-demand during forward passes.
|
||||
|
||||
See supported models list in [Supported Models](../../diffusion_features.md#supported-models).
|
||||
|
||||
**Operating Modes:**
|
||||
|
||||
- **Standalone Mode**: HSDP alone without other parallelism. Must specify `hsdp_shard_size` explicitly.
|
||||
- **Combined Mode**: HSDP overlays on top of other parallelism (Ulysses-SP, CFG-Parallel). HSDP dimensions must match world_size.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Basic Usage
|
||||
|
||||
Simplest working example (standalone HSDP, shard across 4 GPUs):
|
||||
|
||||
```python
|
||||
from vllm_omni import Omni
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
from vllm_omni.diffusion.data import DiffusionParallelConfig
|
||||
|
||||
omni = Omni(
|
||||
model="Wan-AI/Wan2.2-T2V-A14B-Diffusers",
|
||||
parallel_config=DiffusionParallelConfig(
|
||||
use_hsdp=True,
|
||||
hsdp_shard_size=4, # Shard across 4 GPUs
|
||||
),
|
||||
)
|
||||
|
||||
outputs = omni.generate(
|
||||
"A cat playing piano",
|
||||
OmniDiffusionSamplingParams(num_inference_steps=50),
|
||||
)
|
||||
```
|
||||
|
||||
### Combined with Sequence Parallel
|
||||
|
||||
```python
|
||||
omni = Omni(
|
||||
model="Wan-AI/Wan2.2-T2V-A14B-Diffusers",
|
||||
parallel_config=DiffusionParallelConfig(
|
||||
ulysses_degree=4, # Sequence parallel
|
||||
use_hsdp=True, # HSDP overlays on SP
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example Script
|
||||
|
||||
### Offline Inference
|
||||
|
||||
Use Python script under `examples/offline_inference/image_to_video/`:
|
||||
|
||||
```bash
|
||||
# Standalone HSDP: shard across 4 GPUs
|
||||
python examples/offline_inference/image_to_video/image_to_video.py \
|
||||
--model Wan-AI/Wan2.2-T2V-A14B-Diffusers \
|
||||
--use-hsdp \
|
||||
--hsdp-shard-size 4
|
||||
|
||||
# Combined HSDP + Sequence Parallel
|
||||
python examples/offline_inference/image_to_video/image_to_video.py \
|
||||
--model Wan-AI/Wan2.2-T2V-A14B-Diffusers \
|
||||
--ulysses-degree 4 \
|
||||
--use-hsdp
|
||||
```
|
||||
|
||||
### Online Serving
|
||||
|
||||
**Standalone HSDP** (shard model across 4 GPUs):
|
||||
|
||||
```bash
|
||||
vllm serve Wan-AI/Wan2.2-T2V-A14B-Diffusers --omni --port 8091 \
|
||||
--use-hsdp --hsdp-shard-size 4
|
||||
```
|
||||
|
||||
**Combined with Sequence Parallel**:
|
||||
|
||||
```bash
|
||||
vllm serve Wan-AI/Wan2.2-T2V-A14B-Diffusers --omni --port 8091 \
|
||||
--use-hsdp --usp 4
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration Parameters
|
||||
|
||||
In `DiffusionParallelConfig`:
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `use_hsdp` | bool | False | Enable HSDP |
|
||||
| `hsdp_shard_size` | int | -1 | Number of GPUs to shard weights across. `-1` = auto (requires other parallelism > 1) |
|
||||
| `hsdp_replicate_size` | int | 1 | Number of replica groups. Each group holds a full sharded copy |
|
||||
|
||||
**Constraints:**
|
||||
|
||||
- `hsdp_replicate_size × hsdp_shard_size == world_size`
|
||||
- HSDP cannot be used with Tensor Parallelism (`tensor_parallel_size` must be 1)
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### When to Use
|
||||
|
||||
**Good for:**
|
||||
|
||||
- Very large models (e.g., Wan2.2 14B)
|
||||
- Multi-GPU setups where memory reduction is the primary goal
|
||||
- Combining with Sequence Parallelism for large video models
|
||||
|
||||
**Not for:**
|
||||
|
||||
- Models that fit comfortably in single-GPU memory
|
||||
- Use cases requiring Tensor Parallelism (HSDP and TP are mutually exclusive)
|
||||
|
||||
### Adding HSDP Support to New Models
|
||||
|
||||
For detailed instructions on adding HSDP support to new models, see the [HSDP Contributing Guide](../../../design/feature/hsdp.md).
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
1. ✅ **Enable HSDP** - Set `use_hsdp=True` and `hsdp_shard_size` to reduce per-GPU memory for large models
|
||||
2. ✅ **Combine with SP** - Use together with `ulysses_degree` for video models requiring both memory reduction and sequence parallelism
|
||||
3. ⚠️ **Incompatible with TP** - `tensor_parallel_size` must be 1 when HSDP is enabled
|
||||
@@ -1,16 +0,0 @@
|
||||
# Parallelism Acceleration Guide
|
||||
|
||||
This guide covers the parallelism methods in vLLM-Omni for speeding up diffusion model inference and reducing per-device memory requirements.
|
||||
|
||||
## Supported Methods
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| **[Tensor Parallelism](tensor_parallel.md)** | Shards DiT weights across GPUs to reduce per-GPU memory |
|
||||
| **[Sequence Parallelism](sequence_parallel.md)** | Splits sequence dimension across GPUs (Ulysses-SP, Ring-Attention, or hybrid) for high-resolution images and videos |
|
||||
| **[CFG-Parallel](cfg_parallel.md)** | Runs CFG positive/negative branches on separate GPUs for ~1.8x speedup on guided generation |
|
||||
| **[VAE Patch Parallelism](vae_patch_parallel.md)** | Distributes VAE decode spatially across GPUs to reduce peak VAE memory |
|
||||
| **[HSDP](hsdp.md)** | Shards full model weights via PyTorch FSDP2 to enable large-model inference on memory-constrained GPUs |
|
||||
| **[Expert Parallelism](expert_parallel.md)** | Shards MoE expert blocks across GPUs for MoE models (e.g. HunyuanImage3.0) |
|
||||
|
||||
See [Supported Models](../../diffusion_features.md#supported-models) for per-model compatibility.
|
||||
@@ -1,233 +0,0 @@
|
||||
# Sequence Parallelism Guide
|
||||
|
||||
|
||||
## Table of Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Quick Start](#quick-start)
|
||||
- [Example Script](#example-script)
|
||||
- [Best Practices](#best-practices)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Summary](#summary)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Sequence parallelism splits the input along the sequence dimension across multiple GPUs, allowing each device to process only a portion of the sequence. vLLM-Omni provides 1.5x-3.6x speedup for large images and videos using DeepSpeed Ulysses, Ring-Attention, or hybrid approaches. Use sequence parallelism when generating high-resolution images/videos that don't fit on a single GPU or require faster inference.
|
||||
|
||||
See supported models list in [Diffusion Features - Supported Models](../../diffusion_features.md#supported-models).
|
||||
|
||||
**Supported Methods:**
|
||||
|
||||
- **DeepSpeed Ulysses Sequence Parallel (Ulysses-SP)** ([paper](https://arxiv.org/pdf/2309.14509)): Uses all-to-all communication for subset of attention heads per device
|
||||
- **Ring-Attention** ([paper](https://arxiv.org/abs/2310.01889)): Uses ring-based P2P communication with sharded sequence dimension throughout
|
||||
- **Hybrid Ulysses + Ring**: Combines both for larger scale parallelism (`ulysses_degree × ring_degree`)
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Basic Usage - Ulysses-SP
|
||||
|
||||
Simplest working example with Ulysses Sequence Parallel:
|
||||
|
||||
```python
|
||||
from vllm_omni import Omni
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
from vllm_omni.diffusion.data import DiffusionParallelConfig
|
||||
|
||||
omni = Omni(
|
||||
model="Qwen/Qwen-Image",
|
||||
parallel_config=DiffusionParallelConfig(ulysses_degree=2) # Enable Ulysses-SP
|
||||
)
|
||||
|
||||
outputs = omni.generate(
|
||||
"A cat sitting on a windowsill",
|
||||
OmniDiffusionSamplingParams(num_inference_steps=50, width=1024, height=1024),
|
||||
)
|
||||
```
|
||||
|
||||
!!! note "Experimental UAA mode"
|
||||
`ulysses_mode="advanced_uaa"` is an experimental extension to Ulysses-SP. It lets Ulysses attention handle arbitrary sequence lengths and arbitrary attention head counts without relying on `attention_mask`-based token padding.
|
||||
|
||||
In hybrid Ulysses + Ring mode, Ring still requires every rank in the same ring group to observe the same post-Ulysses sequence length. If that condition is not met, vLLM-Omni raises a validation error instead of entering the ring kernel with inconsistent shapes.
|
||||
|
||||
To enable the experimental UAA mode, use a model/configuration that requires it. For example, `Tongyi-MAI/Z-Image-Turbo` has 30 attention heads, so `ulysses_degree=4` requires UAA because 30 is not divisible by 4:
|
||||
|
||||
```python
|
||||
omni = Omni(
|
||||
model="Tongyi-MAI/Z-Image-Turbo",
|
||||
parallel_config=DiffusionParallelConfig(
|
||||
ulysses_degree=4,
|
||||
ulysses_mode="advanced_uaa",
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
### Alternative Methods
|
||||
|
||||
**Ring-Attention** (better for very long sequences):
|
||||
|
||||
```python
|
||||
omni = Omni(
|
||||
model="Qwen/Qwen-Image",
|
||||
parallel_config=DiffusionParallelConfig(ring_degree=2) # Enable Ring-Attention
|
||||
)
|
||||
```
|
||||
|
||||
**Hybrid Ulysses + Ring** (for larger scale):
|
||||
|
||||
```python
|
||||
omni = Omni(
|
||||
model="Qwen/Qwen-Image",
|
||||
parallel_config=DiffusionParallelConfig(ulysses_degree=2, ring_degree=2) # 4 GPUs total
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example Script
|
||||
|
||||
### Offline Inference
|
||||
|
||||
Use Python script under `examples/offline_inference/text_to_image/text_to_image.py`:
|
||||
|
||||
**Ulysses-SP:**
|
||||
|
||||
```bash
|
||||
python examples/offline_inference/text_to_image/text_to_image.py \
|
||||
--model Qwen/Qwen-Image \
|
||||
--prompt "A cat sitting on a windowsill" \
|
||||
--ulysses-degree 2 \
|
||||
--width 1024 --height 1024
|
||||
```
|
||||
|
||||
**Ring-Attention:**
|
||||
|
||||
```bash
|
||||
python examples/offline_inference/text_to_image/text_to_image.py \
|
||||
--model Qwen/Qwen-Image \
|
||||
--prompt "A cat sitting on a windowsill" \
|
||||
--ring-degree 2 \
|
||||
--width 1024 --height 1024
|
||||
```
|
||||
|
||||
**Hybrid Ulysses + Ring:**
|
||||
|
||||
```bash
|
||||
# Hybrid: 2 Ulysses × 2 Ring = 4 GPUs total
|
||||
python examples/offline_inference/text_to_image/text_to_image.py \
|
||||
--model Qwen/Qwen-Image \
|
||||
--prompt "A cat sitting on a windowsill" \
|
||||
--ulysses-degree 2 --ring-degree 2 \
|
||||
--width 1024 --height 1024
|
||||
```
|
||||
|
||||
### Online Serving
|
||||
|
||||
**Ulysses-SP:**
|
||||
|
||||
```bash
|
||||
# Text-to-image (requires >= 2 GPUs)
|
||||
vllm serve Qwen/Qwen-Image --omni --port 8091 --usp 2
|
||||
```
|
||||
|
||||
**Ulysses-SP with UAA mode** (for models with non-divisible head counts):
|
||||
|
||||
```bash
|
||||
vllm serve Tongyi-MAI/Z-Image-Turbo --omni --port 8091 --usp 4 --ulysses-mode advanced_uaa
|
||||
```
|
||||
|
||||
**Ring-Attention:**
|
||||
|
||||
```bash
|
||||
# Text-to-image (requires >= 2 GPUs)
|
||||
vllm serve Qwen/Qwen-Image --omni --port 8091 --ring 2
|
||||
```
|
||||
|
||||
**Hybrid Ulysses + Ring:**
|
||||
|
||||
```bash
|
||||
# Text-to-image (requires >= 4 GPUs)
|
||||
vllm serve Qwen/Qwen-Image --omni --port 8091 --usp 2 --ring 2
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration Parameters
|
||||
|
||||
In `DiffusionParallelConfig`:
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `ulysses_degree` | int | 1 | Number of GPUs for Ulysses-SP. Uses all-to-all communication. |
|
||||
| `ring_degree` | int | 1 | Number of GPUs for Ring-Attention. Uses P2P ring communication. |
|
||||
| `ulysses_mode` | str | `"default"` | Ulysses attention mode. Set to `"advanced_uaa"` to handle arbitrary sequence lengths and head counts without padding. |
|
||||
|
||||
**Notes:**
|
||||
- Total sequence parallel size equals to `ulysses_degree × ring_degree`
|
||||
- Degrees must evenly divide the sequence length for optimal performance (or use `ulysses_mode="advanced_uaa"` for Ulysses-SP)
|
||||
|
||||
|
||||
## Best Practices
|
||||
|
||||
### When to Use
|
||||
|
||||
**Good for:**
|
||||
|
||||
- Large images (1024x1024 or higher) or videos
|
||||
- Fast inter-GPU communication, larger bandwidth (e.g., NVLink)
|
||||
|
||||
**Not for:**
|
||||
|
||||
- Small images (<1024px) - overhead exceeds benefit, use single GPU with cache instead
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issue 1: Performance Not Scaling
|
||||
|
||||
**Symptoms**: Adding GPUs doesn't improve speed proportionally, or higher parallelism degree is slower
|
||||
|
||||
**Diagnosis:**
|
||||
```bash
|
||||
# Check GPU topology
|
||||
nvidia-smi topo -m
|
||||
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Check inter-GPU communication - NVLink is better than PCIe
|
||||
2. Reduce parallelism degree if over-parallelized:
|
||||
```python
|
||||
# If 4 GPUs is slower than 2
|
||||
parallel_config=DiffusionParallelConfig(ulysses_degree=2)
|
||||
```
|
||||
3. Try to switch between Ring-Attention and Ulysses-SP
|
||||
|
||||
- Ring-Attention has advantages, like communication-computation overlap, but the block-wise loop overhead is relatively higher, especially for short sequences
|
||||
- Ulysses-SP: can benefit from larger bandwidth (such as NVLink), with two major constraints, the sequence length should be divisible by usp size, and the number of heads should be divisible by usp size (or use `ulysses_mode="advanced_uaa"`)
|
||||
|
||||
|
||||
### Common Issue 2: Out of Memory (OOM)
|
||||
|
||||
**Symptoms**: CUDA OOM errors or process crashes with memory errors
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Increase parallelism degree to split sequence more:
|
||||
```python
|
||||
parallel_config=DiffusionParallelConfig(ulysses_degree=4) # From 2
|
||||
```
|
||||
2. Combine with other parallelism method, e.g., tensor parallel, and memory optimization methods, e.g., cpu offloading.
|
||||
|
||||
|
||||
## Summary
|
||||
|
||||
1. ✅ **Enable Sequence Parallelism** - Set `ulysses_degree` or `ring_degree` for long sequence generation
|
||||
2. ✅ **UAA mode** - Use `ulysses_mode="advanced_uaa"` when head count is not divisible by `ulysses_degree`
|
||||
3. ✅ **Troubleshooting** - Check GPU topology with `nvidia-smi topo -m`, reduce degree if performance doesn't scale
|
||||
@@ -1,151 +0,0 @@
|
||||
# Tensor Parallelism Guide
|
||||
|
||||
|
||||
## Table of Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Quick Start](#quick-start)
|
||||
- [Example Script](#example-script)
|
||||
- [Configuration Parameters](#configuration-parameters)
|
||||
- [Best Practices](#best-practices)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Summary](#summary)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Tensor Parallelism (TP) shards some model weights across multiple GPUs, usually the Linear layers. This enables running large models that don't fit on a single GPU. It's essential for memory-constrained setups or very large models.
|
||||
|
||||
See supported models list in [Supported Models](../../diffusion_features.md#supported-models).
|
||||
|
||||
!!! note "TP Limitations for Diffusion Models"
|
||||
We currently implement Tensor Parallelism (TP) only for the DiT (Diffusion Transformer) blocks. This is because the `text_encoder` component in vLLM-Omni uses the original Transformers implementation, which does not yet support TP.
|
||||
|
||||
- Good news: The text_encoder typically has minimal impact on overall inference performance.
|
||||
- Bad news: When TP is enabled, every TP process retains a full copy of the text_encoder weights, leading to significant GPU memory waste.
|
||||
|
||||
We are actively refactoring this design to address this. For details and progress, please refer to [Issue #771](https://github.com/vllm-project/vllm-omni/issues/771).
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
|
||||
### Basic Usage
|
||||
|
||||
Simplest working example:
|
||||
|
||||
```python
|
||||
from vllm_omni import Omni
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
from vllm_omni.diffusion.data import DiffusionParallelConfig
|
||||
|
||||
omni = Omni(
|
||||
model="Tongyi-MAI/Z-Image-Turbo",
|
||||
parallel_config=DiffusionParallelConfig(tensor_parallel_size=2), # Enable TP
|
||||
)
|
||||
|
||||
outputs = omni.generate(
|
||||
"a cat reading a book",
|
||||
OmniDiffusionSamplingParams(num_inference_steps=9),
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example Script
|
||||
|
||||
### Offline Inference
|
||||
|
||||
Use Python script under `examples/offline_inference`, and enable TP:
|
||||
|
||||
```bash
|
||||
# Text-to-Image with Qwen-Image
|
||||
python examples/offline_inference/text_to_image/text_to_image.py \
|
||||
--model Qwen/Qwen-Image \
|
||||
--tensor-parallel-size 2
|
||||
|
||||
# Image Editing with Qwen-Image-Edit
|
||||
python examples/offline_inference/image_to_image/image_edit.py \
|
||||
--model Qwen/Qwen-Image-Edit \
|
||||
--image input.png \
|
||||
--prompt "Edit description" \
|
||||
--tensor-parallel-size 2
|
||||
```
|
||||
|
||||
### Online Serving
|
||||
|
||||
You can enable tensor parallelism in online serving via `--tensor-parallel-size`:
|
||||
|
||||
```bash
|
||||
# Text-to-Image with Qwen-Image on 2 GPUs
|
||||
vllm serve Qwen/Qwen-Image --omni --port 8091 \
|
||||
--tensor-parallel-size 2
|
||||
|
||||
# Text-to-Image with Z-Image (TP=2 only)
|
||||
vllm serve Tongyi-MAI/Z-Image-Turbo --omni --port 8091 \
|
||||
--tensor-parallel-size 2
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration Parameters
|
||||
|
||||
In `DiffusionParallelConfig`:
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `tensor_parallel_size` | int | 1 | Number of GPUs to shard model weights across. Must divide number of heads. |
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### When to Use
|
||||
|
||||
**Good for:**
|
||||
|
||||
- Large models that don't fit on a single GPU, especially for models with large DiT blocks (transformer layers)
|
||||
- Memory-constrained environments
|
||||
|
||||
**Not for:**
|
||||
|
||||
- When maximum throughput is needed and memory is sufficient
|
||||
- Models with incompatible dimensions (e.g., Z-Image `num_heads=30`, which now supports `tensor_parallel_size=2`)
|
||||
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issue 1: Out of Memory (OOM)
|
||||
|
||||
**Symptoms**: CUDA OOM errors during model loading or inference, process crashes with memory errors
|
||||
|
||||
**Solution**:
|
||||
```python
|
||||
# Step 1: Enable TP with smallest degree
|
||||
parallel_config=DiffusionParallelConfig(tensor_parallel_size=2)
|
||||
|
||||
# Step 2: If still OOM, increase TP degree
|
||||
parallel_config=DiffusionParallelConfig(tensor_parallel_size=4)
|
||||
|
||||
```
|
||||
|
||||
### Common Issue 2: Divisibility Error
|
||||
|
||||
**Symptoms**: Error like "Model dimension X not divisible by tensor_parallel_size Y"
|
||||
|
||||
**Solutions**:
|
||||
1. Check model-specific constraints (e.g., Z-Image only supports TP=2)
|
||||
2. Use a smaller TP size that divides model dimensions
|
||||
3. Consult [Supported Models](../../diffusion_features.md#supported-models) for compatible TP sizes
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
1. ✅ **Enable TP** - Set `--tensor-parallel-size` to reduce per-GPU memory
|
||||
2. ✅ **Increase TP size** - Only increase if OOM persists
|
||||
3. ⚠️ **Text encoder not sharded** - Known limitation
|
||||
@@ -1,200 +0,0 @@
|
||||
# VAE Patch Parallelism Guide
|
||||
|
||||
|
||||
## Table of Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Quick Start](#quick-start)
|
||||
- [Example Script](#example-script)
|
||||
- [Configuration Parameters](#configuration-parameters)
|
||||
- [Best Practices](#best-practices)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Summary](#summary)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
VAE Patch Parallelism distributes the VAE (Variational AutoEncoder) decode/encode computation across multiple GPUs by splitting the latent space into spatial tiles or patches. Each GPU processes a subset of tiles in parallel, significantly reducing peak memory consumption during the VAE decode stage while maintaining output quality.
|
||||
|
||||
This is particularly useful for:
|
||||
- **High-resolution image generation** where VAE decode can become a memory bottleneck
|
||||
- **Memory-constrained environments** where the VAE decode activation peak exceeds available VRAM
|
||||
- **Multi-GPU setups** where you want to leverage distributed resources for the VAE stage
|
||||
|
||||
See supported models list in [Supported Models](../../diffusion_features.md#supported-models).
|
||||
|
||||
|
||||
VAE Patch Parallelism uses two strategies based on image size:
|
||||
|
||||
| Strategy | Use Case | How It Works | Overlap Handling | Output Quality |
|
||||
|----------|----------|--------------|------------------|----------------|
|
||||
| **Tiled Decode** | Large images (triggers VAE tiling) | Distributes existing VAE tiling computation across ranks. Each rank decodes a subset of overlapping tiles. | Uses VAE's native `blend_v` and `blend_h` functions to seamlessly merge overlapping regions | Bit-identical (same logic as single-GPU tiling) |
|
||||
| **Patch Decode** | Small images (no VAE tiling) | Splits latent into spatial patches with halos. Each rank decodes one patch with boundary context. | Halo regions provide edge context; core regions are directly stitched without blending | Near-identical (diff < 0.5%, visually imperceptible) |
|
||||
|
||||
|
||||
VAE Patch Parallelism **reuses the DiT process group** (`dit_group`) and does not initialize a separate ProcessGroup. This means:
|
||||
|
||||
- **Shared ranks**: VAE patch parallelism uses the same GPU ranks as DiT parallelism (Tensor Parallel, Sequence Parallel, etc.)
|
||||
- **Combined usage**: VAE patch parallelism is typically used together with other parallelism methods
|
||||
- **Configuration alignment**: The `vae_patch_parallel_size` should be no greater than the size of your DiT process group
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Basic Usage
|
||||
|
||||
Simplest working example:
|
||||
|
||||
```python
|
||||
from vllm_omni import Omni
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
from vllm_omni.diffusion.data import DiffusionParallelConfig
|
||||
|
||||
# TP=2 for DiT, VAE patch parallel also uses these 2 GPUs
|
||||
omni = Omni(
|
||||
model="Tongyi-MAI/Z-Image-Turbo",
|
||||
parallel_config=DiffusionParallelConfig(
|
||||
tensor_parallel_size=2, # Enable tensor parallelism for DiT
|
||||
vae_patch_parallel_size=2, # Enable VAE patch parallelism
|
||||
),
|
||||
vae_use_tiling=True, # Required for VAE patch parallelism
|
||||
)
|
||||
|
||||
outputs = omni.generate(
|
||||
"a futuristic city at sunset, high resolution, 8k",
|
||||
OmniDiffusionSamplingParams(
|
||||
num_inference_steps=9,
|
||||
height=1152, # High resolution benefits from VAE patch parallel
|
||||
width=1152,
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example Script
|
||||
|
||||
### Offline Inference
|
||||
|
||||
Use Python script under `examples/offline_inference/text_to_image/`:
|
||||
|
||||
```bash
|
||||
# Text-to-Image with Z-Image
|
||||
python examples/offline_inference/text_to_image/text_to_image.py \
|
||||
--model Tongyi-MAI/Z-Image-Turbo \
|
||||
--prompt "a futuristic city at sunset" \
|
||||
--height 1152 \
|
||||
--width 1152 \
|
||||
--tensor-parallel-size 2 \
|
||||
--vae-patch-parallel-size 2 \
|
||||
--vae-use-tiling
|
||||
```
|
||||
|
||||
### Online Serving
|
||||
|
||||
You can enable VAE patch parallelism in online serving via `--vae-patch-parallel-size`:
|
||||
|
||||
```bash
|
||||
# Text-to-Image with Z-Image, TP=2 + VAE patch parallel=2
|
||||
vllm serve Tongyi-MAI/Z-Image-Turbo --omni --port 8091 \
|
||||
--tensor-parallel-size 2 \
|
||||
--vae-patch-parallel-size 2 \
|
||||
--vae-use-tiling
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration Parameters
|
||||
|
||||
In `DiffusionParallelConfig`:
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `vae_patch_parallel_size` | int | 1 | Number of GPUs for VAE patch/tile parallelism. Set to 2 or higher to enable. Should typically match `tensor_parallel_size` as they share the same process group. |
|
||||
|
||||
Additional requirements:
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `vae_use_tiling` | bool | False | Must be set to `True` when using VAE patch parallelism. |
|
||||
|
||||
!!! note "Automatic VAE Tiling"
|
||||
When `vae_patch_parallel_size > 1` and the model has a distributed VAE (`DistributedVaeMixin`), the system automatically sets `vae_use_tiling=True` if not already enabled.
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### When to Use
|
||||
|
||||
**Good for:**
|
||||
|
||||
- High-resolution image generation and long video generation
|
||||
- Memory-constrained setups where VAE decode causes OOM
|
||||
- Multi-GPU environments
|
||||
|
||||
**Not for:**
|
||||
|
||||
- Low-resolution images/videos where VAE decode is not a bottleneck
|
||||
- Single GPU setups should use vae tiling decode, but not parallel vae tiling decode
|
||||
- Models that do not support vae patch parallel
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issue 1: Model Not Support VAE Patch Parallel
|
||||
|
||||
**Symptoms**:
|
||||
```
|
||||
WARNING: vae_patch_parallel_size=2 is set but VAE patch parallelism is NOT enabled for xxxPipeline; ignoring.
|
||||
```
|
||||
|
||||
**Root Cause**: VAE Patch Parallelism requires the model's VAE to implement `DistributedVaeMixin`. At startup, `vllm_omni/diffusion/registry.py` checks whether the instantiated pipeline has a `.vae` attribute that is an instance of `DistributedVaeMixin`. If it does not, the setting is silently ignored:
|
||||
|
||||
```python
|
||||
vae_pp_size = od_config.parallel_config.vae_patch_parallel_size
|
||||
is_distributed_vae = hasattr(model, "vae") and isinstance(model.vae, DistributedVaeMixin)
|
||||
if vae_pp_size > 1 and not is_distributed_vae:
|
||||
logger.warning(
|
||||
"vae_patch_parallel_size=%d is set but VAE patch parallelism is NOT enabled for %s; ignoring.",
|
||||
vae_pp_size,
|
||||
od_config.model_class_name,
|
||||
)
|
||||
```
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. **Use a supported model** (recommended): check [Supported Models](../../diffusion_features.md#supported-models) for the VAE-Patch-Parallel column.
|
||||
|
||||
2. To add support for a new model, implement `DistributedVaeMixin` on its VAE class (contributions are welcome).
|
||||
|
||||
|
||||
### Common Issue 2: `vae_patch_parallel_size` Exceeds DiT Process Group Size
|
||||
|
||||
**Symptoms**: Shows warning message, and vae patch parallel size is resized to DiT process group size
|
||||
|
||||
**Root Cause**: VAE Patch Parallelism reuses the DiT process group.
|
||||
|
||||
**Recommendation**: Always set `vae_patch_parallel_size` to be no greater than your DiT process group size.
|
||||
|
||||
Note that the size of DiT process group size equals to:
|
||||
```text
|
||||
dit_parallel_size = data_parallel_size
|
||||
× cfg_parallel_size
|
||||
× sequence_parallel_size
|
||||
× pipeline_parallel_size
|
||||
× tensor_parallel_size
|
||||
|
||||
```
|
||||
_sequence_parallel_size = ulysses_degree × ring_degree_
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
1. ✅ **Enable VAE Patch Parallelism** - Set `vae_patch_parallel_size`, `vae_use_tiling=True` in `DiffusionParallelConfig` to reduce VAE decode peak memory
|
||||
2. ✅ **Use Long Sequence** - VAE patch parallelism benefits are most apparent at long sequence decoding
|
||||
3. ✅ **Combine with other parallelism methods** - Suggest to use together with Tensor Parallel or CFG-Parallel for maximum memory savings
|
||||
@@ -0,0 +1,477 @@
|
||||
# Parallelism Acceleration Guide
|
||||
|
||||
This guide includes how to use parallelism methods in vLLM-Omni to speed up diffusion model inference as well as reduce the memory requirement on each device.
|
||||
|
||||
## Overview
|
||||
|
||||
The following parallelism methods are currently supported in vLLM-Omni:
|
||||
|
||||
1. DeepSpeed Ulysses Sequence Parallel (DeepSpeed Ulysses-SP) ([arxiv paper](https://arxiv.org/pdf/2309.14509)): Ulysses-SP splits the input along the sequence dimension and uses all-to-all communication to allow each device to compute only a subset of attention heads.
|
||||
|
||||
2. [Ring-Attention](#ring-attention) - splits the input along the sequence dimension and uses ring-based P2P communication to accumulate attention results, keeping the sequence dimension sharded
|
||||
|
||||
3. Classifier-Free-Guidance Parallel (CFG-Parallel): CFG-Parallel runs the positive/negative prompts of classifier-free guidance (CFG) on different devices, then merges on a single device to perform the scheduler step.
|
||||
|
||||
4. [Tensor Parallelism](#tensor-parallelism): Tensor parallelism shards model weights across devices. This can reduce per-GPU memory usage. Note that for diffusion models we currently shard the majority of layers within the DiT.
|
||||
|
||||
5. [VAE Patch Parallelism](#vae-patch-parallelism): VAE patch parallelism shards VAE decode spatially across ranks. This can reduce the peak memory of VAE decode and (depending on resolution and communication overhead) speed up VAE decode.
|
||||
|
||||
6. [HSDP](#hsdp): Hybrid Sharded Data Parallel shards model weights across GPUs using PyTorch FSDP2. This reduces per-GPU memory usage, enabling inference of large models on GPUs with limited memory.
|
||||
|
||||
7. [Expert Parallel](#expert-parallelism): Expert Parallelism shards the Experts of a Mixture-of-Experts (MoE) layer across multiple devices. During the forward, a gating mechanism routes tokens to their designated experts, necessitating cross-cards communication(all-to-all) to dispatch tokens to the correct ranks and combine the results. This parallelism allows for massive scaling of model parameters without a proportional increase in the computational load per device.
|
||||
|
||||
The following table shows which models are currently supported by parallelism method:
|
||||
|
||||
### ImageGen
|
||||
|
||||
| Model | Model Identifier | Ulysses-SP | Ring-SP | CFG-Parallel | Tensor-Parallel | VAE-Patch-Parallel | Expert-Parallel | HSDP |
|
||||
|--------------------------|--------------------------------------|:----------:|:-------:|:------------:|:---------------:|:------------------:|:---------------:|:----:|
|
||||
| **LongCat-Image** | `meituan-longcat/LongCat-Image` | ✅ | ✅ | ❌ | ✅ | ❌ | N/A | ❌ |
|
||||
| **LongCat-Image-Edit** | `meituan-longcat/LongCat-Image-Edit` | ✅ | ✅ | ❌ | ✅ | ❌ | N/A | ❌ |
|
||||
| **Ovis-Image** | `OvisAI/Ovis-Image` | ❌ | ❌ | ❌ | ❌ | ❌ | N/A | ❌ |
|
||||
| **Qwen-Image** | `Qwen/Qwen-Image` | ✅ | ✅ | ✅ | ✅ | ✅ | N/A | ❌ |
|
||||
| **Qwen-Image-Edit** | `Qwen/Qwen-Image-Edit` | ✅ | ✅ | ✅ | ✅ | ❌ | N/A | ❌ |
|
||||
| **Qwen-Image-Edit-2509** | `Qwen/Qwen-Image-Edit-2509` | ✅ | ✅ | ✅ | ✅ | ❌ | N/A | ❌ |
|
||||
| **Qwen-Image-Layered** | `Qwen/Qwen-Image-Layered` | ✅ | ✅ | ✅ | ✅ | ❌ | N/A | ❌ |
|
||||
| **Z-Image** | `Tongyi-MAI/Z-Image-Turbo` | ✅ | ✅ | ❌ | ✅ (TP=2 only) | ✅ | N/A | ❌ |
|
||||
| **Stable-Diffusion3.5** | `stabilityai/stable-diffusion-3.5` | ❌ | ❌ | ❌ | ✅ | ✅ | N/A | ❌ |
|
||||
| **FLUX.2-klein** | `black-forest-labs/FLUX.2-klein-4B` | ✅ | ✅ | ❌ | ✅ | ❌ | N/A | ✅ |
|
||||
| **FLUX.1-dev** | `black-forest-labs/FLUX.1-dev` | ❌ | ❌ | ✅ | ✅ | ❌ | N/A | ✅ |
|
||||
| **FLUX.2-dev** | `black-forest-labs/FLUX.2-dev` | ❌ | ❌ | ❌ | ✅ | ❌ | N/A | ✅ |
|
||||
| **HunyuanImage3.0** | `tencent/HunyuanImage-3.0`, `tencent/HunyuanImage-3.0-Instruct` | ❌ | ❌ | ❌ | ✅ | ❌ | ✅ | ❌ |
|
||||
| **Bagel** | `ByteDance-Seed/BAGEL-7B-MoT` | ✅ | ✅ | ✅ | ✅ | ❌ | N/A | ❌ |
|
||||
| **DreamID-Omni** | `XuGuo699/DreamID-Omni` | ❌ | ❌ | ✅ | ❌ | ❌ | N/A | ❌ |
|
||||
| **FLUX.1-Kontext-dev** | `black-forest-labs/FLUX.1-Kontext-dev` | ❌ | ❌ | ❌ | ✅ | ❌ | N/A | ✅ |
|
||||
| **OmniGen2** | `OmniGen2/OmniGen2` | ❌ | ❌ | ❌ | ✅ | ❌ | N/A | ❌ |
|
||||
|
||||
!!! note "TP Limitations for Diffusion Models"
|
||||
We currently implement Tensor Parallelism (TP) only for the DiT (Diffusion Transformer) blocks. This is because the `text_encoder` component in vLLM-Omni uses the original Transformers implementation, which does not yet support TP.
|
||||
|
||||
- Good news: The text_encoder typically has minimal impact on overall inference performance.
|
||||
- Bad news: When TP is enabled, every TP process retains a full copy of the text_encoder weights, leading to significant GPU memory waste.
|
||||
|
||||
We are actively refactoring this design to address this. For details and progress, please refer to [Issue #771](https://github.com/vllm-project/vllm-omni/issues/771).
|
||||
|
||||
|
||||
!!! note "Why Z-Image is TP=2 only"
|
||||
Z-Image Turbo is currently limited to `tensor_parallel_size` of **1 or 2** due to model shape divisibility constraints.
|
||||
For example, the model has `n_heads=30` and a final projection out dimension of `64`, so valid TP sizes must divide both 30 and 64; the only common divisors are **1 and 2**.
|
||||
|
||||
### VideoGen
|
||||
|
||||
| Model | Model Identifier | Ulysses-SP | Ring-Attention | Tensor-Parallel | HSDP | VAE-Patch-Parallel |
|
||||
|-------|------------------|:----------:|:--------------:|:---------------:|:----:| :----:|
|
||||
| **Wan2.1** | `Wan-AI/Wan2.1-T2V-1.3B-Diffusers` | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| **Wan2.1** | `Wan-AI/Wan2.1-T2V-14B-Diffusers` | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| **Wan2.2** | `Wan-AI/Wan2.2-T2V-A14B-Diffusers` | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| **LTX-2** | `Lightricks/LTX-2` | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
|
||||
### Tensor Parallelism
|
||||
|
||||
Tensor parallelism splits model parameters across GPUs. In vLLM-Omni, tensor parallelism is configured via `DiffusionParallelConfig.tensor_parallel_size`.
|
||||
|
||||
#### Offline Inference
|
||||
|
||||
```python
|
||||
from vllm_omni import Omni
|
||||
from vllm_omni.diffusion.data import DiffusionParallelConfig
|
||||
|
||||
omni = Omni(
|
||||
model="Tongyi-MAI/Z-Image-Turbo",
|
||||
parallel_config=DiffusionParallelConfig(tensor_parallel_size=2),
|
||||
)
|
||||
|
||||
outputs = omni.generate(
|
||||
"a cat reading a book",
|
||||
OmniDiffusionSamplingParams(
|
||||
num_inference_steps=9,
|
||||
width=512,
|
||||
height=512,
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
### VAE Patch Parallelism
|
||||
|
||||
VAE patch parallelism distributes the VAE decode workload across multiple ranks by splitting the latent spatially. It is configured via `DiffusionParallelConfig.vae_patch_parallel_size` and can be combined with other parallelism methods (e.g., TP).
|
||||
|
||||
!!! note "Enablement and feature gate"
|
||||
- VAE patch parallelism is currently **enabled only for validated pipelines** (check [ImageGen](#imagegen) and [VideoGen](#videogen) for more information).
|
||||
- If `vae_patch_parallel_size > 1` is set for a validated pipeline, vLLM-Omni will automatically enable `vae_use_tiling` as a safety gate. (We use `vae_use_tiling` because it indicates the VAE supports diffusers tiling parameters like `tile_latent_min_size` and `tile_overlap_factor`.)
|
||||
|
||||
#### Offline Inference
|
||||
|
||||
```python
|
||||
from vllm_omni import Omni
|
||||
from vllm_omni.diffusion.data import DiffusionParallelConfig
|
||||
|
||||
omni = Omni(
|
||||
model="Tongyi-MAI/Z-Image-Turbo",
|
||||
parallel_config=DiffusionParallelConfig(
|
||||
tensor_parallel_size=2,
|
||||
vae_patch_parallel_size=2,
|
||||
),
|
||||
vae_use_tiling=True,
|
||||
)
|
||||
|
||||
outputs = omni.generate(
|
||||
prompt="a cat reading a book",
|
||||
num_inference_steps=9,
|
||||
width=1024,
|
||||
height=1024,
|
||||
)
|
||||
```
|
||||
|
||||
#### How it works (method selection)
|
||||
|
||||
VAE patch parallelism automatically selects between two internal decode methods based on whether diffusers tiling would kick in:
|
||||
|
||||
- `_distributed_tiled_decode`: Used when the latent spatial size exceeds `vae.tile_latent_min_size` (i.e., diffusers tiled decode). Each rank decodes a subset of tiles; rank0 gathers and runs the same overlap+blend+stitch logic as diffusers. This matches the single-rank diffusers tiled output.
|
||||
|
||||
- `_distributed_patch_decode`: Used when diffusers tiling would not kick in. Each rank decodes a grid patch expanded with a latent-space halo; then rank0 gathers the cropped core patches and stitches them into the full image. This path has no blending and can introduce small numerical differences compared to the non-parallel decode.
|
||||
|
||||
### Sequence Parallelism
|
||||
|
||||
#### Ulysses-SP
|
||||
|
||||
!!! note "Experimental UAA mode"
|
||||
`ulysses_mode="advanced_uaa"` is an experimental extension to Ulysses-SP. It lets Ulysses attention handle arbitrary sequence lengths and arbitrary attention head counts without relying on `attention_mask`-based token padding.
|
||||
|
||||
In hybrid Ulysses + Ring mode, Ring still requires every rank in the same ring group to observe the same post-Ulysses sequence length. If that condition is not met, vLLM-Omni raises a validation error instead of entering the ring kernel with inconsistent shapes.
|
||||
|
||||
##### Offline Inference
|
||||
|
||||
An example of offline inference script using [Ulysses-SP](https://arxiv.org/pdf/2309.14509) is shown below:
|
||||
```python
|
||||
from vllm_omni import Omni
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
from vllm_omni.diffusion.data import DiffusionParallelConfig
|
||||
ulysses_degree = 2
|
||||
|
||||
omni = Omni(
|
||||
model="Qwen/Qwen-Image",
|
||||
parallel_config=DiffusionParallelConfig(ulysses_degree=2)
|
||||
)
|
||||
|
||||
outputs = omni.generate(
|
||||
"A cat sitting on a windowsill",
|
||||
OmniDiffusionSamplingParams(num_inference_steps=50, width=2048, height=2048),
|
||||
)
|
||||
```
|
||||
|
||||
See `examples/offline_inference/text_to_image/text_to_image.py` for a complete working example.
|
||||
|
||||
To enable the experimental UAA mode explicitly, use a model/configuration that actually requires it. For example, `Tongyi-MAI/Z-Image-Turbo` has 30 attention heads, so `ulysses_degree=4` requires UAA because 30 is not divisible by 4:
|
||||
|
||||
```python
|
||||
omni = Omni(
|
||||
model="Tongyi-MAI/Z-Image-Turbo",
|
||||
parallel_config=DiffusionParallelConfig(
|
||||
ulysses_degree=4,
|
||||
ulysses_mode="advanced_uaa",
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
##### Online Serving
|
||||
|
||||
You can enable Ulysses-SP in online serving for diffusion models via `--usp`:
|
||||
|
||||
```bash
|
||||
# Text-to-image (requires >= 2 GPUs)
|
||||
vllm serve Qwen/Qwen-Image --omni --port 8091 --usp 2
|
||||
|
||||
# Experimental UAA mode for a model with 30 attention heads
|
||||
vllm serve Tongyi-MAI/Z-Image-Turbo --omni --port 8091 --usp 4 --ulysses-mode advanced_uaa
|
||||
```
|
||||
|
||||
##### Benchmarks
|
||||
!!! note "Benchmark Disclaimer"
|
||||
These benchmarks are provided for **general reference only**. The configurations shown use default or common parameter settings and have not been exhaustively optimized for maximum performance. Actual performance may vary based on:
|
||||
|
||||
- Specific model and use case
|
||||
- Hardware configuration
|
||||
- Careful parameter tuning
|
||||
- Different inference settings (e.g., number of steps, image resolution)
|
||||
|
||||
|
||||
To measure the parallelism methods, we run benchmarks with **Qwen/Qwen-Image** model generating images (**2048x2048** as long sequence input) with 50 inference steps. The hardware devices are NVIDIA H800 GPUs. `sdpa` is the attention backends.
|
||||
|
||||
| Configuration | Ulysses degree |Generation Time | Speedup |
|
||||
|---------------|----------------|---------|---------|
|
||||
| **Baseline (diffusers)** | - | 112.5s | 1.0x |
|
||||
| Ulysses-SP | 2 | 65.2s | 1.73x |
|
||||
| Ulysses-SP | 4 | 39.6s | 2.84x |
|
||||
| Ulysses-SP | 8 | 30.8s | 3.65x |
|
||||
|
||||
#### Ring-Attention
|
||||
|
||||
Ring-Attention ([arxiv paper](https://arxiv.org/abs/2310.01889)) splits the input along the sequence dimension and uses ring-based P2P communication to accumulate attention results. Unlike Ulysses-SP which uses all-to-all communication, Ring-Attention keeps the sequence dimension sharded throughout the computation and circulates Key/Value blocks through a ring topology.
|
||||
|
||||
##### Offline Inference
|
||||
|
||||
An example of offline inference script using Ring-Attention is shown below:
|
||||
```python
|
||||
from vllm_omni import Omni
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
from vllm_omni.diffusion.data import DiffusionParallelConfig
|
||||
ring_degree = 2
|
||||
|
||||
omni = Omni(
|
||||
model="Qwen/Qwen-Image",
|
||||
parallel_config=DiffusionParallelConfig(ring_degree=2)
|
||||
)
|
||||
|
||||
outputs = omni.generate(
|
||||
"A cat sitting on a windowsill",
|
||||
OmniDiffusionSamplingParams(num_inference_steps=50, width=2048, height=2048),
|
||||
)
|
||||
```
|
||||
|
||||
See `examples/offline_inference/text_to_image/text_to_image.py` for a complete working example.
|
||||
|
||||
|
||||
##### Online Serving
|
||||
|
||||
You can enable Ring-Attention in online serving for diffusion models via `--ring`:
|
||||
|
||||
```bash
|
||||
# Text-to-image (requires >= 2 GPUs)
|
||||
vllm serve Qwen/Qwen-Image --omni --port 8091 --ring 2
|
||||
```
|
||||
|
||||
##### Benchmarks
|
||||
!!! note "Benchmark Disclaimer"
|
||||
These benchmarks are provided for **general reference only**. The configurations shown use default or common parameter settings and have not been exhaustively optimized for maximum performance. Actual performance may vary based on:
|
||||
|
||||
- Specific model and use case
|
||||
- Hardware configuration
|
||||
- Careful parameter tuning
|
||||
- Different inference settings (e.g., number of steps, image resolution)
|
||||
|
||||
|
||||
To measure the parallelism methods, we run benchmarks with **Qwen/Qwen-Image** model generating images (**1024x1024** as long sequence input) with 50 inference steps. The hardware devices are NVIDIA A100 GPUs. `flash_attn` is the attention backends.
|
||||
|
||||
| Configuration | Ring degree |Generation Time | Speedup |
|
||||
|---------------|----------------|---------|---------|
|
||||
| **Baseline (diffusers)** | - | 45.2s | 1.0x |
|
||||
| Ring-Attention | 2 | 29.9s | 1.51x |
|
||||
| Ring-Attention | 4 | 23.3s | 1.94x |
|
||||
|
||||
|
||||
#### Hybrid Ulysses + Ring
|
||||
|
||||
You can combine both Ulysses-SP and Ring-Attention for larger scale parallelism. The total sequence parallel size equals `ulysses_degree × ring_degree`.
|
||||
|
||||
!!! note "Experimental UAA in hybrid mode"
|
||||
`ulysses_mode="advanced_uaa"` can also be used with hybrid Ulysses + Ring, but this does not remove Ring's shape requirement. Every rank in the same ring group must still have the same post-Ulysses sequence length.
|
||||
|
||||
##### Offline Inference
|
||||
|
||||
```python
|
||||
from vllm_omni import Omni
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
from vllm_omni.diffusion.data import DiffusionParallelConfig
|
||||
|
||||
# Hybrid: 2 Ulysses × 2 Ring = 4 GPUs total
|
||||
omni = Omni(
|
||||
model="Qwen/Qwen-Image",
|
||||
parallel_config=DiffusionParallelConfig(
|
||||
ulysses_degree=2,
|
||||
ring_degree=2,
|
||||
)
|
||||
)
|
||||
|
||||
outputs = omni.generate(
|
||||
"A cat sitting on a windowsill",
|
||||
OmniDiffusionSamplingParams(num_inference_steps=50, width=2048, height=2048),
|
||||
)
|
||||
```
|
||||
|
||||
##### Online Serving
|
||||
|
||||
```bash
|
||||
# Text-to-image (requires >= 4 GPUs)
|
||||
vllm serve Qwen/Qwen-Image --omni --port 8091 --usp 2 --ring 2
|
||||
```
|
||||
|
||||
##### Benchmarks
|
||||
!!! note "Benchmark Disclaimer"
|
||||
These benchmarks are provided for **general reference only**. The configurations shown use default or common parameter settings and have not been exhaustively optimized for maximum performance. Actual performance may vary based on:
|
||||
|
||||
- Specific model and use case
|
||||
- Hardware configuration
|
||||
- Careful parameter tuning
|
||||
- Different inference settings (e.g., number of steps, image resolution)
|
||||
|
||||
|
||||
To measure the parallelism methods, we run benchmarks with **Qwen/Qwen-Image** model generating images (**1024x1024** as long sequence input) with 50 inference steps. The hardware devices are NVIDIA A100 GPUs. `flash_attn` is the attention backends.
|
||||
|
||||
| Configuration | Ulysses degree | Ring degree | Generation Time | Speedup |
|
||||
|---------------|----------------|-------------|-----------------|---------|
|
||||
| **Baseline (diffusers)** | - | - | 45.2s | 1.0x |
|
||||
| Hybrid Ulysses + Ring | 2 | 2 | 24.3s | 1.87x |
|
||||
|
||||
|
||||
### CFG-Parallel
|
||||
|
||||
#### Offline Inference
|
||||
|
||||
CFG-Parallel is enabled through `DiffusionParallelConfig(cfg_parallel_size=2)`, which runs one rank for the positive branch and one rank for the negative branch.
|
||||
|
||||
An example of offline inference using CFG-Parallel (image-to-image) is shown below:
|
||||
|
||||
```python
|
||||
from vllm_omni import Omni
|
||||
from vllm_omni.diffusion.data import DiffusionParallelConfig
|
||||
|
||||
image_path = "path_to_image.png"
|
||||
omni = Omni(
|
||||
model="Qwen/Qwen-Image-Edit",
|
||||
parallel_config=DiffusionParallelConfig(cfg_parallel_size=2),
|
||||
)
|
||||
input_image = Image.open(image_path).convert("RGB")
|
||||
|
||||
outputs = omni.generate(
|
||||
{
|
||||
"prompt": "turn this cat to a dog",
|
||||
"negative_prompt": "low quality, blurry",
|
||||
"multi_modal_data": {"image": input_image},
|
||||
},
|
||||
OmniDiffusionSamplingParams(
|
||||
true_cfg_scale=4.0,
|
||||
num_inference_steps=50,
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- CFG-Parallel is only effective when a `negative_prompt` is provided AND a guidance scale (or `cfg_scale`) is greater than 1.
|
||||
|
||||
See `examples/offline_inference/image_to_image/image_edit.py` for a complete working example.
|
||||
```bash
|
||||
cd examples/offline_inference/image_to_image/
|
||||
python image_edit.py \
|
||||
--model "Qwen/Qwen-Image-Edit" \
|
||||
--image "qwen_image_output.png" \
|
||||
--prompt "turn this cat to a dog" \
|
||||
--negative-prompt "low quality, blurry" \
|
||||
--cfg-scale 4.0 \
|
||||
--output "edited_image.png" \
|
||||
--cfg-parallel-size 2
|
||||
```
|
||||
|
||||
#### Online Serving
|
||||
|
||||
You can enable CFG-Parallel in online serving for diffusion models via `--cfg-parallel-size`:
|
||||
|
||||
```bash
|
||||
vllm serve Qwen/Qwen-Image-Edit --omni --port 8091 --cfg-parallel-size 2
|
||||
```
|
||||
|
||||
### HSDP
|
||||
|
||||
HSDP (Hybrid Sharded Data Parallel) shards model weights across GPUs to reduce per-GPU memory usage. This enables inference of large models (e.g., Wan2.2 14B) on GPUs with limited memory.
|
||||
|
||||
Unlike Tensor Parallelism which splits computation, HSDP uses PyTorch's FSDP2 to shard and redistribute weights at runtime. Each GPU only holds a fraction of the model weights, and weights are gathered on-demand during forward passes.
|
||||
|
||||
#### Configuration
|
||||
|
||||
HSDP is configured via `DiffusionParallelConfig`:
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `use_hsdp` | bool | False | Enable HSDP |
|
||||
| `hsdp_shard_size` | int | -1 | Number of GPUs to shard weights across. -1 = auto (requires other parallelism > 1) |
|
||||
| `hsdp_replicate_size` | int | 1 | Number of replica groups. Each group holds a full sharded copy |
|
||||
|
||||
**Constraints:**
|
||||
|
||||
- `hsdp_replicate_size × hsdp_shard_size == world_size`
|
||||
- HSDP cannot be used with Tensor Parallelism (`tensor_parallel_size` must be 1)
|
||||
|
||||
#### Operating Modes
|
||||
|
||||
HSDP can work in two modes:
|
||||
|
||||
- **Standalone Mode**: HSDP alone without other parallelism. Must specify `hsdp_shard_size` explicitly.
|
||||
- **Combined Mode**: HSDP overlays on top of other parallelism (Ulysses Sequence Parallel, CFG Parallel). HSDP dimensions must match world_size.
|
||||
|
||||
#### Offline Inference
|
||||
|
||||
**Standalone HSDP** (shard across 4 GPUs, no other parallelism):
|
||||
|
||||
```python
|
||||
from vllm_omni import Omni
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
from vllm_omni.diffusion.data import DiffusionParallelConfig
|
||||
|
||||
omni = Omni(
|
||||
model="Wan-AI/Wan2.2-T2V-A14B-Diffusers",
|
||||
parallel_config=DiffusionParallelConfig(
|
||||
use_hsdp=True,
|
||||
hsdp_shard_size=4, # Shard across 4 GPUs
|
||||
),
|
||||
)
|
||||
|
||||
outputs = omni.generate(
|
||||
"A cat playing piano",
|
||||
OmniDiffusionSamplingParams(num_inference_steps=50),
|
||||
)
|
||||
```
|
||||
|
||||
**Combined HSDP + Sequence Parallel**:
|
||||
|
||||
```python
|
||||
omni = Omni(
|
||||
model="Wan-AI/Wan2.2-T2V-A14B-Diffusers",
|
||||
parallel_config=DiffusionParallelConfig(
|
||||
ulysses_degree=4, # Sequence parallel
|
||||
use_hsdp=True, # HSDP overlays on SP
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
#### Online Serving
|
||||
|
||||
**Standalone HSDP** (shard model across 4 GPUs):
|
||||
|
||||
```bash
|
||||
vllm serve Wan-AI/Wan2.2-T2V-A14B-Diffusers --omni --port 8091 --use-hsdp --hsdp-shard-size 4
|
||||
```
|
||||
|
||||
**Combined with Sequence Parallel**:
|
||||
|
||||
```bash
|
||||
vllm serve Wan-AI/Wan2.2-T2V-A14B-Diffusers --omni --port 8091 --use-hsdp --usp 4
|
||||
```
|
||||
|
||||
#### Adding HSDP Support to New Models
|
||||
|
||||
For detailed instructions on adding HSDP support to new models, see the [HSDP Contributing Guide](../../design/feature/hsdp.md).
|
||||
|
||||
### Expert Parallelism
|
||||
|
||||
Unlike Tensor Parallelism which shards every layer's weights, EP only shards the MoE expert MLP blocks. This significantly reduces the memory footprint of MoE models (e.g., HunyuanImage3.0) while maintaining constant dense-equivalent compute efficiency. Expert Parallelism is enabled via `DiffusionParallelConfig.enable_expert_parallel`. And `self.ep = tp * sp * cfg * dp` for now, so at least one of TP/SP/CFG/DP should set when EP enabled.
|
||||
|
||||
#### Offline Inference
|
||||
|
||||
```python
|
||||
from vllm_omni import Omni
|
||||
from vllm_omni.diffusion.data import DiffusionParallelConfig
|
||||
|
||||
omni = Omni(
|
||||
model="tencent/HunyuanImage-3.0",
|
||||
parallel_config=DiffusionParallelConfig(tensor_parallel_size=8, enable_expert_parallel=True),
|
||||
)
|
||||
|
||||
outputs = omni.generate(
|
||||
"A brown and white dog is running on the grass",
|
||||
OmniDiffusionSamplingParams(
|
||||
num_inference_steps=50,
|
||||
width=1024,
|
||||
height=1024,
|
||||
),
|
||||
)
|
||||
```
|
||||
@@ -46,7 +46,7 @@ its stepwise request state machine. For normal diffusion inference, leave it
|
||||
disabled unless your workflow depends on this mode.
|
||||
|
||||
If you are looking for general diffusion speedups, see
|
||||
[Diffusion Features Overview](../diffusion_features.md).
|
||||
[Diffusion Acceleration Overview](../diffusion_acceleration.md).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
# TeaCache Configuration Guide
|
||||
|
||||
TeaCache speeds up diffusion model inference by caching transformer computations when consecutive timesteps are similar. This typically provides **1.5x-2.0x speedup** with minimal quality loss.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Enable TeaCache by setting `cache_backend` to `"tea_cache"`:
|
||||
|
||||
```python
|
||||
from vllm_omni import Omni
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
|
||||
# Simple configuration - model_type is automatically extracted from pipeline.__class__.__name__
|
||||
omni = Omni(
|
||||
model="Qwen/Qwen-Image",
|
||||
cache_backend="tea_cache",
|
||||
cache_config={
|
||||
"rel_l1_thresh": 0.2 # Optional, defaults to 0.2
|
||||
}
|
||||
)
|
||||
outputs = omni.generate(
|
||||
"A cat sitting on a windowsill",
|
||||
OmniDiffusionSamplingParams(
|
||||
num_inference_steps=50,
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
### Using Environment Variable
|
||||
|
||||
You can also enable TeaCache via environment variable:
|
||||
|
||||
```bash
|
||||
export DIFFUSION_CACHE_BACKEND=tea_cache
|
||||
```
|
||||
|
||||
Then initialize without explicitly setting `cache_backend`:
|
||||
|
||||
```python
|
||||
from vllm_omni import Omni
|
||||
|
||||
omni = Omni(
|
||||
model="Qwen/Qwen-Image",
|
||||
cache_config={"rel_l1_thresh": 0.2} # Optional
|
||||
)
|
||||
```
|
||||
|
||||
## Online Serving (OpenAI-Compatible)
|
||||
|
||||
Enable TeaCache for online serving by passing `--cache-backend tea_cache` when starting the server:
|
||||
|
||||
```bash
|
||||
vllm serve Qwen/Qwen-Image --omni --port 8091 \
|
||||
--cache-backend tea_cache \
|
||||
--cache-config '{"rel_l1_thresh": 0.2}'
|
||||
```
|
||||
|
||||
## Configuration Parameters
|
||||
|
||||
### `rel_l1_thresh` (float, default: `0.2`)
|
||||
|
||||
Controls the balance between speed and quality. Lower values prioritize quality, higher values prioritize speed.
|
||||
|
||||
**Recommended values:**
|
||||
|
||||
- `0.2` - **~1.5x speedup** with minimal quality loss (recommended)
|
||||
- `0.4` - **~1.8x speedup** with slight quality loss
|
||||
- `0.6` - **~2.0x speedup** with noticeable quality loss
|
||||
- `0.8` - **~2.25x speedup** with significant quality loss
|
||||
|
||||
## Examples
|
||||
|
||||
### Python API
|
||||
|
||||
```python
|
||||
from vllm_omni import Omni
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
|
||||
omni = Omni(
|
||||
model="Qwen/Qwen-Image",
|
||||
cache_backend="tea_cache",
|
||||
cache_config={"rel_l1_thresh": 0.2}
|
||||
)
|
||||
outputs = omni.generate(
|
||||
"A cat sitting on a windowsill",
|
||||
OmniDiffusionSamplingParams(
|
||||
num_inference_steps=50,
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
## Performance Tuning
|
||||
|
||||
Start with the default `rel_l1_thresh=0.2` and adjust based on your needs:
|
||||
|
||||
- **Maximum quality**: Use `0.1-0.2`
|
||||
- **Balanced**: Use `0.2-0.4` (recommended)
|
||||
- **Maximum speed**: Use `0.6-0.8` (may reduce quality)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Quality Degradation
|
||||
|
||||
If you notice quality issues, lower the threshold:
|
||||
|
||||
```python
|
||||
cache_config={"rel_l1_thresh": 0.1} # More conservative caching
|
||||
```
|
||||
|
||||
## Supported Models
|
||||
|
||||
### ImageGen
|
||||
|
||||
<style>
|
||||
th {
|
||||
white-space: nowrap;
|
||||
min-width: 0 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
| Architecture | Models | Example HF Models |
|
||||
|--------------|--------|-------------------|
|
||||
| `QwenImagePipeline` | Qwen-Image | `Qwen/Qwen-Image` |
|
||||
| `QwenImageEditPipeline` | Qwen-Image-Edit | `Qwen/Qwen-Image-Edit` |
|
||||
| `QwenImageEditPlusPipeline` | Qwen-Image-Edit-2509 | `Qwen/Qwen-Image-Edit-2509` |
|
||||
| `QwenImageLayeredPipeline` | Qwen-Image-Layered | `Qwen/Qwen-Image-Layered` |
|
||||
| `BagelForConditionalGeneration` | BAGEL (DiT-only) | `ByteDance-Seed/BAGEL-7B-MoT` |
|
||||
| `HunyuanImage3Pipeline` | HunyuanImage3 | `tencent/HunyuanImage-3.0-Instruct` |
|
||||
|
||||
### VideoGen
|
||||
|
||||
No VideoGen models are supported by TeaCache yet.
|
||||
|
||||
### Coming Soon
|
||||
|
||||
<style>
|
||||
th {
|
||||
white-space: nowrap;
|
||||
min-width: 0 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
| Architecture | Models | Example HF Models |
|
||||
|--------------|--------|-------------------|
|
||||
| `FluxPipeline` | Flux | - |
|
||||
| `CogVideoXPipeline` | CogVideoX | - |
|
||||
@@ -0,0 +1,380 @@
|
||||
# Diffusion Acceleration Overview
|
||||
|
||||
vLLM-Omni supports various acceleration methods to speed up diffusion model inference with minimal quality degradation. These include **cache methods** that intelligently cache intermediate computations to avoid redundant work across diffusion timesteps, **parallelism methods** that distribute the computation across multiple devices, and **quantization methods** that reduce memory footprint while preserving accuracy.
|
||||
|
||||
## Supported Acceleration Methods
|
||||
|
||||
vLLM-Omni currently supports two main cache acceleration backends:
|
||||
|
||||
1. **[TeaCache](diffusion/teacache.md)** - Hook-based adaptive caching that caches transformer computations when consecutive timesteps are similar
|
||||
2. **[Cache-DiT](diffusion/cache_dit_acceleration.md)** - Library-based acceleration using multiple techniques:
|
||||
- **DBCache** (Dual Block Cache): Caches intermediate transformer block outputs based on residual differences
|
||||
- **TaylorSeer**: Uses Taylor expansion-based forecasting for faster inference
|
||||
- **SCM** (Step Computation Masking): Selectively computes steps based on adaptive masking
|
||||
|
||||
Both methods can provide significant speedups (typically **1.5x-2.0x**) while maintaining high output quality.
|
||||
|
||||
vLLM-Omni also supports quantization methods:
|
||||
|
||||
3. **[Quantization](diffusion/quantization/overview.md)** - Reduces DiT linear layers from BF16 to FP8 or Int8, providing ~1.28x speedup with minimal quality loss. Supports per-layer skip for sensitive layers.
|
||||
|
||||
vLLM-Omni also supports parallelism methods for diffusion models, including:
|
||||
|
||||
1. [Ulysses-SP](diffusion/parallelism_acceleration.md#ulysses-sp) - splits the input along the sequence dimension and uses all-to-all communication to allow each device to compute only a subset of attention heads.
|
||||
|
||||
2. [Ring-Attention](diffusion/parallelism_acceleration.md#ring-attention) - splits the input along the sequence dimension and uses ring-based P2P communication to accumulate attention results, keeping the sequence dimension sharded.
|
||||
|
||||
3. [CFG-Parallel](diffusion/parallelism_acceleration.md#cfg-parallel) - runs the positive/negative prompts of classifier-free guidance (CFG) on different devices, then merges on a single device to perform the scheduler step.
|
||||
|
||||
4. [Tensor Parallelism](diffusion/parallelism_acceleration.md#tensor-parallelism) - shards DiT weights across devices to reduce per-GPU memory usage.
|
||||
|
||||
5. [VAE Patch Parallelism](diffusion/parallelism_acceleration.md#vae-patch-parallelism) - shards VAE decode/encode spatially across ranks to reduce VAE peak memory (and can speed up VAE decode).
|
||||
|
||||
6. [HSDP](diffusion/parallelism_acceleration.md#hsdp) - Hybrid Sharded Data Parallel shards model weights across GPUs to reduce per-GPU memory usage, enabling inference of large models on limited GPU memory.
|
||||
|
||||
## Quick Comparison
|
||||
|
||||
### Cache Methods
|
||||
|
||||
| Method | Configuration | Description | Best For |
|
||||
|--------|--------------|-------------|----------|
|
||||
| **TeaCache** | `cache_backend="tea_cache"` | Simple, adaptive caching with minimal configuration | Quick setup, balanced speed/quality |
|
||||
| **Cache-DiT** | `cache_backend="cache_dit"` | Advanced caching with multiple techniques (DBCache, TaylorSeer, SCM) | Maximum acceleration, fine-grained control |
|
||||
|
||||
### Quantization Methods
|
||||
|
||||
| Method | Configuration | Description | Best For |
|
||||
|--------|--------------|-------------|----------|
|
||||
| **FP8** | `quantization="fp8"` | FP8 W8A8 on Ada/Hopper, weight-only on older GPUs | Memory reduction, inference speedup |
|
||||
| **Int8** | `quantization="int8"` | Int8 W8A8 | Memory reduction, inference speedup |
|
||||
|
||||
## Supported Models
|
||||
|
||||
The following table shows which models are currently supported by each acceleration method:
|
||||
|
||||
### ImageGen
|
||||
|
||||
| Model | Model Identifier | TeaCache | Cache-DiT | Ulysses-SP | Ring-Attention | CFG-Parallel | Tensor-Parallel | VAE-Patch-Parallel |
|
||||
|-------|------------------|:--------:|:---------:|:----------:|:--------------:|:------------:|:---------------:|:------------------:|
|
||||
| **LongCat-Image** | `meituan-longcat/LongCat-Image` | ❌ | ✅ | ❌ | ❌ | ✅ | ✅ | ❌ |
|
||||
| **LongCat-Image-Edit** | `meituan-longcat/LongCat-Image-Edit` | ❌ | ✅ | ❌ | ❌ | ✅ | ✅ | ❌ |
|
||||
| **Ovis-Image** | `OvisAI/Ovis-Image` | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ |
|
||||
| **Qwen-Image** | `Qwen/Qwen-Image` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| **Qwen-Image-2512** | `Qwen/Qwen-Image-2512` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| **Qwen-Image-Edit** | `Qwen/Qwen-Image-Edit` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
| **Qwen-Image-Edit-2509** | `Qwen/Qwen-Image-Edit-2509` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
| **Qwen-Image-Layered** | `Qwen/Qwen-Image-Layered` | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
| **Z-Image** | `Tongyi-MAI/Z-Image-Turbo` | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ (TP=2 only) | ✅ |
|
||||
| **Stable-Diffusion3.5** | `stabilityai/stable-diffusion-3.5` | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ |
|
||||
| **Bagel** | `ByteDance-Seed/BAGEL-7B-MoT` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
| **FLUX.1-dev** | `black-forest-labs/FLUX.1-dev` | ❌ | ✅ | ❌ | ❌ | ✅ | ✅ | ❌ |
|
||||
| **NextStep-1.1** | `stepfun-ai/NextStep-1.1` | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ |
|
||||
| **FLUX.2-klein** | `black-forest-labs/FLUX.2-klein-4B` | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ | ❌ |
|
||||
| **FLUX.2-dev** | `black-forest-labs/FLUX.2-dev` | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | ❌ |
|
||||
| **FLUX.1-Kontext-dev** | `black-forest-labs/FLUX.1-Kontext-dev` | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ |
|
||||
| **GLM-Image** | `zai-org/GLM-Image` | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ |
|
||||
|
||||
### VideoGen
|
||||
|
||||
| Model | Model Identifier | TeaCache | Cache-DiT | Ulysses-SP | Ring-Attention | CFG-Parallel | HSDP | VAE-Patch-Parallel |
|
||||
|-------|------------------|:--------:|:---------:|:----------:|:--------------:|:------------:|:----:|:----:|
|
||||
| **Wan2.1-T2V** | `Wan-AI/Wan2.1-T2V-1.3B-Diffusers` | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| **Wan2.1-T2V** | `Wan-AI/Wan2.1-T2V-14B-Diffusers` | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| **Wan2.2** | `Wan-AI/Wan2.2-T2V-A14B-Diffusers` | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| **LTX-2** | `Lightricks/LTX-2` | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| **DreamID-Omni** | `XuGuo699/DreamID-Omni` | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ |
|
||||
|
||||
### Quantization
|
||||
|
||||
| Model | Model Identifier | FP8 | Int8 |
|
||||
|-------|------------------|:---:|:---:|
|
||||
| **Qwen-Image** | `Qwen/Qwen-Image` | ✅ | ✅ |
|
||||
| **Qwen-Image-2512** | `Qwen/Qwen-Image-2512` | ✅ | ✅ |
|
||||
| **Z-Image** | `Tongyi-MAI/Z-Image-Turbo` | ✅ | ✅ |
|
||||
|
||||
|
||||
### AudioGen
|
||||
|
||||
| Model | Model Identifier | TeaCache | Cache-DiT | Ulysses-SP | Ring-Attention | CFG-Parallel |
|
||||
|------------------------|------------------------------------------|:--------:|:---------:|:----------:|:--------------:|:------------:|
|
||||
| **Stable-Audio-Open** | `stabilityai/stable-audio-open-1.0` | ✅ | | | | |
|
||||
|
||||
## Performance Benchmarks
|
||||
|
||||
The following benchmarks were measured on **Qwen/Qwen-Image** and **Qwen/Qwen-Image-Edit** models generating 1024x1024 images with 50 inference steps:
|
||||
|
||||
!!! note "Benchmark Disclaimer"
|
||||
These benchmarks are provided for **general reference only**. The configurations shown use default or common parameter settings and have not been exhaustively optimized for maximum performance. Actual performance may vary based on:
|
||||
|
||||
- Specific model and use case
|
||||
- Hardware configuration
|
||||
- Careful parameter tuning
|
||||
- Different inference settings (e.g., number of steps, image resolution)
|
||||
|
||||
For optimal performance in your specific scenario, we recommend experimenting with different parameter configurations as described in the detailed guides below.
|
||||
|
||||
| Model | Cache Backend | Cache Config | Generation Time | Speedup | Notes |
|
||||
|-------|---------------|--------------|----------------|---------|-------|
|
||||
| **Qwen/Qwen-Image** | None | None | 20.0s | 1.0x | Baseline (diffusers) |
|
||||
| **Qwen/Qwen-Image** | TeaCache | `rel_l1_thresh=0.2` | 10.47s | **1.91x** | Recommended default setting |
|
||||
| **Qwen/Qwen-Image** | Cache-DiT | DBCache + TaylorSeer (Fn=1, Bn=0, W=8, TaylorSeer order=1) | 10.8s | **1.85x** | - |
|
||||
| **Qwen/Qwen-Image** | Cache-DiT | DBCache + TaylorSeer + SCM (Fn=8, Bn=0, W=4, TaylorSeer order=1, SCM fast) | 14.0s | **1.43x** | - |
|
||||
| **Qwen/Qwen-Image-Edit** | None | No acceleration | 51.5s | 1.0x | Baseline (diffusers) |
|
||||
| **Qwen/Qwen-Image-Edit** | Cache-DiT | Default (Fn=1, Bn=0, W=4, TaylorSeer disabled, SCM disabled) | 21.6s | **2.38x** | - |
|
||||
|
||||
To measure the parallelism methods, we run benchmarks with **Qwen/Qwen-Image** model generating images (**2048x2048** as long sequence input) with 50 inference steps. The hardware devices are NVIDIA H800 GPUs. `sdpa` is the attention backends.
|
||||
|
||||
| Configuration | Ulysses degree |Generation Time | Speedup |
|
||||
|---------------|----------------|---------|---------|
|
||||
| **Baseline (diffusers)** | - | 112.5s | 1.0x |
|
||||
| Ulysses-SP | 2 | 65.2s | 1.73x |
|
||||
| Ulysses-SP | 4 | 39.6s | 2.84x |
|
||||
| Ulysses-SP | 8 | 30.8s | 3.65x |
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Using TeaCache
|
||||
|
||||
```python
|
||||
from vllm_omni import Omni
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
|
||||
omni = Omni(
|
||||
model="Qwen/Qwen-Image",
|
||||
cache_backend="tea_cache",
|
||||
cache_config={"rel_l1_thresh": 0.2} # Optional, defaults to 0.2
|
||||
)
|
||||
|
||||
outputs = omni.generate(
|
||||
"A cat sitting on a windowsill",
|
||||
OmniDiffusionSamplingParams(
|
||||
num_inference_steps=50,
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
### Using Cache-DiT
|
||||
|
||||
```python
|
||||
from vllm_omni import Omni
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
|
||||
omni = Omni(
|
||||
model="Qwen/Qwen-Image",
|
||||
cache_backend="cache_dit",
|
||||
cache_config={
|
||||
"Fn_compute_blocks": 1,
|
||||
"Bn_compute_blocks": 0,
|
||||
"max_warmup_steps": 8,
|
||||
"enable_taylorseer": True,
|
||||
"taylorseer_order": 1,
|
||||
}
|
||||
)
|
||||
|
||||
outputs = omni.generate(
|
||||
"A cat sitting on a windowsill",
|
||||
OmniDiffusionSamplingParams(
|
||||
num_inference_steps=50,
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
### Using Ulysses-SP
|
||||
|
||||
Run text-to-image:
|
||||
```python
|
||||
from vllm_omni import Omni
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
from vllm_omni.diffusion.data import DiffusionParallelConfig
|
||||
ulysses_degree = 2
|
||||
|
||||
omni = Omni(
|
||||
model="Qwen/Qwen-Image",
|
||||
parallel_config=DiffusionParallelConfig(ulysses_degree=ulysses_degree)
|
||||
)
|
||||
|
||||
outputs = omni.generate(
|
||||
"A cat sitting on a windowsill",
|
||||
OmniDiffusionSamplingParams(num_inference_steps=50, width=2048, height=2048),
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
Run image-to-image:
|
||||
```python
|
||||
from vllm_omni import Omni
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
from vllm_omni.diffusion.data import DiffusionParallelConfig
|
||||
ulysses_degree = 2
|
||||
|
||||
omni = Omni(
|
||||
model="Qwen/Qwen-Image-Edit",
|
||||
parallel_config=DiffusionParallelConfig(ulysses_degree=ulysses_degree)
|
||||
)
|
||||
|
||||
outputs = omni.generate(
|
||||
{
|
||||
"prompt": "turn this cat to a dog",
|
||||
"multi_modal_data": {"image": input_image}
|
||||
},
|
||||
OmniDiffusionSamplingParams(num_inference_steps=50),
|
||||
)
|
||||
```
|
||||
|
||||
### Using Ring-Attention
|
||||
|
||||
Run text-to-image:
|
||||
```python
|
||||
from vllm_omni import Omni
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
from vllm_omni.diffusion.data import DiffusionParallelConfig
|
||||
ring_degree = 2
|
||||
|
||||
omni = Omni(
|
||||
model="Qwen/Qwen-Image",
|
||||
parallel_config=DiffusionParallelConfig(ring_degree=2)
|
||||
)
|
||||
|
||||
outputs = omni.generate(
|
||||
"A cat sitting on a windowsill",
|
||||
OmniDiffusionSamplingParams(num_inference_steps=50, width=2048, height=2048),
|
||||
)
|
||||
```
|
||||
|
||||
### Using Tensor Parallelism
|
||||
|
||||
```python
|
||||
from vllm_omni import Omni
|
||||
from vllm_omni.diffusion.data import DiffusionParallelConfig
|
||||
|
||||
omni = Omni(
|
||||
model="Tongyi-MAI/Z-Image-Turbo",
|
||||
parallel_config=DiffusionParallelConfig(tensor_parallel_size=2),
|
||||
)
|
||||
|
||||
outputs = omni.generate(
|
||||
prompt="a cat reading a book",
|
||||
num_inference_steps=9,
|
||||
width=512,
|
||||
height=512,
|
||||
)
|
||||
```
|
||||
|
||||
### Using VAE Patch Parallelism
|
||||
|
||||
```python
|
||||
from vllm_omni import Omni
|
||||
from vllm_omni.diffusion.data import DiffusionParallelConfig
|
||||
|
||||
omni = Omni(
|
||||
model="Tongyi-MAI/Z-Image-Turbo",
|
||||
parallel_config=DiffusionParallelConfig(
|
||||
tensor_parallel_size=2,
|
||||
vae_patch_parallel_size=2,
|
||||
),
|
||||
)
|
||||
|
||||
outputs = omni.generate(
|
||||
prompt="a cat reading a book",
|
||||
num_inference_steps=9,
|
||||
width=1024,
|
||||
height=1024,
|
||||
)
|
||||
```
|
||||
|
||||
### Using HSDP
|
||||
|
||||
HSDP (Hybrid Sharded Data Parallel) shards model weights across GPUs to reduce per-GPU memory usage. This enables inference of large models (e.g., Wan2.2 14B) on GPUs with limited memory.
|
||||
|
||||
```python
|
||||
from vllm_omni import Omni
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
from vllm_omni.diffusion.data import DiffusionParallelConfig
|
||||
|
||||
omni = Omni(
|
||||
model="Wan-AI/Wan2.2-T2V-A14B-Diffusers",
|
||||
parallel_config=DiffusionParallelConfig(
|
||||
use_hsdp=True,
|
||||
hsdp_replicate_size=1, # Number of replica groups
|
||||
hsdp_shard_size=8, # Number of GPUs to shard across (or -1 for auto)
|
||||
),
|
||||
)
|
||||
|
||||
outputs = omni.generate(
|
||||
"A cat playing piano",
|
||||
OmniDiffusionSamplingParams(num_inference_steps=50),
|
||||
)
|
||||
```
|
||||
|
||||
### Using CFG-Parallel
|
||||
|
||||
Run image-to-image:
|
||||
|
||||
CFG-Parallel splits the CFG positive/negative branches across GPUs. Use it when you set a non-trivial `true_cfg_scale`.
|
||||
|
||||
```python
|
||||
from vllm_omni import Omni
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
from vllm_omni.diffusion.data import DiffusionParallelConfig
|
||||
cfg_parallel_size = 2
|
||||
|
||||
omni = Omni(
|
||||
model="Qwen/Qwen-Image-Edit",
|
||||
parallel_config=DiffusionParallelConfig(cfg_parallel_size=cfg_parallel_size)
|
||||
)
|
||||
|
||||
outputs = omni.generate(
|
||||
{
|
||||
"prompt": "turn this cat to a dog",
|
||||
"multi_modal_data": {"image": input_image}
|
||||
},
|
||||
OmniDiffusionSamplingParams(num_inference_steps=50, true_cfg_scale=4.0),
|
||||
)
|
||||
```
|
||||
|
||||
### Using FP8 Quantization
|
||||
|
||||
```python
|
||||
from vllm_omni import Omni
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
|
||||
omni = Omni(
|
||||
model="<your-model>",
|
||||
quantization="fp8",
|
||||
)
|
||||
|
||||
outputs = omni.generate(
|
||||
"A cat sitting on a windowsill",
|
||||
OmniDiffusionSamplingParams(num_inference_steps=50),
|
||||
)
|
||||
```
|
||||
|
||||
### Using Int8 Quantization
|
||||
|
||||
```python
|
||||
from vllm_omni import Omni
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
|
||||
omni = Omni(
|
||||
model="<your-model>",
|
||||
quantization="int8",
|
||||
)
|
||||
|
||||
outputs = omni.generate(
|
||||
"A cat sitting on a windowsill",
|
||||
OmniDiffusionSamplingParams(num_inference_steps=50),
|
||||
)
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
For detailed information on each acceleration method:
|
||||
|
||||
- **[TeaCache Guide](diffusion/teacache.md)** - Complete TeaCache documentation, configuration options, and best practices
|
||||
- **[Cache-DiT Acceleration Guide](diffusion/cache_dit_acceleration.md)** - Comprehensive Cache-DiT guide covering DBCache, TaylorSeer, SCM, and configuration parameters
|
||||
- **[Quantization Guide](diffusion/quantization/overview.md)** - Quantization for DiT models with per-layer control
|
||||
- **[Tensor Parallelism](diffusion/parallelism_acceleration.md#tensor-parallelism)** - Guidance on how to enable TP for diffusion models.
|
||||
- **[Sequence Parallelism](diffusion/parallelism_acceleration.md#sequence-parallelism)** - Guidance on how to set sequence parallelism with configuration.
|
||||
- **[CFG-Parallel](diffusion/parallelism_acceleration.md#cfg-parallel)** - Guidance on how to set CFG-Parallel to run positive/negative branches across ranks.
|
||||
- **[VAE Patch Parallelism](diffusion/parallelism_acceleration.md#vae-patch-parallelism)** - Guidance on how to reduce VAE memory via patch/tile parallelism.
|
||||
- **[HSDP](diffusion/parallelism_acceleration.md#hsdp)** - Hybrid Sharded Data Parallel for memory-efficient inference of large models.
|
||||
@@ -1,190 +0,0 @@
|
||||
# Diffusion Advanced Features
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Supported Features](#supported-features)
|
||||
- [Supported Models](#supported-models)
|
||||
- [Feature Compatibility](#feature-compatibility)
|
||||
- [Learn More](#learn-more)
|
||||
|
||||
## Overview
|
||||
|
||||
vLLM-Omni supports various advanced features for diffusion models:
|
||||
|
||||
- Acceleration: **cache methods**, **parallelism methods**
|
||||
- Memory optimization: **cpu offloading**, **quantization**
|
||||
- Extensions: **LoRA inference**
|
||||
|
||||
## Supported Features
|
||||
|
||||
### Acceleration
|
||||
|
||||
#### Lossy Acceleration
|
||||
|
||||
Cache methods trade minimal quality for significant speedup. Quality loss is typically imperceptible with proper tuning.
|
||||
|
||||
| Method | Description | Best For |
|
||||
|--------|-------------|----------|
|
||||
| **[TeaCache](diffusion/cache_acceleration/teacache.md)** | Adaptive caching using modulated inputs | Quick setup, balanced quality/speed on single GPU |
|
||||
| **[Cache-DiT](diffusion/cache_acceleration/cache_dit.md)** | Multiple caching techniques: DBCache, TaylorSeer, SCM | Fine-grained control, tunable quality-speed tradeoff |
|
||||
|
||||
|
||||
#### Lossless Acceleration
|
||||
|
||||
Parallelism methods distribute computation across GPUs without quality loss (mathematically equivalent to single-GPU).
|
||||
|
||||
| Method | Description | Best For |
|
||||
|--------|-------------|----------|
|
||||
| **[Ulysses-SP](diffusion/parallelism/sequence_parallel.md)** | Sequence parallelism via all-to-all communication | High-resolution images (>1536px) or long videos with 2-8 GPUs |
|
||||
| **[Ring-Attention](diffusion/parallelism/sequence_parallel.md)** | Sequence parallelism via ring-based communication | Videos, very long sequences, memory-constrained, with 2-8 GPUs |
|
||||
| **[CFG-Parallel](diffusion/parallelism/cfg_parallel.md)** | Splits CFG positive/negative branches across devices | Image editing with CFG guidance (true_cfg_scale > 1) on 2 GPUs |
|
||||
| **[Tensor Parallelism](diffusion/parallelism/tensor_parallel.md)** | Shards model weights across devices | Large models that don't fit in single GPU, with 2+ GPUs |
|
||||
| **[HSDP](diffusion/parallelism/hsdp.md)** | Weight sharding via FSDP2, redistributed on-demand at runtime | Very large models (14B+) on limited VRAM, combinable with SP |
|
||||
| **[Expert Parallelism](diffusion/parallelism/expert_parallel.md)** | Shards MoE expert MLP blocks across devices | MoE diffusion models (e.g., HunyuanImage3.0) |
|
||||
|
||||
**Note:** Some acceleration methods can be combined together for optimized performance. See [Feature Compatibility Table](#feature-compatibility) and [Feature Compatibility Tutorial](feature_compatibility.md) for detailed configuration examples.
|
||||
|
||||
### Memory Optimization
|
||||
|
||||
Memory optimization methods help reduce GPU memory usage, enabling inference on resource-constrained hardware or larger models.
|
||||
|
||||
| Method | Description | Best For |
|
||||
|--------|-------------|----------|
|
||||
| **[CPU Offload](diffusion/cpu_offload_diffusion.md)** | Offloads model components to CPU memory | Limited VRAM, large models on consumer GPUs |
|
||||
| **[Quantization](diffusion/quantization/overview.md)** | Reduces DiT layers from BF16 to FP8/INT8/etc. | Limited VRAM, minimal accuracy loss |
|
||||
| **[VAE Patch Parallelism](diffusion/parallelism/vae_patch_parallel.md)** | Distributes VAE decode tiling across GPUs | High-resolution generation with reduced VAE memory peak |
|
||||
|
||||
### Extensions
|
||||
|
||||
Extension methods add specialized capabilities to diffusion models beyond standard inference.
|
||||
|
||||
| Method | Description | Best For |
|
||||
|--------|-------------|----------|
|
||||
| **[LoRA Inference](diffusion/lora.md)** | Enables inference with Low-Rank Adaptation (LoRA) adapters weights | Reinforcement learning extensions |
|
||||
|
||||
|
||||
### Quantization Methods
|
||||
|
||||
| Method | Configuration | Description | Best For |
|
||||
|--------|--------------|-------------|----------|
|
||||
| **[FP8](diffusion/quantization/fp8.md)** | `quantization="fp8"` | FP8 W8A8 on Ada/Hopper, weight-only on older GPUs | Memory reduction, inference speedup |
|
||||
| **[INT8](diffusion/quantization/int8.md)** | `quantization="int8"` | INT8 weight-only, no calibration or pre-quantized checkpoint needed | Memory reduction, broad GPU compatibility |
|
||||
| **[GGUF](diffusion/quantization/gguf.md)** | `quantization="gguf"` | Native GGUF transformer-only weights (Q4, Q8, etc.) | Memory reduction on consumer GPUs |
|
||||
|
||||
## Supported Models
|
||||
|
||||
The following tables show which models support each feature:
|
||||
|
||||
- **🔀SP (Ulysses & Ring)**: Includes both Ulysses-SP and Ring-Attention methods
|
||||
- ✅ = Fully supported
|
||||
- ❌ = Not supported
|
||||
|
||||
> Notes:
|
||||
|
||||
> 1. CPU Offload has two methods: Module-wise (default for models with DiT + text encoder) and Layerwise. The tables below show **Layerwise support** only.
|
||||
> 2. The **💾Quantization** column is collapsed for readability. See [Quantization Overview](diffusion/quantization/overview.md) for per-method (FP8, GGUF, …) and per-model support details.
|
||||
|
||||
### ImageGen
|
||||
|
||||
| Model | ⚡TeaCache | ⚡Cache-DiT | 🔀SP (Ulysses & Ring) | 🔀CFG-Parallel | 🔀Tensor-Parallel | 🔀HSDP | 💾CPU Offload (Layerwise) | 💾VAE-Patch-Parallel | 💾Quantization |
|
||||
|-------|:----------:|:-----------:|:---------------------:|:--------------:|:-----------------:|:------:|:------------------------:|:--------------------:|:--------------:|
|
||||
| **Bagel** | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
|
||||
| **FLUX.1-dev** | ❌ | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ |
|
||||
| **FLUX.2-klein** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ |
|
||||
| **FLUX.1-Kontext-dev** | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ |
|
||||
| **FLUX.2-dev** | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ |
|
||||
| **GLM-Image** | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
|
||||
| **HunyuanImage3** | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ |
|
||||
| **LongCat-Image** | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
|
||||
| **LongCat-Image-Edit** | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
|
||||
| **MammothModa2(T2I)** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||
| **Nextstep_1(T2I)** | ❓ | ❓ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
|
||||
| **OmniGen2** | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||
| **Ovis-Image** | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||
| **Qwen-Image** | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ |
|
||||
| **Qwen-Image-2512** | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ |
|
||||
| **Qwen-Image-Edit** | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ |
|
||||
| **Qwen-Image-Edit-2509** | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ |
|
||||
| **Qwen-Image-Layered** | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ |
|
||||
| **Stable-Diffusion3.5** | ❌ | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
|
||||
| **Z-Image** | ✅ | ✅ | ✅ | ❓ | ✅ (TP=2 only) | ❌ | ❌ | ✅ | ✅ |
|
||||
|
||||
> Notes:
|
||||
> 1. Nextstep_1(T2I) does not support cache acceleration methods such as TeaCache or Cache-DiT.
|
||||
> 2. `Tongyi-MAI/Z-Image-Turbo` is a distilled model with minimal NFEs; CFG-Parallel is not necessary.
|
||||
|
||||
### VideoGen
|
||||
|
||||
| Model | ⚡TeaCache | ⚡Cache-DiT | 🔀SP (Ulysses & Ring) | 🔀CFG-Parallel | 🔀Tensor-Parallel | 🔀HSDP | 💾CPU Offload (Layerwise) | 💾VAE-Patch-Parallel | 💾Quantization |
|
||||
|-------|:----------:|:-----------:|:---------------------:|:--------------:|:-----------------:|:------:|:------------------------:|:--------------------:|:--------------:|
|
||||
| **Wan2.2** | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
| **LTX-2** | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
|
||||
| **Helios** | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| **HunyuanVideo-1.5 T2V I2V** | ❌ | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| **DreamID-Omni** | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||
|
||||
### AudioGen
|
||||
|
||||
| Model | ⚡TeaCache | ⚡Cache-DiT | 🔀SP (Ulysses & Ring) | 🔀CFG-Parallel | 🔀Tensor-Parallel | 🔀HSDP | 💾CPU Offload (Layerwise) | 💾VAE-Patch-Parallel | 💾Quantization |
|
||||
|-------|:----------:|:-----------:|:---------------------:|:--------------:|:-----------------:|:------:|:------------------------:|:--------------------:|:--------------:|
|
||||
| **Stable-Audio-Open** | ❌ | ❌ | ❓ | ❓ | ❌ | ❌ | ❌ | ❌ | ✅ |
|
||||
|
||||
|
||||
## Feature Compatibility
|
||||
|
||||
**Legend:**
|
||||
|
||||
- ✅: Functionality is supported
|
||||
- ❌: No support plan
|
||||
- ❓: Not verified yet and Not Recommended
|
||||
|
||||
| | ⚡TeaCache | ⚡Cache-DiT | 🔀Ulysses-SP | 🔀Ring-Attn | 🔀CFG-Parallel | 🔀Tensor Parallel | 🔀HSDP | 🔀Expert Parallel | 💾CPU Offloading (Layerwise) | 💾CPU Offloading (Module-wise) | 💾VAE Patch Parallel | 💾FP8 Quant | 🔧LoRA Inference |
|
||||
|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
||||
| **⚡TeaCache** | | | | | | | | | | | | | |
|
||||
| **⚡Cache-DiT** | ❌ | | | | | | | | | | | | |
|
||||
| **🔀Ulysses-SP** | ✅ | ✅ | | | | | | | | | | | |
|
||||
| **🔀Ring-Attn** | ✅ | ✅ | ✅ | | | | | | | | | | |
|
||||
| **🔀CFG-Parallel** | ✅ | ✅ | ✅ | ✅ | | | | | | | | | |
|
||||
| **🔀Tensor Parallel** | ✅ | ✅ | ✅ | ✅ | ✅ | | | | | | | | |
|
||||
| **🔀HSDP** | ❓ | ❓ | ❓ | ❓ | ❓ | ❌ | | | | | | | |
|
||||
| **🔀Expert Parallel** | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | | | | | | |
|
||||
| **💾CPU Offloading (Layerwise)** | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | | | | |
|
||||
| **💾CPU Offloading (Module-wise)** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❓ | ❓ | ❌ | | | | |
|
||||
| **💾VAE Patch Parallel** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | | | |
|
||||
| **💾FP8 Quant** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❓ | ❓ | ✅ | ✅ | ✅ | | |
|
||||
| **🔧LoRA Inference** | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | |
|
||||
|
||||
!!! info
|
||||
|
||||
1. Tensor Parallel and HSDP are not compatible.
|
||||
2. TeaCache and Cache-DiT are not compatible.
|
||||
3. CPU Offloading (Layerwise) and CPU Offloading (Module-wise) are not compatible.
|
||||
4. CPU Offloading (Layerwise) supports single-card for now.
|
||||
5. Using FP8-Quant as an example of qunatization methods.
|
||||
|
||||
|
||||
## Learn More
|
||||
|
||||
**Cache Acceleration:**
|
||||
|
||||
- **[TeaCache Configuration Guide](diffusion/cache_acceleration/teacache.md)** - Parameter tuning, performance tips, troubleshooting
|
||||
- **[Cache-DiT Advanced Guide](diffusion/cache_acceleration/cache_dit.md)** - DBCache, TaylorSeer, SCM techniques and optimization
|
||||
|
||||
**Parallelism Methods:**
|
||||
|
||||
- **[Parallelism Overview](diffusion/parallelism/overview.md)** - Tensor Parallelism, Sequence Parallelism, CFG Parallelism, HSDP, and Expert Parallelism
|
||||
|
||||
**Memory Optimization:**
|
||||
|
||||
- **[CPU Offload Guide](diffusion/cpu_offload_diffusion.md)** - Offload model components to CPU, reduce GPU memory usage
|
||||
- **[VAE Patch Parallelism Guide](diffusion/parallelism/vae_patch_parallel.md)** - Distribute VAE decode tiling across GPUs for high-resolution images
|
||||
- **[Quantization Overview](diffusion/quantization/overview.md)** - Overview of quantization methods for diffusion models
|
||||
|
||||
**Extensions:**
|
||||
|
||||
- **[LoRA Inference Guide](diffusion/lora.md)** - Low-Rank Adaptation for style customization and fine-tuning
|
||||
|
||||
**Advanced Topics:**
|
||||
|
||||
- **[Feature Compatibility](feature_compatibility.md)** - How to combine multiple features for maximum performance
|
||||
@@ -1,218 +0,0 @@
|
||||
# Feature Compatibility
|
||||
|
||||
This guide explains the compatibility matrix of different diffusion features in vLLM-Omni. You can use cache methods together with parallelism methods and other features to achieve optimal speed and efficiency.
|
||||
|
||||
## Overview
|
||||
|
||||
vLLM-Omni supports combining:
|
||||
|
||||
- **Cache methods** (TeaCache, Cache-DiT) with **Parallelism methods** (Ulysses-SP, Ring-Attention, CFG-Parallel, Tensor Parallelism)
|
||||
- **Multiple parallelism methods** together (e.g., Ulysses-SP + Ring-Attention, CFG-Parallel + Sequence Parallelism)
|
||||
- **LoRA adapters** with most acceleration features
|
||||
- **CPU offloading** with other memory optimization features
|
||||
|
||||
See the feature compatibility matrix in [Table](diffusion_features.md#feature-compatibility)
|
||||
|
||||
## Common Combinations
|
||||
|
||||
### 1. Cache + Sequence Parallelism (Recommended)
|
||||
|
||||
Best for: **Large images (>1536px) or videos**
|
||||
|
||||
Combines cache acceleration with sequence parallelism for maximum speedup on single-device-challenging workloads.
|
||||
|
||||
**Using TeaCache + Ulysses-SP:**
|
||||
|
||||
```bash
|
||||
python examples/offline_inference/text_to_image/text_to_image.py \
|
||||
--model Qwen/Qwen-Image \
|
||||
--prompt "A beautiful mountain landscape" \
|
||||
--cache-backend tea_cache \
|
||||
--ulysses-degree 2
|
||||
```
|
||||
|
||||
**Using Cache-DiT + Ring-Attention:**
|
||||
|
||||
```bash
|
||||
python examples/offline_inference/text_to_image/text_to_image.py \
|
||||
--model Qwen/Qwen-Image \
|
||||
--prompt "A futuristic city" \
|
||||
--cache-backend cache_dit \
|
||||
--ring-degree 2
|
||||
```
|
||||
|
||||
### 2. Cache + CFG-Parallel
|
||||
|
||||
Best for: **Image editing with Classifier-Free Guidance**
|
||||
|
||||
Accelerates both the diffusion process and CFG computation.
|
||||
|
||||
```bash
|
||||
python examples/offline_inference/image_to_image/image_edit.py \
|
||||
--model Qwen/Qwen-Image-Edit \
|
||||
--prompt "make it sunset" \
|
||||
--negative-prompt "low quality, blurry" \
|
||||
--image input.png \
|
||||
--cache-backend cache_dit \
|
||||
--cfg-parallel-size 2 \
|
||||
--cfg-scale 4.0
|
||||
```
|
||||
|
||||
### 3. CFG-Parallel + Sequence Parallelism
|
||||
|
||||
Best for: **Large resolution image editing with CFG**
|
||||
|
||||
Combines both CFG branch splitting and sequence parallelism for maximum GPU utilization.
|
||||
|
||||
**CFG-Parallel + Ulysses-SP:**
|
||||
|
||||
```bash
|
||||
python examples/offline_inference/image_to_image/image_edit.py \
|
||||
--model Qwen/Qwen-Image-Edit \
|
||||
--prompt "transform into autumn scene" \
|
||||
--negative-prompt "low quality" \
|
||||
--image input.png \
|
||||
--cache-backend cache_dit \
|
||||
--cfg-parallel-size 2 \
|
||||
--ulysses-degree 2 \
|
||||
--cfg-scale 4.0
|
||||
```
|
||||
|
||||
### 4. Hybrid Ulysses + Ring + Vae tiling
|
||||
|
||||
Best for: **Very large images or videos on multiple devices**
|
||||
|
||||
Combines Ulysses-SP (all-to-all) with Ring-Attention (ring P2P) for scalable parallelism.
|
||||
|
||||
```bash
|
||||
python examples/offline_inference/text_to_image/text_to_image.py \
|
||||
--model Qwen/Qwen-Image \
|
||||
--prompt "Epic fantasy landscape" \
|
||||
--cache-backend cache_dit \
|
||||
--ulysses-degree 2 \
|
||||
--ring-degree 2 \
|
||||
--num-inference-steps 50 \
|
||||
--width 2048 \
|
||||
--height 2048 \
|
||||
--vae-use-tiling
|
||||
```
|
||||
|
||||
### 5. Cache + Tensor Parallelism
|
||||
|
||||
Best for: **Large models that don't fit in single GPU memory**
|
||||
|
||||
Reduces per-GPU memory usage while maintaining cache acceleration.
|
||||
|
||||
```bash
|
||||
python examples/offline_inference/text_to_image/text_to_image.py \
|
||||
--model Tongyi-MAI/Z-Image-Turbo \
|
||||
--prompt "A cat reading a book" \
|
||||
--cache-backend tea_cache \
|
||||
--tensor-parallel-size 2 \
|
||||
--num-inference-steps 9 \
|
||||
```
|
||||
|
||||
## Online Serving
|
||||
|
||||
### Cache + Sequence Parallelism
|
||||
|
||||
```bash
|
||||
# TeaCache + Ulysses-SP
|
||||
vllm serve Qwen/Qwen-Image --omni --port 8091 \
|
||||
--cache-backend tea_cache \
|
||||
--cache-config '{"rel_l1_thresh": 0.2}' \
|
||||
--usp 2
|
||||
|
||||
# Cache-DiT + Ring-Attention
|
||||
vllm serve Qwen/Qwen-Image --omni --port 8091 \
|
||||
--cache-backend cache_dit \
|
||||
--cache-config '{"Fn_compute_blocks": 1, "max_warmup_steps": 8}' \
|
||||
--ring 2
|
||||
```
|
||||
|
||||
### Cache + CFG-Parallel
|
||||
|
||||
```bash
|
||||
vllm serve Qwen/Qwen-Image-Edit --omni --port 8091 \
|
||||
--cache-backend cache_dit \
|
||||
--cfg-parallel-size 2
|
||||
```
|
||||
|
||||
### Multiple Parallelism Methods
|
||||
|
||||
```bash
|
||||
# CFG-Parallel + Ulysses-SP (4 GPUs total)
|
||||
vllm serve Qwen/Qwen-Image-Edit --omni --port 8091 \
|
||||
--cache-backend cache_dit \
|
||||
--cfg-parallel-size 2 \
|
||||
--usp 2
|
||||
|
||||
# Hybrid Ulysses + Ring (4 GPUs total)
|
||||
vllm serve Qwen/Qwen-Image --omni --port 8091 \
|
||||
--cache-backend cache_dit \
|
||||
--usp 2 \
|
||||
--ring 2
|
||||
```
|
||||
|
||||
|
||||
## Limitations
|
||||
|
||||
### Incompatibilities
|
||||
|
||||
- **TeaCache + Cache-DiT**: These two cache methods cannot be used together. Only one cache backend can be active at a time. Attempting to enable both will result in an error.
|
||||
|
||||
### Partial Support
|
||||
|
||||
- **Tensor Parallelism — Text Encoder Not Sharded**: TP currently only shards the DiT blocks. Each TP rank retains a **full copy of the text encoder weights**, leading to significant GPU memory overhead proportional to TP degree. Tracked in [Issue #771](https://github.com/vllm-project/vllm-omni/issues/771).
|
||||
|
||||
- **CPU Offloading — Two Modes Are Mutually Exclusive**: Model-level offload (`enable_cpu_offload`) and layerwise offload (`enable_layerwise_offload`) cannot be used simultaneously. If both are set, layerwise takes priority and model-level is silently ignored.
|
||||
|
||||
- **CPU Offloading — VAE stays on GPU**: Both offloading strategies keep the VAE on GPU at all times. For high-resolution generation, VAE decode can still cause OOM. Mitigate by combining with `vae_use_tiling=True` or VAE Patch Parallelism.
|
||||
|
||||
- **VAE Patch Parallelism — DistributedVaeExecutor Required**: VAE Patch Parallelism is only enabled for models that have `DistributedVaeExecutor`. Unsupported models will silently ignore `vae_patch_parallel_size`, and use sequential vae tiling instead.
|
||||
|
||||
### Configuration Constraints
|
||||
|
||||
- **GPU Count Must Match Parallel Degrees**: Total GPU count must satisfy:
|
||||
```
|
||||
total_gpus = ulysses_degree × ring_degree × cfg_parallel_size × tensor_parallel_size
|
||||
```
|
||||
Any mismatch will cause a configuration error at startup.
|
||||
|
||||
- **VAE Patch Parallel Size ≤ DiT Process Group Size**: `vae_patch_parallel_size` reuses the DiT process group and cannot exceed it. Larger values are automatically clamped with a warning.
|
||||
|
||||
- **Model-Specific TP Constraints**: Some models impose divisibility constraints on TP size. For example, Z-Image Turbo (`num_heads=30`) only supports `tensor_parallel_size=2`. Check [Supported Models](diffusion_features.md#supported-models) for per-model constraints.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Performance Not Scaling
|
||||
|
||||
**Symptoms:** Adding more GPUs doesn't improve speed proportionally
|
||||
|
||||
**Solutions:**
|
||||
1. Check GPU communication bandwidth (use `nvidia-smi topo -m`)
|
||||
2. Reduce parallelism degree if communication overhead is high
|
||||
3. For very long sequences, prefer Ring-Attention over Ulysses-SP
|
||||
4. Ensure batch size is large enough to saturate GPUs
|
||||
|
||||
### Out of Memory with Parallelism
|
||||
|
||||
**Symptoms:** OOM errors when combining methods
|
||||
|
||||
**Solutions:**
|
||||
1. Enable Tensor Parallelism to shard weights
|
||||
2. Reduce resolution or batch size
|
||||
3. Combine with memory efficient methods, such as cpu offloading
|
||||
|
||||
### Configuration Errors
|
||||
|
||||
**Symptoms:** Errors about invalid parallel configuration
|
||||
|
||||
**Solutions:**
|
||||
1. Verify total GPU count matches: `ulysses × ring × cfg × tp`
|
||||
2. Check model supports all enabled methods
|
||||
3. Ensure divisibility constraints (e.g., Z-Image TP=1 or 2 only)
|
||||
|
||||
## See Also
|
||||
|
||||
- [Diffusion Acceleration Overview](diffusion_features.md) - Main acceleration guide
|
||||
@@ -53,7 +53,6 @@ dev = [
|
||||
"av", # for ComfyUI tests
|
||||
"openpyxl>=3.0.0", # for nightly CI
|
||||
"pyttsx3>=2.99",
|
||||
"opencc>=1.2.0",
|
||||
"mistune>=3.2.0", # for example tests
|
||||
]
|
||||
|
||||
|
||||
+51
-679
@@ -5,7 +5,6 @@ import json
|
||||
import math
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import tempfile
|
||||
|
||||
import requests
|
||||
@@ -23,7 +22,6 @@ import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
from collections.abc import Generator
|
||||
from dataclasses import dataclass
|
||||
from io import BytesIO
|
||||
@@ -39,7 +37,6 @@ import torch
|
||||
import yaml
|
||||
from openai import OpenAI, omit
|
||||
from PIL import Image
|
||||
from transformers import pipeline
|
||||
from vllm import TextPrompt
|
||||
from vllm.distributed.parallel_state import cleanup_dist_env_and_memory
|
||||
from vllm.logger import init_logger
|
||||
@@ -55,13 +52,6 @@ PromptAudioInput = list[tuple[Any, int]] | tuple[Any, int] | None
|
||||
PromptImageInput = list[Any] | Any | None
|
||||
PromptVideoInput = list[Any] | Any | None
|
||||
|
||||
_GENDER_PIPELINE = None
|
||||
# transformers.Pipeline is not thread-safe; concurrent e2e requests must serialize inference.
|
||||
_GENDER_PIPELINE_LOCK = threading.Lock()
|
||||
|
||||
# int16 mono PCM from /v1/audio/speech when response_format=pcm (Qwen3-TTS code2wav output rate).
|
||||
_PCM_SPEECH_SAMPLE_RATE_HZ = 24_000
|
||||
|
||||
|
||||
class OmniServerParams(NamedTuple):
|
||||
model: str
|
||||
@@ -273,6 +263,7 @@ def assert_video_valid(
|
||||
|
||||
def assert_audio_valid(path: Path, *, sample_rate: int, channels: int, duration_s: float) -> None:
|
||||
"""Assert the WAV has the expected sample rate, channel count, and duration."""
|
||||
|
||||
assert path.exists(), f"Audio not found: {path}"
|
||||
info = sf.info(str(path))
|
||||
assert info.samplerate == sample_rate, f"Expected sample_rate={sample_rate}, got {info.samplerate}"
|
||||
@@ -475,6 +466,7 @@ def generate_synthetic_audio(
|
||||
"""
|
||||
Generate TTS speech with pyttsx3 and return base64 string.
|
||||
"""
|
||||
import tempfile
|
||||
|
||||
import pyttsx3
|
||||
import soundfile as sf
|
||||
@@ -682,8 +674,8 @@ def generate_synthetic_audio(
|
||||
# Return result
|
||||
base64_audio = base64.b64encode(audio_bytes).decode("utf-8")
|
||||
result["base64"] = base64_audio
|
||||
# Always include file_path to avoid KeyError in callers.
|
||||
result["file_path"] = output_path if save_to_file and output_path else None
|
||||
if save_to_file and output_path:
|
||||
result["file_path"] = output_path
|
||||
|
||||
return result
|
||||
|
||||
@@ -895,7 +887,7 @@ def generate_synthetic_image(width: int, height: int, save_to_file: bool = False
|
||||
|
||||
|
||||
def preprocess_text(text):
|
||||
import opencc
|
||||
import re
|
||||
|
||||
word_to_num = {
|
||||
"zero": "0",
|
||||
@@ -917,14 +909,6 @@ def preprocess_text(text):
|
||||
|
||||
text = re.sub(r"[^\w\s]", "", text)
|
||||
text = re.sub(r"\s+", " ", text)
|
||||
cc = opencc.OpenCC("t2s")
|
||||
text = cc.convert(text)
|
||||
|
||||
# Special handling for spaces between Chinese characters:
|
||||
# - Keep single spaces between English words/numbers
|
||||
# - Remove spaces only when surrounded by Chinese characters on both sides to prevent incorrect word segmentation
|
||||
text = re.sub(r"(?<=[\u4e00-\u9fff])\s+(?=[\u4e00-\u9fff])", "", text)
|
||||
|
||||
return text.lower().strip()
|
||||
|
||||
|
||||
@@ -936,7 +920,6 @@ def cosine_similarity_text(text1, text2, n: int = 3):
|
||||
|
||||
text1 = preprocess_text(text1)
|
||||
text2 = preprocess_text(text2)
|
||||
print(f"cosine similarity text1 is: {text1}, text2 is: {text2}")
|
||||
|
||||
ngrams1 = [text1[i : i + n] for i in range(len(text1) - n + 1)]
|
||||
ngrams2 = [text2[i : i + n] for i in range(len(text2) - n + 1)]
|
||||
@@ -962,7 +945,7 @@ def convert_audio_to_text(audio_data):
|
||||
Convert base64 encoded audio data to text using speech recognition.
|
||||
"""
|
||||
audio_data = base64.b64decode(audio_data)
|
||||
output_path = f"./test_{int(time.time())}.wav"
|
||||
output_path = f"./test_{int(time.time())}"
|
||||
with open(output_path, "wb") as audio_file:
|
||||
audio_file.write(audio_data)
|
||||
|
||||
@@ -971,23 +954,11 @@ def convert_audio_to_text(audio_data):
|
||||
return text
|
||||
|
||||
|
||||
def _merge_base64_audio_to_segment(base64_list: list[str]):
|
||||
"""Merge a list of base64-encoded audio chunks into one pydub AudioSegment."""
|
||||
from pydub import AudioSegment
|
||||
|
||||
merged = None
|
||||
for b64 in base64_list:
|
||||
raw = base64.b64decode(b64.split(",", 1)[-1])
|
||||
seg = AudioSegment.from_file(io.BytesIO(raw))
|
||||
merged = seg if merged is None else merged + seg
|
||||
return merged
|
||||
|
||||
|
||||
def _whisper_transcribe_in_current_process(output_path: str) -> str:
|
||||
import whisper
|
||||
|
||||
# Keep Whisper on CPU to avoid consuming GPU memory in tests.
|
||||
model = whisper.load_model("small", device="cpu")
|
||||
device = "cuda" if torch.cuda.is_available() else "cpu"
|
||||
model = whisper.load_model("small", device=device)
|
||||
try:
|
||||
text = model.transcribe(
|
||||
output_path,
|
||||
@@ -996,8 +967,14 @@ def _whisper_transcribe_in_current_process(output_path: str) -> str:
|
||||
condition_on_previous_text=False,
|
||||
)["text"]
|
||||
finally:
|
||||
# Sync GPU so in-flight ops finish before we free the model; otherwise
|
||||
# freed memory may not show up until those ops complete.
|
||||
if torch is not None and torch.cuda.is_available():
|
||||
torch.cuda.synchronize()
|
||||
del model
|
||||
gc.collect()
|
||||
if torch is not None and torch.cuda.is_available():
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
return text or ""
|
||||
|
||||
@@ -1011,26 +988,22 @@ def convert_audio_file_to_text(output_path: str) -> str:
|
||||
return future.result()
|
||||
|
||||
|
||||
def convert_audio_bytes_to_text(raw_bytes: bytes) -> str:
|
||||
"""
|
||||
Write container audio bytes (WAV, etc.) to a temp WAV file suitable for Whisper/ffmpeg.
|
||||
Normalizes with soundfile to PCM_16 WAV when possible to avoid codec issues.
|
||||
"""
|
||||
output_path = f"./test_{uuid.uuid4().hex}.wav"
|
||||
data, samplerate = sf.read(io.BytesIO(raw_bytes))
|
||||
sf.write(output_path, data, samplerate, format="WAV", subtype="PCM_16")
|
||||
text = convert_audio_file_to_text(output_path)
|
||||
return text
|
||||
|
||||
|
||||
def merge_base64_and_convert_to_text(base64_list):
|
||||
"""
|
||||
Merge a list of base64 encoded audio data and convert to text.
|
||||
"""
|
||||
merged_audio = _merge_base64_audio_to_segment(base64_list)
|
||||
output_path = f"./test_{uuid.uuid4().hex}.wav"
|
||||
from pydub import AudioSegment
|
||||
|
||||
merged_audio = None
|
||||
for base64_data in base64_list:
|
||||
audio_data = base64.b64decode(base64_data.split(",", 1)[-1])
|
||||
seg = AudioSegment.from_file(io.BytesIO(audio_data))
|
||||
if merged_audio is None:
|
||||
merged_audio = seg
|
||||
else:
|
||||
merged_audio += seg
|
||||
output_path = f"./test_{int(time.time())}"
|
||||
merged_audio.export(output_path, format="wav")
|
||||
print(f"audio data is saved: {output_path}")
|
||||
text = convert_audio_file_to_text(output_path)
|
||||
return text
|
||||
|
||||
@@ -1211,7 +1184,8 @@ def modify_stage_config(
|
||||
break
|
||||
|
||||
if target_stage is None:
|
||||
continue
|
||||
available_ids = [s.get("stage_id") for s in stage_args if "stage_id" in s]
|
||||
raise KeyError(f"Stage ID {stage_id} not found, available: {available_ids}")
|
||||
|
||||
# Delete specified paths in this stage
|
||||
for path in delete_paths:
|
||||
@@ -1489,8 +1463,6 @@ class OmniResponse:
|
||||
text_content: str | None = None
|
||||
audio_data: list[str] | None = None
|
||||
audio_content: str | None = None
|
||||
audio_format: str | None = None
|
||||
audio_bytes: bytes | None = None
|
||||
similarity: float | None = None
|
||||
e2e_latency: float | None = None
|
||||
success: bool = False
|
||||
@@ -1508,189 +1480,6 @@ class DiffusionResponse:
|
||||
error_message: str | None = None
|
||||
|
||||
|
||||
def _load_gender_pipeline():
|
||||
"""
|
||||
Lazy-load a cached audio-classification pipeline for gender.
|
||||
|
||||
We prefer the pipeline wrapper because it encapsulates processor/model loading
|
||||
and avoids direct AutoProcessor.from_pretrained call sites in this file.
|
||||
"""
|
||||
global _GENDER_PIPELINE
|
||||
if _GENDER_PIPELINE is not None:
|
||||
return _GENDER_PIPELINE
|
||||
|
||||
model_name = "7wolf/wav2vec2-base-gender-classification"
|
||||
try:
|
||||
# device=-1 forces CPU for pipeline.
|
||||
_GENDER_PIPELINE = pipeline(
|
||||
task="audio-classification",
|
||||
model=model_name,
|
||||
device=-1,
|
||||
)
|
||||
return _GENDER_PIPELINE
|
||||
except Exception as exc: # pragma: no cover - best-effort fallback
|
||||
print(f"Warning: failed to create gender pipeline '{model_name}': {exc}")
|
||||
_GENDER_PIPELINE = None
|
||||
return None
|
||||
|
||||
|
||||
def _median_pitch_hz_from_autocorr(mono: np.ndarray, sr: int) -> float | None:
|
||||
"""
|
||||
Rough median F0 (Hz) over short-time frames. Used to debias wav2vec2 gender head on TTS,
|
||||
which often labels lower-pitched synthetic speech as female under load or on clean signals.
|
||||
Returns None if the clip is too short or mostly unvoiced.
|
||||
"""
|
||||
x = np.asarray(mono, dtype=np.float64)
|
||||
x = x - np.mean(x)
|
||||
if x.size < int(0.15 * sr):
|
||||
return None
|
||||
frame_len = int(0.04 * sr)
|
||||
hop = max(frame_len // 2, 1)
|
||||
f0_min_hz, f0_max_hz = 70.0, 400.0
|
||||
lag_min = max(1, int(sr / f0_max_hz))
|
||||
lag_max = min(frame_len - 2, int(sr / f0_min_hz))
|
||||
if lag_max <= lag_min:
|
||||
return None
|
||||
win = np.hamming(frame_len)
|
||||
pitches: list[float] = []
|
||||
for start in range(0, int(x.shape[0]) - frame_len, hop):
|
||||
frame = x[start : start + frame_len] * win
|
||||
frame = frame - np.mean(frame)
|
||||
if float(np.sqrt(np.mean(frame**2))) < 1e-4:
|
||||
continue
|
||||
ac = np.correlate(frame, frame, mode="full")[frame_len - 1 :]
|
||||
ac = ac / (float(ac[0]) + 1e-12)
|
||||
region = ac[lag_min : lag_max + 1]
|
||||
peak_rel = int(np.argmax(region))
|
||||
peak_lag = peak_rel + lag_min
|
||||
if peak_lag <= 0:
|
||||
continue
|
||||
f0 = float(sr) / float(peak_lag)
|
||||
if f0_min_hz <= f0 <= f0_max_hz:
|
||||
pitches.append(f0)
|
||||
if len(pitches) < 4:
|
||||
return None
|
||||
return float(np.median(np.asarray(pitches, dtype=np.float64)))
|
||||
|
||||
|
||||
def _estimate_voice_gender_from_audio(audio_bytes: bytes) -> str:
|
||||
"""
|
||||
Estimate voice gender from audio using a small pre-trained classification model.
|
||||
|
||||
Uses a cached `audio-classification` pipeline to classify the clip.
|
||||
Returns 'male' / 'female' when the model confidence is >= 0.9 and the label
|
||||
maps to one of these; otherwise returns 'unknown'. If the model is unavailable
|
||||
or inference fails, returns 'unknown' to keep tests stable.
|
||||
|
||||
Under concurrent tests, a global lock serializes pipeline calls (the HF pipeline is not
|
||||
thread-safe). A coarse F0 median can correct systematic "male -> female" errors on TTS audio.
|
||||
"""
|
||||
data, sr = sf.read(io.BytesIO(audio_bytes), dtype="float32", always_2d=True)
|
||||
if data.size == 0:
|
||||
raise ValueError("Empty audio")
|
||||
mono = np.mean(data, axis=1)
|
||||
|
||||
try:
|
||||
target_sr = 16000
|
||||
if int(sr) != target_sr and mono.size > 1:
|
||||
src_len = int(mono.shape[0])
|
||||
dst_len = max(1, int(round(src_len * float(target_sr) / float(sr))))
|
||||
src_idx = np.arange(src_len, dtype=np.float32)
|
||||
dst_idx = np.linspace(0, src_len - 1, dst_len, dtype=np.float32)
|
||||
mono = np.interp(dst_idx, src_idx, mono.astype(np.float32, copy=False)).astype(np.float32)
|
||||
sr = target_sr
|
||||
|
||||
median_f0 = _median_pitch_hz_from_autocorr(mono, sr)
|
||||
|
||||
clf = _load_gender_pipeline()
|
||||
if clf is None:
|
||||
print("gender model not available, returning 'unknown'")
|
||||
return "unknown"
|
||||
|
||||
# transformers pipeline returns a list of {label, score} (highest score first).
|
||||
with _GENDER_PIPELINE_LOCK:
|
||||
outputs = clf(mono, sampling_rate=sr)
|
||||
if not outputs:
|
||||
return "unknown"
|
||||
|
||||
top = outputs[0]
|
||||
label = str(top.get("label", "")).lower()
|
||||
conf = float(top.get("score", 0.0))
|
||||
|
||||
if conf < 0.6:
|
||||
gender = "unknown"
|
||||
# Some models use non-English labels (e.g., Russian). Normalize to 'male'/'female'.
|
||||
elif ("female" in label) or ("жен" in label):
|
||||
gender = "female"
|
||||
elif ("male" in label) or ("муж" in label):
|
||||
gender = "male"
|
||||
else:
|
||||
gender = "unknown"
|
||||
|
||||
# Debias: wav2vec2 gender heads often call TTS / band-limited male speech "female".
|
||||
# Low median F0 (~speech male range) + female label -> trust pitch when score is not overwhelming.
|
||||
if gender == "female" and median_f0 is not None and median_f0 < 165.0 and conf < 0.88:
|
||||
print(f"gender pitch assist: reclassifying female->male (median_f0={median_f0:.1f} Hz, conf={conf:.3f})")
|
||||
gender = "male"
|
||||
elif gender == "male" and median_f0 is not None and median_f0 > 230.0 and conf < 0.88:
|
||||
print(f"gender pitch assist: reclassifying male->female (median_f0={median_f0:.1f} Hz, conf={conf:.3f})")
|
||||
gender = "female"
|
||||
|
||||
print(
|
||||
f"gender classifier: label={label}, conf={conf:.3f}, gender={gender}"
|
||||
+ (f", median_f0={median_f0:.1f}Hz" if median_f0 is not None else "")
|
||||
)
|
||||
return gender
|
||||
except Exception as exc: # pragma: no cover - best-effort fallback
|
||||
print(f"Warning: gender classification failed, returning 'unknown': {exc}")
|
||||
return "unknown"
|
||||
|
||||
|
||||
# Threshold aligned with _compute_pcm_hnr_db docstring (clean clone vs distorted).
|
||||
_MIN_PCM_SPEECH_HNR_DB = 1.0
|
||||
|
||||
|
||||
def _compute_pcm_hnr_db(pcm_samples: np.ndarray, sr: int = _PCM_SPEECH_SAMPLE_RATE_HZ) -> float:
|
||||
"""Compute mean Harmonic-to-Noise Ratio (dB) for speech quality.
|
||||
|
||||
Clean cloned speech has HNR > 1.2 dB; distorted speech (e.g. lost
|
||||
ref_code decoder context) drops below 1.0 dB.
|
||||
"""
|
||||
frame_len = int(0.03 * sr) # 30ms frames
|
||||
hop = frame_len // 2
|
||||
hnr_values: list[float] = []
|
||||
|
||||
for start in range(0, len(pcm_samples) - frame_len, hop):
|
||||
frame = pcm_samples[start : start + frame_len].astype(np.float32, copy=False)
|
||||
frame = frame - np.mean(frame)
|
||||
if np.max(np.abs(frame)) < 0.01:
|
||||
continue
|
||||
ac = np.correlate(frame, frame, mode="full")[len(frame) - 1 :]
|
||||
ac = ac / (ac[0] + 1e-10)
|
||||
min_lag = int(sr / 400)
|
||||
max_lag = min(int(sr / 80), len(ac))
|
||||
if min_lag >= max_lag:
|
||||
continue
|
||||
peak = float(np.max(ac[min_lag:max_lag]))
|
||||
if 0 < peak < 1:
|
||||
hnr_values.append(10 * np.log10(peak / (1 - peak + 1e-10)))
|
||||
|
||||
return float(np.mean(hnr_values)) if hnr_values else 0.0
|
||||
|
||||
|
||||
def _assert_pcm_int16_speech_hnr(audio_bytes: bytes) -> None:
|
||||
"""Validate harmonic-to-noise ratio on raw int16 PCM from /v1/audio/speech."""
|
||||
assert audio_bytes is not None and len(audio_bytes) >= 2, "missing PCM bytes"
|
||||
assert len(audio_bytes) % 2 == 0, "PCM byte length must be aligned to int16"
|
||||
pcm_samples = np.frombuffer(audio_bytes, dtype=np.int16).astype(np.float32) / 32768.0
|
||||
hnr = _compute_pcm_hnr_db(pcm_samples)
|
||||
print(f"PCM speech HNR: {hnr:.2f} dB (threshold: {_MIN_PCM_SPEECH_HNR_DB} dB)")
|
||||
assert hnr >= _MIN_PCM_SPEECH_HNR_DB, (
|
||||
f"Audio distortion detected: HNR={hnr:.2f} dB < {_MIN_PCM_SPEECH_HNR_DB} dB. "
|
||||
"Voice clone decoder may be losing ref_code speaker context on later chunks."
|
||||
)
|
||||
|
||||
|
||||
def assert_omni_response(response: OmniResponse, request_config: dict[str, Any], run_level):
|
||||
"""
|
||||
Validate response results.
|
||||
@@ -1704,7 +1493,7 @@ def assert_omni_response(response: OmniResponse, request_config: dict[str, Any],
|
||||
assert response.success, "The request failed."
|
||||
e2e_latency = response.e2e_latency
|
||||
if e2e_latency is not None:
|
||||
print(f"the e2e latency is: {e2e_latency}")
|
||||
print(f"the avg e2e latency is: {e2e_latency}")
|
||||
|
||||
modalities = request_config.get("modalities", ["text", "audio"])
|
||||
|
||||
@@ -1739,68 +1528,6 @@ def assert_omni_response(response: OmniResponse, request_config: dict[str, Any],
|
||||
print(f"similarity is: {response.similarity}")
|
||||
|
||||
|
||||
def assert_audio_speech_response(
|
||||
response: OmniResponse,
|
||||
request_config: dict[str, Any],
|
||||
run_level: str,
|
||||
) -> None:
|
||||
"""
|
||||
Validate /v1/audio/speech response: success, optional format check, transcription similarity
|
||||
and gender (non-PCM only for advanced_model), and int16 PCM HNR when response_format is pcm.
|
||||
"""
|
||||
assert response.success, "The request failed."
|
||||
|
||||
req_fmt = request_config.get("response_format")
|
||||
|
||||
if req_fmt == "pcm" and response.audio_bytes:
|
||||
_assert_pcm_int16_speech_hnr(response.audio_bytes)
|
||||
if response.audio_format:
|
||||
assert "pcm" in response.audio_format.lower(), (
|
||||
f"Expected audio/pcm content-type, got {response.audio_format!r}"
|
||||
)
|
||||
|
||||
elif req_fmt == "wav" and response.audio_format:
|
||||
assert req_fmt in response.audio_format, (
|
||||
f"The response audio format {response.audio_format} don't match the request audio format {req_fmt}"
|
||||
)
|
||||
|
||||
e2e_latency = response.e2e_latency
|
||||
if e2e_latency is not None:
|
||||
print(f"the avg e2e latency is: {e2e_latency}")
|
||||
|
||||
if run_level == "advanced_model" and req_fmt != "pcm":
|
||||
# Text–audio semantic similarity check (skipped for raw PCM: no Whisper transcript).
|
||||
expected_text = request_config.get("input")
|
||||
if expected_text:
|
||||
transcript = (response.audio_content or "").strip()
|
||||
print(f"audio content is: {transcript}")
|
||||
print(f"input text is: {expected_text}")
|
||||
similarity = cosine_similarity_text(transcript.lower(), expected_text.lower())
|
||||
print(f"Cosine similarity: {similarity:.3f}")
|
||||
assert similarity > 0.9, (
|
||||
f"Transcript doesn't match input: similarity={similarity:.2f}, transcript='{transcript}'"
|
||||
)
|
||||
|
||||
# Voice gender consistency check:
|
||||
# When the estimator returns 'unknown', we treat it as inconclusive and do NOT fail the test.
|
||||
voice = (request_config.get("voice") or "").lower()
|
||||
if voice and response.audio_bytes:
|
||||
estimated_gender = _estimate_voice_gender_from_audio(response.audio_bytes)
|
||||
voice_gender_map = {
|
||||
# adjust this mapping to your actual voice names
|
||||
"serena": "female",
|
||||
"uncle_fu": "male",
|
||||
"clone": "female",
|
||||
}
|
||||
expected_gender = voice_gender_map.get(voice)
|
||||
if expected_gender is not None:
|
||||
print(f"Estimated voice gender from audio: {estimated_gender} (voice='{voice}')")
|
||||
if estimated_gender != "unknown":
|
||||
assert estimated_gender == expected_gender, (
|
||||
f"Voice '{voice}' is expected {expected_gender}, but estimated gender is '{estimated_gender}'"
|
||||
)
|
||||
|
||||
|
||||
def assert_diffusion_response(response: DiffusionResponse, request_config: dict[str, Any], run_level: str = None):
|
||||
"""
|
||||
Validate diffusion response results.
|
||||
@@ -2027,109 +1754,6 @@ class OpenAIClientHandler:
|
||||
|
||||
return result
|
||||
|
||||
def _process_stream_audio_speech_response(self, response, *, response_format: str | None = None) -> OmniResponse:
|
||||
"""
|
||||
Process streaming /v1/audio/speech responses into an OmniResponse.
|
||||
|
||||
This mirrors _process_stream_omni_response but operates on low-level
|
||||
audio bytes and produces an OmniResponse with audio_content filled
|
||||
from Whisper transcription.
|
||||
"""
|
||||
result = OmniResponse()
|
||||
start_time = time.perf_counter()
|
||||
|
||||
try:
|
||||
# Aggregate all audio bytes from the streaming response.
|
||||
data = bytearray()
|
||||
|
||||
# Preferred OpenAI helper.
|
||||
if hasattr(response, "iter_bytes") and callable(getattr(response, "iter_bytes")):
|
||||
for chunk in response.iter_bytes():
|
||||
if chunk:
|
||||
data.extend(chunk)
|
||||
else:
|
||||
# Generic iterable-of-bytes fallback (e.g., generator or list of chunks).
|
||||
try:
|
||||
iterator = iter(response)
|
||||
except TypeError:
|
||||
iterator = None
|
||||
|
||||
if iterator is not None:
|
||||
for chunk in iterator:
|
||||
if not chunk:
|
||||
continue
|
||||
if isinstance(chunk, (bytes, bytearray)):
|
||||
data.extend(chunk)
|
||||
elif hasattr(chunk, "data"):
|
||||
data.extend(chunk.data) # type: ignore[arg-type]
|
||||
elif hasattr(chunk, "content"):
|
||||
data.extend(chunk.content) # type: ignore[arg-type]
|
||||
else:
|
||||
raise TypeError(f"Unsupported stream chunk type: {type(chunk)}")
|
||||
else:
|
||||
raise TypeError(f"Unsupported audio speech streaming response type: {type(response)}")
|
||||
|
||||
raw_bytes = bytes(data)
|
||||
if response_format == "pcm":
|
||||
transcript = None
|
||||
else:
|
||||
transcript = convert_audio_bytes_to_text(raw_bytes)
|
||||
|
||||
# Populate OmniResponse.
|
||||
result.audio_bytes = raw_bytes
|
||||
result.audio_content = transcript
|
||||
result.e2e_latency = time.perf_counter() - start_time
|
||||
result.success = True
|
||||
result.audio_format = getattr(response, "response", None)
|
||||
if result.audio_format is not None:
|
||||
result.audio_format = result.audio_format.headers.get("content-type", "")
|
||||
|
||||
except Exception as e:
|
||||
result.error_message = f"Audio speech stream processing error: {str(e)}"
|
||||
print(f"Error: {result.error_message}")
|
||||
|
||||
return result
|
||||
|
||||
def _process_non_stream_audio_speech_response(
|
||||
self, response, *, response_format: str | None = None
|
||||
) -> OmniResponse:
|
||||
"""
|
||||
Process non-streaming /v1/audio/speech responses into an OmniResponse.
|
||||
|
||||
This mirrors _process_non_stream_omni_response but for the binary
|
||||
audio payload returned by audio.speech.create.
|
||||
"""
|
||||
result = OmniResponse()
|
||||
start_time = time.perf_counter()
|
||||
|
||||
try:
|
||||
# OpenAI non-streaming audio.speech.create returns HttpxBinaryResponseContent (.read() or .content)
|
||||
if hasattr(response, "read") and callable(getattr(response, "read")):
|
||||
raw_bytes = response.read()
|
||||
elif hasattr(response, "content"):
|
||||
raw_bytes = response.content # type: ignore[assignment]
|
||||
else:
|
||||
raise TypeError(f"Unsupported audio speech response type: {type(response)}")
|
||||
|
||||
if response_format == "pcm":
|
||||
transcript = None
|
||||
else:
|
||||
transcript = convert_audio_bytes_to_text(raw_bytes)
|
||||
|
||||
result.audio_bytes = raw_bytes
|
||||
result.audio_content = transcript
|
||||
result.e2e_latency = time.perf_counter() - start_time
|
||||
result.success = True
|
||||
result.audio_format = getattr(response, "response", None)
|
||||
if result.audio_format is not None:
|
||||
result.audio_format = result.audio_format.headers.get("content-type", "")
|
||||
|
||||
except Exception as e:
|
||||
result.error_message = f"Audio speech non-stream processing error: {str(e)}"
|
||||
print(f"Error: {result.error_message}")
|
||||
|
||||
return result
|
||||
|
||||
def send_omni_request(self, request_config: dict[str, Any], request_num: int = 1) -> list[OmniResponse]:
|
||||
"""
|
||||
Send OpenAI requests.
|
||||
@@ -2164,138 +1788,35 @@ class OpenAIClientHandler:
|
||||
responses.append(response)
|
||||
|
||||
else:
|
||||
# Send concurrent requests: run create + process in worker so e2e_latency includes full round-trip.
|
||||
def _one_omni_request():
|
||||
start = time.perf_counter()
|
||||
chat_completion = self.client.chat.completions.create(
|
||||
model=request_config.get("model"),
|
||||
messages=request_config.get("messages"),
|
||||
modalities=modalities,
|
||||
stream=stream,
|
||||
)
|
||||
if stream:
|
||||
response = self._process_stream_omni_response(chat_completion)
|
||||
else:
|
||||
response = self._process_non_stream_omni_response(chat_completion)
|
||||
response.e2e_latency = time.perf_counter() - start
|
||||
return response
|
||||
|
||||
# Send concurrent requests
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=request_num) as executor:
|
||||
futures = [executor.submit(_one_omni_request) for _ in range(request_num)]
|
||||
futures = []
|
||||
|
||||
# Submit all request tasks
|
||||
for _ in range(request_num):
|
||||
future = executor.submit(
|
||||
self.client.chat.completions.create,
|
||||
model=request_config.get("model"),
|
||||
messages=request_config.get("messages"),
|
||||
modalities=modalities,
|
||||
stream=stream,
|
||||
)
|
||||
futures.append(future)
|
||||
|
||||
# Process completed tasks
|
||||
for future in concurrent.futures.as_completed(futures):
|
||||
response = future.result()
|
||||
chat_completion = future.result()
|
||||
|
||||
if stream:
|
||||
response = self._process_stream_omni_response(chat_completion)
|
||||
else:
|
||||
response = self._process_non_stream_omni_response(chat_completion)
|
||||
|
||||
assert_omni_response(response, request_config, run_level=self.run_level)
|
||||
responses.append(response)
|
||||
|
||||
return responses
|
||||
|
||||
def send_audio_speech_request(self, request_config: dict[str, Any], request_num: int = 1) -> list[OmniResponse]:
|
||||
"""
|
||||
Call the /v1/audio/speech endpoint using the same configuration-dict
|
||||
style as send_omni_request, but via the OpenAI Python client's
|
||||
audio.speech APIs.
|
||||
|
||||
Expected keys in request_config:
|
||||
- model: model name/path (required)
|
||||
- input: text to synthesize (required)
|
||||
- response_format: audio format such as "wav" or "pcm" (optional)
|
||||
- task_type, ref_text, ref_audio: TTS-specific extras (optional, passed via extra_body)
|
||||
- timeout: request timeout in seconds (float, optional, default 120.0)
|
||||
- stream: whether to use streaming API (bool, optional, default False)
|
||||
"""
|
||||
timeout = float(request_config.get("timeout", 120.0))
|
||||
|
||||
model = request_config["model"]
|
||||
text_input = request_config["input"]
|
||||
stream = bool(request_config.get("stream", False))
|
||||
voice = request_config.get("voice", None)
|
||||
|
||||
# Standard OpenAI param: use omit when not provided to keep default behavior.
|
||||
response_format = request_config.get("response_format", omit)
|
||||
|
||||
# Qwen3-TTS custom fields, forwarded via extra_body.
|
||||
extra_body: dict[str, Any] = {}
|
||||
# Keep this list aligned with vllm_omni.entrypoints.openai.protocol.audio params.
|
||||
for key in ("task_type", "ref_text", "ref_audio", "language", "max_new_tokens"):
|
||||
if key in request_config:
|
||||
extra_body[key] = request_config[key]
|
||||
|
||||
responses: list[OmniResponse] = []
|
||||
|
||||
speech_fmt: str | None = None if response_format is omit else str(response_format).lower()
|
||||
|
||||
if request_num == 1:
|
||||
if stream:
|
||||
# Use streaming response helper.
|
||||
with self.client.audio.speech.with_streaming_response.create(
|
||||
model=model,
|
||||
input=text_input,
|
||||
response_format=response_format,
|
||||
extra_body=extra_body or None,
|
||||
timeout=timeout,
|
||||
voice=voice,
|
||||
) as resp:
|
||||
omni_resp = self._process_stream_audio_speech_response(resp, response_format=speech_fmt)
|
||||
else:
|
||||
# Non-streaming response.
|
||||
resp = self.client.audio.speech.create(
|
||||
model=model,
|
||||
input=text_input,
|
||||
response_format=response_format,
|
||||
extra_body=extra_body or None,
|
||||
timeout=timeout,
|
||||
voice=voice,
|
||||
)
|
||||
omni_resp = self._process_non_stream_audio_speech_response(resp, response_format=speech_fmt)
|
||||
|
||||
assert_audio_speech_response(omni_resp, request_config, run_level=self.run_level)
|
||||
responses.append(omni_resp)
|
||||
return responses
|
||||
else:
|
||||
# request_num > 1: concurrent requests (use same params as single-request path)
|
||||
|
||||
if stream:
|
||||
|
||||
def _stream_task():
|
||||
with self.client.audio.speech.with_streaming_response.create(
|
||||
model=model,
|
||||
input=text_input,
|
||||
response_format=response_format,
|
||||
extra_body=extra_body or None,
|
||||
timeout=timeout,
|
||||
voice=voice,
|
||||
) as resp:
|
||||
return self._process_stream_audio_speech_response(resp, response_format=speech_fmt)
|
||||
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=request_num) as executor:
|
||||
futures = [executor.submit(_stream_task) for _ in range(request_num)]
|
||||
for future in concurrent.futures.as_completed(futures):
|
||||
omni_resp = future.result()
|
||||
assert_audio_speech_response(omni_resp, request_config, run_level=self.run_level)
|
||||
responses.append(omni_resp)
|
||||
else:
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=request_num) as executor:
|
||||
futures = []
|
||||
for _ in range(request_num):
|
||||
future = executor.submit(
|
||||
self.client.audio.speech.create,
|
||||
model=model,
|
||||
input=text_input,
|
||||
response_format=response_format,
|
||||
extra_body=extra_body or None,
|
||||
timeout=timeout,
|
||||
voice=voice,
|
||||
)
|
||||
futures.append(future)
|
||||
|
||||
for future in concurrent.futures.as_completed(futures):
|
||||
resp = future.result()
|
||||
omni_resp = self._process_non_stream_audio_speech_response(resp, response_format=speech_fmt)
|
||||
assert_audio_speech_response(omni_resp, request_config, run_level=self.run_level)
|
||||
responses.append(omni_resp)
|
||||
|
||||
return responses
|
||||
|
||||
def send_diffusion_request(self, request_config: dict[str, Any], request_num: int = 1) -> list[OmniResponse]:
|
||||
"""
|
||||
Send OpenAI requests for diffusion models.
|
||||
@@ -2524,44 +2045,6 @@ class OmniRunner:
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def _estimate_prompt_len(
|
||||
self,
|
||||
additional_information: dict[str, Any],
|
||||
model_name: str,
|
||||
_cache: dict[str, Any] = {},
|
||||
) -> int:
|
||||
"""Estimate prompt_token_ids placeholder length for the Talker stage.
|
||||
|
||||
The AR Talker replaces all input embeddings via ``preprocess``, so the
|
||||
placeholder values are irrelevant but the **length** must match the
|
||||
embeddings that ``preprocess`` will produce.
|
||||
"""
|
||||
try:
|
||||
from vllm_omni.model_executor.models.qwen3_tts.configuration_qwen3_tts import Qwen3TTSConfig
|
||||
from vllm_omni.model_executor.models.qwen3_tts.qwen3_tts_talker import (
|
||||
Qwen3TTSTalkerForConditionalGeneration,
|
||||
)
|
||||
|
||||
if model_name not in _cache:
|
||||
from transformers import AutoTokenizer
|
||||
|
||||
tok = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True, padding_side="left")
|
||||
cfg = Qwen3TTSConfig.from_pretrained(model_name, trust_remote_code=True)
|
||||
_cache[model_name] = (tok, getattr(cfg, "talker_config", None))
|
||||
|
||||
tok, tcfg = _cache[model_name]
|
||||
task_type = (additional_information.get("task_type") or ["CustomVoice"])[0]
|
||||
return Qwen3TTSTalkerForConditionalGeneration.estimate_prompt_len_from_additional_information(
|
||||
additional_information=additional_information,
|
||||
task_type=task_type,
|
||||
tokenize_prompt=lambda t: tok(t, padding=False)["input_ids"],
|
||||
codec_language_id=getattr(tcfg, "codec_language_id", None),
|
||||
spk_is_dialect=getattr(tcfg, "spk_is_dialect", None),
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning("Failed to estimate prompt length, using fallback 2048: %s", exc)
|
||||
return 2048
|
||||
|
||||
def get_default_sampling_params_list(self) -> list[OmniSamplingParams]:
|
||||
"""
|
||||
Get a list of default sampling parameters for all stages.
|
||||
@@ -2616,42 +2099,6 @@ class OmniRunner:
|
||||
if isinstance(prompts, str):
|
||||
prompts = [prompts]
|
||||
|
||||
# Qwen-TTS: follow examples/offline_inference/qwen3_tts/end2end.py style.
|
||||
# Stage 0 expects token placeholders + additional_information (text/speaker/task_type/...),
|
||||
# and Talker replaces embeddings in preprocess based on additional_information only.
|
||||
is_tts_model = "Qwen3-TTS" in self.model_name or "qwen3_tts" in self.model_name.lower()
|
||||
if is_tts_model and modalities == ["audio"]:
|
||||
tts_kw = mm_processor_kwargs or {}
|
||||
task_type = tts_kw.get("task_type", "CustomVoice")
|
||||
speaker = tts_kw.get("speaker", "Vivian")
|
||||
language = tts_kw.get("language", "Auto")
|
||||
max_new_tokens = int(tts_kw.get("max_new_tokens", 2048))
|
||||
ref_audio = tts_kw.get("ref_audio", None)
|
||||
ref_text = tts_kw.get("ref_text", None)
|
||||
|
||||
omni_inputs: list[TextPrompt] = []
|
||||
for prompt_text in prompts:
|
||||
text_str = str(prompt_text).strip() or " "
|
||||
additional_information: dict[str, Any] = {
|
||||
"task_type": [task_type],
|
||||
"text": [text_str],
|
||||
"language": [language],
|
||||
"speaker": [speaker],
|
||||
"max_new_tokens": [max_new_tokens],
|
||||
}
|
||||
if ref_audio is not None:
|
||||
additional_information["ref_audio"] = [ref_audio]
|
||||
if ref_text is not None:
|
||||
additional_information["ref_text"] = [ref_text]
|
||||
# Use official helper to get correct placeholder length
|
||||
plen = self._estimate_prompt_len(additional_information, self.model_name)
|
||||
input_dict: TextPrompt = {
|
||||
"prompt_token_ids": [0] * plen,
|
||||
"additional_information": additional_information,
|
||||
}
|
||||
omni_inputs.append(input_dict)
|
||||
return omni_inputs
|
||||
|
||||
def _normalize_mm_input(mm_input, num_prompts):
|
||||
if mm_input is None:
|
||||
return [None] * num_prompts
|
||||
@@ -2919,84 +2366,9 @@ class OmniRunnerHandler:
|
||||
prompts=prompts, videos=videos, images=images, audios=audios, modalities=modalities
|
||||
)
|
||||
response = self._process_output(outputs)
|
||||
assert_omni_response(response, request_config, run_level="core_model")
|
||||
assert_omni_response(response, request_config, run_level="L2")
|
||||
return response
|
||||
|
||||
def send_audio_speech_request(
|
||||
self,
|
||||
request_config: dict[str, Any],
|
||||
) -> OmniResponse:
|
||||
"""
|
||||
Offline TTS: text -> audio via generate_multimodal, then validate with assert_audio_speech_response.
|
||||
|
||||
request_config must contain:
|
||||
- 'input' or 'prompts': text to synthesize.
|
||||
Optional keys:
|
||||
- 'voice' -> speaker (CustomVoice)
|
||||
- 'task_type' -> task_type in additional_information (default: "CustomVoice")
|
||||
- 'language' -> language in additional_information (default: "Auto")
|
||||
- 'max_new_tokens' -> max_new_tokens in additional_information (default: 2048)
|
||||
- 'response_format' -> desired audio format (used only for assertion)
|
||||
"""
|
||||
input_text = request_config.get("input") or request_config.get("prompts")
|
||||
if input_text is None:
|
||||
raise ValueError("request_config must contain 'input' or 'prompts' for TTS")
|
||||
if isinstance(input_text, list):
|
||||
input_text = input_text[0] if input_text else ""
|
||||
|
||||
# Build TTS-specific kwargs passed through to get_omni_inputs for Qwen3-TTS,
|
||||
# matching examples/offline_inference/qwen3_tts/end2end.py.
|
||||
mm_processor_kwargs: dict[str, Any] = {}
|
||||
if "voice" in request_config:
|
||||
mm_processor_kwargs["speaker"] = request_config["voice"]
|
||||
if "task_type" in request_config:
|
||||
mm_processor_kwargs["task_type"] = request_config["task_type"]
|
||||
if "ref_audio" in request_config:
|
||||
mm_processor_kwargs["ref_audio"] = request_config["ref_audio"]
|
||||
if "ref_text" in request_config:
|
||||
mm_processor_kwargs["ref_text"] = request_config["ref_text"]
|
||||
if "language" in request_config:
|
||||
mm_processor_kwargs["language"] = request_config["language"]
|
||||
if "max_new_tokens" in request_config:
|
||||
mm_processor_kwargs["max_new_tokens"] = request_config["max_new_tokens"]
|
||||
|
||||
outputs = self.runner.generate_multimodal(
|
||||
prompts=input_text,
|
||||
modalities=["audio"],
|
||||
mm_processor_kwargs=mm_processor_kwargs or None,
|
||||
)
|
||||
mm_out: dict[str, Any] | None = None
|
||||
for stage_out in outputs:
|
||||
if getattr(stage_out, "final_output_type", None) == "audio":
|
||||
mm_out = stage_out.request_output.outputs[0].multimodal_output
|
||||
break
|
||||
if mm_out is None:
|
||||
result = OmniResponse(success=False, error_message="No audio output from pipeline")
|
||||
assert result.success, result.error_message
|
||||
return result
|
||||
|
||||
audio_data = mm_out.get("audio")
|
||||
if audio_data is None:
|
||||
result = OmniResponse(success=False, error_message="No audio tensor in multimodal output")
|
||||
assert result.success, result.error_message
|
||||
return result
|
||||
|
||||
sr_raw = mm_out.get("sr")
|
||||
sr_val = sr_raw[-1] if isinstance(sr_raw, list) and sr_raw else sr_raw
|
||||
sr = int(sr_val.item() if hasattr(sr_val, "item") else sr_val)
|
||||
wav_tensor = torch.cat(audio_data, dim=-1) if isinstance(audio_data, list) else audio_data
|
||||
wav_buf = io.BytesIO()
|
||||
sf.write(
|
||||
wav_buf,
|
||||
wav_tensor.float().cpu().numpy().reshape(-1),
|
||||
samplerate=sr,
|
||||
format="WAV",
|
||||
subtype="PCM_16",
|
||||
)
|
||||
result = OmniResponse(success=True, audio_bytes=wav_buf.getvalue(), audio_format="audio/wav")
|
||||
assert_audio_speech_response(result, request_config, run_level="core_model")
|
||||
return result
|
||||
|
||||
def start_profile(
|
||||
self,
|
||||
profile_prefix: str | None = None,
|
||||
|
||||
@@ -209,9 +209,12 @@ def _test_cfg_parallel_worker(
|
||||
cfg_normalize=test_config["cfg_normalize"],
|
||||
)
|
||||
|
||||
# CFG parallel returns the combined prediction on every rank.
|
||||
assert noise_pred is not None
|
||||
result_queue.put((cfg_rank, noise_pred.cpu()))
|
||||
# Only rank 0 has valid output in CFG parallel mode
|
||||
if cfg_rank == 0:
|
||||
assert noise_pred is not None
|
||||
result_queue.put(noise_pred.cpu())
|
||||
else:
|
||||
assert noise_pred is None
|
||||
|
||||
destroy_distributed_env()
|
||||
|
||||
@@ -345,18 +348,7 @@ def test_predict_noise_maybe_with_cfg(cfg_parallel_size: int, dtype: torch.dtype
|
||||
|
||||
# Get results from queues
|
||||
baseline_output = baseline_queue.get()
|
||||
cfg_parallel_outputs = [cfg_parallel_queue.get() for _ in range(cfg_parallel_size)]
|
||||
cfg_parallel_outputs.sort(key=lambda item: item[0])
|
||||
cfg_parallel_output = cfg_parallel_outputs[0][1]
|
||||
|
||||
for cfg_rank, rank_output in cfg_parallel_outputs[1:]:
|
||||
torch.testing.assert_close(
|
||||
rank_output,
|
||||
cfg_parallel_output,
|
||||
rtol=0,
|
||||
atol=0,
|
||||
msg=f"CFG parallel ranks produced different outputs (rank 0 vs rank {cfg_rank})",
|
||||
)
|
||||
cfg_parallel_output = cfg_parallel_queue.get()
|
||||
|
||||
# Verify shapes match
|
||||
assert baseline_output.shape == cfg_parallel_output.shape, (
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""
|
||||
E2E offline tests for Qwen3-TTS Base model with text input and audio output.
|
||||
|
||||
Async_chunk disable, cuda_graph disabled (no_async_chunk stage config).
|
||||
CUDA graph is disabled by setting engine_args.enforce_eager=true via modify_stage_config().
|
||||
Same structure as test_qwen3_omni (models, stage_configs, test_params, parametrize omni_runner).
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
|
||||
os.environ["VLLM_TEST_CLEAN_GPU_MEMORY"] = "0"
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.conftest import modify_stage_config
|
||||
from tests.utils import hardware_test
|
||||
|
||||
MODEL = "Qwen/Qwen3-TTS-12Hz-0.6B-Base"
|
||||
REF_AUDIO_URL = "https://qianwen-res.oss-cn-beijing.aliyuncs.com/Qwen3-TTS-Repo/clone_2.wav"
|
||||
REF_TEXT = "Okay. Yeah. I resent you. I love you. I respect you. But you know what? You blew it! And thanks to you."
|
||||
|
||||
|
||||
def get_cuda_graph_config():
|
||||
path = modify_stage_config(
|
||||
get_stage_config(),
|
||||
updates={
|
||||
"stage_args": {
|
||||
0: {
|
||||
"engine_args.enforce_eager": "true",
|
||||
},
|
||||
1: {"engine_args.enforce_eager": "true"},
|
||||
},
|
||||
},
|
||||
)
|
||||
return path
|
||||
|
||||
|
||||
def get_stage_config(name: str = "qwen3_tts_no_async_chunk.yaml"):
|
||||
"""Get the no_async_chunk stage config path (async_chunk disable, cuda_graph disabled)."""
|
||||
return str(Path(__file__).parent.parent.parent.parent / "vllm_omni" / "model_executor" / "stage_configs" / name)
|
||||
|
||||
|
||||
# Same structure as test_qwen3_omni: models, stage_configs, test_params
|
||||
tts_server_params = [
|
||||
pytest.param(
|
||||
(MODEL, get_cuda_graph_config()),
|
||||
id="no_cuda_graph",
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def get_prompt():
|
||||
"""Text prompt for text-to-audio (same role as get_question in test_qwen3_omni)."""
|
||||
return "Hello, this is a test for text to audio."
|
||||
|
||||
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.omni
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
@pytest.mark.parametrize("omni_runner", tts_server_params, indirect=True)
|
||||
def test_text_to_audio_001(omni_runner, omni_runner_handler) -> None:
|
||||
"""
|
||||
Test text input processing and audio output via offline Omni runner.
|
||||
Deploy Setting: qwen3_tts_no_async_chunk.yaml + enforce_eager=true
|
||||
Input Modal: text
|
||||
Output Modal: audio
|
||||
Input Setting: stream=False
|
||||
Extra Setting: task_type=Base, voice=clone, ref_audio/ref_text provided
|
||||
Datasets: few requests
|
||||
"""
|
||||
request_config = {
|
||||
"input": get_prompt(),
|
||||
"task_type": "Base",
|
||||
"voice": "clone",
|
||||
"ref_audio": REF_AUDIO_URL,
|
||||
"ref_text": REF_TEXT,
|
||||
}
|
||||
omni_runner_handler.send_audio_speech_request(request_config)
|
||||
@@ -1,74 +0,0 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""
|
||||
E2E offline tests for Qwen3-TTS CustomVoice model with text input and audio output.
|
||||
|
||||
Async_chunk disable, cuda_graph disabled (no_async_chunk stage config).
|
||||
CUDA graph is disabled by setting engine_args.enforce_eager=true via modify_stage_config().
|
||||
Same structure as test_qwen3_omni (models, stage_configs, test_params, parametrize omni_runner).
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
|
||||
os.environ["VLLM_TEST_CLEAN_GPU_MEMORY"] = "0"
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.conftest import modify_stage_config
|
||||
from tests.utils import hardware_test
|
||||
|
||||
MODEL = "Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice"
|
||||
|
||||
|
||||
def get_cuda_graph_config():
|
||||
path = modify_stage_config(
|
||||
get_stage_config(),
|
||||
updates={
|
||||
"stage_args": {
|
||||
0: {
|
||||
"engine_args.enforce_eager": "true",
|
||||
},
|
||||
1: {"engine_args.enforce_eager": "true"},
|
||||
},
|
||||
},
|
||||
)
|
||||
return path
|
||||
|
||||
|
||||
def get_stage_config(name: str = "qwen3_tts_no_async_chunk.yaml"):
|
||||
"""Get the no_async_chunk stage config path (async_chunk disable, cuda_graph disabled)."""
|
||||
return str(Path(__file__).parent.parent.parent.parent / "vllm_omni" / "model_executor" / "stage_configs" / name)
|
||||
|
||||
|
||||
# Same structure as test_qwen3_omni: models, stage_configs, test_params
|
||||
tts_server_params = [
|
||||
pytest.param(
|
||||
(MODEL, get_cuda_graph_config()),
|
||||
id="no_cuda_graph",
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def get_prompt():
|
||||
"""Text prompt for text-to-audio (same role as get_question in test_qwen3_omni)."""
|
||||
return "Hello, this is a test for text to audio."
|
||||
|
||||
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.omni
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
@pytest.mark.parametrize("omni_runner", tts_server_params, indirect=True)
|
||||
def test_text_to_audio_001(omni_runner, omni_runner_handler) -> None:
|
||||
"""
|
||||
Test text input processing and audio output via offline Omni runner.
|
||||
Deploy Setting: qwen3_tts_no_async_chunk.yaml + enforce_eager=true
|
||||
Input Modal: text
|
||||
Output Modal: audio
|
||||
Input Setting: stream=False
|
||||
Datasets: few requests
|
||||
"""
|
||||
request_config = {"input": get_prompt(), "voice": "vivian"}
|
||||
omni_runner_handler.send_audio_speech_request(request_config)
|
||||
@@ -0,0 +1,285 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""
|
||||
E2E Online tests for Qwen3-TTS model with text input and audio output.
|
||||
|
||||
These tests verify the /v1/audio/speech endpoint works correctly with
|
||||
actual model inference, not mocks.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
|
||||
os.environ["VLLM_TEST_CLEAN_GPU_MEMORY"] = "0"
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from tests.conftest import OmniServer
|
||||
from tests.utils import hardware_test
|
||||
|
||||
MODEL = "Qwen/Qwen3-TTS-12Hz-0.6B-CustomVoice"
|
||||
|
||||
|
||||
def get_stage_config(name: str = "qwen3_tts.yaml"):
|
||||
"""Get the stage config path for Qwen3-TTS."""
|
||||
return str(Path(__file__).parent.parent.parent.parent / "vllm_omni" / "model_executor" / "stage_configs" / name)
|
||||
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def omni_server():
|
||||
"""Start vLLM-Omni server with CustomVoice model."""
|
||||
stage_config_path = get_stage_config()
|
||||
|
||||
print(f"Starting OmniServer with model: {MODEL}")
|
||||
|
||||
with OmniServer(
|
||||
MODEL,
|
||||
[
|
||||
"--stage-configs-path",
|
||||
stage_config_path,
|
||||
"--stage-init-timeout",
|
||||
"120",
|
||||
"--trust-remote-code",
|
||||
"--enforce-eager",
|
||||
"--disable-log-stats",
|
||||
],
|
||||
) as server:
|
||||
print("OmniServer started successfully")
|
||||
yield server
|
||||
print("OmniServer stopping...")
|
||||
|
||||
print("OmniServer stopped")
|
||||
|
||||
|
||||
def make_speech_request(
|
||||
host: str,
|
||||
port: int,
|
||||
text: str,
|
||||
voice: str = "vivian",
|
||||
language: str = "English",
|
||||
task_type: str | None = None,
|
||||
instructions: str | None = None,
|
||||
timeout: float = 120.0,
|
||||
) -> httpx.Response:
|
||||
"""Make a request to the /v1/audio/speech endpoint."""
|
||||
url = f"http://{host}:{port}/v1/audio/speech"
|
||||
payload = {
|
||||
"input": text,
|
||||
"voice": voice,
|
||||
"language": language,
|
||||
}
|
||||
if task_type:
|
||||
payload["task_type"] = task_type
|
||||
if instructions:
|
||||
payload["instructions"] = instructions
|
||||
|
||||
with httpx.Client(timeout=timeout) as client:
|
||||
return client.post(url, json=payload)
|
||||
|
||||
|
||||
def verify_wav_audio(content: bytes) -> bool:
|
||||
"""Verify that content is valid WAV audio data."""
|
||||
# WAV files start with "RIFF" header
|
||||
if len(content) < 44: # Minimum WAV header size
|
||||
return False
|
||||
return content[:4] == b"RIFF" and content[8:12] == b"WAVE"
|
||||
|
||||
|
||||
# Minimum expected audio size for a short sentence (~1 second of 24kHz 16-bit mono WAV)
|
||||
MIN_AUDIO_BYTES = 10000
|
||||
|
||||
|
||||
class TestQwen3TTSCustomVoice:
|
||||
"""E2E tests for Qwen3-TTS CustomVoice model."""
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.omni
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=4)
|
||||
def test_speech_english_basic(self, omni_server) -> None:
|
||||
"""Test basic English TTS generation."""
|
||||
response = make_speech_request(
|
||||
host=omni_server.host,
|
||||
port=omni_server.port,
|
||||
text="Hello, how are you?",
|
||||
voice="vivian",
|
||||
language="English",
|
||||
)
|
||||
|
||||
assert response.status_code == 200, f"Request failed: {response.text}"
|
||||
assert response.headers.get("content-type") == "audio/wav"
|
||||
assert verify_wav_audio(response.content), "Response is not valid WAV audio"
|
||||
assert len(response.content) > MIN_AUDIO_BYTES, (
|
||||
f"Audio content too small ({len(response.content)} bytes), expected at least {MIN_AUDIO_BYTES} bytes"
|
||||
)
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.omni
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=4)
|
||||
def test_speech_chinese_basic(self, omni_server) -> None:
|
||||
"""Test basic Chinese TTS generation."""
|
||||
response = make_speech_request(
|
||||
host=omni_server.host,
|
||||
port=omni_server.port,
|
||||
text="你好,我是通义千问",
|
||||
voice="vivian",
|
||||
language="Chinese",
|
||||
)
|
||||
|
||||
assert response.status_code == 200, f"Request failed: {response.text}"
|
||||
assert response.headers.get("content-type") == "audio/wav"
|
||||
assert verify_wav_audio(response.content), "Response is not valid WAV audio"
|
||||
assert len(response.content) > MIN_AUDIO_BYTES, (
|
||||
f"Audio content too small ({len(response.content)} bytes), expected at least {MIN_AUDIO_BYTES} bytes"
|
||||
)
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.omni
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=4)
|
||||
def test_speech_different_voices(self, omni_server) -> None:
|
||||
"""Test TTS with different voice options."""
|
||||
voices = ["vivian", "ryan"]
|
||||
for voice in voices:
|
||||
response = make_speech_request(
|
||||
host=omni_server.host,
|
||||
port=omni_server.port,
|
||||
text="Testing voice selection.",
|
||||
voice=voice,
|
||||
language="English",
|
||||
)
|
||||
|
||||
assert response.status_code == 200, f"Request failed for voice {voice}: {response.text}"
|
||||
assert verify_wav_audio(response.content), f"Invalid WAV for voice {voice}"
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.omni
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=4)
|
||||
def test_speech_binary_response_not_utf8_error(self, omni_server) -> None:
|
||||
"""
|
||||
Regression test: Verify binary audio is returned, not UTF-8 error.
|
||||
|
||||
This test ensures the multimodal_output property correctly retrieves
|
||||
audio from completion outputs, preventing the "TTS model did not
|
||||
produce audio output" error.
|
||||
"""
|
||||
response = make_speech_request(
|
||||
host=omni_server.host,
|
||||
port=omni_server.port,
|
||||
text="This should return binary audio, not a JSON error.",
|
||||
voice="vivian",
|
||||
language="English",
|
||||
)
|
||||
|
||||
# Should NOT be a JSON error response
|
||||
assert response.status_code == 200, f"Request failed: {response.text}"
|
||||
|
||||
# Verify it's binary audio, not JSON
|
||||
try:
|
||||
# If this succeeds and starts with {"error", it's a bug
|
||||
text = response.content.decode("utf-8")
|
||||
assert not text.startswith('{"error"'), f"Got error response instead of audio: {text}"
|
||||
except UnicodeDecodeError:
|
||||
# This is expected - binary audio can't be decoded as UTF-8
|
||||
pass
|
||||
|
||||
assert verify_wav_audio(response.content), "Response is not valid WAV audio"
|
||||
|
||||
|
||||
class TestQwen3TTSAPIEndpoints:
|
||||
"""Test API endpoint functionality."""
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.omni
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=4)
|
||||
def test_list_voices_endpoint(self, omni_server) -> None:
|
||||
"""Test the /v1/audio/voices endpoint returns available voices."""
|
||||
url = f"http://{omni_server.host}:{omni_server.port}/v1/audio/voices"
|
||||
|
||||
with httpx.Client(timeout=30.0) as client:
|
||||
response = client.get(url)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "voices" in data
|
||||
assert isinstance(data["voices"], list)
|
||||
assert len(data["voices"]) > 0
|
||||
# Check some expected voices are present
|
||||
voices_lower = [v.lower() for v in data["voices"]]
|
||||
assert "vivian" in voices_lower or "ryan" in voices_lower
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.omni
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=4)
|
||||
def test_models_endpoint(self, omni_server) -> None:
|
||||
"""Test the /v1/models endpoint returns loaded model."""
|
||||
url = f"http://{omni_server.host}:{omni_server.port}/v1/models"
|
||||
|
||||
with httpx.Client(timeout=30.0) as client:
|
||||
response = client.get(url)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "data" in data
|
||||
assert len(data["data"]) > 0
|
||||
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def omni_server_no_async_chunk():
|
||||
"""Start vLLM-Omni server with non-async-chunk config."""
|
||||
stage_config_path = get_stage_config("qwen3_tts_no_async_chunk.yaml")
|
||||
|
||||
with OmniServer(
|
||||
MODEL,
|
||||
[
|
||||
"--stage-configs-path",
|
||||
stage_config_path,
|
||||
"--stage-init-timeout",
|
||||
"120",
|
||||
"--trust-remote-code",
|
||||
"--enforce-eager",
|
||||
"--disable-log-stats",
|
||||
],
|
||||
) as server:
|
||||
yield server
|
||||
|
||||
|
||||
class TestQwen3TTSNoAsyncChunk:
|
||||
"""E2E tests for Qwen3-TTS in non-async-chunk (full decode) mode."""
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.omni
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=4)
|
||||
def test_speech_english(self, omni_server_no_async_chunk) -> None:
|
||||
"""Test English TTS with non-async-chunk pipeline."""
|
||||
response = make_speech_request(
|
||||
host=omni_server_no_async_chunk.host,
|
||||
port=omni_server_no_async_chunk.port,
|
||||
text="Hello, how are you?",
|
||||
voice="vivian",
|
||||
language="English",
|
||||
)
|
||||
|
||||
assert response.status_code == 200, f"Request failed: {response.text}"
|
||||
assert response.headers.get("content-type") == "audio/wav"
|
||||
assert verify_wav_audio(response.content), "Response is not valid WAV audio"
|
||||
assert len(response.content) > MIN_AUDIO_BYTES
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.omni
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=4)
|
||||
def test_speech_chinese(self, omni_server_no_async_chunk) -> None:
|
||||
"""Test Chinese TTS with non-async-chunk pipeline."""
|
||||
response = make_speech_request(
|
||||
host=omni_server_no_async_chunk.host,
|
||||
port=omni_server_no_async_chunk.port,
|
||||
text="你好,我是通义千问",
|
||||
voice="vivian",
|
||||
language="Chinese",
|
||||
)
|
||||
|
||||
assert response.status_code == 200, f"Request failed: {response.text}"
|
||||
assert response.headers.get("content-type") == "audio/wav"
|
||||
assert verify_wav_audio(response.content), "Response is not valid WAV audio"
|
||||
assert len(response.content) > MIN_AUDIO_BYTES
|
||||
@@ -1,10 +1,10 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""
|
||||
E2E Online tests for Qwen3-TTS model with text input and audio output.
|
||||
E2E tests for Qwen3-TTS Base voice-clone model.
|
||||
|
||||
These tests verify the /v1/audio/speech endpoint works correctly with
|
||||
actual model inference, not mocks.
|
||||
Regression test for #1663: speech tokenizer loaded in bfloat16 produced
|
||||
all-silence PCM due to NaN overflow in SnakeBeta activation.
|
||||
"""
|
||||
|
||||
import os
|
||||
@@ -12,99 +12,212 @@ import os
|
||||
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
|
||||
os.environ["VLLM_TEST_CLEAN_GPU_MEMORY"] = "0"
|
||||
|
||||
import struct
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from tests.conftest import OmniServerParams
|
||||
from tests.conftest import (
|
||||
OmniServer,
|
||||
convert_audio_file_to_text,
|
||||
cosine_similarity_text,
|
||||
)
|
||||
from tests.utils import hardware_test
|
||||
|
||||
MODEL = "Qwen/Qwen3-TTS-12Hz-0.6B-Base"
|
||||
MODEL = "Qwen/Qwen3-TTS-12Hz-1.7B-Base"
|
||||
|
||||
# Official Qwen3-TTS reference audio/text from examples/offline_inference/qwen3_tts/end2end.py
|
||||
REF_AUDIO_URL = "https://qianwen-res.oss-cn-beijing.aliyuncs.com/Qwen3-TTS-Repo/clone_2.wav"
|
||||
REF_TEXT = "Okay. Yeah. I resent you. I love you. I respect you. But you know what? You blew it! And thanks to you."
|
||||
SYN_TEXT = "Good one. Okay, fine, I'm just gonna leave this sock monkey here. Goodbye."
|
||||
|
||||
|
||||
def get_stage_config(name: str = "qwen3_tts.yaml"):
|
||||
"""Get the stage config path from vllm_omni model_executor stage_configs."""
|
||||
return str(Path(__file__).parent.parent.parent.parent / "vllm_omni" / "model_executor" / "stage_configs" / name)
|
||||
|
||||
|
||||
def get_prompt(prompt_type="text"):
|
||||
"""Text prompt for text-to-audio tests (same as test_qwen3_omni - beijing test case)."""
|
||||
prompts = {
|
||||
"text": "The weather is nice today, perfect for a walk in the park.",
|
||||
}
|
||||
return prompts.get(prompt_type, prompts["text"])
|
||||
|
||||
|
||||
def get_max_batch_size(size_type="few"):
|
||||
"""Batch size for concurrent requests (same as test_qwen3_omni)."""
|
||||
batch_sizes = {"few": 5, "medium": 100, "large": 256}
|
||||
return batch_sizes.get(size_type, 5)
|
||||
|
||||
|
||||
tts_server_params = [
|
||||
pytest.param(
|
||||
OmniServerParams(
|
||||
model=MODEL,
|
||||
stage_config_path=get_stage_config("qwen3_tts.yaml"),
|
||||
server_args=["--trust-remote-code", "--disable-log-stats"],
|
||||
),
|
||||
id="async_chunk",
|
||||
def get_stage_config():
|
||||
return str(
|
||||
Path(__file__).parent.parent.parent.parent / "vllm_omni" / "model_executor" / "stage_configs" / "qwen3_tts.yaml"
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.omni
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
@pytest.mark.parametrize("omni_server", tts_server_params, indirect=True)
|
||||
def test_text_to_audio_001(omni_server, openai_client) -> None:
|
||||
"""
|
||||
Test text input processing and audio output via OpenAI API.
|
||||
Deploy Setting: default yaml
|
||||
Input Modal: text
|
||||
Output Modal: audio
|
||||
Input Setting: stream=False
|
||||
Datasets: few requests
|
||||
"""
|
||||
@pytest.fixture(scope="module")
|
||||
def omni_server():
|
||||
"""Start vLLM-Omni server with Base voice-clone model."""
|
||||
stage_config_path = get_stage_config()
|
||||
|
||||
request_config = {
|
||||
"model": omni_server.model,
|
||||
"input": get_prompt(),
|
||||
"stream": False,
|
||||
"response_format": "wav",
|
||||
with OmniServer(
|
||||
MODEL,
|
||||
[
|
||||
"--stage-configs-path",
|
||||
stage_config_path,
|
||||
"--stage-init-timeout",
|
||||
"120",
|
||||
"--trust-remote-code",
|
||||
"--enforce-eager",
|
||||
"--disable-log-stats",
|
||||
],
|
||||
) as server:
|
||||
yield server
|
||||
|
||||
|
||||
def make_base_speech_request(
|
||||
host: str,
|
||||
port: int,
|
||||
text: str = SYN_TEXT,
|
||||
ref_text: str = REF_TEXT,
|
||||
ref_audio: str = REF_AUDIO_URL,
|
||||
response_format: str = "wav",
|
||||
timeout: float = 120.0,
|
||||
) -> httpx.Response:
|
||||
url = f"http://{host}:{port}/v1/audio/speech"
|
||||
payload = {
|
||||
"model": MODEL,
|
||||
"input": text,
|
||||
"task_type": "Base",
|
||||
"voice": "clone",
|
||||
"ref_audio": REF_AUDIO_URL,
|
||||
"ref_text": REF_TEXT,
|
||||
"ref_text": ref_text,
|
||||
"ref_audio": ref_audio,
|
||||
"response_format": response_format,
|
||||
}
|
||||
openai_client.send_audio_speech_request(request_config, request_num=get_max_batch_size("few"))
|
||||
with httpx.Client(timeout=timeout) as client:
|
||||
return client.post(url, json=payload)
|
||||
|
||||
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.omni
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
@pytest.mark.parametrize("omni_server", tts_server_params, indirect=True)
|
||||
def test_text_to_audio_002(omni_server, openai_client) -> None:
|
||||
def verify_wav_audio(content: bytes) -> bool:
|
||||
if len(content) < 44:
|
||||
return False
|
||||
return content[:4] == b"RIFF" and content[8:12] == b"WAVE"
|
||||
|
||||
|
||||
def assert_not_silence(pcm_bytes: bytes):
|
||||
"""Assert PCM16 samples are not all identical (e.g. all-silence)."""
|
||||
samples = struct.unpack(f"<{len(pcm_bytes) // 2}h", pcm_bytes)
|
||||
unique = set(samples)
|
||||
assert len(unique) > 1, (
|
||||
f"All-silence detected: {len(samples)} samples, unique values: {unique}. "
|
||||
"See https://github.com/vllm-project/vllm-omni/issues/1663"
|
||||
)
|
||||
|
||||
|
||||
MIN_AUDIO_BYTES = 10000
|
||||
MIN_HNR_DB = 1.2 # Clean voice clone > 1.2 dB; distorted < 1.0 dB
|
||||
|
||||
|
||||
def compute_hnr_db(pcm_samples: np.ndarray, sr: int = 24000) -> float:
|
||||
"""Compute mean Harmonic-to-Noise Ratio (dB) for speech quality.
|
||||
|
||||
Clean cloned speech has HNR > 1.2 dB; distorted speech (e.g. lost
|
||||
ref_code decoder context) drops below 1.0 dB.
|
||||
"""
|
||||
Test text input processing and audio output via OpenAI API.
|
||||
Deploy Setting: default yaml
|
||||
Input Modal: text
|
||||
Output Modal: audio
|
||||
Input Setting: stream=True
|
||||
Datasets: single request
|
||||
"""
|
||||
request_config = {
|
||||
"model": omni_server.model,
|
||||
"input": get_prompt(),
|
||||
"stream": True,
|
||||
"response_format": "wav",
|
||||
"task_type": "Base",
|
||||
"voice": "clone",
|
||||
"ref_audio": REF_AUDIO_URL,
|
||||
"ref_text": REF_TEXT,
|
||||
}
|
||||
openai_client.send_audio_speech_request(request_config)
|
||||
frame_len = int(0.03 * sr) # 30ms frames
|
||||
hop = frame_len // 2
|
||||
hnr_values: list[float] = []
|
||||
|
||||
for start in range(0, len(pcm_samples) - frame_len, hop):
|
||||
frame = pcm_samples[start : start + frame_len]
|
||||
if np.max(np.abs(frame)) < 0.01:
|
||||
continue
|
||||
ac = np.correlate(frame, frame, mode="full")[len(frame) - 1 :]
|
||||
ac = ac / (ac[0] + 1e-10)
|
||||
min_lag = int(sr / 400)
|
||||
max_lag = min(int(sr / 80), len(ac))
|
||||
if min_lag >= max_lag:
|
||||
continue
|
||||
peak = float(np.max(ac[min_lag:max_lag]))
|
||||
if 0 < peak < 1:
|
||||
hnr_values.append(10 * np.log10(peak / (1 - peak + 1e-10)))
|
||||
|
||||
return float(np.mean(hnr_values)) if hnr_values else 0.0
|
||||
|
||||
|
||||
class TestQwen3TTSBaseVoiceClone:
|
||||
"""Regression tests for Base voice-clone (fix #1663)."""
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.omni
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=4)
|
||||
def test_base_voice_clone_not_silence(self, omni_server) -> None:
|
||||
"""PCM output must contain real audio, not all-silence."""
|
||||
response = make_base_speech_request(
|
||||
host=omni_server.host,
|
||||
port=omni_server.port,
|
||||
response_format="pcm",
|
||||
)
|
||||
|
||||
assert response.status_code == 200, f"Request failed: {response.text}"
|
||||
assert len(response.content) > MIN_AUDIO_BYTES, f"Audio too small: {len(response.content)} bytes"
|
||||
assert_not_silence(response.content)
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.omni
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=4)
|
||||
def test_base_voice_clone_whisper_transcription(self, omni_server) -> None:
|
||||
"""Whisper must transcribe the output as intelligible speech."""
|
||||
response = make_base_speech_request(
|
||||
host=omni_server.host,
|
||||
port=omni_server.port,
|
||||
response_format="wav",
|
||||
)
|
||||
|
||||
assert response.status_code == 200, f"Request failed: {response.text}"
|
||||
assert verify_wav_audio(response.content), "Response is not valid WAV"
|
||||
assert len(response.content) > MIN_AUDIO_BYTES
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
|
||||
f.write(response.content)
|
||||
wav_path = f.name
|
||||
|
||||
try:
|
||||
transcript = convert_audio_file_to_text(wav_path)
|
||||
print(f"Whisper transcript: {transcript}")
|
||||
assert len(transcript.strip()) > 0, "Whisper returned empty transcript — audio is likely silence"
|
||||
similarity = cosine_similarity_text(transcript.lower(), SYN_TEXT.lower())
|
||||
print(f"Cosine similarity: {similarity:.3f}")
|
||||
assert similarity > 0.9, (
|
||||
f"Transcript doesn't match input: similarity={similarity:.2f}, transcript='{transcript}'"
|
||||
)
|
||||
finally:
|
||||
os.unlink(wav_path)
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.omni
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=4)
|
||||
def test_base_voice_clone_wav_format(self, omni_server) -> None:
|
||||
"""WAV response must have valid headers and sufficient size."""
|
||||
response = make_base_speech_request(
|
||||
host=omni_server.host,
|
||||
port=omni_server.port,
|
||||
response_format="wav",
|
||||
)
|
||||
|
||||
assert response.status_code == 200, f"Request failed: {response.text}"
|
||||
assert response.headers.get("content-type") == "audio/wav"
|
||||
assert verify_wav_audio(response.content), "Response is not valid WAV"
|
||||
assert len(response.content) > MIN_AUDIO_BYTES
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.omni
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=4)
|
||||
def test_base_voice_clone_no_distortion(self, omni_server) -> None:
|
||||
"""Audio must not be distorted (regression for ref_code context loss).
|
||||
|
||||
When the decoder loses ref_code speaker context on later streaming
|
||||
chunks, HNR drops below 1.0 dB. Clean voice clone should be > 1.2 dB.
|
||||
See https://github.com/vllm-project/vllm-omni/issues/1944
|
||||
"""
|
||||
response = make_base_speech_request(
|
||||
host=omni_server.host,
|
||||
port=omni_server.port,
|
||||
response_format="pcm",
|
||||
)
|
||||
|
||||
assert response.status_code == 200, f"Request failed: {response.text}"
|
||||
assert len(response.content) > MIN_AUDIO_BYTES
|
||||
|
||||
pcm_samples = np.frombuffer(response.content, dtype=np.int16).astype(np.float32) / 32768.0
|
||||
hnr = compute_hnr_db(pcm_samples)
|
||||
print(f"Voice clone HNR: {hnr:.2f} dB (threshold: {MIN_HNR_DB} dB)")
|
||||
assert hnr >= MIN_HNR_DB, (
|
||||
f"Audio distortion detected: HNR={hnr:.2f} dB < {MIN_HNR_DB} dB. "
|
||||
"Voice clone decoder may be losing ref_code speaker context on later chunks."
|
||||
)
|
||||
@@ -1,119 +0,0 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""
|
||||
E2E Online tests for Qwen3-TTS model with text input and audio output.
|
||||
|
||||
These tests verify the /v1/audio/speech endpoint works correctly with
|
||||
actual model inference, not mocks.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
|
||||
os.environ["VLLM_TEST_CLEAN_GPU_MEMORY"] = "0"
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.conftest import OmniServerParams
|
||||
from tests.utils import hardware_test
|
||||
|
||||
MODEL = "Qwen/Qwen3-TTS-12Hz-0.6B-Base"
|
||||
|
||||
REF_AUDIO_URL = "https://qianwen-res.oss-cn-beijing.aliyuncs.com/Qwen3-TTS-Repo/clone_2.wav"
|
||||
REF_TEXT = "Okay. Yeah. I resent you. I love you. I respect you. But you know what? You blew it! And thanks to you."
|
||||
|
||||
|
||||
def get_stage_config(name: str = "qwen3_tts.yaml"):
|
||||
"""Get the stage config path from vllm_omni model_executor stage_configs."""
|
||||
return str(Path(__file__).parent.parent.parent.parent / "vllm_omni" / "model_executor" / "stage_configs" / name)
|
||||
|
||||
|
||||
def get_prompt(prompt_type="text"):
|
||||
"""Text prompt for text-to-audio tests (same as test_qwen3_omni - beijing test case)."""
|
||||
prompts = {
|
||||
"text": "The weather is nice today, perfect for a walk in the park.",
|
||||
}
|
||||
return prompts.get(prompt_type, prompts["text"])
|
||||
|
||||
|
||||
def get_max_batch_size(size_type="few"):
|
||||
"""Batch size for concurrent requests (same as test_qwen3_omni)."""
|
||||
batch_sizes = {"few": 5, "medium": 100, "large": 256}
|
||||
return batch_sizes.get(size_type, 5)
|
||||
|
||||
|
||||
tts_server_params = [
|
||||
pytest.param(
|
||||
OmniServerParams(
|
||||
model=MODEL,
|
||||
stage_config_path=get_stage_config("qwen3_tts.yaml"),
|
||||
server_args=["--trust-remote-code", "--disable-log-stats"],
|
||||
),
|
||||
id="async_chunk",
|
||||
),
|
||||
pytest.param(
|
||||
OmniServerParams(
|
||||
model=MODEL,
|
||||
stage_config_path=get_stage_config("qwen3_tts_no_async_chunk.yaml"),
|
||||
server_args=["--trust-remote-code", "--disable-log-stats"],
|
||||
),
|
||||
id="no_async_chunk",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.omni
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
@pytest.mark.parametrize("omni_server", tts_server_params, indirect=True)
|
||||
def test_voice_clone_streaming_001(omni_server, openai_client) -> None:
|
||||
"""
|
||||
Test text input processing and audio output via OpenAI API.
|
||||
Deploy Setting: default yaml
|
||||
Input Modal: text
|
||||
Output Modal: audio
|
||||
Input Setting: stream=True
|
||||
Datasets: few requests
|
||||
"""
|
||||
|
||||
request_config = {
|
||||
"model": omni_server.model,
|
||||
"input": get_prompt(),
|
||||
"stream": True,
|
||||
"response_format": "wav",
|
||||
"task_type": "Base",
|
||||
"voice": "clone",
|
||||
"ref_audio": REF_AUDIO_URL,
|
||||
"ref_text": REF_TEXT,
|
||||
}
|
||||
openai_client.send_audio_speech_request(request_config, request_num=get_max_batch_size("few"))
|
||||
|
||||
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.omni
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
@pytest.mark.parametrize("omni_server", tts_server_params, indirect=True)
|
||||
def test_response_format_001(omni_server, openai_client) -> None:
|
||||
"""
|
||||
Test text input processing and audio output via OpenAI API.
|
||||
Deploy Setting: default yaml
|
||||
Input Modal: text
|
||||
Output Modal: audio
|
||||
Input Setting: non-stream PCM
|
||||
Datasets: few requests
|
||||
"""
|
||||
|
||||
request_config = {
|
||||
"model": omni_server.model,
|
||||
"input": get_prompt(),
|
||||
"response_format": "pcm",
|
||||
"task_type": "Base",
|
||||
"voice": "clone",
|
||||
"ref_audio": REF_AUDIO_URL,
|
||||
"ref_text": REF_TEXT,
|
||||
}
|
||||
openai_client.send_audio_speech_request(request_config)
|
||||
@@ -1,105 +0,0 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""
|
||||
E2E Online tests for Qwen3-TTS model with text input and audio output.
|
||||
|
||||
These tests verify the /v1/audio/speech endpoint works correctly with
|
||||
actual model inference, not mocks.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
|
||||
os.environ["VLLM_TEST_CLEAN_GPU_MEMORY"] = "0"
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.conftest import OmniServerParams
|
||||
from tests.utils import hardware_test
|
||||
|
||||
MODEL = "Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice"
|
||||
|
||||
|
||||
def get_stage_config(name: str = "qwen3_tts.yaml"):
|
||||
"""Get the stage config path from vllm_omni model_executor stage_configs."""
|
||||
return str(Path(__file__).parent.parent.parent.parent / "vllm_omni" / "model_executor" / "stage_configs" / name)
|
||||
|
||||
|
||||
def get_prompt(prompt_type="text"):
|
||||
"""Text prompt for text-to-audio tests (same as test_qwen3_omni - beijing test case)."""
|
||||
prompts = {
|
||||
"text": "Beijing, China's capital, blends ancient wonders like the Great Wall with modern marvels. This vibrant metropolis offers rich culture, delicious Peking duck, and endless exploration opportunities.",
|
||||
}
|
||||
return prompts.get(prompt_type, prompts["text"])
|
||||
|
||||
|
||||
def get_max_batch_size(size_type="few"):
|
||||
"""Batch size for concurrent requests (same as test_qwen3_omni)."""
|
||||
batch_sizes = {"few": 5, "medium": 100, "large": 256}
|
||||
return batch_sizes.get(size_type, 5)
|
||||
|
||||
|
||||
tts_server_params = [
|
||||
pytest.param(
|
||||
OmniServerParams(
|
||||
model=MODEL,
|
||||
stage_config_path=get_stage_config("qwen3_tts.yaml"),
|
||||
server_args=["--trust-remote-code", "--disable-log-stats"],
|
||||
),
|
||||
id="async_chunk",
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.omni
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
@pytest.mark.parametrize("omni_server", tts_server_params, indirect=True)
|
||||
def test_text_to_audio_001(omni_server, openai_client) -> None:
|
||||
"""
|
||||
Test text input processing and audio output via OpenAI API.
|
||||
Deploy Setting: default yaml
|
||||
Input Modal: text
|
||||
Output Modal: audio
|
||||
Input Setting: stream=False
|
||||
Datasets: few requests
|
||||
"""
|
||||
request_config = {
|
||||
"model": omni_server.model,
|
||||
"input": get_prompt(),
|
||||
"stream": False,
|
||||
"response_format": "wav",
|
||||
"task_type": "CustomVoice",
|
||||
"voice": "vivian",
|
||||
}
|
||||
|
||||
openai_client.send_audio_speech_request(request_config, request_num=get_max_batch_size())
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.omni
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
@pytest.mark.parametrize("omni_server", tts_server_params, indirect=True)
|
||||
def test_text_to_audio_002(omni_server, openai_client) -> None:
|
||||
"""
|
||||
Test text input processing and audio output via OpenAI API.
|
||||
Deploy Setting: default yaml
|
||||
Input Modal: text
|
||||
Output Modal: audio
|
||||
Input Setting: stream=True
|
||||
Datasets: single request
|
||||
"""
|
||||
request_config = {
|
||||
"model": omni_server.model,
|
||||
"input": get_prompt(),
|
||||
"stream": True,
|
||||
"response_format": "wav",
|
||||
"task_type": "CustomVoice",
|
||||
"voice": "vivian",
|
||||
}
|
||||
|
||||
openai_client.send_audio_speech_request(request_config)
|
||||
@@ -1,145 +0,0 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""
|
||||
E2E Online tests for Qwen3-TTS model with text input and audio output.
|
||||
|
||||
These tests verify the /v1/audio/speech endpoint works correctly with
|
||||
actual model inference, not mocks.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
|
||||
os.environ["VLLM_TEST_CLEAN_GPU_MEMORY"] = "0"
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.conftest import OmniServerParams
|
||||
from tests.utils import hardware_test
|
||||
|
||||
MODEL = "Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice"
|
||||
|
||||
|
||||
def get_stage_config(name: str = "qwen3_tts.yaml"):
|
||||
"""Get the stage config path from vllm_omni model_executor stage_configs."""
|
||||
return str(Path(__file__).parent.parent.parent.parent / "vllm_omni" / "model_executor" / "stage_configs" / name)
|
||||
|
||||
|
||||
def get_prompt(prompt_type="english"):
|
||||
"""Text prompt for text-to-audio tests (same as test_qwen3_omni - beijing test case)."""
|
||||
prompts = {
|
||||
"english": "Paris is a city in France. It has a tall tower by the river. The streets are old and small. You can walk and see art and food. The sky is light and soft. It is a calm place to rest.",
|
||||
"chinese": "北京,中国的首都,是一座融合了长城等历史地点与现代建筑的国际化大都市,充满了独特的文化与活力",
|
||||
}
|
||||
return prompts.get(prompt_type, prompts["english"])
|
||||
|
||||
|
||||
def get_max_batch_size(size_type="few"):
|
||||
"""Batch size for concurrent requests (same as test_qwen3_omni)."""
|
||||
batch_sizes = {"few": 3, "medium": 100, "large": 256}
|
||||
return batch_sizes.get(size_type)
|
||||
|
||||
|
||||
tts_server_params = [
|
||||
pytest.param(
|
||||
OmniServerParams(
|
||||
model=MODEL,
|
||||
stage_config_path=get_stage_config("qwen3_tts.yaml"),
|
||||
server_args=["--trust-remote-code", "--disable-log-stats"],
|
||||
),
|
||||
id="async_chunk",
|
||||
),
|
||||
pytest.param(
|
||||
OmniServerParams(
|
||||
model=MODEL,
|
||||
stage_config_path=get_stage_config("qwen3_tts_no_async_chunk.yaml"),
|
||||
server_args=["--trust-remote-code", "--disable-log-stats"],
|
||||
),
|
||||
id="no_async_chunk",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.omni
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
@pytest.mark.parametrize("omni_server", tts_server_params, indirect=True)
|
||||
def test_voice_001(omni_server, openai_client) -> None:
|
||||
"""
|
||||
Test text input processing and audio output via OpenAI API.
|
||||
Deploy Setting: default yaml
|
||||
Input Modal: text
|
||||
Output Modal: audio
|
||||
Input Setting: stream=False, voice=eric
|
||||
Datasets: few requests
|
||||
"""
|
||||
request_config = {
|
||||
"model": omni_server.model,
|
||||
"input": get_prompt(),
|
||||
"stream": True,
|
||||
"response_format": "wav",
|
||||
"task_type": "CustomVoice",
|
||||
"voice": "uncle_fu",
|
||||
}
|
||||
|
||||
# Retry once on assertion failures from transcript similarity / gender checks (flaky ASR or estimators).
|
||||
for attempt in range(2):
|
||||
try:
|
||||
openai_client.send_audio_speech_request(request_config, request_num=get_max_batch_size())
|
||||
break
|
||||
except AssertionError:
|
||||
if attempt == 0:
|
||||
continue
|
||||
raise
|
||||
|
||||
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.omni
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
@pytest.mark.parametrize("omni_server", tts_server_params, indirect=True)
|
||||
def test_voice_002(omni_server, openai_client) -> None:
|
||||
"""
|
||||
Test text input processing and audio output via OpenAI API.
|
||||
Deploy Setting: default yaml
|
||||
Input Modal: text
|
||||
Output Modal: audio
|
||||
Input Setting: stream=False, language=chinese
|
||||
Datasets: few requests
|
||||
"""
|
||||
request_config = {
|
||||
"model": omni_server.model,
|
||||
"input": get_prompt(),
|
||||
"stream": False,
|
||||
"response_format": "wav",
|
||||
"task_type": "CustomVoice",
|
||||
"voice": "Serena",
|
||||
}
|
||||
|
||||
openai_client.send_audio_speech_request(request_config)
|
||||
|
||||
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.omni
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
@pytest.mark.parametrize("omni_server", tts_server_params, indirect=True)
|
||||
def test_language_001(omni_server, openai_client) -> None:
|
||||
"""
|
||||
Test text input processing and audio output via OpenAI API.
|
||||
Deploy Setting: default yaml
|
||||
Input Modal: text
|
||||
Output Modal: audio
|
||||
Input Setting: stream=False, language=chinese
|
||||
Datasets: few requests
|
||||
"""
|
||||
request_config = {
|
||||
"model": omni_server.model,
|
||||
"input": get_prompt("chinese"),
|
||||
"stream": False,
|
||||
"response_format": "wav",
|
||||
"task_type": "CustomVoice",
|
||||
"voice": "vivian",
|
||||
}
|
||||
|
||||
openai_client.send_audio_speech_request(request_config)
|
||||
@@ -621,13 +621,6 @@ def test_parameter_validation():
|
||||
with pytest.raises(ValueError):
|
||||
ImageGenerationRequest(prompt="test", guidance_scale=21.0)
|
||||
|
||||
# Invalid layers for layered models (must stay within the backend-supported range)
|
||||
with pytest.raises(ValueError):
|
||||
ImageGenerationRequest(prompt="test", layers=2)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
ImageGenerationRequest(prompt="test", layers=11)
|
||||
|
||||
|
||||
# Pass-Through Tests
|
||||
|
||||
@@ -747,29 +740,6 @@ def test_image_edit_images_processing(async_omni_test_client):
|
||||
assert processed_images[1].size == (24, 24)
|
||||
|
||||
|
||||
def test_image_edit_rejects_multiple_images_when_model_does_not_support_them(async_omni_test_client):
|
||||
img_bytes_1 = make_test_image_bytes((16, 16))
|
||||
img_bytes_2 = make_test_image_bytes((32, 32))
|
||||
|
||||
engine = async_omni_test_client.app.state.engine_client
|
||||
engine.get_diffusion_od_config = lambda: SimpleNamespace(supports_multimodal_inputs=False)
|
||||
|
||||
response = async_omni_test_client.post(
|
||||
"/v1/images/edits",
|
||||
files=[
|
||||
("image", img_bytes_1),
|
||||
("image", img_bytes_2),
|
||||
],
|
||||
data={"prompt": "hello world."},
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert (
|
||||
response.json()["detail"] == "Received multiple input images. Only a single image is supported by this model."
|
||||
)
|
||||
assert engine.captured_prompt is None
|
||||
|
||||
|
||||
def test_image_edit_parameter_pass(async_omni_test_client):
|
||||
img_bytes_1 = make_test_image_bytes((16, 16))
|
||||
|
||||
@@ -875,36 +845,35 @@ def test_image_edit_invalid_resolution(async_omni_test_client):
|
||||
|
||||
|
||||
def test_image_edit_invalid_layers(async_omni_test_client):
|
||||
"""Test that layered image edits reject out-of-range layers with 400."""
|
||||
"""Test that invalid layers values (< 1) are rejected with 400."""
|
||||
img_bytes = make_test_image_bytes((16, 16))
|
||||
|
||||
# Test layers below the supported range
|
||||
# Test layers = 0
|
||||
response = async_omni_test_client.post(
|
||||
"/v1/images/edits",
|
||||
files=[("image", img_bytes)],
|
||||
data={
|
||||
"prompt": "test",
|
||||
"layers": 2,
|
||||
"layers": 0,
|
||||
},
|
||||
)
|
||||
assert response.status_code == 400
|
||||
detail = response.json()["detail"]
|
||||
assert "Invalid layers" in detail
|
||||
assert "layers must be between 3 and 10 inclusive" in detail
|
||||
assert "layers must be >= 1" in detail
|
||||
|
||||
# Test layers above the supported range
|
||||
# Test layers = -1
|
||||
response = async_omni_test_client.post(
|
||||
"/v1/images/edits",
|
||||
files=[("image", img_bytes)],
|
||||
data={
|
||||
"prompt": "test",
|
||||
"layers": 11,
|
||||
"layers": -1,
|
||||
},
|
||||
)
|
||||
assert response.status_code == 400
|
||||
detail = response.json()["detail"]
|
||||
assert "Invalid layers" in detail
|
||||
assert "layers must be between 3 and 10 inclusive" in detail
|
||||
|
||||
|
||||
def test_image_edit_resolution_and_size_conflict(async_omni_test_client):
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
import pytest
|
||||
|
||||
from vllm_omni.entrypoints.async_omni_diffusion import AsyncOmniDiffusion
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||
|
||||
|
||||
def test_get_diffusion_od_config_returns_direct_config():
|
||||
diffusion = object.__new__(AsyncOmniDiffusion)
|
||||
diffusion.od_config = object()
|
||||
|
||||
assert diffusion.get_diffusion_od_config() is diffusion.od_config
|
||||
@@ -379,19 +379,6 @@ def test_openai_serving_models_can_consume_async_omni_compat_attrs():
|
||||
assert serving_models.input_processor is input_processor
|
||||
|
||||
|
||||
def test_get_diffusion_od_config_returns_diffusion_stage_config():
|
||||
diffusion_od_config = object()
|
||||
omni = object.__new__(AsyncOmni)
|
||||
omni.engine = SimpleNamespace(
|
||||
stage_clients=[
|
||||
SimpleNamespace(stage_type="llm"),
|
||||
SimpleNamespace(stage_type="diffusion", _engine=SimpleNamespace(od_config=diffusion_od_config)),
|
||||
]
|
||||
)
|
||||
|
||||
assert omni.get_diffusion_od_config() is diffusion_od_config
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_omni_yields_only_final_stage_outputs(monkeypatch: pytest.MonkeyPatch):
|
||||
engine = FakeAsyncOmniEngine(
|
||||
|
||||
@@ -17,38 +17,12 @@ from vllm_omni.diffusion.distributed.parallel_state import (
|
||||
)
|
||||
|
||||
|
||||
def _wrap(pred: torch.Tensor | tuple[torch.Tensor, ...]) -> tuple[torch.Tensor, ...]:
|
||||
"""Normalize prediction to tuple form."""
|
||||
return pred if isinstance(pred, tuple) else (pred,)
|
||||
|
||||
|
||||
def _unwrap(pred: tuple[torch.Tensor, ...]) -> torch.Tensor | tuple[torch.Tensor, ...]:
|
||||
"""Unwrap single-element tuple to plain tensor; keep multi-element as tuple."""
|
||||
return pred[0] if len(pred) == 1 else pred
|
||||
|
||||
|
||||
def _slice_pred(pred: tuple[torch.Tensor, ...], output_slice: int) -> tuple[torch.Tensor, ...]:
|
||||
"""Slice each element along dim 1."""
|
||||
return tuple(p[:, :output_slice] for p in pred)
|
||||
|
||||
|
||||
class CFGParallelMixin(metaclass=ABCMeta):
|
||||
"""
|
||||
Base Mixin class for Diffusion pipelines providing shared CFG methods.
|
||||
|
||||
All pipelines should inherit from this class to reuse
|
||||
classifier-free guidance logic.
|
||||
|
||||
CFG Parallel Architecture:
|
||||
When cfg_world_size > 1, each rank computes one branch (positive or
|
||||
negative), then all_gather exchanges results. All ranks then compute
|
||||
the CFG combine and scheduler step locally — no broadcast needed
|
||||
because the operations are deterministic.
|
||||
|
||||
Multi-output models:
|
||||
Models that return tuple from predict_noise() (e.g., video + audio)
|
||||
should override combine_cfg_noise() to define per-element combine logic,
|
||||
and set self.scheduler to a composite scheduler that handles tuples.
|
||||
"""
|
||||
|
||||
def predict_noise_maybe_with_cfg(
|
||||
@@ -59,7 +33,7 @@ class CFGParallelMixin(metaclass=ABCMeta):
|
||||
negative_kwargs: dict[str, Any] | None,
|
||||
cfg_normalize: bool = True,
|
||||
output_slice: int | None = None,
|
||||
) -> torch.Tensor | tuple[torch.Tensor, ...]:
|
||||
) -> torch.Tensor | None:
|
||||
"""
|
||||
Predict noise with optional classifier-free guidance.
|
||||
|
||||
@@ -69,64 +43,57 @@ class CFGParallelMixin(metaclass=ABCMeta):
|
||||
positive_kwargs: Kwargs for positive/conditional prediction
|
||||
negative_kwargs: Kwargs for negative/unconditional prediction
|
||||
cfg_normalize: Whether to normalize CFG output (default: True)
|
||||
output_slice: If set, slice each output to [:, :output_slice] for image editing
|
||||
output_slice: If set, slice output to [:, :output_slice] for image editing
|
||||
|
||||
Returns:
|
||||
Predicted noise tensor or tuple of tensors.
|
||||
In CFG parallel mode, result is valid on ALL ranks (not just rank 0).
|
||||
|
||||
Note:
|
||||
For multi-output models (e.g., video + audio where predict_noise
|
||||
returns a tuple), override combine_cfg_noise() for per-element CFG
|
||||
logic and set self.scheduler to a composite scheduler.
|
||||
Predicted noise tensor (only valid on rank 0 in CFG parallel mode)
|
||||
"""
|
||||
if do_true_cfg:
|
||||
# Automatically detect CFG parallel configuration
|
||||
cfg_parallel_ready = get_classifier_free_guidance_world_size() > 1
|
||||
|
||||
if cfg_parallel_ready:
|
||||
# Enable CFG-parallel: rank0 computes positive, rank1 computes negative.
|
||||
cfg_group = get_cfg_group()
|
||||
cfg_rank = get_classifier_free_guidance_rank()
|
||||
|
||||
# Each rank computes one branch
|
||||
kwargs = positive_kwargs if cfg_rank == 0 else negative_kwargs
|
||||
local_pred = _wrap(self.predict_noise(**kwargs))
|
||||
if cfg_rank == 0:
|
||||
local_pred = self.predict_noise(**positive_kwargs)
|
||||
else:
|
||||
local_pred = self.predict_noise(**negative_kwargs)
|
||||
|
||||
# Slice output for image editing pipelines (remove condition latents)
|
||||
if output_slice is not None:
|
||||
local_pred = _slice_pred(local_pred, output_slice)
|
||||
local_pred = local_pred[:, :output_slice]
|
||||
|
||||
# All-gather each element, reconstruct positive/negative tuples
|
||||
gathered = [cfg_group.all_gather(p, separate_tensors=True) for p in local_pred]
|
||||
positive_noise_pred = tuple(g[0] for g in gathered)
|
||||
negative_noise_pred = tuple(g[1] for g in gathered)
|
||||
gathered = cfg_group.all_gather(local_pred, separate_tensors=True)
|
||||
|
||||
# All ranks compute combine (deterministic, same result)
|
||||
return self.combine_cfg_noise(
|
||||
positive_noise_pred,
|
||||
negative_noise_pred,
|
||||
true_cfg_scale,
|
||||
cfg_normalize,
|
||||
)
|
||||
if cfg_rank == 0:
|
||||
noise_pred = gathered[0]
|
||||
neg_noise_pred = gathered[1]
|
||||
noise_pred = self.combine_cfg_noise(noise_pred, neg_noise_pred, true_cfg_scale, cfg_normalize)
|
||||
return noise_pred
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
# Sequential CFG: compute both positive and negative
|
||||
positive_noise_pred = _wrap(self.predict_noise(**positive_kwargs))
|
||||
negative_noise_pred = _wrap(self.predict_noise(**negative_kwargs))
|
||||
positive_noise_pred = self.predict_noise(**positive_kwargs)
|
||||
negative_noise_pred = self.predict_noise(**negative_kwargs)
|
||||
|
||||
# Slice output for image editing pipelines
|
||||
if output_slice is not None:
|
||||
positive_noise_pred = _slice_pred(positive_noise_pred, output_slice)
|
||||
negative_noise_pred = _slice_pred(negative_noise_pred, output_slice)
|
||||
positive_noise_pred = positive_noise_pred[:, :output_slice]
|
||||
negative_noise_pred = negative_noise_pred[:, :output_slice]
|
||||
|
||||
return self.combine_cfg_noise(
|
||||
positive_noise_pred,
|
||||
negative_noise_pred,
|
||||
true_cfg_scale,
|
||||
cfg_normalize,
|
||||
noise_pred = self.combine_cfg_noise(
|
||||
positive_noise_pred, negative_noise_pred, true_cfg_scale, cfg_normalize
|
||||
)
|
||||
return noise_pred
|
||||
else:
|
||||
# No CFG: only compute positive/conditional prediction
|
||||
pred = self.predict_noise(**positive_kwargs)
|
||||
if output_slice is not None:
|
||||
pred = _unwrap(_slice_pred(_wrap(pred), output_slice))
|
||||
pred = pred[:, :output_slice]
|
||||
return pred
|
||||
|
||||
def cfg_normalize_function(self, noise_pred: torch.Tensor, comb_pred: torch.Tensor) -> torch.Tensor:
|
||||
@@ -146,61 +113,35 @@ class CFGParallelMixin(metaclass=ABCMeta):
|
||||
return noise_pred
|
||||
|
||||
def combine_cfg_noise(
|
||||
self,
|
||||
positive_noise_pred: torch.Tensor | tuple[torch.Tensor, ...],
|
||||
negative_noise_pred: torch.Tensor | tuple[torch.Tensor, ...],
|
||||
true_cfg_scale: float,
|
||||
cfg_normalize: bool = False,
|
||||
) -> torch.Tensor | tuple[torch.Tensor, ...]:
|
||||
self, noise_pred: torch.Tensor, neg_noise_pred: torch.Tensor, true_cfg_scale: float, cfg_normalize: bool = False
|
||||
) -> torch.Tensor:
|
||||
"""
|
||||
Combine conditional and unconditional noise predictions with CFG.
|
||||
|
||||
Accepts both plain tensors (backward-compatible, used by LTX2 etc.)
|
||||
and tuples (multi-output models). Default implementation applies the
|
||||
standard CFG formula to every element.
|
||||
|
||||
Multi-output models can override this to apply different logic per element.
|
||||
|
||||
Example override for a model returning (video_pred, audio_pred)::
|
||||
|
||||
def combine_cfg_noise(self, positive_noise_pred, negative_noise_pred, scale, normalize):
|
||||
(video_pos, audio_pos) = positive_noise_pred
|
||||
(video_neg, audio_neg) = negative_noise_pred
|
||||
video_combined = super().combine_cfg_noise(video_pos, video_neg, scale, normalize)
|
||||
return (video_combined, audio_pos) # audio: positive only, no CFG
|
||||
|
||||
Args:
|
||||
positive_noise_pred: Positive/conditional prediction(s) — Tensor or tuple
|
||||
negative_noise_pred: Negative/unconditional prediction(s) — Tensor or tuple
|
||||
noise_pred: Conditional noise prediction
|
||||
neg_noise_pred: Unconditional noise prediction
|
||||
true_cfg_scale: CFG scale factor
|
||||
cfg_normalize: Whether to normalize the combined prediction (default: False)
|
||||
|
||||
Returns:
|
||||
Combined noise prediction(s) — same type as inputs
|
||||
Combined noise prediction tensor
|
||||
"""
|
||||
pos_t = _wrap(positive_noise_pred)
|
||||
neg_t = _wrap(negative_noise_pred)
|
||||
comb_pred = neg_noise_pred + true_cfg_scale * (noise_pred - neg_noise_pred)
|
||||
|
||||
results = []
|
||||
for p, n in zip(pos_t, neg_t):
|
||||
comb = n + true_cfg_scale * (p - n)
|
||||
if cfg_normalize:
|
||||
comb = self.cfg_normalize_function(p, comb)
|
||||
results.append(comb)
|
||||
return _unwrap(tuple(results))
|
||||
if cfg_normalize:
|
||||
noise_pred = self.cfg_normalize_function(noise_pred, comb_pred)
|
||||
else:
|
||||
noise_pred = comb_pred
|
||||
|
||||
def predict_noise(self, *args: Any, **kwargs: Any) -> torch.Tensor | tuple[torch.Tensor, ...]:
|
||||
return noise_pred
|
||||
|
||||
def predict_noise(self, *args: Any, **kwargs: Any) -> torch.Tensor:
|
||||
"""
|
||||
Forward pass through transformer to predict noise.
|
||||
|
||||
Subclasses should override this if they need custom behavior,
|
||||
but the default implementation calls self.transformer.
|
||||
|
||||
Returns:
|
||||
Single Tensor for standard models, or tuple of Tensors for
|
||||
multi-output models (e.g., video + audio). Multi-output models
|
||||
must also override combine_cfg_noise() and set self.scheduler
|
||||
to a composite scheduler that handles tuples.
|
||||
"""
|
||||
return self.transformer(*args, **kwargs)[0]
|
||||
|
||||
@@ -215,89 +156,52 @@ class CFGParallelMixin(metaclass=ABCMeta):
|
||||
Subclasses MUST implement this method to define the complete
|
||||
diffusion/denoising loop for their specific model.
|
||||
|
||||
Typical implementation pattern (single output):
|
||||
Typical implementation pattern:
|
||||
```python
|
||||
def diffuse(self, latents, timesteps, prompt_embeds, negative_embeds, ...):
|
||||
for t in timesteps:
|
||||
# Prepare kwargs for positive and negative predictions
|
||||
positive_kwargs = {...}
|
||||
negative_kwargs = {...}
|
||||
|
||||
# Predict noise with automatic CFG handling
|
||||
noise_pred = self.predict_noise_maybe_with_cfg(
|
||||
do_true_cfg=do_true_cfg,
|
||||
do_true_cfg=True,
|
||||
true_cfg_scale=self.guidance_scale,
|
||||
positive_kwargs=positive_kwargs,
|
||||
negative_kwargs=negative_kwargs,
|
||||
)
|
||||
|
||||
# Step scheduler with automatic CFG sync
|
||||
latents = self.scheduler_step_maybe_with_cfg(
|
||||
noise_pred, t, latents, do_true_cfg=do_true_cfg
|
||||
noise_pred, t, latents, do_true_cfg=True
|
||||
)
|
||||
|
||||
return latents
|
||||
```
|
||||
|
||||
Multi-output models (e.g., video + audio) should:
|
||||
1. Override ``predict_noise()`` to return a tuple
|
||||
2. Override ``combine_cfg_noise()`` for per-element CFG logic
|
||||
3. Set ``self.scheduler`` to a composite scheduler that handles tuples
|
||||
|
||||
```python
|
||||
def diffuse(self, video_latents, audio_latents, timesteps_video, timesteps_audio, ...):
|
||||
for t_v, t_a in zip(timesteps_video, timesteps_audio):
|
||||
positive_kwargs = {...}
|
||||
negative_kwargs = {...}
|
||||
|
||||
# Returns tuple: (video_pred, audio_pred)
|
||||
video_pred, audio_pred = self.predict_noise_maybe_with_cfg(
|
||||
do_true_cfg=do_true_cfg,
|
||||
true_cfg_scale=self.guidance_scale,
|
||||
positive_kwargs=positive_kwargs,
|
||||
negative_kwargs=negative_kwargs,
|
||||
)
|
||||
|
||||
# self.scheduler = VideoAudioScheduler(video_sched, audio_sched)
|
||||
# which accepts and returns tuples
|
||||
video_latents, audio_latents = self.scheduler_step_maybe_with_cfg(
|
||||
(video_pred, audio_pred),
|
||||
(t_v, t_a),
|
||||
(video_latents, audio_latents),
|
||||
do_true_cfg=do_true_cfg,
|
||||
)
|
||||
|
||||
return video_latents, audio_latents
|
||||
```
|
||||
"""
|
||||
raise NotImplementedError("Subclasses must implement diffuse")
|
||||
|
||||
def scheduler_step(
|
||||
self,
|
||||
noise_pred: torch.Tensor | tuple[torch.Tensor, ...],
|
||||
t: torch.Tensor | tuple[torch.Tensor, ...],
|
||||
latents: torch.Tensor | tuple[torch.Tensor, ...],
|
||||
noise_pred: torch.Tensor,
|
||||
t: torch.Tensor,
|
||||
latents: torch.Tensor,
|
||||
per_request_scheduler: Any | None = None,
|
||||
generator: torch.Generator | None = None,
|
||||
) -> torch.Tensor | tuple[torch.Tensor, ...]:
|
||||
) -> torch.Tensor:
|
||||
"""
|
||||
Step the scheduler.
|
||||
|
||||
Default implementation passes inputs directly to ``self.scheduler.step()``.
|
||||
For multi-output models, set ``self.scheduler`` to a composite scheduler
|
||||
that handles tuples (e.g., ``VideoAudioScheduler``).
|
||||
|
||||
Args:
|
||||
noise_pred: Predicted noise (Tensor or tuple for multi-output)
|
||||
t: Current timestep (Tensor or tuple when schedulers differ per output)
|
||||
latents: Current latents (Tensor or tuple for multi-output)
|
||||
noise_pred: Predicted noise
|
||||
t: Current timestep
|
||||
latents: Current latents
|
||||
per_request_scheduler: Optional request-scoped scheduler that
|
||||
overrides ``self.scheduler`` for this call. This is
|
||||
primarily used by step-wise execution, where each request
|
||||
may keep scheduler state in its own runner-managed state
|
||||
object. Request-level execution should usually leave this
|
||||
as ``None`` and continue using ``self.scheduler``.
|
||||
generator: Optional torch Generator for reproducible sampling.
|
||||
When using CFG parallel, both ranks should receive generators
|
||||
initialized with the same seed so that non-deterministic
|
||||
schedulers (e.g., DDPM) produce identical results.
|
||||
|
||||
Returns:
|
||||
Updated latents after scheduler step
|
||||
@@ -307,49 +211,62 @@ class CFGParallelMixin(metaclass=ABCMeta):
|
||||
raise ValueError("No scheduler is available. Set self.scheduler or pass per_request_scheduler.")
|
||||
if not callable(getattr(sched, "step", None)):
|
||||
raise TypeError("per_request_scheduler must provide a callable step(...) method.")
|
||||
step_kwargs = dict(return_dict=False)
|
||||
if generator is not None:
|
||||
step_kwargs["generator"] = generator
|
||||
return sched.step(noise_pred, t, latents, **step_kwargs)[0]
|
||||
return sched.step(noise_pred, t, latents, return_dict=False)[0]
|
||||
|
||||
def scheduler_step_maybe_with_cfg(
|
||||
self,
|
||||
noise_pred: torch.Tensor | tuple[torch.Tensor, ...],
|
||||
t: torch.Tensor | tuple[torch.Tensor, ...],
|
||||
latents: torch.Tensor | tuple[torch.Tensor, ...],
|
||||
noise_pred: torch.Tensor,
|
||||
t: torch.Tensor,
|
||||
latents: torch.Tensor,
|
||||
do_true_cfg: bool,
|
||||
per_request_scheduler: Any | None = None,
|
||||
generator: torch.Generator | None = None,
|
||||
) -> torch.Tensor | tuple[torch.Tensor, ...]:
|
||||
) -> torch.Tensor:
|
||||
"""
|
||||
Step the scheduler with automatic CFG parallel handling.
|
||||
Step the scheduler with (maybe) automatic CFG parallel synchronization.
|
||||
|
||||
All ranks compute the scheduler step locally — no broadcast needed
|
||||
because predict_noise_maybe_with_cfg already ensures all ranks have
|
||||
identical noise_pred after all_gather + local combine.
|
||||
In CFG parallel mode, only rank 0 computes the scheduler step,
|
||||
then broadcasts the result to other ranks.
|
||||
|
||||
Args:
|
||||
noise_pred: Predicted noise (Tensor or tuple, valid on all ranks)
|
||||
t: Current timestep (Tensor or tuple when schedulers differ per output)
|
||||
latents: Current latents (Tensor or tuple)
|
||||
noise_pred: Predicted noise (only valid on rank 0 in CFG parallel)
|
||||
t: Current timestep
|
||||
latents: Current latents
|
||||
do_true_cfg: Whether CFG is enabled
|
||||
per_request_scheduler: Optional request-scoped scheduler that
|
||||
overrides ``self.scheduler`` for this call. This is mainly
|
||||
needed by step-wise execution, where scheduler state may be
|
||||
stored per request. Request-level execution should normally
|
||||
leave this as ``None``.
|
||||
generator: Optional torch Generator for reproducible sampling.
|
||||
When using CFG parallel, both ranks should receive generators
|
||||
initialized with the same seed so that non-deterministic
|
||||
schedulers (e.g., DDPM) produce identical results.
|
||||
|
||||
Returns:
|
||||
Updated latents (identical across all CFG ranks)
|
||||
Updated latents (synchronized across all CFG ranks)
|
||||
"""
|
||||
return self.scheduler_step(
|
||||
noise_pred,
|
||||
t,
|
||||
latents,
|
||||
per_request_scheduler=per_request_scheduler,
|
||||
generator=generator,
|
||||
)
|
||||
# Automatically detect CFG parallel configuration
|
||||
cfg_parallel_ready = do_true_cfg and get_classifier_free_guidance_world_size() > 1
|
||||
|
||||
if cfg_parallel_ready:
|
||||
cfg_group = get_cfg_group()
|
||||
cfg_rank = get_classifier_free_guidance_rank()
|
||||
|
||||
# Only rank 0 computes the scheduler step
|
||||
if cfg_rank == 0:
|
||||
latents = self.scheduler_step(
|
||||
noise_pred,
|
||||
t,
|
||||
latents,
|
||||
per_request_scheduler=per_request_scheduler,
|
||||
)
|
||||
|
||||
# Broadcast the updated latents to all ranks
|
||||
latents = latents.contiguous()
|
||||
cfg_group.broadcast(latents, src=0)
|
||||
else:
|
||||
# No CFG parallel: directly compute scheduler step
|
||||
latents = self.scheduler_step(
|
||||
noise_pred,
|
||||
t,
|
||||
latents,
|
||||
per_request_scheduler=per_request_scheduler,
|
||||
)
|
||||
|
||||
return latents
|
||||
@@ -118,23 +118,6 @@ class AsyncOmni(EngineClient, OmniBase):
|
||||
"""Compatibility helper for call sites expecting async vllm config access."""
|
||||
return self.vllm_config
|
||||
|
||||
def get_diffusion_od_config(self) -> Any | None:
|
||||
"""Return the diffusion-stage config when the pipeline has one."""
|
||||
for stage_client in self.engine.stage_clients:
|
||||
if getattr(stage_client, "stage_type", None) != "diffusion":
|
||||
continue
|
||||
|
||||
od_config = getattr(stage_client, "od_config", None)
|
||||
if od_config is not None:
|
||||
return od_config
|
||||
|
||||
inner_engine = getattr(stage_client, "_engine", None)
|
||||
od_config = getattr(inner_engine, "od_config", None)
|
||||
if od_config is not None:
|
||||
return od_config
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def model_config(self):
|
||||
"""Return the model config for the comprehension stage when present."""
|
||||
|
||||
@@ -251,10 +251,6 @@ class AsyncOmniDiffusion:
|
||||
finished=True,
|
||||
)
|
||||
|
||||
def get_diffusion_od_config(self) -> OmniDiffusionConfig:
|
||||
"""Return the diffusion config used by this engine."""
|
||||
return self.od_config
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Public generate API
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@@ -84,10 +84,8 @@ from vllm.utils.system_utils import decorate_logs
|
||||
from vllm_omni.entrypoints.async_omni import AsyncOmni
|
||||
from vllm_omni.entrypoints.openai.errors import InvalidInputReferenceError
|
||||
from vllm_omni.entrypoints.openai.image_api_utils import (
|
||||
SUPPORTED_LAYERED_RESOLUTIONS,
|
||||
encode_image_base64,
|
||||
parse_size,
|
||||
validate_layered_layers,
|
||||
)
|
||||
from vllm_omni.entrypoints.openai.protocol.audio import BatchSpeechRequest, OpenAICreateSpeechRequest
|
||||
from vllm_omni.entrypoints.openai.protocol.images import (
|
||||
@@ -118,6 +116,8 @@ from vllm_omni.inputs.data import OmniDiffusionSamplingParams, OmniSamplingParam
|
||||
logger = init_logger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
# Supported resolution buckets for layered models (e.g., Qwen-Image-Layered)
|
||||
SUPPORTED_LAYERED_RESOLUTIONS = (640, 1024)
|
||||
profiler_router = APIRouter()
|
||||
|
||||
|
||||
@@ -1292,7 +1292,6 @@ async def generate_images(request: ImageGenerationRequest, raw_request: Request)
|
||||
gen_params, "seed", request.seed if request.seed is not None else random.randint(0, 2**32 - 1)
|
||||
)
|
||||
_update_if_not_none(gen_params, "generator_device", request.generator_device)
|
||||
_update_if_not_none(gen_params, "layers", request.layers)
|
||||
|
||||
request_id = f"img_gen-{random_uuid()}"
|
||||
|
||||
@@ -1407,11 +1406,6 @@ async def edit_images(
|
||||
if not input_images_list:
|
||||
raise HTTPException(status_code=422, detail="Field 'image' or 'url' is required")
|
||||
pil_images = await _load_input_images(input_images_list)
|
||||
if len(pil_images) > 1 and not _supports_multimodal_image_inputs(raw_request, engine_client):
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST.value,
|
||||
detail="Received multiple input images. Only a single image is supported by this model.",
|
||||
)
|
||||
prompt["multi_modal_data"] = {}
|
||||
prompt["multi_modal_data"]["image"] = pil_images
|
||||
|
||||
@@ -1446,13 +1440,11 @@ async def edit_images(
|
||||
detail=f"Invalid resolution {resolution}. Supported resolutions: {SUPPORTED_LAYERED_RESOLUTIONS}.",
|
||||
)
|
||||
# 3.2.1 Validate layers if provided
|
||||
try:
|
||||
layers = validate_layered_layers(layers)
|
||||
except ValueError as e:
|
||||
if layers is not None and layers < 1:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST.value,
|
||||
detail=str(e),
|
||||
) from e
|
||||
detail=f"Invalid layers value {layers}. layers must be >= 1.",
|
||||
)
|
||||
# 3.2.2 Check for conflicting size and resolution parameters
|
||||
if resolution is not None and size.lower() != "auto":
|
||||
raise HTTPException(
|
||||
@@ -1592,20 +1584,6 @@ def _get_engine_and_model(raw_request: Request):
|
||||
return engine_client, model_name, normalized_stage_configs
|
||||
|
||||
|
||||
def _supports_multimodal_image_inputs(raw_request: Request, engine_client: Any) -> bool:
|
||||
diffusion_engine = getattr(raw_request.app.state, "diffusion_engine", None) or engine_client
|
||||
get_diffusion_od_config = getattr(diffusion_engine, "get_diffusion_od_config", None)
|
||||
od_config = (
|
||||
get_diffusion_od_config() if callable(get_diffusion_od_config) else getattr(diffusion_engine, "od_config", None)
|
||||
)
|
||||
|
||||
if od_config is None:
|
||||
# Preserve the existing compatibility behavior when the diffusion
|
||||
# config is not exposed on the serving surface.
|
||||
return True
|
||||
return bool(getattr(od_config, "supports_multimodal_inputs", False))
|
||||
|
||||
|
||||
def _get_lora_from_json_str(lora_body):
|
||||
if lora_body is None:
|
||||
return None
|
||||
|
||||
@@ -13,9 +13,6 @@ import io
|
||||
|
||||
import PIL.Image
|
||||
|
||||
SUPPORTED_LAYERED_RESOLUTIONS = (640, 1024)
|
||||
SUPPORTED_LAYERED_LAYERS_RANGE = range(3, 11)
|
||||
|
||||
|
||||
def parse_size(size_str: str) -> tuple[int, int]:
|
||||
"""Parse size string to width and height tuple.
|
||||
@@ -66,16 +63,3 @@ def encode_image_base64(image: PIL.Image.Image) -> str:
|
||||
image.save(buffer, format="PNG")
|
||||
buffer.seek(0)
|
||||
return base64.b64encode(buffer.read()).decode("utf-8")
|
||||
|
||||
|
||||
def validate_layered_layers(layers: int | None) -> int | None:
|
||||
"""Validate the Qwen-Image-Layered ``layers`` parameter."""
|
||||
if layers is None:
|
||||
return None
|
||||
if layers not in SUPPORTED_LAYERED_LAYERS_RANGE:
|
||||
raise ValueError(
|
||||
f"Invalid layers value {layers}. layers must be between "
|
||||
f"{SUPPORTED_LAYERED_LAYERS_RANGE.start} and "
|
||||
f"{SUPPORTED_LAYERED_LAYERS_RANGE.stop - 1} inclusive."
|
||||
)
|
||||
return layers
|
||||
@@ -12,8 +12,6 @@ from typing import Any
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
from vllm_omni.entrypoints.openai.image_api_utils import validate_layered_layers
|
||||
|
||||
|
||||
class ResponseFormat(str, Enum):
|
||||
"""Image response format"""
|
||||
@@ -45,10 +43,6 @@ class ImageGenerationRequest(BaseModel):
|
||||
)
|
||||
response_format: ResponseFormat = Field(default=ResponseFormat.B64_JSON, description="Format of the returned image")
|
||||
user: str | None = Field(default=None, description="User identifier for tracking")
|
||||
layers: int | None = Field(
|
||||
default=None,
|
||||
description="Number of output layers for layered image models. Supported range: 3-10.",
|
||||
)
|
||||
|
||||
@field_validator("size")
|
||||
@classmethod
|
||||
@@ -73,12 +67,6 @@ class ImageGenerationRequest(BaseModel):
|
||||
raise ValueError(f"Only 'b64_json' response format is supported, got: {v}")
|
||||
return v
|
||||
|
||||
@field_validator("layers")
|
||||
@classmethod
|
||||
def validate_layers(cls, v):
|
||||
"""Validate the layers parameter for layered image models."""
|
||||
return validate_layered_layers(v)
|
||||
|
||||
# vllm-omni extensions for diffusion control
|
||||
negative_prompt: str | None = Field(default=None, description="Text describing what to avoid in the image")
|
||||
num_inference_steps: int | None = Field(
|
||||
|
||||
@@ -82,7 +82,6 @@ from vllm.tool_parsers.mistral_tool_parser import MistralToolCall
|
||||
from vllm.utils.collection_utils import as_list
|
||||
|
||||
from vllm_omni.entrypoints.openai.audio_utils_mixin import AudioMixin
|
||||
from vllm_omni.entrypoints.openai.image_api_utils import validate_layered_layers
|
||||
from vllm_omni.entrypoints.openai.protocol import OmniChatCompletionStreamResponse
|
||||
from vllm_omni.entrypoints.openai.protocol.audio import AudioResponse, CreateAudio
|
||||
from vllm_omni.entrypoints.openai.utils import parse_lora_request
|
||||
@@ -2095,11 +2094,6 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin):
|
||||
layers = extra_body.get("layers")
|
||||
resolution = extra_body.get("resolution")
|
||||
|
||||
try:
|
||||
layers = validate_layered_layers(layers)
|
||||
except ValueError as e:
|
||||
return self._create_error_response(str(e), status_code=400)
|
||||
|
||||
logger.info(
|
||||
"Diffusion chat request %s: prompt=%r, ref_images=%d, params=%s",
|
||||
request_id,
|
||||
|
||||
@@ -59,8 +59,8 @@ stages:
|
||||
engine_output_type: audio
|
||||
gpu_memory_utilization: 0.08
|
||||
distributed_executor_backend: "mp"
|
||||
max_num_batched_tokens: 65536
|
||||
max_model_len: 65536
|
||||
max_num_batched_tokens: 8192
|
||||
max_model_len: 32768
|
||||
input_connectors:
|
||||
from_stage_0: connector_of_shared_memory
|
||||
tts_args:
|
||||
|
||||
@@ -51,10 +51,9 @@ stage_args:
|
||||
gpu_memory_utilization: 0.3
|
||||
distributed_executor_backend: "mp"
|
||||
# Must be divisible by num_code_groups and cover (left_context + chunk).
|
||||
# Prefill length is Q * num_frames (e.g. 16 * 2148 = 34368); keep headroom past 32k.
|
||||
max_num_batched_tokens: 65536
|
||||
# async_chunk appends windows per step; max_model_len must cover accumulated flat codec stream.
|
||||
max_model_len: 65536
|
||||
max_num_batched_tokens: 8192
|
||||
# async_chunk appends windows per step; max_model_len must cover accumulated stream.
|
||||
max_model_len: 32768
|
||||
engine_input_source: [0]
|
||||
final_output: true
|
||||
final_output_type: audio
|
||||
|
||||
@@ -55,9 +55,9 @@ stage_args:
|
||||
gpu_memory_utilization: 0.2
|
||||
distributed_executor_backend: "mp"
|
||||
# Must be divisible by num_code_groups and cover (left_context + chunk).
|
||||
max_num_batched_tokens: 65536
|
||||
# Flat codec prompt can exceed 32k tokens (Q * frames); align with max_tokens below.
|
||||
max_model_len: 65536
|
||||
max_num_batched_tokens: 8192
|
||||
# async_chunk appends windows per step; max_model_len must cover accumulated stream.
|
||||
max_model_len: 32768
|
||||
engine_input_source: [0]
|
||||
final_output: true
|
||||
final_output_type: audio
|
||||
|
||||
@@ -46,8 +46,8 @@ stage_args:
|
||||
engine_output_type: audio
|
||||
gpu_memory_utilization: 0.2
|
||||
distributed_executor_backend: "mp"
|
||||
max_num_batched_tokens: 65536
|
||||
max_model_len: 65536
|
||||
max_num_batched_tokens: 8192
|
||||
max_model_len: 32768
|
||||
engine_input_source: [0]
|
||||
custom_process_input_func: vllm_omni.model_executor.stage_input_processors.qwen3_tts.talker2code2wav
|
||||
final_output: true
|
||||
|
||||
@@ -50,8 +50,10 @@ stage_args:
|
||||
engine_output_type: audio
|
||||
gpu_memory_utilization: 0.2
|
||||
distributed_executor_backend: "mp"
|
||||
max_num_batched_tokens: 65536
|
||||
max_model_len: 65536
|
||||
# Must be divisible by num_code_groups and cover (left_context + chunk).
|
||||
max_num_batched_tokens: 8192
|
||||
# async_chunk appends windows per step; max_model_len must cover accumulated stream.
|
||||
max_model_len: 32768
|
||||
engine_input_source: [0]
|
||||
final_output: true
|
||||
final_output_type: audio
|
||||
|
||||
Reference in new issue
Block a user