Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e9af81da3 |
No files matched your search
@@ -139,12 +139,7 @@ steps:
|
|||||||
timeout_in_minutes: 50
|
timeout_in_minutes: 50
|
||||||
commands:
|
commands:
|
||||||
- pytest -s -v tests/e2e/offline_inference/test_minicpmo_4_5.py tests/e2e/online_serving/test_minicpmo_4_5.py -m 'advanced_model and cuda' --run-level 'advanced_model'
|
- pytest -s -v tests/e2e/offline_inference/test_minicpmo_4_5.py tests/e2e/online_serving/test_minicpmo_4_5.py -m 'advanced_model and cuda' --run-level 'advanced_model'
|
||||||
# h100_2, not h100_1: advanced_model asserts transcribe every audio
|
mirror_hardwares: h100_1
|
||||||
# response with Whisper, and tests/helpers/media.py only uses an
|
|
||||||
# accelerator for Whisper when a spare GPU exists. On a single card it
|
|
||||||
# grades on CPU: ~8-10 min per clip vs ~30 s, which alone overruns the
|
|
||||||
# 50-minute job timeout (build 2956).
|
|
||||||
mirror_hardwares: h100_2
|
|
||||||
|
|
||||||
- label: "Omni · MiniCPM-o 4.5 Duplex Test"
|
- label: "Omni · MiniCPM-o 4.5 Duplex Test"
|
||||||
source_file_dependencies:
|
source_file_dependencies:
|
||||||
|
|||||||
@@ -345,7 +345,7 @@ steps:
|
|||||||
commands:
|
commands:
|
||||||
- export DIFFUSION_BENCHMARK_DIR=tests/dfx/perf/results
|
- export DIFFUSION_BENCHMARK_DIR=tests/dfx/perf/results
|
||||||
- export DIFFUSION_ATTENTION_BACKEND=FLASH_ATTN
|
- export DIFFUSION_ATTENTION_BACKEND=FLASH_ATTN
|
||||||
- export CACHE_DIT_VERSION=1.3.0
|
- export CACHE_DIT_VERSION=1.5.0
|
||||||
- |
|
- |
|
||||||
set +e
|
set +e
|
||||||
pytest -s -v tests/dfx/perf/scripts/run_diffusion_benchmark.py --test-config-file tests/dfx/perf/tests/test_qwen_image_vllm_omni.json
|
pytest -s -v tests/dfx/perf/scripts/run_diffusion_benchmark.py --test-config-file tests/dfx/perf/tests/test_qwen_image_vllm_omni.json
|
||||||
@@ -361,7 +361,7 @@ steps:
|
|||||||
commands:
|
commands:
|
||||||
- export DIFFUSION_BENCHMARK_DIR=tests/dfx/perf/results
|
- export DIFFUSION_BENCHMARK_DIR=tests/dfx/perf/results
|
||||||
- export DIFFUSION_ATTENTION_BACKEND=FLASH_ATTN
|
- export DIFFUSION_ATTENTION_BACKEND=FLASH_ATTN
|
||||||
- export CACHE_DIT_VERSION=1.3.0
|
- export CACHE_DIT_VERSION=1.5.0
|
||||||
- |
|
- |
|
||||||
set +e
|
set +e
|
||||||
pytest -s -v tests/dfx/perf/scripts/run_diffusion_benchmark.py --test-config-file tests/dfx/perf/tests/test_qwen_image_edit_vllm_omni.json
|
pytest -s -v tests/dfx/perf/scripts/run_diffusion_benchmark.py --test-config-file tests/dfx/perf/tests/test_qwen_image_edit_vllm_omni.json
|
||||||
@@ -377,7 +377,7 @@ steps:
|
|||||||
commands:
|
commands:
|
||||||
- export DIFFUSION_BENCHMARK_DIR=tests/dfx/perf/results
|
- export DIFFUSION_BENCHMARK_DIR=tests/dfx/perf/results
|
||||||
- export DIFFUSION_ATTENTION_BACKEND=FLASH_ATTN
|
- export DIFFUSION_ATTENTION_BACKEND=FLASH_ATTN
|
||||||
- export CACHE_DIT_VERSION=1.3.0
|
- export CACHE_DIT_VERSION=1.5.0
|
||||||
- |
|
- |
|
||||||
set +e
|
set +e
|
||||||
pytest -s -v tests/dfx/perf/scripts/run_diffusion_benchmark.py --test-config-file tests/dfx/perf/tests/test_qwen_image_edit_2511_vllm_omni.json
|
pytest -s -v tests/dfx/perf/scripts/run_diffusion_benchmark.py --test-config-file tests/dfx/perf/tests/test_qwen_image_edit_2511_vllm_omni.json
|
||||||
@@ -393,7 +393,7 @@ steps:
|
|||||||
commands:
|
commands:
|
||||||
- export DIFFUSION_BENCHMARK_DIR=tests/dfx/perf/results
|
- export DIFFUSION_BENCHMARK_DIR=tests/dfx/perf/results
|
||||||
- export DIFFUSION_ATTENTION_BACKEND=FLASH_ATTN
|
- export DIFFUSION_ATTENTION_BACKEND=FLASH_ATTN
|
||||||
- export CACHE_DIT_VERSION=1.3.0
|
- export CACHE_DIT_VERSION=1.5.0
|
||||||
- |
|
- |
|
||||||
set +e
|
set +e
|
||||||
pytest -s -v tests/dfx/perf/scripts/run_diffusion_benchmark.py --test-config-file tests/dfx/perf/tests/test_qwen_image_layered_vllm_omni.json
|
pytest -s -v tests/dfx/perf/scripts/run_diffusion_benchmark.py --test-config-file tests/dfx/perf/tests/test_qwen_image_layered_vllm_omni.json
|
||||||
@@ -409,7 +409,7 @@ steps:
|
|||||||
commands:
|
commands:
|
||||||
- export DIFFUSION_BENCHMARK_DIR=tests/dfx/perf/results
|
- export DIFFUSION_BENCHMARK_DIR=tests/dfx/perf/results
|
||||||
- export DIFFUSION_ATTENTION_BACKEND=FLASH_ATTN
|
- export DIFFUSION_ATTENTION_BACKEND=FLASH_ATTN
|
||||||
- export CACHE_DIT_VERSION=1.3.0
|
- export CACHE_DIT_VERSION=1.5.0
|
||||||
- |
|
- |
|
||||||
set +e
|
set +e
|
||||||
pytest -s -v tests/dfx/perf/scripts/run_diffusion_benchmark.py --test-config-file tests/dfx/perf/tests/test_bagel_vllm_omni.json
|
pytest -s -v tests/dfx/perf/scripts/run_diffusion_benchmark.py --test-config-file tests/dfx/perf/tests/test_bagel_vllm_omni.json
|
||||||
|
|||||||
@@ -128,21 +128,6 @@ steps:
|
|||||||
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
|
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
|
||||||
always-pull: true
|
always-pull: true
|
||||||
propagate-environment: true
|
propagate-environment: true
|
||||||
# vLLM 0.27 added a pre-flight capacity check in
|
|
||||||
# distributed/device_communicators/shm_broadcast.py. Without this
|
|
||||||
# the container gets Docker's 64 MiB default and every test in
|
|
||||||
# this step dies at engine start with
|
|
||||||
# RuntimeError: Insufficient space in /dev/shm:
|
|
||||||
# 240 MiB required, 64 MiB available
|
|
||||||
# (9 failed in 8.7s on build #2949). vLLM 0.26 had no such check
|
|
||||||
# and allocated regardless, so this step passed on main while
|
|
||||||
# being under-provisioned the whole time — 0.27 turned a silent
|
|
||||||
# risk into an explicit error.
|
|
||||||
#
|
|
||||||
# This is the only inline docker plugin block in .buildkite/cuda/;
|
|
||||||
# every other step inherits common/ci_mirror_hardwares.yml, which
|
|
||||||
# already sets 8gb. Matching that rather than inventing a value.
|
|
||||||
shm-size: "8gb"
|
|
||||||
environment:
|
environment:
|
||||||
- "HF_HOME=/fsx/hf_cache"
|
- "HF_HOME=/fsx/hf_cache"
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ steps:
|
|||||||
DOCKER_BUILDKIT: "1"
|
DOCKER_BUILDKIT: "1"
|
||||||
# Buildkite will automatically replace this with the actual commit hash
|
# Buildkite will automatically replace this with the actual commit hash
|
||||||
VLLM_IMAGE_TAG: "${BUILDKITE_COMMIT}"
|
VLLM_IMAGE_TAG: "${BUILDKITE_COMMIT}"
|
||||||
VLLM_VERSION: "v0.27.0"
|
VLLM_VERSION: "v0.26.0"
|
||||||
priority: 100
|
priority: 100
|
||||||
timeout_in_minutes: 60
|
timeout_in_minutes: 60
|
||||||
soft_fail: false
|
soft_fail: false
|
||||||
@@ -34,23 +34,8 @@ docker_build() {
|
|||||||
docker build "${out[@]}" "$@" -f docker/Dockerfile.xpu .
|
docker build "${out[@]}" "$@" -f docker/Dockerfile.xpu .
|
||||||
}
|
}
|
||||||
|
|
||||||
build_vllm_base() (
|
|
||||||
local vllm_source_dir
|
|
||||||
vllm_source_dir=$(mktemp -d)
|
|
||||||
trap 'rm -rf "${vllm_source_dir}"' EXIT
|
|
||||||
|
|
||||||
git clone --depth 1 --branch "${VLLM_VERSION}" \
|
|
||||||
https://github.com/vllm-project/vllm "${vllm_source_dir}"
|
|
||||||
|
|
||||||
local out=("${export_args[@]/'{{IMAGE}}'/${base_image_name}}")
|
|
||||||
docker build "${out[@]}" \
|
|
||||||
--target vllm-openai \
|
|
||||||
-f "${vllm_source_dir}/docker/Dockerfile.xpu" \
|
|
||||||
"${vllm_source_dir}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if [ -z "$(docker images -q "${base_image_name}")" ]; then
|
if [ -z "$(docker images -q "${base_image_name}")" ]; then
|
||||||
build_vllm_base
|
docker_build "${base_image_name}" --target vllm-base --build-arg "VLLM_VERSION=${VLLM_VERSION}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Try building the docker image
|
# Try building the docker image
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ steps:
|
|||||||
DIFFUSION_ATTENTION_BACKEND: TORCH_SDPA
|
DIFFUSION_ATTENTION_BACKEND: TORCH_SDPA
|
||||||
commands:
|
commands:
|
||||||
- export DIFFUSION_BENCHMARK_DIR=tests/dfx/perf/results
|
- export DIFFUSION_BENCHMARK_DIR=tests/dfx/perf/results
|
||||||
- export CACHE_DIT_VERSION=1.3.0
|
- export CACHE_DIT_VERSION=1.5.0
|
||||||
- |
|
- |
|
||||||
set +e
|
set +e
|
||||||
pytest -s -v tests/dfx/perf/scripts/run_diffusion_benchmark.py -m npu --test-config-file tests/dfx/perf/tests/test_qwen_image_edit_2511_vllm_omni.json
|
pytest -s -v tests/dfx/perf/scripts/run_diffusion_benchmark.py -m npu --test-config-file tests/dfx/perf/tests/test_qwen_image_edit_2511_vllm_omni.json
|
||||||
|
|||||||
@@ -139,34 +139,6 @@ python benchmarks/tts/bench_tts.py \
|
|||||||
--output-dir ./results
|
--output-dir ./results
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Fixed IndexTTS 2.5 performance sweep
|
|
||||||
|
|
||||||
IndexTTS 2.5 is registered in `model_configs.yaml` like the other TTS models.
|
|
||||||
Use `--served-model-name` when the server was launched from a local native
|
|
||||||
bundle instead of the registry key. The command below reproduces the acceptance
|
|
||||||
workload: Seed-TTS Eval EN, `n=500`, concurrency `4/8/16/32`, five warmups, and
|
|
||||||
dataset seed 0.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python benchmarks/tts/bench_tts.py \
|
|
||||||
--model IndexTeam/IndexTTS-2.5 \
|
|
||||||
--served-model-name /path/to/indextts-2.5 \
|
|
||||||
--task voice_clone --locale en \
|
|
||||||
--dataset-path /path/to/seedtts_testset \
|
|
||||||
--host 127.0.0.1 --port 8092 \
|
|
||||||
--concurrency 4 8 16 32 \
|
|
||||||
--num-prompts 500 --num-warmups 5 \
|
|
||||||
--output-dir ./results/indextts2_5 \
|
|
||||||
-- \
|
|
||||||
--tokenizer /path/to/indextts-2.5/qwen0.6bemo4-merge \
|
|
||||||
--seed 0 --metric-percentiles 50,95,99 --disable-tqdm --save-detailed
|
|
||||||
```
|
|
||||||
|
|
||||||
The wrapper benchmarks an already-running server. For reproducible quality
|
|
||||||
comparisons, add `--request-seed 42`; production-performance runs should omit
|
|
||||||
it. The trailing `--seed 0` controls Seed-TTS row selection rather than model
|
|
||||||
sampling.
|
|
||||||
|
|
||||||
### 4. Plot a sweep
|
### 4. Plot a sweep
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -84,17 +84,12 @@ def build_bench_args(
|
|||||||
result_filename: str | None,
|
result_filename: str | None,
|
||||||
extra_cli_args: list[str],
|
extra_cli_args: list[str],
|
||||||
output_len: int | None = None,
|
output_len: int | None = None,
|
||||||
served_model_name: str | None = None,
|
|
||||||
num_warmups: int = 2,
|
|
||||||
request_seed: int | None = None,
|
|
||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
"""Build the ``vllm bench serve --omni`` command for one (task, concurrency) run."""
|
"""Build the ``vllm bench serve --omni`` command for one (task, concurrency) run."""
|
||||||
dataset_name = _TASK_TO_DATASET[task]
|
dataset_name = _TASK_TO_DATASET[task]
|
||||||
backend: str = model_cfg["backend"]
|
backend: str = model_cfg["backend"]
|
||||||
endpoint: str = model_cfg["endpoint"]
|
endpoint: str = model_cfg["endpoint"]
|
||||||
task_extra_body: dict[str, Any] = dict((model_cfg.get("task_extra_body") or {}).get(task) or {})
|
task_extra_body: dict[str, Any] = (model_cfg.get("task_extra_body") or {}).get(task) or {}
|
||||||
if request_seed is not None:
|
|
||||||
task_extra_body["seed"] = request_seed
|
|
||||||
|
|
||||||
# Resolve dataset path
|
# Resolve dataset path
|
||||||
if dataset_path:
|
if dataset_path:
|
||||||
@@ -114,7 +109,7 @@ def build_bench_args(
|
|||||||
"--port",
|
"--port",
|
||||||
str(port),
|
str(port),
|
||||||
"--model",
|
"--model",
|
||||||
served_model_name or model,
|
model,
|
||||||
"--backend",
|
"--backend",
|
||||||
backend,
|
backend,
|
||||||
"--endpoint",
|
"--endpoint",
|
||||||
@@ -124,7 +119,7 @@ def build_bench_args(
|
|||||||
"--num-prompts",
|
"--num-prompts",
|
||||||
str(num_prompts),
|
str(num_prompts),
|
||||||
"--num-warmups",
|
"--num-warmups",
|
||||||
str(num_warmups),
|
"2",
|
||||||
"--percentile-metrics",
|
"--percentile-metrics",
|
||||||
"ttft,e2el,audio_rtf,audio_ttfp,audio_duration,audio_underrun",
|
"ttft,e2el,audio_rtf,audio_ttfp,audio_duration,audio_underrun",
|
||||||
]
|
]
|
||||||
@@ -157,8 +152,7 @@ def build_bench_args(
|
|||||||
if result_filename:
|
if result_filename:
|
||||||
cmd += ["--result-filename", result_filename]
|
cmd += ["--result-filename", result_filename]
|
||||||
|
|
||||||
passthrough = extra_cli_args[1:] if extra_cli_args[:1] == ["--"] else extra_cli_args
|
cmd += extra_cli_args
|
||||||
cmd += passthrough
|
|
||||||
return cmd
|
return cmd
|
||||||
|
|
||||||
|
|
||||||
@@ -231,12 +225,6 @@ def main() -> None:
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--model", required=True, help="HuggingFace model ID (e.g. Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice)"
|
"--model", required=True, help="HuggingFace model ID (e.g. Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice)"
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
|
||||||
"--served-model-name",
|
|
||||||
default=None,
|
|
||||||
help="Model name/path accepted by the running server. Use this when --model selects a registry entry "
|
|
||||||
"but the server was launched from a local bundle path.",
|
|
||||||
)
|
|
||||||
parser.add_argument("--task", default="all", help="Task type: voice_clone | default_voice | voice_design | all")
|
parser.add_argument("--task", default="all", help="Task type: voice_clone | default_voice | voice_design | all")
|
||||||
parser.add_argument("--locale", default="en", choices=["en", "zh"])
|
parser.add_argument("--locale", default="en", choices=["en", "zh"])
|
||||||
parser.add_argument("--concurrency", type=int, nargs="+", default=[1, 4], metavar="N")
|
parser.add_argument("--concurrency", type=int, nargs="+", default=[1, 4], metavar="N")
|
||||||
@@ -248,13 +236,6 @@ def main() -> None:
|
|||||||
metavar="N",
|
metavar="N",
|
||||||
help="Number of prompts per run. If one value, applied to all concurrency levels.",
|
help="Number of prompts per run. If one value, applied to all concurrency levels.",
|
||||||
)
|
)
|
||||||
parser.add_argument("--num-warmups", type=int, default=2, help="Warmup requests before each measured run")
|
|
||||||
parser.add_argument(
|
|
||||||
"--request-seed",
|
|
||||||
type=int,
|
|
||||||
default=None,
|
|
||||||
help="Optional model sampling seed added to every request body for reproducible quality runs",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--dataset-path", default=None, help="Root of seed-tts-eval dataset (required for voice_clone/default_voice)"
|
"--dataset-path", default=None, help="Root of seed-tts-eval dataset (required for voice_clone/default_voice)"
|
||||||
)
|
)
|
||||||
@@ -275,9 +256,6 @@ def main() -> None:
|
|||||||
parser.add_argument("extra", nargs=argparse.REMAINDER, help="Extra args passed directly to vllm bench serve")
|
parser.add_argument("extra", nargs=argparse.REMAINDER, help="Extra args passed directly to vllm bench serve")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if args.num_warmups < 0:
|
|
||||||
parser.error("--num-warmups cannot be negative")
|
|
||||||
|
|
||||||
model_configs = load_model_configs(Path(args.model_configs))
|
model_configs = load_model_configs(Path(args.model_configs))
|
||||||
if args.model not in model_configs:
|
if args.model not in model_configs:
|
||||||
known = "\n ".join(model_configs.keys())
|
known = "\n ".join(model_configs.keys())
|
||||||
@@ -330,9 +308,6 @@ def main() -> None:
|
|||||||
result_filename=result_filename,
|
result_filename=result_filename,
|
||||||
extra_cli_args=args.extra or [],
|
extra_cli_args=args.extra or [],
|
||||||
output_len=args.output_len,
|
output_len=args.output_len,
|
||||||
served_model_name=args.served_model_name,
|
|
||||||
num_warmups=args.num_warmups,
|
|
||||||
request_seed=args.request_seed,
|
|
||||||
)
|
)
|
||||||
result = run_one_benchmark(cmd)
|
result = run_one_benchmark(cmd)
|
||||||
if result is not None:
|
if result is not None:
|
||||||
|
|||||||
@@ -7,17 +7,6 @@
|
|||||||
# is tracked here.
|
# is tracked here.
|
||||||
|
|
||||||
models:
|
models:
|
||||||
IndexTeam/IndexTTS-2.5:
|
|
||||||
supported_tasks: [voice_clone]
|
|
||||||
backend: openai-audio-speech
|
|
||||||
endpoint: /v1/audio/speech
|
|
||||||
trust_remote_code: true
|
|
||||||
task_extra_body:
|
|
||||||
voice_clone:
|
|
||||||
extra_params:
|
|
||||||
lang: en
|
|
||||||
text_normalization: true
|
|
||||||
|
|
||||||
# -CustomVoice checkpoints lack speaker_encoder weights, so voice_clone is
|
# -CustomVoice checkpoints lack speaker_encoder weights, so voice_clone is
|
||||||
# NOT supported (an attempt raises ValueError from _extract_speaker_embedding
|
# NOT supported (an attempt raises ValueError from _extract_speaker_embedding
|
||||||
# at model runtime). Use -Base for voice_clone.
|
# at model runtime). Use -Base for voice_clone.
|
||||||
|
|||||||
+2
-19
@@ -1,24 +1,7 @@
|
|||||||
# Target vllm version: v0.27.0 (releases/v0.27.0 branch).
|
# Target vllm version: v0.26.0 (releases/v0.26.0 branch).
|
||||||
# Keep VLLM_BASE_TAG aligned with the target vLLM release on every rebase.
|
# Keep VLLM_BASE_TAG aligned with the target vLLM release on every rebase.
|
||||||
#
|
|
||||||
# Release-boundary form: there is no precompiled-wheel pin here, because the
|
|
||||||
# base image already ships exactly the target vLLM. Everything the runtime
|
|
||||||
# needs — torch, flashinfer, nvJitLink, numpy — comes from that image as one
|
|
||||||
# self-consistent set.
|
|
||||||
#
|
|
||||||
# This block reappears only when the rebase targets a vLLM commit that is NOT a
|
|
||||||
# released tag. During the 0.27 rebase, targeting v0.27.0rc1 on top of the
|
|
||||||
# v0.26.0 base image required reinstalling vLLM from wheels.vllm.ai and then
|
|
||||||
# hand-repairing the fallout: pinning torch ahead of flashinfer (installing it
|
|
||||||
# after bound flashinfer's TVM FFI runtime to the old torch and aborted engine
|
|
||||||
# core init), registering libnvJitLink.so.13 with ldconfig, and re-pinning numpy
|
|
||||||
# under numba's ceiling. All of it was compensation for a base image and a vLLM
|
|
||||||
# that disagreed. At a real release boundary they agree, so none of it is
|
|
||||||
# needed. If you pin an untagged commit again, restore the reinstall block —
|
|
||||||
# agent/tasks/20_update_wheel_url.sh fails the run rather than letting the
|
|
||||||
# Dockerfile silently ship the wrong vLLM.
|
|
||||||
ARG VLLM_BASE_IMAGE=vllm/vllm-openai
|
ARG VLLM_BASE_IMAGE=vllm/vllm-openai
|
||||||
ARG VLLM_BASE_TAG=v0.27.0
|
ARG VLLM_BASE_TAG=v0.26.0
|
||||||
|
|
||||||
# We use registry-based layer caching for the dependencies,
|
# We use registry-based layer caching for the dependencies,
|
||||||
# where the tag is determined based on the hash of the contents
|
# where the tag is determined based on the hash of the contents
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ ENV UV_EXTRA_INDEX_URL=${PIP_EXTRA_INDEX_URL}
|
|||||||
ENV UV_INDEX_STRATEGY="unsafe-best-match"
|
ENV UV_INDEX_STRATEGY="unsafe-best-match"
|
||||||
ENV UV_LINK_MODE="copy"
|
ENV UV_LINK_MODE="copy"
|
||||||
|
|
||||||
ARG VLLM_VERSION=v0.27.0
|
ARG VLLM_VERSION=v0.26.0
|
||||||
RUN git clone -b ${VLLM_VERSION} https://github.com/vllm-project/vllm
|
RUN git clone -b ${VLLM_VERSION} https://github.com/vllm-project/vllm
|
||||||
WORKDIR /workspace/vllm
|
WORKDIR /workspace/vllm
|
||||||
|
|
||||||
@@ -108,7 +108,7 @@ ENV VLLM_WORKER_MULTIPROC_METHOD=spawn
|
|||||||
|
|
||||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||||
uv pip uninstall triton triton-xpu && \
|
uv pip uninstall triton triton-xpu && \
|
||||||
uv pip install triton-xpu==3.7.2 && \
|
uv pip install triton-xpu==3.7.1 && \
|
||||||
uv pip install --no-build-isolation --no-deps .
|
uv pip install --no-build-isolation --no-deps .
|
||||||
|
|
||||||
CMD ["/bin/bash"]
|
CMD ["/bin/bash"]
|
||||||
@@ -144,13 +144,12 @@ WORKDIR /workspace/vllm-omni
|
|||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
ENV VLLM_OMNI_TARGET_DEVICE=xpu
|
ENV VLLM_OMNI_TARGET_DEVICE=xpu
|
||||||
# Keep the upstream oneCCL packages paired with the XPU torch runtime.
|
|
||||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||||
uv pip install --no-build-isolation \
|
uv pip install --no-build-isolation \
|
||||||
--extra-index-url https://mirrors.ustc.edu.cn/pypi/simple/ \
|
--extra-index-url https://mirrors.ustc.edu.cn/pypi/simple/ \
|
||||||
".[dev]" && \
|
".[dev]" && \
|
||||||
uv pip uninstall triton && \
|
uv pip uninstall triton oneccl oneccl-devel && \
|
||||||
uv pip install triton-xpu==3.7.2 --reinstall
|
uv pip install triton-xpu==3.7.1 --reinstall
|
||||||
|
|
||||||
FROM vllm-omni AS vllm-omni-openai
|
FROM vllm-omni AS vllm-omni-openai
|
||||||
|
|
||||||
|
|||||||
+25
-31
@@ -40,30 +40,16 @@ nav:
|
|||||||
- Models:
|
- Models:
|
||||||
- models/supported_models.md
|
- models/supported_models.md
|
||||||
- Features:
|
- Features:
|
||||||
- Overview: features/README.md
|
- Sleep Mode: features/sleep_mode.md
|
||||||
- Runtime and Stage Execution:
|
- Diffusion Features:
|
||||||
- Execution Modes and Streaming: user_guide/diffusion/execution_modes.md
|
- Overview: user_guide/diffusion_features.md
|
||||||
- Sleep Mode: features/sleep_mode.md
|
- Feature Compatibility: user_guide/feature_compatibility.md
|
||||||
- Quantization:
|
- Regional Compilation: user_guide/diffusion/regional_compilation.md
|
||||||
- Overview: user_guide/quantization/overview.md
|
|
||||||
- Online Quantization: user_guide/quantization/online.md
|
|
||||||
- Quantized KV Cache: user_guide/quantization/quantized_kvcache.md
|
|
||||||
- FP8 W8A8: user_guide/quantization/fp8.md
|
|
||||||
- Int8 W8A8: user_guide/quantization/int8.md
|
|
||||||
- BitsAndBytes W4: user_guide/quantization/bitsandbytes.md
|
|
||||||
- MXFP8 W8A8: user_guide/quantization/mxfp8.md
|
|
||||||
- MXFP4 W4A4: user_guide/quantization/mxfp4.md
|
|
||||||
- ModelOpt: user_guide/quantization/modelopt.md
|
|
||||||
- AutoRound: user_guide/quantization/autoround.md
|
|
||||||
- msModelSlim: user_guide/quantization/msmodelslim.md
|
|
||||||
- Diffusion Acceleration:
|
|
||||||
- Overview and Compatibility:
|
|
||||||
- Overview: user_guide/diffusion_features.md
|
|
||||||
- Feature Compatibility: user_guide/feature_compatibility.md
|
|
||||||
- CPU Offloading: user_guide/diffusion/cpu_offload.md
|
|
||||||
- Cache Acceleration:
|
- Cache Acceleration:
|
||||||
- TeaCache: user_guide/diffusion/cache_acceleration/teacache.md
|
- TeaCache: user_guide/diffusion/cache_acceleration/teacache.md
|
||||||
- Cache-DiT: user_guide/diffusion/cache_acceleration/cache_dit.md
|
- Cache-DiT: user_guide/diffusion/cache_acceleration/cache_dit.md
|
||||||
|
- Attention Backends: user_guide/diffusion/attention_backends.md
|
||||||
|
- Frame Interpolation: user_guide/diffusion/frame_interpolation.md
|
||||||
- Parallelism:
|
- Parallelism:
|
||||||
- Overview: user_guide/diffusion/parallelism/overview.md
|
- Overview: user_guide/diffusion/parallelism/overview.md
|
||||||
- CFG Parallel: user_guide/diffusion/parallelism/cfg_parallel.md
|
- CFG Parallel: user_guide/diffusion/parallelism/cfg_parallel.md
|
||||||
@@ -72,15 +58,23 @@ nav:
|
|||||||
- Sequence Parallel: user_guide/diffusion/parallelism/sequence_parallel.md
|
- Sequence Parallel: user_guide/diffusion/parallelism/sequence_parallel.md
|
||||||
- Tensor Parallel: user_guide/diffusion/parallelism/tensor_parallel.md
|
- Tensor Parallel: user_guide/diffusion/parallelism/tensor_parallel.md
|
||||||
- VAE Parallelism: user_guide/diffusion/parallelism/vae_parallelism.md
|
- VAE Parallelism: user_guide/diffusion/parallelism/vae_parallelism.md
|
||||||
- Attention Backends: user_guide/diffusion/attention_backends.md
|
- CPU Offloading: user_guide/diffusion/cpu_offload.md
|
||||||
- Regional Compilation: user_guide/diffusion/regional_compilation.md
|
|
||||||
- Frame Interpolation: user_guide/diffusion/frame_interpolation.md
|
|
||||||
- Startup and Loading: user_guide/diffusion/startup_and_loading.md
|
|
||||||
- LoRA: user_guide/diffusion/lora.md
|
- LoRA: user_guide/diffusion/lora.md
|
||||||
- Experimental:
|
- Custom Pipeline: features/custom_pipeline.md
|
||||||
- Session State Manager: features/session_state_manager.md
|
- Execution Modes: user_guide/diffusion/execution_modes.md
|
||||||
- Integrations:
|
- Quantization:
|
||||||
- Custom Diffusion Pipeline: features/custom_pipeline.md
|
- Quantization: user_guide/quantization/overview.md
|
||||||
|
- Online Quantization: user_guide/quantization/online.md
|
||||||
|
- Quantized KV Cache: user_guide/quantization/quantized_kvcache.md
|
||||||
|
- FP8 W8A8: user_guide/quantization/fp8.md
|
||||||
|
- Int8 W8A8: user_guide/quantization/int8.md
|
||||||
|
- BitsAndBytes W4: user_guide/quantization/bitsandbytes.md
|
||||||
|
- MXFP8 W8A8: user_guide/quantization/mxfp8.md
|
||||||
|
- MXFP4 W4A4: user_guide/quantization/mxfp4.md
|
||||||
|
- ModelOpt: user_guide/quantization/modelopt.md
|
||||||
|
- GGUF: user_guide/quantization/gguf.md
|
||||||
|
- AutoRound: user_guide/quantization/autoround.md
|
||||||
|
- msModelSlim: user_guide/quantization/msmodelslim.md
|
||||||
- ComfyUI: features/comfyui.md
|
- ComfyUI: features/comfyui.md
|
||||||
- Fault Tolerance:
|
- Fault Tolerance:
|
||||||
- Failure Modes: user_guide/fault_injection_reliability_matrix.md
|
- Failure Modes: user_guide/fault_injection_reliability_matrix.md
|
||||||
@@ -114,7 +108,6 @@ nav:
|
|||||||
- Shared Memory Connector: design/feature/omni_connectors/shared_memory_connector.md
|
- Shared Memory Connector: design/feature/omni_connectors/shared_memory_connector.md
|
||||||
- Yuanrong Store Connector: design/feature/omni_connectors/yuanrong_connector.md
|
- Yuanrong Store Connector: design/feature/omni_connectors/yuanrong_connector.md
|
||||||
- Yuanrong Transfer Engine Connector: design/feature/omni_connectors/yuanrong_transfer_engine_connector.md
|
- Yuanrong Transfer Engine Connector: design/feature/omni_connectors/yuanrong_transfer_engine_connector.md
|
||||||
- Quantization: design/feature/quantization.md
|
|
||||||
- Diffusion Acceleration:
|
- Diffusion Acceleration:
|
||||||
- Parallelism:
|
- Parallelism:
|
||||||
- CFG-Parallel: design/feature/cfg_parallel.md
|
- CFG-Parallel: design/feature/cfg_parallel.md
|
||||||
@@ -124,8 +117,9 @@ nav:
|
|||||||
- Sequence Parallel: design/feature/sequence_parallel.md
|
- Sequence Parallel: design/feature/sequence_parallel.md
|
||||||
- Tensor Parallel: design/feature/tensor_parallel.md
|
- Tensor Parallel: design/feature/tensor_parallel.md
|
||||||
- VAE Patch Parallelism: design/feature/vae_parallel.md
|
- VAE Patch Parallelism: design/feature/vae_parallel.md
|
||||||
- Attention Optimization:
|
- Attention Backends:
|
||||||
- Skip-Softmax: design/feature/skip_softmax.md
|
- Skip-Softmax: design/feature/skip_softmax.md
|
||||||
|
- Quantization: design/feature/quantization.md
|
||||||
- Cache-DiT: design/feature/cache_dit.md
|
- Cache-DiT: design/feature/cache_dit.md
|
||||||
- TeaCache: design/feature/teacache.md
|
- TeaCache: design/feature/teacache.md
|
||||||
- Diffusion Continuous Batching: design/feature/diffusion_continuous_batching.md
|
- Diffusion Continuous Batching: design/feature/diffusion_continuous_batching.md
|
||||||
|
|||||||
+5
-13
@@ -11,10 +11,6 @@ in the active navigation.
|
|||||||
|
|
||||||
## Feature Design Documents
|
## Feature Design Documents
|
||||||
|
|
||||||
For user-facing configuration and current compatibility, see the
|
|
||||||
[Features overview](../features/README.md). A design document defines an
|
|
||||||
implementation contract; it is not, by itself, a general support claim.
|
|
||||||
|
|
||||||
### Runtime and stage execution
|
### Runtime and stage execution
|
||||||
|
|
||||||
- [Disaggregated Inference](feature/disaggregated_inference.md)
|
- [Disaggregated Inference](feature/disaggregated_inference.md)
|
||||||
@@ -34,10 +30,6 @@ implementation contract; it is not, by itself, a general support claim.
|
|||||||
- [Yuanrong Store Connector](feature/omni_connectors/yuanrong_connector.md)
|
- [Yuanrong Store Connector](feature/omni_connectors/yuanrong_connector.md)
|
||||||
- [Yuanrong Transfer Engine Connector](feature/omni_connectors/yuanrong_transfer_engine_connector.md)
|
- [Yuanrong Transfer Engine Connector](feature/omni_connectors/yuanrong_transfer_engine_connector.md)
|
||||||
|
|
||||||
### Quantization
|
|
||||||
|
|
||||||
- [Quantization](feature/quantization.md)
|
|
||||||
|
|
||||||
### Diffusion acceleration
|
### Diffusion acceleration
|
||||||
|
|
||||||
#### Parallelism
|
#### Parallelism
|
||||||
@@ -50,14 +42,14 @@ implementation contract; it is not, by itself, a general support claim.
|
|||||||
- [Tensor Parallel](feature/tensor_parallel.md)
|
- [Tensor Parallel](feature/tensor_parallel.md)
|
||||||
- [VAE Patch Parallelism](feature/vae_parallel.md)
|
- [VAE Patch Parallelism](feature/vae_parallel.md)
|
||||||
|
|
||||||
#### Attention optimization
|
#### Attention Backends
|
||||||
|
|
||||||
The [Diffusion Attention Backends](../user_guide/diffusion/attention_backends.md)
|
|
||||||
guide lists all selectable backends and their platform defaults. Skip-Softmax
|
|
||||||
is the backend-related optimization with a standalone feature-design contract:
|
|
||||||
|
|
||||||
- [Skip-Softmax](feature/skip_softmax.md)
|
- [Skip-Softmax](feature/skip_softmax.md)
|
||||||
|
|
||||||
|
#### Quantization
|
||||||
|
|
||||||
|
- [Quantization](feature/quantization.md)
|
||||||
|
|
||||||
- [Cache-DiT](feature/cache_dit.md)
|
- [Cache-DiT](feature/cache_dit.md)
|
||||||
- [TeaCache](feature/teacache.md)
|
- [TeaCache](feature/teacache.md)
|
||||||
- [Diffusion Continuous Batching](feature/diffusion_continuous_batching.md)
|
- [Diffusion Continuous Batching](feature/diffusion_continuous_batching.md)
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
# Features
|
|
||||||
|
|
||||||
Use this section to choose and configure vLLM-Omni runtime and optimization
|
|
||||||
features. The navigation follows the active
|
|
||||||
[Feature Design](../design/index.md#feature-design-documents) taxonomy whenever
|
|
||||||
an implementation contract has a user-facing workflow.
|
|
||||||
|
|
||||||
!!! note "User guides, design documents, and recipes"
|
|
||||||
|
|
||||||
User guides explain how to enable a feature and where it is supported.
|
|
||||||
Design documents define internal contracts and do not, by themselves,
|
|
||||||
imply general or production support. For model-specific launch flags and
|
|
||||||
hardware requirements, use the linked examples and validated recipes from
|
|
||||||
[Supported Models](../models/supported_models.md).
|
|
||||||
|
|
||||||
## Runtime and Stage Execution
|
|
||||||
|
|
||||||
| Goal | User guide | Related design contract |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| Choose serial, batched, step-wise, or streaming diffusion execution | [Execution Modes and Streaming](../user_guide/diffusion/execution_modes.md) | [Diffusion Continuous Batching](../design/feature/diffusion_continuous_batching.md), [Async Diffusion Output](../design/feature/async_diffusion_output.md) |
|
|
||||||
| Reclaim stage memory without restarting the server | [Sleep Mode](sleep_mode.md) | Runtime lifecycle behavior is documented in the user guide |
|
|
||||||
|
|
||||||
Some runtime designs are deliberately not promoted as standalone User Guide
|
|
||||||
features yet:
|
|
||||||
|
|
||||||
- [Disaggregated Inference](../design/feature/disaggregated_inference.md) and
|
|
||||||
[OmniConnector implementations](../design/index.md#communication) describe
|
|
||||||
topology and transport contracts. Practical deployment configuration remains
|
|
||||||
under [Pipeline and deploy configurations](../configuration/stage_configs.md).
|
|
||||||
- [Async Chunk](../design/feature/async_chunk.md) and
|
|
||||||
[Async Omni Output Materialization](../design/feature/omni_async_output_materialization.md)
|
|
||||||
are model- and pipeline-dependent. Use the selected model's deploy
|
|
||||||
configuration and recipe for the supported settings.
|
|
||||||
- [Automatic Prefix Caching](../design/feature/prefix_caching.md) remains a
|
|
||||||
design-level contract until its user-facing configuration and compatibility
|
|
||||||
surface is consolidated.
|
|
||||||
|
|
||||||
## Quantization
|
|
||||||
|
|
||||||
Quantization is a cross-model feature rather than a diffusion-only
|
|
||||||
optimization. The unified [`quantization_config` guide](../user_guide/quantization/overview.md)
|
|
||||||
covers diffusion-only models, multi-stage omni/TTS models, and multi-stage
|
|
||||||
diffusion models. Its [design contract](../design/feature/quantization.md)
|
|
||||||
defines the shared configuration and backend extension points.
|
|
||||||
|
|
||||||
## Diffusion Acceleration
|
|
||||||
|
|
||||||
| Goal | User guide | Related design contract |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| Compare acceleration methods and supported combinations | [Overview](../user_guide/diffusion_features.md), [Feature Compatibility](../user_guide/feature_compatibility.md) | [Diffusion acceleration designs](../design/index.md#diffusion-acceleration) |
|
|
||||||
| Move weights between host and device memory | [CPU Offloading](../user_guide/diffusion/cpu_offload.md) | [Distributed Layerwise Offload](../design/feature/distributed_layerwise_offload.md) |
|
|
||||||
| Reuse denoising computation | [Cache Acceleration](../user_guide/diffusion/cache_acceleration/cache_dit.md) | [Cache-DiT](../design/feature/cache_dit.md), [TeaCache](../design/feature/teacache.md) |
|
|
||||||
| Distribute diffusion work across devices | [Parallelism](../user_guide/diffusion/parallelism/overview.md) | [Parallelism designs](../design/index.md#parallelism) |
|
|
||||||
| Select dense, sparse, or quantized attention paths | [Attention Backends](../user_guide/diffusion/attention_backends.md) | [Skip-Softmax](../design/feature/skip_softmax.md) |
|
|
||||||
| Compile repeated diffusion regions | [Regional Compilation](../user_guide/diffusion/regional_compilation.md) | User-facing optimization guide |
|
|
||||||
| Add generated video frames | [Frame Interpolation](../user_guide/diffusion/frame_interpolation.md) | User-facing extension guide |
|
|
||||||
| Reduce diffusion model startup time | [Startup and Loading](../user_guide/diffusion/startup_and_loading.md) | User-facing loading guide |
|
|
||||||
| Apply diffusion adapters | [LoRA](../user_guide/diffusion/lora.md) | User-facing extension guide |
|
|
||||||
|
|
||||||
The [Pipeline Parallelism guide](../user_guide/diffusion/parallelism/pipeline_parallel.md)
|
|
||||||
remains available by direct link, but it is not promoted in the primary User
|
|
||||||
Guide navigation until its user-facing support and placement are settled.
|
|
||||||
|
|
||||||
## Experimental
|
|
||||||
|
|
||||||
[Session State Manager](session_state_manager.md) is opt-in and experimental.
|
|
||||||
Its APIs and compatibility may change without notice.
|
|
||||||
@@ -80,7 +80,6 @@ th {
|
|||||||
| `HiggsAudioV2ForConditionalGeneration` | Higgs-Audio v2 | `bosonai/higgs-audio-v2-generation-3B-base` | ✅︎ | | | | — |
|
| `HiggsAudioV2ForConditionalGeneration` | Higgs-Audio v2 | `bosonai/higgs-audio-v2-generation-3B-base` | ✅︎ | | | | — |
|
||||||
| `HiggsMultimodalQwen3ForConditionalGeneration` | Higgs-Audio v3 (TTS) | `bosonai/higgs-audio-v3-tts-4b` | ✅︎ | | | | [Published](https://recipes.vllm.ai/bosonai/higgs-audio-v3-tts-4b) |
|
| `HiggsMultimodalQwen3ForConditionalGeneration` | Higgs-Audio v3 (TTS) | `bosonai/higgs-audio-v3-tts-4b` | ✅︎ | | | | [Published](https://recipes.vllm.ai/bosonai/higgs-audio-v3-tts-4b) |
|
||||||
| `IndexTTS2TalkerForConditionalGeneration` | IndexTTS-2 | `IndexTeam/IndexTTS-2` | ✅︎ | | | | — |
|
| `IndexTTS2TalkerForConditionalGeneration` | IndexTTS-2 | `IndexTeam/IndexTTS-2` | ✅︎ | | | | — |
|
||||||
| `IndexTTS25TalkerForConditionalGeneration` | IndexTTS-2.5 | native `checkpoints/` bundle | ✅︎ | | | | [Repository](https://github.com/vllm-project/vllm-omni/blob/main/recipes/IndexTeam/IndexTTS-2_5.md) |
|
|
||||||
| `NextStep11Pipeline` | NextStep-1.1 | `stepfun-ai/NextStep-1.1` | ✅︎ | ✅︎ | | ✅︎ | — |
|
| `NextStep11Pipeline` | NextStep-1.1 | `stepfun-ai/NextStep-1.1` | ✅︎ | ✅︎ | | ✅︎ | — |
|
||||||
| `MiMoAudioModel` | MiMo-Audio-7B-Instruct | `XiaomiMiMo/MiMo-Audio-7B-Instruct` | ✅︎ | ✅︎ | | | — |
|
| `MiMoAudioModel` | MiMo-Audio-7B-Instruct | `XiaomiMiMo/MiMo-Audio-7B-Instruct` | ✅︎ | ✅︎ | | | — |
|
||||||
| `MiMoV2ASRForCausalLM` | MiMo-V2.5-ASR | `XiaomiMiMo/MiMo-V2.5-ASR` | ✅︎ | ✅︎ | | | — |
|
| `MiMoV2ASRForCausalLM` | MiMo-V2.5-ASR | `XiaomiMiMo/MiMo-V2.5-ASR` | ✅︎ | ✅︎ | | | — |
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
# Diffusion Startup and Loading
|
|
||||||
|
|
||||||
Large diffusion models can take several minutes to load at startup. vLLM-Omni
|
|
||||||
loads safetensors shards in parallel to reduce this initialization time.
|
|
||||||
|
|
||||||
Multi-thread weight loading is enabled by default with four threads. No
|
|
||||||
configuration is needed for the default behavior.
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
| Parameter | CLI flag | Default | Description |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| `enable_multithread_weight_load` | `--disable-multithread-weight-load` | `True` | Pass the flag to disable multi-thread loading |
|
|
||||||
| `num_weight_load_threads` | `--num-weight-load-threads` | `4` | Number of parallel weight-loading threads |
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
|
|
||||||
The default balances startup speed and disk I/O contention. Fast NVMe
|
|
||||||
storage may benefit from more threads, while network storage or hard disks
|
|
||||||
may not.
|
|
||||||
|
|
||||||
## Online Serving
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Default: multi-thread loading with four threads
|
|
||||||
vllm serve Qwen/Qwen-Image --omni --port 8091
|
|
||||||
|
|
||||||
# Increase the thread count
|
|
||||||
vllm serve Wan-AI/Wan2.2-I2V-A14B-Diffusers --omni \
|
|
||||||
--num-weight-load-threads 8
|
|
||||||
|
|
||||||
# Disable multi-thread loading
|
|
||||||
vllm serve Qwen/Qwen-Image --omni --disable-multithread-weight-load
|
|
||||||
```
|
|
||||||
|
|
||||||
## Offline Inference
|
|
||||||
|
|
||||||
```python
|
|
||||||
from vllm_omni import Omni
|
|
||||||
|
|
||||||
# Default: multi-thread loading with four threads
|
|
||||||
omni = Omni(model="Qwen/Qwen-Image")
|
|
||||||
|
|
||||||
# Increase the thread count
|
|
||||||
omni = Omni(
|
|
||||||
model="Wan-AI/Wan2.2-I2V-A14B-Diffusers",
|
|
||||||
num_weight_load_threads=8,
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Reference Benchmarks
|
|
||||||
|
|
||||||
The following measurements were collected on NVIDIA H800 hardware. Treat them
|
|
||||||
as reference results rather than a guarantee for other storage or hardware
|
|
||||||
configurations.
|
|
||||||
|
|
||||||
| Model | Sequential loading | Multi-thread loading | Speedup |
|
|
||||||
| --- | ---: | ---: | ---: |
|
|
||||||
| **Qwen/Qwen-Image** (53.7 GiB) | 168 s | 27 s | **6.2x** |
|
|
||||||
| **Wan-AI/Wan2.2-I2V-A14B-Diffusers** (64.5 GiB) | 283 s | 56 s | **5.1x** |
|
|
||||||
@@ -49,7 +49,7 @@ Parallelism methods distribute computation across GPUs without quality loss (mat
|
|||||||
|
|
||||||
| Method | Description | Best For |
|
| Method | Description | Best For |
|
||||||
|--------|-------------|----------|
|
|--------|-------------|----------|
|
||||||
| **[Multi-Thread Weight Loading](diffusion/startup_and_loading.md)** | Loads safetensors shards in parallel using a thread pool | All diffusion models; reduces startup from minutes to seconds |
|
| **[Multi-Thread Weight Loading](#multi-thread-weight-loading)** | Loads safetensors shards in parallel using a thread pool | All diffusion models; reduces startup from minutes to seconds |
|
||||||
|
|
||||||
**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.
|
**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.
|
||||||
|
|
||||||
@@ -100,9 +100,7 @@ The following tables show which models support each feature:
|
|||||||
|
|
||||||
- **🔀SP (Ulysses & Ring)**: Includes both Ulysses-SP and Ring-Attention methods
|
- **🔀SP (Ulysses & Ring)**: Includes both Ulysses-SP and Ring-Attention methods
|
||||||
- ✅ = Fully supported
|
- ✅ = Fully supported
|
||||||
- ✅* = Supported with the constraint listed below the table
|
|
||||||
- ❌ = Not supported
|
- ❌ = Not supported
|
||||||
- ❓ = Not verified; not recommended
|
|
||||||
|
|
||||||
> Notes:
|
> Notes:
|
||||||
|
|
||||||
@@ -121,7 +119,7 @@ The following tables show which models support each feature:
|
|||||||
| **FLUX.2-dev** | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ |
|
| **FLUX.2-dev** | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||||
| **GLM-Image** | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ |
|
| **GLM-Image** | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ |
|
||||||
| **Hidream-I1-Full** | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
| **Hidream-I1-Full** | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||||
| **HunyuanImage3** | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅* |
|
| **HunyuanImage3** | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ |
|
||||||
| **Krea 2** | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ (decode) | ❌ | ❌ |
|
| **Krea 2** | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ (decode) | ❌ | ❌ |
|
||||||
| **LongCat-Image** | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
|
| **LongCat-Image** | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
|
||||||
| **LongCat-Image-Edit** | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
|
| **LongCat-Image-Edit** | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
|
||||||
@@ -147,7 +145,6 @@ The following tables show which models support each feature:
|
|||||||
> 2. `Tongyi-MAI/Z-Image-Turbo` and `SII-GAIR/daVinci-MagiHuman-Base-1080p` are distilled models with minimal NFEs; CFG-Parallel is not necessary.
|
> 2. `Tongyi-MAI/Z-Image-Turbo` and `SII-GAIR/daVinci-MagiHuman-Base-1080p` are distilled models with minimal NFEs; CFG-Parallel is not necessary.
|
||||||
> 3. Cosmos3 T2I uses `Cosmos3OmniDiffusersPipeline` with `modalities=["image"]`. Model-level CPU offload swaps the nested UND reasoner and GEN generator pathways; layerwise offload remains available for blockwise GEN/UND offload.
|
> 3. Cosmos3 T2I uses `Cosmos3OmniDiffusersPipeline` with `modalities=["image"]`. Model-level CPU offload swaps the nested UND reasoner and GEN generator pathways; layerwise offload remains available for blockwise GEN/UND offload.
|
||||||
> 4. Krea 2 currently supports single-GPU inference plus LoRA, Cache-DiT, HSDP, CPU/layerwise offload, and VAE-patch-parallel (decode). TP/SP/CFG-Parallel are not yet wired. The few-step distilled (Turbo) checkpoint uses `is_distilled=true` (fixed timestep shift `mu=1.15`); generate at 2048x2048 by default with `num_inference_steps≈8` and `guidance_scale=0`. The Raw checkpoint uses 1024x1024, `num_inference_steps=28`, and `guidance_scale=4.5`.
|
> 4. Krea 2 currently supports single-GPU inference plus LoRA, Cache-DiT, HSDP, CPU/layerwise offload, and VAE-patch-parallel (decode). TP/SP/CFG-Parallel are not yet wired. The few-step distilled (Turbo) checkpoint uses `is_distilled=true` (fixed timestep shift `mu=1.15`); generate at 2048x2048 by default with `num_inference_steps≈8` and `guidance_scale=0`. The Raw checkpoint uses 1024x1024, `num_inference_steps=28`, and `guidance_scale=4.5`.
|
||||||
> 5. HunyuanImage3 supports step execution. Multi-request step execution requires `TORCH_SDPA`; see [Diffusion Execution Modes](diffusion/execution_modes.md#step-execution).
|
|
||||||
|
|
||||||
### VideoGen
|
### VideoGen
|
||||||
|
|
||||||
@@ -158,16 +155,13 @@ The following tables show which models support each feature:
|
|||||||
| **Wan2.1-VACE** | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ (decode) | ❌ | ❌ |
|
| **Wan2.1-VACE** | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ (decode) | ❌ | ❌ |
|
||||||
| **LTX-2** | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ (decode) | ❌ | ❌ |
|
| **LTX-2** | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ (decode) | ❌ | ❌ |
|
||||||
| **LTX-2.3** | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ (decode) | ❌ | ❌ |
|
| **LTX-2.3** | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ (decode) | ❌ | ❌ |
|
||||||
| **Helios** | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ | ✅* |
|
| **Helios** | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ |
|
||||||
| **HunyuanVideo-1.5 T2V I2V** | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ (encode/decode) | ✅ | ❌ |
|
| **HunyuanVideo-1.5 T2V I2V** | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ (encode/decode) | ✅ | ❌ |
|
||||||
| **DreamID-Omni** | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ |
|
| **DreamID-Omni** | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ |
|
||||||
| **Cosmos3** | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ (encode/decode) | ✅ | ❌ |
|
| **Cosmos3** | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ (encode/decode) | ✅ | ❌ |
|
||||||
| **LongCat-Video-Avatar-1.5** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
| **LongCat-Video-Avatar-1.5** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||||
| **MiniMax-H3** | ✅ (FL2VA) | ✅ | ✅ | ❌ | ✅ (DiT/TE) | ❌ | ✅ | ✅ | ✅ (tile) | ✅ (DiT) | ❌ |
|
| **MiniMax-H3** | ✅ (FL2VA) | ✅ | ✅ | ❌ | ✅ (DiT/TE) | ❌ | ✅ | ✅ | ✅ (tile) | ✅ (DiT) | ❌ |
|
||||||
|
|
||||||
> **Step execution note:** Helios supports single-request step execution only;
|
|
||||||
> use `max_num_seqs=1`.
|
|
||||||
|
|
||||||
**Frame Interpolation Support**
|
**Frame Interpolation Support**
|
||||||
|
|
||||||
- **Supported**: Wan2.2 text-to-video, image-to-video, and TI2V pipelines
|
- **Supported**: Wan2.2 text-to-video, image-to-video, and TI2V pipelines
|
||||||
@@ -196,7 +190,7 @@ The following tables show which models support each feature:
|
|||||||
| **🔀Ring-Attn** | ✅ | ✅ | ✅ | | | | | | | | | | | |
|
| **🔀Ring-Attn** | ✅ | ✅ | ✅ | | | | | | | | | | | |
|
||||||
| **🔀CFG-Parallel** | ✅ | ✅ | ✅ | ✅ | | | | | | | | | | |
|
| **🔀CFG-Parallel** | ✅ | ✅ | ✅ | ✅ | | | | | | | | | | |
|
||||||
| **🔀Tensor Parallel** | ✅ | ✅ | ✅ | ✅ | ✅ | | | | | | | | | |
|
| **🔀Tensor Parallel** | ✅ | ✅ | ✅ | ✅ | ✅ | | | | | | | | | |
|
||||||
| **🔀HSDP** | ❓ | ❓ | ✅ | ❓ | ✅ | ❌ | | | | | | | | |
|
| **🔀HSDP** | ❓ | ❓ | ❓ | ❓ | ❓ | ❌ | | | | | | | | |
|
||||||
| **🔀Expert Parallel** | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | | | | | | | |
|
| **🔀Expert Parallel** | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | | | | | | | |
|
||||||
| **💾CPU Offloading (Layerwise)** | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | | | | | |
|
| **💾CPU Offloading (Layerwise)** | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | | | | | |
|
||||||
| **💾CPU Offloading (Module-wise)** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❓ | ❓ | ❌ | | | | | |
|
| **💾CPU Offloading (Module-wise)** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❓ | ❓ | ❌ | | | | | |
|
||||||
@@ -207,43 +201,97 @@ The following tables show which models support each feature:
|
|||||||
|
|
||||||
!!! info
|
!!! info
|
||||||
|
|
||||||
1. HSDP can be combined with Ulysses-SP or CFG-Parallel. Tensor Parallel
|
1. Tensor Parallel and HSDP are not compatible.
|
||||||
and HSDP are not compatible; other HSDP combinations in the table
|
|
||||||
remain unverified.
|
|
||||||
2. TeaCache and Cache-DiT are not compatible.
|
2. TeaCache and Cache-DiT are not compatible.
|
||||||
3. CPU Offloading (Layerwise) and CPU Offloading (Module-wise) are not compatible.
|
3. CPU Offloading (Layerwise) and CPU Offloading (Module-wise) are not compatible.
|
||||||
4. The CPU Offloading (Layerwise) row describes local layerwise offload.
|
4. CPU Offloading (Layerwise) supports single-card for now.
|
||||||
Multi-device Distributed Layerwise Offload has a separate topology and
|
5. Using FP8-Quant as an example of qunatization methods.
|
||||||
compatibility matrix in the [CPU Offloading guide](diffusion/cpu_offload.md#distributed-layerwise-offloading).
|
|
||||||
5. The compatibility matrix uses FP8 as the representative quantization method.
|
|
||||||
6. Step Execution is not compatible with any diffusion cache backend. LoRA is supported, but each scheduled batch must use a single adapter (requests with different `lora_request` or `lora_scale` are kept in separate batches).
|
6. Step Execution is not compatible with any diffusion cache backend. LoRA is supported, but each scheduled batch must use a single adapter (requests with different `lora_request` or `lora_scale` are kept in separate batches).
|
||||||
|
|
||||||
|
|
||||||
## Multi-Thread Weight Loading
|
## Multi-Thread Weight Loading
|
||||||
|
|
||||||
The loading guide now lives at [Diffusion Startup and
|
Large diffusion models can take several minutes to load weights at startup (e.g., ~3 min for Qwen-Image, ~5 min for Wan2.2 I2V 14B). Multi-thread weight loading speeds up this process by loading safetensors shards in parallel using a thread pool instead of sequentially.
|
||||||
Loading](diffusion/startup_and_loading.md). This heading remains so existing
|
|
||||||
links to this section continue to work.
|
This optimization is **enabled by default** with 4 threads. No configuration is needed for the default behavior.
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
| Parameter | CLI Flag | Default | Description |
|
||||||
|
|-----------|----------|---------|-------------|
|
||||||
|
| `enable_multithread_weight_load` | `--disable-multithread-weight-load` | `True` (enabled) | Pass the flag to disable multi-thread loading |
|
||||||
|
| `num_weight_load_threads` | `--num-weight-load-threads` | `4` | Number of threads for parallel weight loading |
|
||||||
|
|
||||||
|
!!! tip
|
||||||
|
The default of 4 threads balances speed and disk I/O contention. On fast NVMe storage you may benefit from more threads (e.g., 8). On HDD or network storage, the default of 4 avoids saturating I/O bandwidth.
|
||||||
|
|
||||||
|
### Online Serving
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Default (multi-thread enabled, 4 threads)
|
||||||
|
vllm serve Qwen/Qwen-Image --omni --port 8091
|
||||||
|
|
||||||
|
# Custom thread count
|
||||||
|
vllm serve Wan-AI/Wan2.2-I2V-A14B-Diffusers --omni --num-weight-load-threads 8
|
||||||
|
|
||||||
|
# Disable multi-thread loading
|
||||||
|
vllm serve Qwen/Qwen-Image --omni --disable-multithread-weight-load
|
||||||
|
```
|
||||||
|
|
||||||
|
### Offline Inference
|
||||||
|
|
||||||
|
```python
|
||||||
|
from vllm_omni import Omni
|
||||||
|
|
||||||
|
# Default (multi-thread enabled, 4 threads)
|
||||||
|
omni = Omni(model="Qwen/Qwen-Image")
|
||||||
|
|
||||||
|
# Custom thread count
|
||||||
|
omni = Omni(
|
||||||
|
model="Wan-AI/Wan2.2-I2V-A14B-Diffusers",
|
||||||
|
num_weight_load_threads=8,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Benchmarks
|
||||||
|
|
||||||
|
Measured on NVIDIA H800:
|
||||||
|
|
||||||
|
| Model | Before | After | Speedup |
|
||||||
|
|-------|--------|-------|---------|
|
||||||
|
| **Qwen/Qwen-Image** (53.7 GiB) | 168s | 27s | **6.2x** |
|
||||||
|
| **Wan-AI/Wan2.2-I2V-A14B-Diffusers** (64.5 GiB) | 283s | 56s | **5.1x** |
|
||||||
|
|
||||||
## Learn More
|
## Learn More
|
||||||
|
|
||||||
The Diffusion Acceleration navigation groups the remaining guides as follows:
|
**Cache Acceleration:**
|
||||||
|
|
||||||
| Area | Guide |
|
- **[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
|
||||||
| Compatibility | [Feature Compatibility](feature_compatibility.md) |
|
|
||||||
| CPU offloading | [CPU Offloading](diffusion/cpu_offload.md) |
|
|
||||||
| Cache acceleration | [TeaCache](diffusion/cache_acceleration/teacache.md), [Cache-DiT](diffusion/cache_acceleration/cache_dit.md) |
|
|
||||||
| Parallelism | [Parallelism Overview](diffusion/parallelism/overview.md) |
|
|
||||||
| Attention | [Attention Backends](diffusion/attention_backends.md) |
|
|
||||||
| Compilation | [Regional Compilation](diffusion/regional_compilation.md) |
|
|
||||||
| Video extension | [Frame Interpolation](diffusion/frame_interpolation.md) |
|
|
||||||
| Startup | [Startup and Loading](diffusion/startup_and_loading.md) |
|
|
||||||
| Adapters | [LoRA](diffusion/lora.md) |
|
|
||||||
|
|
||||||
Related cross-model and runtime features are documented separately:
|
**Parallelism Methods:**
|
||||||
|
|
||||||
- [Quantization](quantization/overview.md) covers diffusion-only models,
|
- **[Parallelism Overview](diffusion/parallelism/overview.md)** - Tensor Parallelism, Sequence Parallelism, CFG Parallelism, Pipeline Parallelism, HSDP, and Expert Parallelism
|
||||||
multi-stage omni/TTS models, and multi-stage diffusion models.
|
|
||||||
- [Execution Modes and Streaming](diffusion/execution_modes.md) covers the
|
**Memory Optimization:**
|
||||||
diffusion runtime, including batching, step execution, and streaming output.
|
|
||||||
|
- **[CPU Offload Guide](diffusion/cpu_offload.md)** - Offload model components to CPU, reduce GPU memory usage
|
||||||
|
- **[VAE Parallelism Guide](diffusion/parallelism/vae_parallelism.md)** - Distribute VAE decode work across GPUs for high-resolution images and videos
|
||||||
|
- **[Quantization](quantization/overview.md)** - Quantization methods for diffusion, multi-stage omni/TTS, and multi-stage diffusion models
|
||||||
|
|
||||||
|
**Extensions:**
|
||||||
|
|
||||||
|
- **[LoRA Inference Guide](diffusion/lora.md)** - Low-Rank Adaptation for style customization and fine-tuning
|
||||||
|
- **[Frame Interpolation Guide](diffusion/frame_interpolation.md)** - Worker-side post-generation video frame interpolation for smoother motion
|
||||||
|
|
||||||
|
**Execution Modes:**
|
||||||
|
|
||||||
|
- **[Diffusion Execution Modes](diffusion/execution_modes.md)** - Configure request batching, step execution, continuous batching, and streaming output
|
||||||
|
|
||||||
|
**Startup Optimization:**
|
||||||
|
|
||||||
|
- **[Multi-Thread Weight Loading](#multi-thread-weight-loading)** - Speed up model startup by loading safetensors shards in parallel
|
||||||
|
|
||||||
|
**Advanced Topics:**
|
||||||
|
|
||||||
|
- **[Feature Compatibility](feature_compatibility.md)** - How to combine multiple features for maximum performance
|
||||||
@@ -28,7 +28,6 @@ However, as we strive to ensure output similarity between vLLM-Omni's diffuser b
|
|||||||
- Qwen/Qwen-Image
|
- Qwen/Qwen-Image
|
||||||
- Tongyi-MAI/Z-Image-Turbo
|
- Tongyi-MAI/Z-Image-Turbo
|
||||||
- Wan2.2-I2V-A14B-Diffusers
|
- Wan2.2-I2V-A14B-Diffusers
|
||||||
- Lightricks/LTX-2.5-Diffusers (source-preview dependencies)
|
|
||||||
|
|
||||||
If you find that a model not listed above also produces different outputs from running diffusers model directly.
|
If you find that a model not listed above also produces different outputs from running diffusers model directly.
|
||||||
Please consider file an issue.
|
Please consider file an issue.
|
||||||
@@ -117,36 +116,6 @@ The model loading and inference strictly follows the diffusers library, and they
|
|||||||
Users are encouraged to double-check the model pipeline's interface in [diffusers' official documentation](https://huggingface.co/docs/diffusers/api/pipelines/overview).
|
Users are encouraged to double-check the model pipeline's interface in [diffusers' official documentation](https://huggingface.co/docs/diffusers/api/pipelines/overview).
|
||||||
Some particular examples are below.
|
Some particular examples are below.
|
||||||
|
|
||||||
#### LTX-2.5 (source preview)
|
|
||||||
|
|
||||||
Use the gated Diffusers checkpoint
|
|
||||||
[`Lightricks/LTX-2.5-Diffusers`](https://huggingface.co/Lightricks/LTX-2.5-Diffusers);
|
|
||||||
[`Lightricks/LTX-2.5`](https://huggingface.co/Lightricks/LTX-2.5) contains
|
|
||||||
component weights and is not directly loadable by this backend. Accept the
|
|
||||||
model terms, authenticate, and install the source-preview dependencies:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
hf auth login
|
|
||||||
python -m pip install --upgrade 'transformers>=5.10.1' \
|
|
||||||
'diffusers @ git+https://github.com/huggingface/diffusers.git@7564fb016dabda0c943416190fc92398c50b1b20'
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
vllm serve Lightricks/LTX-2.5-Diffusers \
|
|
||||||
--omni \
|
|
||||||
--diffusion-load-format diffusers \
|
|
||||||
--dtype bfloat16 \
|
|
||||||
--enable-cpu-offload \
|
|
||||||
--vae-use-tiling
|
|
||||||
```
|
|
||||||
|
|
||||||
The converted repository loads the distilled transformer. When no `sigmas`,
|
|
||||||
`timesteps`, or `num_inference_steps` is supplied, the adapter applies the
|
|
||||||
official eight-sigma unguided schedule and returns synchronized 48 kHz audio.
|
|
||||||
Support is limited to one-stage distilled T2V; `transformer_full`, two-stage
|
|
||||||
upsampling, image/video conditioning, and the diffusion video decoder are not
|
|
||||||
supported. The general Diffusers backend [limitations](#limitations) apply.
|
|
||||||
|
|
||||||
#### Wan Series
|
#### Wan Series
|
||||||
|
|
||||||
The Wan series video generation models takes `boundary_ratio` and `flow_shift` during model initialization ([ref](https://huggingface.co/docs/diffusers/api/pipelines/wan)), not during inference.
|
The Wan series video generation models takes `boundary_ratio` and `flow_shift` during model initialization ([ref](https://huggingface.co/docs/diffusers/api/pipelines/wan)), not during inference.
|
||||||
|
|||||||
@@ -63,9 +63,7 @@ from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
|||||||
from vllm_omni.lora.request import LoRARequest
|
from vllm_omni.lora.request import LoRARequest
|
||||||
from vllm_omni.lora.utils import stable_lora_int_id
|
from vllm_omni.lora.utils import stable_lora_int_id
|
||||||
from vllm_omni.model_extras import (
|
from vllm_omni.model_extras import (
|
||||||
build_image_to_video_prompt as build_model_image_to_video_prompt,
|
build_image_to_video_prompt,
|
||||||
)
|
|
||||||
from vllm_omni.model_extras import (
|
|
||||||
get_extra_body_params,
|
get_extra_body_params,
|
||||||
get_model_class_name,
|
get_model_class_name,
|
||||||
)
|
)
|
||||||
@@ -87,22 +85,6 @@ def parse_json_object(value: str, flag_name: str = "argument") -> dict[str, Any]
|
|||||||
parse_profiler_config = functools.partial(parse_json_object, flag_name="--profiler-config")
|
parse_profiler_config = functools.partial(parse_json_object, flag_name="--profiler-config")
|
||||||
|
|
||||||
|
|
||||||
def build_image_to_video_prompt(
|
|
||||||
prompt: str,
|
|
||||||
negative_prompt: str | None,
|
|
||||||
media_inputs: dict[str, Any],
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""Build the canonical request envelope for the shared I2V example."""
|
|
||||||
result: dict[str, Any] = {
|
|
||||||
"prompt": prompt,
|
|
||||||
"modalities": ["video"],
|
|
||||||
"multi_modal_data": media_inputs,
|
|
||||||
}
|
|
||||||
if negative_prompt is not None:
|
|
||||||
result["negative_prompt"] = negative_prompt
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def parse_args() -> argparse.Namespace:
|
def parse_args() -> argparse.Namespace:
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description=(
|
description=(
|
||||||
@@ -584,13 +566,10 @@ def main():
|
|||||||
# Preserve the historical empty-prompt behavior for non-LTX examples.
|
# Preserve the historical empty-prompt behavior for non-LTX examples.
|
||||||
negative_prompt = ""
|
negative_prompt = ""
|
||||||
prompt_dict = build_image_to_video_prompt(
|
prompt_dict = build_image_to_video_prompt(
|
||||||
|
model_class_name=model_class_name,
|
||||||
prompt=args.prompt,
|
prompt=args.prompt,
|
||||||
negative_prompt=negative_prompt,
|
negative_prompt=negative_prompt,
|
||||||
media_inputs=media_inputs,
|
media_inputs=media_inputs,
|
||||||
)
|
|
||||||
prompt_dict = build_model_image_to_video_prompt(
|
|
||||||
model_class_name=model_class_name,
|
|
||||||
prompt=prompt_dict,
|
|
||||||
height=height,
|
height=height,
|
||||||
width=width,
|
width=width,
|
||||||
num_frames=num_frames,
|
num_frames=num_frames,
|
||||||
|
|||||||
@@ -57,9 +57,8 @@ python examples/offline_inference/text_to_image/text_to_image.py \
|
|||||||
--output lingbot_t2i.png
|
--output lingbot_t2i.png
|
||||||
```
|
```
|
||||||
|
|
||||||
The shared T2I entry point selects the `image` output modality. The LingBot
|
The LingBot prompt builder selects the `image` output modality and requests one
|
||||||
pipeline validates that T2I requests produce one frame and do not include an
|
frame. Passing an input image is not supported in LingBot T2I mode.
|
||||||
input image.
|
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
|
|||||||
@@ -21,9 +21,7 @@ from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
|||||||
from vllm_omni.lora.request import LoRARequest
|
from vllm_omni.lora.request import LoRARequest
|
||||||
from vllm_omni.lora.utils import stable_lora_int_id
|
from vllm_omni.lora.utils import stable_lora_int_id
|
||||||
from vllm_omni.model_extras import (
|
from vllm_omni.model_extras import (
|
||||||
build_text_to_image_prompt as build_model_text_to_image_prompt,
|
build_text_to_image_prompt,
|
||||||
)
|
|
||||||
from vllm_omni.model_extras import (
|
|
||||||
get_extra_body_params,
|
get_extra_body_params,
|
||||||
get_model_class_name,
|
get_model_class_name,
|
||||||
should_init_extra_args_for_non_diffusion_stages,
|
should_init_extra_args_for_non_diffusion_stages,
|
||||||
@@ -58,17 +56,6 @@ def parse_json_object(value: str, flag_name: str = "argument") -> dict[str, Any]
|
|||||||
parse_profiler_config = functools.partial(parse_json_object, flag_name="--profiler-config")
|
parse_profiler_config = functools.partial(parse_json_object, flag_name="--profiler-config")
|
||||||
|
|
||||||
|
|
||||||
def build_text_to_image_prompt(prompt: str, negative_prompt: str | None) -> dict[str, Any]:
|
|
||||||
"""Build the canonical request envelope for the shared T2I example."""
|
|
||||||
result: dict[str, Any] = {
|
|
||||||
"prompt": prompt,
|
|
||||||
"modalities": ["image"],
|
|
||||||
}
|
|
||||||
if negative_prompt is not None:
|
|
||||||
result["negative_prompt"] = negative_prompt
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def _normalize_images_for_save(images: list[Any]) -> list[Any]:
|
def _normalize_images_for_save(images: list[Any]) -> list[Any]:
|
||||||
"""Convert NumPy diffusion outputs to PIL images before saving."""
|
"""Convert NumPy diffusion outputs to PIL images before saving."""
|
||||||
normalized = []
|
normalized = []
|
||||||
@@ -536,12 +523,9 @@ def main():
|
|||||||
generation_start = time.perf_counter()
|
generation_start = time.perf_counter()
|
||||||
|
|
||||||
prompt_dict = build_text_to_image_prompt(
|
prompt_dict = build_text_to_image_prompt(
|
||||||
|
model_class_name=model_class_name,
|
||||||
prompt=args.prompt,
|
prompt=args.prompt,
|
||||||
negative_prompt=args.negative_prompt,
|
negative_prompt=args.negative_prompt,
|
||||||
)
|
|
||||||
prompt_dict = build_model_text_to_image_prompt(
|
|
||||||
model_class_name=model_class_name,
|
|
||||||
prompt=prompt_dict,
|
|
||||||
height=args.height,
|
height=args.height,
|
||||||
width=args.width,
|
width=args.width,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ list of supported architectures across all modalities, see
|
|||||||
| Qwen3-TTS | `Qwen/Qwen3-TTS-12Hz-1.7B-{CustomVoice,VoiceDesign,Base}` | 2 (talker + code2wav) | ✓ (Base) | ✓ | 3 task variants | 24 kHz |
|
| Qwen3-TTS | `Qwen/Qwen3-TTS-12Hz-1.7B-{CustomVoice,VoiceDesign,Base}` | 2 (talker + code2wav) | ✓ (Base) | ✓ | 3 task variants | 24 kHz |
|
||||||
| VoxCPM2 | `openbmb/VoxCPM2` | single (native AR) | ✓ | ✓ (online) | continuation | 48 kHz |
|
| VoxCPM2 | `openbmb/VoxCPM2` | single (native AR) | ✓ | ✓ (online) | continuation | 48 kHz |
|
||||||
| IndexTTS-2 | `IndexTeam/IndexTTS-2` | 2 (AR talker + S2Mel DiT + BigVGAN) | ✓ (required) | — | emotion control (`--emo-audio`, `--emo-text`, `--emo-vector`) | 22.05 kHz |
|
| IndexTTS-2 | `IndexTeam/IndexTTS-2` | 2 (AR talker + S2Mel DiT + BigVGAN) | ✓ (required) | — | emotion control (`--emo-audio`, `--emo-text`, `--emo-vector`) | 22.05 kHz |
|
||||||
| IndexTTS-2.5 | native `checkpoints/` bundle | 2 (AR talker + EnhancedCodec + S2Mel DiT + BigVGAN) | ✓ (required) | — | multilingual (`--lang`) + emotion control | 22.05 kHz |
|
|
||||||
| Voxtral TTS | `mistralai/Voxtral-4B-TTS-2603` | varies | ✓ | ✓ | voice presets | 24 kHz |
|
| Voxtral TTS | `mistralai/Voxtral-4B-TTS-2603` | varies | ✓ | ✓ | voice presets | 24 kHz |
|
||||||
|
|
||||||
## Common Quick Start
|
## Common Quick Start
|
||||||
@@ -438,14 +437,11 @@ Streaming is exposed through the online OpenAI Speech API (`stream=true`). See [
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## IndexTTS-2 and IndexTTS-2.5
|
## IndexTTS-2
|
||||||
|
|
||||||
Both versions use a 2-stage TTS pipeline and produce 22.05 kHz mono audio. Stage 0 is a GPT AR talker. IndexTTS-2 Stage 1 uses RepCodec semantic embeddings, GPT latent, S2Mel CFM/DiT, and BigVGAN. IndexTTS-2.5 replaces RepCodec with EnhancedCodec and uses the official code-only path (`use_gpt_latent=false`). Every request requires reference audio for zero-shot voice cloning.
|
2-stage TTS pipeline (GPT AR talker + S2Mel CFM DiT + BigVGAN vocoder) at 22.05 kHz. Every request requires reference audio for zero-shot voice cloning. Supports emotion conditioning via audio, text, or 8-dim vector.
|
||||||
|
|
||||||
### Quick start
|
### Quick start
|
||||||
|
|
||||||
IndexTTS-2:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python examples/offline_inference/text_to_speech/indextts2/end2end.py \
|
python examples/offline_inference/text_to_speech/indextts2/end2end.py \
|
||||||
--model IndexTeam/IndexTTS-2 \
|
--model IndexTeam/IndexTTS-2 \
|
||||||
@@ -453,24 +449,6 @@ python examples/offline_inference/text_to_speech/indextts2/end2end.py \
|
|||||||
--ref-audio /path/to/reference.wav
|
--ref-audio /path/to/reference.wav
|
||||||
```
|
```
|
||||||
|
|
||||||
IndexTTS-2 uses its existing frontend and does not require the IndexTTS-2.5 text dependencies. Before running IndexTTS-2.5, install its optional frontend dependencies:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pip install 'vllm-omni[indextts2]'
|
|
||||||
```
|
|
||||||
|
|
||||||
IndexTTS-2.5 with a native repository/lab bundle whose model root contains `checkpoints/`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python examples/offline_inference/text_to_speech/indextts2/end2end.py \
|
|
||||||
--model /path/to/indextts-2.5 \
|
|
||||||
--model-version 2.5 \
|
|
||||||
--lang ja \
|
|
||||||
--text "こんにちは、音声合成のテストです。" \
|
|
||||||
--ref-audio /path/to/reference.wav
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### Emotion control
|
### Emotion control
|
||||||
```bash
|
```bash
|
||||||
# Emotion from reference audio
|
# Emotion from reference audio
|
||||||
@@ -496,13 +474,10 @@ python examples/offline_inference/text_to_speech/indextts2/end2end.py \
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Notes
|
### Notes
|
||||||
- `--ref-audio` is **required** — neither version provides text-only synthesis.
|
- `--ref-audio` is **required** — IndexTTS-2 does not support text-only synthesis.
|
||||||
- IndexTTS-2.5 Stage 0 (AR Talker) generates mel codes from text + reference audio using plain vLLM sampling. It does not reproduce the official default `num_beams=3` beam search. For parity comparisons, run upstream with `num_beams=1`; output quality can differ from the official beam-search result.
|
- Stage 0 (AR Talker): GPT-2 generates mel codes from text + reference audio.
|
||||||
- IndexTTS-2.5 accepts language codes such as `zh`, `en`, `zhen` (mixed Chinese/English), `ja`, and `yue`. `Mandarin` is a vLLM-Omni convenience alias for `zh`. Japanese (`ja`) uses `fugashi` tokenization and produces audio, but it does not automatically expand numbers, dates, or percentages; write those inputs as readable Japanese text before calling the model. Use `--no-text-normalization` only when the input is already normalized.
|
- Stage 1 (S2Mel + BigVGAN): CFM DiT converts mel codes to waveform at 22.05 kHz.
|
||||||
- A seed controls Stage 0 AR sampling and per-request CFM noise. Different concurrent batch compositions do not guarantee a bit-identical waveform.
|
- Deploy config: `vllm_omni/deploy/indextts2.yaml`. Stage 1 runs with `enforce_eager: true` (DiT has dynamic shapes).
|
||||||
- Stage 1 (semantic codec + S2Mel + BigVGAN): CFM/DiT converts the complete semantic-code sequence to waveform.
|
|
||||||
- Deploy configs: `vllm_omni/deploy/indextts2.yaml` and `indextts2_5.yaml`.
|
|
||||||
- `async_chunk=false` is intentional for IndexTTS-2.5 correctness: S2Mel consumes the completed semantic sequence.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,13 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||||
"""Offline inference example for IndexTTS 2.0 and 2.5 via vLLM-Omni.
|
"""Offline inference example for IndexTTS2 via vLLM-Omni.
|
||||||
|
|
||||||
Two-stage pipeline: GPT AR (Stage 0) → semantic codec + S2Mel + BigVGAN
|
Two-stage pipeline: GPT AR (Stage 0) → S2Mel + BigVGAN (Stage 1).
|
||||||
(Stage 1).
|
|
||||||
Output is 22050 Hz mono WAV.
|
Output is 22050 Hz mono WAV.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
python end2end.py \
|
python end2end.py \
|
||||||
--model /path/to/indextts-2.5 \
|
--model /path/to/IndexTeam/IndexTTS-2 \
|
||||||
--model-version 2.5 \
|
|
||||||
--lang zh \
|
|
||||||
--text "你好,这是一个语音合成测试。" \
|
--text "你好,这是一个语音合成测试。" \
|
||||||
--ref-audio /path/to/ref.wav
|
--ref-audio /path/to/ref.wav
|
||||||
|
|
||||||
@@ -50,10 +47,6 @@ def build_request(
|
|||||||
emo_alpha: float | None = None,
|
emo_alpha: float | None = None,
|
||||||
use_emo_text: bool = False,
|
use_emo_text: bool = False,
|
||||||
use_random: bool = False,
|
use_random: bool = False,
|
||||||
model_type: str = "indextts2",
|
|
||||||
lang: str = "zh",
|
|
||||||
text_normalization: bool = True,
|
|
||||||
tokenizer_file: str | None = None,
|
|
||||||
) -> dict:
|
) -> dict:
|
||||||
additional: dict = {"text": [text]}
|
additional: dict = {"text": [text]}
|
||||||
if ref_audio_path:
|
if ref_audio_path:
|
||||||
@@ -70,44 +63,12 @@ def build_request(
|
|||||||
additional["use_emo_text"] = [True]
|
additional["use_emo_text"] = [True]
|
||||||
if use_random:
|
if use_random:
|
||||||
additional["use_random"] = [True]
|
additional["use_random"] = [True]
|
||||||
if model_type == "indextts2_5":
|
|
||||||
additional["lang"] = [lang]
|
|
||||||
additional["text_normalization"] = [text_normalization]
|
|
||||||
prompt_kwargs = {
|
|
||||||
"model_type": model_type,
|
|
||||||
"lang": lang,
|
|
||||||
"text_normalization": text_normalization,
|
|
||||||
}
|
|
||||||
if tokenizer_file is not None:
|
|
||||||
prompt_kwargs["tokenizer_file"] = tokenizer_file
|
|
||||||
return {
|
return {
|
||||||
"prompt_token_ids": build_indextts2_prefill_prompt_ids(
|
"prompt_token_ids": build_indextts2_prefill_prompt_ids(model, text),
|
||||||
model,
|
|
||||||
text,
|
|
||||||
**prompt_kwargs,
|
|
||||||
),
|
|
||||||
"additional_information": additional,
|
"additional_information": additional,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _get_stage0_tokenizer_file(omni: Omni) -> str | None:
|
|
||||||
"""Read the effective tokenizer override from the resolved Stage 0 config."""
|
|
||||||
stage_configs = omni.stage_configs
|
|
||||||
if not stage_configs:
|
|
||||||
return None
|
|
||||||
engine_args = getattr(stage_configs[0], "engine_args", None)
|
|
||||||
if engine_args is None:
|
|
||||||
return None
|
|
||||||
hf_overrides = getattr(engine_args, "hf_overrides", None)
|
|
||||||
if hf_overrides is None and isinstance(engine_args, dict):
|
|
||||||
hf_overrides = engine_args.get("hf_overrides")
|
|
||||||
if hf_overrides is None:
|
|
||||||
return None
|
|
||||||
if hasattr(hf_overrides, "get"):
|
|
||||||
return hf_overrides.get("tokenizer_file")
|
|
||||||
return getattr(hf_overrides, "tokenizer_file", None)
|
|
||||||
|
|
||||||
|
|
||||||
def save_audio(waveform: torch.Tensor, path: str, sample_rate: int = 22050) -> None:
|
def save_audio(waveform: torch.Tensor, path: str, sample_rate: int = 22050) -> None:
|
||||||
audio_np = waveform.float().numpy()
|
audio_np = waveform.float().numpy()
|
||||||
sf.write(path, audio_np, sample_rate)
|
sf.write(path, audio_np, sample_rate)
|
||||||
@@ -134,14 +95,9 @@ def extract_audio(mm: dict) -> tuple[torch.Tensor | None, int]:
|
|||||||
|
|
||||||
|
|
||||||
def main(args) -> None:
|
def main(args) -> None:
|
||||||
model_type = "indextts2_5" if args.model_version == "2.5" else "indextts2"
|
|
||||||
deploy_config = args.deploy_config
|
|
||||||
if deploy_config is None and args.model_version == "2.5":
|
|
||||||
repo_root = Path(__file__).resolve().parents[4]
|
|
||||||
deploy_config = str(repo_root / "vllm_omni" / "deploy" / "indextts2_5.yaml")
|
|
||||||
omni = Omni(
|
omni = Omni(
|
||||||
model=args.model,
|
model=args.model,
|
||||||
deploy_config=deploy_config,
|
deploy_config=args.deploy_config,
|
||||||
stage_init_timeout=args.stage_init_timeout,
|
stage_init_timeout=args.stage_init_timeout,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -170,7 +126,7 @@ def main(args) -> None:
|
|||||||
print(f"Synthesizing: {args.text!r}")
|
print(f"Synthesizing: {args.text!r}")
|
||||||
if args.ref_audio:
|
if args.ref_audio:
|
||||||
print(f" ref_audio: {args.ref_audio}")
|
print(f" ref_audio: {args.ref_audio}")
|
||||||
request_kwargs = dict(
|
inputs = build_request(
|
||||||
model=args.model,
|
model=args.model,
|
||||||
text=args.text,
|
text=args.text,
|
||||||
ref_audio_path=args.ref_audio,
|
ref_audio_path=args.ref_audio,
|
||||||
@@ -180,17 +136,7 @@ def main(args) -> None:
|
|||||||
emo_alpha=args.emo_alpha,
|
emo_alpha=args.emo_alpha,
|
||||||
use_emo_text=args.use_emo_text,
|
use_emo_text=args.use_emo_text,
|
||||||
use_random=args.use_random,
|
use_random=args.use_random,
|
||||||
model_type=model_type,
|
|
||||||
lang=args.lang,
|
|
||||||
text_normalization=args.text_normalization,
|
|
||||||
)
|
)
|
||||||
if model_type == "indextts2_5":
|
|
||||||
tokenizer_file = _get_stage0_tokenizer_file(omni)
|
|
||||||
if tokenizer_file is not None:
|
|
||||||
request_kwargs["tokenizer_file"] = tokenizer_file
|
|
||||||
inputs = build_request(**request_kwargs)
|
|
||||||
if model_type == "indextts2_5":
|
|
||||||
inputs["additional_information"]["duration_factor"] = [1.0 / getattr(args, "speed", 1.0)]
|
|
||||||
|
|
||||||
for i, omni_out in enumerate(omni.generate(inputs, sampling_params_list=sampling_params)):
|
for i, omni_out in enumerate(omni.generate(inputs, sampling_params_list=sampling_params)):
|
||||||
mm = omni_out.multimodal_output
|
mm = omni_out.multimodal_output
|
||||||
@@ -208,37 +154,8 @@ def main(args) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def parse_args():
|
def parse_args():
|
||||||
parser = FlexibleArgumentParser(description="IndexTTS 2.0/2.5 offline inference")
|
parser = FlexibleArgumentParser(description="IndexTTS2 offline inference")
|
||||||
parser.add_argument(
|
parser.add_argument("--model", required=True, help="HF model path for IndexTTS2.")
|
||||||
"--model",
|
|
||||||
required=True,
|
|
||||||
help="HF model path or native bundle containing checkpoints/.",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--model-version",
|
|
||||||
choices=("2.0", "2.5"),
|
|
||||||
default="2.0",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--lang",
|
|
||||||
default="zh",
|
|
||||||
help=(
|
|
||||||
"IndexTTS 2.5 language code, for example zh/en/zhen/ja/yue; "
|
|
||||||
"zhen is mixed Chinese/English and Mandarin is a vLLM-Omni alias for zh."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--no-text-normalization",
|
|
||||||
action="store_false",
|
|
||||||
dest="text_normalization",
|
|
||||||
help="Disable IndexTTS 2.5 text normalization.",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--speed",
|
|
||||||
type=float,
|
|
||||||
default=1.0,
|
|
||||||
help="IndexTTS 2.5 native synthesis speed in [0.5, 2.0]; 2.0 is faster.",
|
|
||||||
)
|
|
||||||
parser.add_argument("--text", default="你好,这是IndexTTS2语音合成测试。")
|
parser.add_argument("--text", default="你好,这是IndexTTS2语音合成测试。")
|
||||||
parser.add_argument("--ref-audio", required=True, help="Reference audio for voice cloning.")
|
parser.add_argument("--ref-audio", required=True, help="Reference audio for voice cloning.")
|
||||||
parser.add_argument("--emo-audio", default=None, help="Emotion reference audio.")
|
parser.add_argument("--emo-audio", default=None, help="Emotion reference audio.")
|
||||||
@@ -253,15 +170,7 @@ def parse_args():
|
|||||||
parser.add_argument("--emo-alpha", type=float, default=None, help="Emotion weight in [0, 1].")
|
parser.add_argument("--emo-alpha", type=float, default=None, help="Emotion weight in [0, 1].")
|
||||||
parser.add_argument("--use-emo-text", action="store_true", help="Infer emotion vector from emo-text or text.")
|
parser.add_argument("--use-emo-text", action="store_true", help="Infer emotion vector from emo-text or text.")
|
||||||
parser.add_argument("--use-random", action="store_true", help="Use random emotion prototypes.")
|
parser.add_argument("--use-random", action="store_true", help="Use random emotion prototypes.")
|
||||||
parser.add_argument(
|
parser.add_argument("--seed", type=int, default=None)
|
||||||
"--seed",
|
|
||||||
type=int,
|
|
||||||
default=None,
|
|
||||||
help=(
|
|
||||||
"Seed Stage 0 AR sampling and per-request CFM noise; changing the "
|
|
||||||
"concurrent batch composition may still change the final waveform."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--output-dir",
|
"--output-dir",
|
||||||
default=os.path.join(
|
default=os.path.join(
|
||||||
@@ -271,10 +180,7 @@ def parse_args():
|
|||||||
)
|
)
|
||||||
parser.add_argument("--deploy-config", default=None)
|
parser.add_argument("--deploy-config", default=None)
|
||||||
parser.add_argument("--stage-init-timeout", type=int, default=600)
|
parser.add_argument("--stage-init-timeout", type=int, default=600)
|
||||||
args = parser.parse_args()
|
return parser.parse_args()
|
||||||
if args.model_version == "2.5" and not 0.5 <= args.speed <= 2.0:
|
|
||||||
parser.error("IndexTTS 2.5 --speed must be between 0.5 and 2.0")
|
|
||||||
return args
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ However, as we strive to ensure output similarity between vLLM-Omni's diffuser b
|
|||||||
- Qwen/Qwen-Image
|
- Qwen/Qwen-Image
|
||||||
- Tongyi-MAI/Z-Image-Turbo
|
- Tongyi-MAI/Z-Image-Turbo
|
||||||
- Wan2.2-I2V-A14B-Diffusers
|
- Wan2.2-I2V-A14B-Diffusers
|
||||||
- Lightricks/LTX-2.5-Diffusers (source-preview dependencies)
|
|
||||||
|
|
||||||
If you find that a model not listed above also produces different outputs from running diffusers model directly.
|
If you find that a model not listed above also produces different outputs from running diffusers model directly.
|
||||||
Please consider file an issue or submit a PR to fix.
|
Please consider file an issue or submit a PR to fix.
|
||||||
@@ -119,36 +118,6 @@ The model loading and inference strictly follows the diffusers library, and they
|
|||||||
Users are encouraged to double-check the model pipeline's interface in [diffusers' official documentation](https://huggingface.co/docs/diffusers/api/pipelines/overview).
|
Users are encouraged to double-check the model pipeline's interface in [diffusers' official documentation](https://huggingface.co/docs/diffusers/api/pipelines/overview).
|
||||||
Some particular examples are below.
|
Some particular examples are below.
|
||||||
|
|
||||||
#### LTX-2.5 (source preview)
|
|
||||||
|
|
||||||
Use the gated Diffusers checkpoint
|
|
||||||
[`Lightricks/LTX-2.5-Diffusers`](https://huggingface.co/Lightricks/LTX-2.5-Diffusers);
|
|
||||||
[`Lightricks/LTX-2.5`](https://huggingface.co/Lightricks/LTX-2.5) contains
|
|
||||||
component weights and is not directly loadable by this backend. Accept the
|
|
||||||
model terms, authenticate, and install the source-preview dependencies:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
hf auth login
|
|
||||||
python -m pip install --upgrade 'transformers>=5.10.1' \
|
|
||||||
'diffusers @ git+https://github.com/huggingface/diffusers.git@7564fb016dabda0c943416190fc92398c50b1b20'
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
vllm serve Lightricks/LTX-2.5-Diffusers \
|
|
||||||
--omni \
|
|
||||||
--diffusion-load-format diffusers \
|
|
||||||
--dtype bfloat16 \
|
|
||||||
--enable-cpu-offload \
|
|
||||||
--vae-use-tiling
|
|
||||||
```
|
|
||||||
|
|
||||||
The converted repository loads the distilled transformer. When no `sigmas`,
|
|
||||||
`timesteps`, or `num_inference_steps` is supplied, the adapter applies the
|
|
||||||
official eight-sigma unguided schedule and returns synchronized 48 kHz audio.
|
|
||||||
Support is limited to one-stage distilled T2V; `transformer_full`, two-stage
|
|
||||||
upsampling, image/video conditioning, and the diffusion video decoder are not
|
|
||||||
supported. The general Diffusers backend [limitations](#limitations) apply.
|
|
||||||
|
|
||||||
#### Wan Series
|
#### Wan Series
|
||||||
|
|
||||||
The Wan series video generation models takes `boundary_ratio` and `flow_shift` during model initialization ([ref](https://huggingface.co/docs/diffusers/api/pipelines/wan)), not during inference.
|
The Wan series video generation models takes `boundary_ratio` and `flow_shift` during model initialization ([ref](https://huggingface.co/docs/diffusers/api/pipelines/wan)), not during inference.
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
# Image-To-Video
|
# Image-To-Video
|
||||||
|
|
||||||
This example demonstrates online image-to-video generation with vLLM-Omni.
|
This example demonstrates how to deploy the Wan2.2 image-to-video model for online video generation using vLLM-Omni.
|
||||||
The startup script defaults to Wan2.2, but `MODEL` can select any supported
|
|
||||||
image-to-video model. The existing curl helper remains a concrete Wan2.2
|
|
||||||
request through the standard Videos API.
|
|
||||||
|
|
||||||
## Start Server
|
## Start Server
|
||||||
|
|
||||||
@@ -23,7 +20,9 @@ bash run_server.sh
|
|||||||
|
|
||||||
The script allows overriding:
|
The script allows overriding:
|
||||||
- `MODEL` (default: `Wan-AI/Wan2.2-I2V-A14B-Diffusers`)
|
- `MODEL` (default: `Wan-AI/Wan2.2-I2V-A14B-Diffusers`)
|
||||||
- `PORT` (default: `8099`)
|
- `PORT` (default: `8091`)
|
||||||
|
- `BOUNDARY_RATIO` (default: `0.875`)
|
||||||
|
- `FLOW_SHIFT` (default: `12.0`)
|
||||||
- `CACHE_BACKEND` (default: `none`)
|
- `CACHE_BACKEND` (default: `none`)
|
||||||
- `ENABLE_CACHE_DIT_SUMMARY` (default: `0`)
|
- `ENABLE_CACHE_DIT_SUMMARY` (default: `0`)
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Shared image-to-video server start script
|
# Wan2.2 image-to-video server start script
|
||||||
|
|
||||||
MODEL="${MODEL:-Wan-AI/Wan2.2-I2V-A14B-Diffusers}"
|
MODEL="${MODEL:-Wan-AI/Wan2.2-I2V-A14B-Diffusers}"
|
||||||
PORT="${PORT:-8099}"
|
PORT="${PORT:-8099}"
|
||||||
CACHE_BACKEND="${CACHE_BACKEND:-none}"
|
CACHE_BACKEND="${CACHE_BACKEND:-none}"
|
||||||
ENABLE_CACHE_DIT_SUMMARY="${ENABLE_CACHE_DIT_SUMMARY:-0}"
|
ENABLE_CACHE_DIT_SUMMARY="${ENABLE_CACHE_DIT_SUMMARY:-0}"
|
||||||
|
|
||||||
echo "Starting image-to-video server..."
|
echo "Starting Wan2.2 I2V server..."
|
||||||
echo "Model: $MODEL"
|
echo "Model: $MODEL"
|
||||||
echo "Port: $PORT"
|
echo "Port: $PORT"
|
||||||
echo "Cache backend: $CACHE_BACKEND"
|
echo "Cache backend: $CACHE_BACKEND"
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
# Text-To-Image
|
# Text-To-Image
|
||||||
|
|
||||||
This example demonstrates online text-to-image generation with vLLM-Omni. The
|
This example demonstrates how to deploy Qwen-Image model for online image generation service using vLLM-Omni.
|
||||||
startup script and Python clients default to Qwen-Image, but `MODEL` can select
|
|
||||||
any supported text-to-image model. The existing curl helper remains a concrete
|
|
||||||
Qwen-Image request through the standard Images API.
|
|
||||||
|
|
||||||
## Start Server
|
## Start Server
|
||||||
|
|
||||||
@@ -245,8 +242,8 @@ count, use `size` and `n` rather than `height`, `width`, or
|
|||||||
| `height` | int | None | Image height in pixels |
|
| `height` | int | None | Image height in pixels |
|
||||||
| `width` | int | None | Image width in pixels |
|
| `width` | int | None | Image width in pixels |
|
||||||
| `size` | str | None | Image size (e.g., "1024x1024") |
|
| `size` | str | None | Image size (e.g., "1024x1024") |
|
||||||
| `num_inference_steps` | int | model | Number of denoising steps |
|
| `num_inference_steps` | int | 50 | Number of denoising steps |
|
||||||
| `true_cfg_scale` | float | model | True CFG scale when supported |
|
| `true_cfg_scale` | float | 4.0 | Qwen-Image CFG scale |
|
||||||
| `seed` | int | None | Random seed (reproducible) |
|
| `seed` | int | None | Random seed (reproducible) |
|
||||||
| `negative_prompt` | str | None | Negative prompt |
|
| `negative_prompt` | str | None | Negative prompt |
|
||||||
| `num_outputs_per_prompt` | int | 1 | Number of images to generate |
|
| `num_outputs_per_prompt` | int | 1 | Number of images to generate |
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
Shared text-to-image Gradio demo for online serving.
|
Qwen-Image Gradio Demo for online serving.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
python gradio_demo.py [--server http://localhost:8091] [--port 7860]
|
python gradio_demo.py [--server http://localhost:8091] [--port 7860]
|
||||||
@@ -77,9 +77,9 @@ def generate_image(
|
|||||||
def create_demo(server_url: str):
|
def create_demo(server_url: str):
|
||||||
"""Create Gradio demo interface."""
|
"""Create Gradio demo interface."""
|
||||||
|
|
||||||
with gr.Blocks(title="Text-to-Image Demo") as demo:
|
with gr.Blocks(title="Qwen-Image Demo") as demo:
|
||||||
gr.Markdown("# Text-to-Image Online Generation")
|
gr.Markdown("# Qwen-Image Online Generation")
|
||||||
gr.Markdown("Generate images using the model configured on the server")
|
gr.Markdown("Generate images using Qwen-Image model")
|
||||||
|
|
||||||
with gr.Row():
|
with gr.Row():
|
||||||
with gr.Column(scale=1):
|
with gr.Column(scale=1):
|
||||||
@@ -115,6 +115,7 @@ def create_demo(server_url: str):
|
|||||||
label="Inference Steps",
|
label="Inference Steps",
|
||||||
minimum=10,
|
minimum=10,
|
||||||
maximum=100,
|
maximum=100,
|
||||||
|
# Default steps aligned with run_curl_text_to_image.sh to 100
|
||||||
value=100,
|
value=100,
|
||||||
step=5,
|
step=5,
|
||||||
)
|
)
|
||||||
@@ -172,7 +173,7 @@ def create_demo(server_url: str):
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(description="Shared text-to-image Gradio demo")
|
parser = argparse.ArgumentParser(description="Qwen-Image Gradio Demo")
|
||||||
parser.add_argument("--server", default="http://localhost:8091", help="Server URL")
|
parser.add_argument("--server", default="http://localhost:8091", help="Server URL")
|
||||||
parser.add_argument("--port", type=int, default=7860, help="Gradio port")
|
parser.add_argument("--port", type=int, default=7860, help="Gradio port")
|
||||||
parser.add_argument("--share", action="store_true", help="Create public link")
|
parser.add_argument("--share", action="store_true", help="Create public link")
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
Shared OpenAI-compatible text-to-image generation client.
|
Qwen-Image OpenAI-compatible image generation client.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
python openai_chat_client.py --prompt "A beautiful landscape" --output output.png
|
python openai_chat_client.py --prompt "A beautiful landscape" --output output.png
|
||||||
@@ -39,7 +39,7 @@ def generate_image(
|
|||||||
height: Image height in pixels
|
height: Image height in pixels
|
||||||
width: Image width in pixels
|
width: Image width in pixels
|
||||||
steps: Number of diffusion steps
|
steps: Number of diffusion steps
|
||||||
true_cfg_scale: True CFG scale for models that support it
|
true_cfg_scale: Qwen-Image CFG scale
|
||||||
seed: Random seed
|
seed: Random seed
|
||||||
negative_prompt: Negative prompt
|
negative_prompt: Negative prompt
|
||||||
num_outputs_per_prompt: Number of images to generate
|
num_outputs_per_prompt: Number of images to generate
|
||||||
@@ -70,7 +70,7 @@ def generate_image(
|
|||||||
payload["num_inference_steps"] = steps
|
payload["num_inference_steps"] = steps
|
||||||
if true_cfg_scale is not None:
|
if true_cfg_scale is not None:
|
||||||
payload["true_cfg_scale"] = true_cfg_scale
|
payload["true_cfg_scale"] = true_cfg_scale
|
||||||
if negative_prompt is not None:
|
if negative_prompt:
|
||||||
payload["negative_prompt"] = negative_prompt
|
payload["negative_prompt"] = negative_prompt
|
||||||
if seed is not None:
|
if seed is not None:
|
||||||
payload["seed"] = seed
|
payload["seed"] = seed
|
||||||
@@ -115,15 +115,15 @@ def generate_image(
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(description="Shared text-to-image client")
|
parser = argparse.ArgumentParser(description="Qwen-Image chat client")
|
||||||
parser.add_argument("--prompt", "-p", default="a cup of coffee on the table", help="Text prompt")
|
parser.add_argument("--prompt", "-p", default="a cup of coffee on the table", help="Text prompt")
|
||||||
parser.add_argument("--output", "-o", default="text_to_image_output.png", help="Output file")
|
parser.add_argument("--output", "-o", default="qwen_image_output.png", help="Output file")
|
||||||
parser.add_argument("--server", "-s", default="http://localhost:8091", help="Server URL")
|
parser.add_argument("--server", "-s", default="http://localhost:8091", help="Server URL")
|
||||||
parser.add_argument("--height", type=int, help="Image height; defaults to the model setting")
|
parser.add_argument("--height", type=int, default=1024, help="Image height")
|
||||||
parser.add_argument("--width", type=int, help="Image width; defaults to the model setting")
|
parser.add_argument("--width", type=int, default=1024, help="Image width")
|
||||||
parser.add_argument("--steps", type=int, help="Inference steps; defaults to the model setting")
|
parser.add_argument("--steps", type=int, default=50, help="Inference steps")
|
||||||
parser.add_argument("--cfg-scale", type=float, help="True CFG scale when supported")
|
parser.add_argument("--cfg-scale", type=float, default=4.0, help="True CFG scale")
|
||||||
parser.add_argument("--seed", type=int, help="Random seed")
|
parser.add_argument("--seed", type=int, default=0, help="Random seed")
|
||||||
parser.add_argument("--negative", help="Negative prompt")
|
parser.add_argument("--negative", help="Negative prompt")
|
||||||
|
|
||||||
parser.add_argument("--lora-path", default=None, help="Server-local LoRA adapter folder (PEFT format)")
|
parser.add_argument("--lora-path", default=None, help="Server-local LoRA adapter folder (PEFT format)")
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Shared text-to-image online serving startup script
|
# Qwen-Image online serving startup script
|
||||||
|
|
||||||
MODEL="${MODEL:-Qwen/Qwen-Image}"
|
MODEL="${MODEL:-Qwen/Qwen-Image}"
|
||||||
PORT="${PORT:-8091}"
|
PORT="${PORT:-8091}"
|
||||||
|
|
||||||
echo "Starting text-to-image server..."
|
echo "Starting Qwen-Image server..."
|
||||||
echo "Model: $MODEL"
|
echo "Model: $MODEL"
|
||||||
echo "Port: $PORT"
|
echo "Port: $PORT"
|
||||||
|
|
||||||
|
|||||||
@@ -16,8 +16,7 @@ For the full list of supported architectures across all modalities, see
|
|||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
| Fish Speech S2 Pro | `fishaudio/s2-pro` | ✓ (`ref_audio`+`ref_text`) | ✓ (PCM stream) | — | ✓ |
|
| Fish Speech S2 Pro | `fishaudio/s2-pro` | ✓ (`ref_audio`+`ref_text`) | ✓ (PCM stream) | — | ✓ |
|
||||||
| GLM-TTS | `zai-org/GLM-TTS` | ✓ (`ref_audio`+`ref_text`, required) | ✓ (PCM stream) | — | ✓ |
|
| GLM-TTS | `zai-org/GLM-TTS` | ✓ (`ref_audio`+`ref_text`, required) | ✓ (PCM stream) | — | ✓ |
|
||||||
| IndexTTS-2 | `IndexTeam/IndexTTS-2` | ✓ (`ref_audio` or uploaded `voice`) | `stream=true` response, non-chunk | uploaded audio voice only; no presets | — |
|
| IndexTTS-2 | `IndexTeam/IndexTTS-2` | ✓ (`ref_audio` or uploaded `voice`) | compat only, non-chunk | uploaded audio voice only; no presets | — |
|
||||||
| IndexTTS-2.5 | native `checkpoints/` bundle | ✓ (`ref_audio` or uploaded `voice`) | `stream=true` response, non-chunk | uploaded audio voice only; no presets | — |
|
|
||||||
| Ming-omni-tts | `inclusionAI/Ming-omni-tts-0.5B` | ✓ (`ref_audio` / `speaker_embedding`) | ✓ (PCM stream) | IP labels + structured `instructions` | — |
|
| Ming-omni-tts | `inclusionAI/Ming-omni-tts-0.5B` | ✓ (`ref_audio` / `speaker_embedding`) | ✓ (PCM stream) | IP labels + structured `instructions` | — |
|
||||||
| Ming-flash-omni-TTS | `Jonathan1909/Ming-flash-omni-2.0` | — (caption-controlled) | — | caption fields (`instructions`) | — |
|
| Ming-flash-omni-TTS | `Jonathan1909/Ming-flash-omni-2.0` | — (caption-controlled) | — | caption fields (`instructions`) | — |
|
||||||
| MOSS-TTS-Nano | `OpenMOSS-Team/MOSS-TTS-Nano` | ✓ (`ref_audio` required) | ✓ (PCM stream) | — | ✓ |
|
| MOSS-TTS-Nano | `OpenMOSS-Team/MOSS-TTS-Nano` | ✓ (`ref_audio` required) | ✓ (PCM stream) | — | ✓ |
|
||||||
@@ -139,29 +138,15 @@ bash examples/online_serving/text_to_speech/glm_tts/run_gradio_demo.sh
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## IndexTTS-2 and IndexTTS-2.5
|
## IndexTTS-2
|
||||||
|
|
||||||
2-stage TTS at 22.05 kHz. Requests use `ref_audio` for voice cloning, or an uploaded audio `voice` from `/v1/audio/voices`. IndexTTS-2.5 uses a multilingual tokenizer, CAMPPlus speaker projection, EnhancedCodec, and code-only Stage 0→1 transfer by default. Both versions support emotion conditioning via `emo_audio`, `emo_text`, or `emo_vector` passed in `extra_params`.
|
2-stage TTS (GPT AR + S2Mel CFM DiT + BigVGAN) at 22.05 kHz. Requests use `ref_audio` for voice cloning, or an uploaded audio `voice` from `/v1/audio/voices`. Supports emotion conditioning via `emo_audio`, `emo_text`, or `emo_vector` passed in `extra_params`.
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
IndexTTS-2 uses its existing frontend and does not require the IndexTTS-2.5 text dependencies. Before launching IndexTTS-2.5, install its optional frontend dependencies:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pip install 'vllm-omni[indextts2]'
|
|
||||||
```
|
|
||||||
|
|
||||||
### Launch
|
### Launch
|
||||||
```bash
|
```bash
|
||||||
vllm serve IndexTeam/IndexTTS-2 --omni --trust-remote-code --port 8092
|
vllm serve IndexTeam/IndexTTS-2 --omni --trust-remote-code --port 8092
|
||||||
# or, to pass the bundled deploy config explicitly:
|
# or, to pass the bundled deploy config explicitly:
|
||||||
bash examples/online_serving/text_to_speech/indextts2/run_server.sh
|
bash examples/online_serving/text_to_speech/indextts2/run_server.sh
|
||||||
|
|
||||||
# IndexTTS-2.5 default: use_gpt_latent=false
|
|
||||||
MODEL_VERSION=2.5 \
|
|
||||||
MODEL=/path/to/indextts-2.5 \
|
|
||||||
bash examples/online_serving/text_to_speech/indextts2/run_server.sh
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Sending requests
|
### Sending requests
|
||||||
@@ -176,35 +161,14 @@ python examples/online_serving/text_to_speech/indextts2/speech_client.py \
|
|||||||
--text "今天心情很好!" \
|
--text "今天心情很好!" \
|
||||||
--ref-audio /path/to/ref.wav \
|
--ref-audio /path/to/ref.wav \
|
||||||
--emo-audio /path/to/happy.wav
|
--emo-audio /path/to/happy.wav
|
||||||
|
|
||||||
# IndexTTS-2.5 Japanese request
|
|
||||||
python examples/online_serving/text_to_speech/indextts2/speech_client.py \
|
|
||||||
--model-version 2.5 \
|
|
||||||
--model /path/to/indextts-2.5 \
|
|
||||||
--lang ja \
|
|
||||||
--text "こんにちは、音声合成のテストです。" \
|
|
||||||
--ref-audio /path/to/ref.wav
|
|
||||||
|
|
||||||
# IndexTTS-2.5 native speed control
|
|
||||||
python examples/online_serving/text_to_speech/indextts2/speech_client.py \
|
|
||||||
--model-version 2.5 \
|
|
||||||
--model /path/to/indextts-2.5 \
|
|
||||||
--speed 2.0 \
|
|
||||||
--text "这是两倍速度的语音合成测试。" \
|
|
||||||
--ref-audio /path/to/ref.wav
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Notes
|
### Notes
|
||||||
- Output: 22.05 kHz mono WAV.
|
- Output: 22.05 kHz mono WAV.
|
||||||
- Provide `ref_audio` on the documented raw request path, or pass `voice` only when it names an uploaded audio voice; neither version provides a built-in text-only preset voice.
|
- Provide `ref_audio` on the documented raw request path, or pass `voice` only when it names an uploaded audio voice; IndexTTS-2 does not provide a built-in text-only preset voice.
|
||||||
- IndexTTS-2.5 request controls `lang` and `text_normalization` are carried in `extra_params`; the client does this for `--model-version 2.5`.
|
|
||||||
- IndexTTS-2.5 accepts the public `speed` request field in `[0.5, 2.0]`: `2.0` generates shorter, faster speech and `0.5` generates longer, slower speech. This is native Stage 1 duration control, so serving does not apply the generic playback-speed adjustment a second time. IndexTTS-2 does not use this control.
|
|
||||||
- IndexTTS-2.5 accepts language codes such as `zh`, `en`, `zhen` (mixed Chinese/English), `ja`, and `yue`. `Mandarin` is a vLLM-Omni convenience alias for `zh`. Japanese (`ja`) uses `fugashi` tokenization and produces audio, but it does not automatically expand numbers, dates, or percentages; callers should first write those inputs as readable Japanese text.
|
|
||||||
- A request `seed` controls Stage 0 AR sampling and per-request CFM noise. Different concurrent batch compositions do not guarantee a bit-identical waveform.
|
|
||||||
- IndexTTS-2.5 Stage 0 uses plain vLLM sampling and does not reproduce the official default `num_beams=3` beam search. For parity comparisons, run upstream with `num_beams=1`; output quality can differ from the official beam-search result.
|
|
||||||
- Emotion params (`emo_audio`, `emo_text`, `emo_vector`, `emo_alpha`, `use_emo_text`, `use_random`) are passed via the `extra_params` field. Official precedence is `use_emo_text` > `emo_vector` > `emo_audio` > same emotion as the speaker reference.
|
- Emotion params (`emo_audio`, `emo_text`, `emo_vector`, `emo_alpha`, `use_emo_text`, `use_random`) are passed via the `extra_params` field. Official precedence is `use_emo_text` > `emo_vector` > `emo_audio` > same emotion as the speaker reference.
|
||||||
- IndexTTS-2.5 uses `vllm_omni/deploy/indextts2_5.yaml` with the official code-only Stage 0 to Stage 1 contract.
|
- `stream=true` is accepted as an OpenAI-compatible response path, but IndexTTS-2 is not async-chunk streaming; audio is produced after S2Mel receives the full mel-code sequence.
|
||||||
- For IndexTTS-2.5, set `MODEL` to the local native bundle; asset discovery accepts its nested `checkpoints/` layout.
|
- Deploy config: `vllm_omni/deploy/indextts2.yaml` (auto-loaded).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,37 +1,20 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Launch vLLM-Omni server for IndexTTS 2.0 or 2.5.
|
# Launch vLLM-Omni server for IndexTTS2
|
||||||
#
|
#
|
||||||
# Usage from repository root:
|
# Usage from repository root:
|
||||||
# examples/online_serving/text_to_speech/indextts2/run_server.sh
|
# examples/online_serving/text_to_speech/indextts2/run_server.sh
|
||||||
# MODEL_VERSION=2.5 MODEL=/path/to/native/bundle \
|
# CUDA_VISIBLE_DEVICES=0 PORT=8092 MODEL=/path/to/IndexTeam/IndexTTS-2 examples/online_serving/text_to_speech/indextts2/run_server.sh
|
||||||
# examples/online_serving/text_to_speech/indextts2/run_server.sh
|
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
ROOT_DIR="$(cd -- "$SCRIPT_DIR/../../../.." && pwd)"
|
ROOT_DIR="$(cd -- "$SCRIPT_DIR/../../../.." && pwd)"
|
||||||
|
|
||||||
MODEL_VERSION="${MODEL_VERSION:-2.0}"
|
MODEL="${MODEL:-IndexTeam/IndexTTS-2}"
|
||||||
PORT="${PORT:-8092}"
|
PORT="${PORT:-8092}"
|
||||||
|
DEPLOY_CONFIG="${DEPLOY_CONFIG:-$ROOT_DIR/vllm_omni/deploy/indextts2.yaml}"
|
||||||
|
|
||||||
if [[ "$MODEL_VERSION" == "2.5" ]]; then
|
echo "Starting IndexTTS2 server with model: $MODEL"
|
||||||
if [[ -z "${MODEL:-}" ]]; then
|
|
||||||
echo "Usage: MODEL_VERSION=2.5 MODEL=/path/to/native/bundle bash $0" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
DEFAULT_DEPLOY_CONFIG="$ROOT_DIR/vllm_omni/deploy/indextts2_5.yaml"
|
|
||||||
elif [[ "$MODEL_VERSION" == "2.0" ]]; then
|
|
||||||
MODEL="${MODEL:-IndexTeam/IndexTTS-2}"
|
|
||||||
DEFAULT_DEPLOY_CONFIG="$ROOT_DIR/vllm_omni/deploy/indextts2.yaml"
|
|
||||||
else
|
|
||||||
echo "MODEL_VERSION must be 2.0 or 2.5" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
DEPLOY_CONFIG="${DEPLOY_CONFIG:-$DEFAULT_DEPLOY_CONFIG}"
|
|
||||||
|
|
||||||
echo "Starting IndexTTS $MODEL_VERSION server with model: $MODEL"
|
|
||||||
echo "Deploy config: $DEPLOY_CONFIG"
|
|
||||||
|
|
||||||
FLASHINFER_DISABLE_VERSION_CHECK=1 \
|
FLASHINFER_DISABLE_VERSION_CHECK=1 \
|
||||||
vllm serve "$MODEL" \
|
vllm serve "$MODEL" \
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||||
"""OpenAI-compatible client for IndexTTS 2.0/2.5 via /v1/audio/speech.
|
"""OpenAI-compatible client for IndexTTS2 TTS via /v1/audio/speech endpoint.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
# With reference audio for voice cloning
|
# With reference audio for voice cloning
|
||||||
@@ -54,44 +54,13 @@ def main() -> None:
|
|||||||
parser.add_argument("--emo-alpha", type=float, default=None, help="Emotion weight in [0, 1]")
|
parser.add_argument("--emo-alpha", type=float, default=None, help="Emotion weight in [0, 1]")
|
||||||
parser.add_argument("--use-emo-text", action="store_true", help="Infer emotion vector from emo-text or text")
|
parser.add_argument("--use-emo-text", action="store_true", help="Infer emotion vector from emo-text or text")
|
||||||
parser.add_argument("--use-random", action="store_true", help="Use random emotion prototypes")
|
parser.add_argument("--use-random", action="store_true", help="Use random emotion prototypes")
|
||||||
parser.add_argument(
|
parser.add_argument("--model", type=str, default="IndexTeam/IndexTTS-2")
|
||||||
"--model-version",
|
|
||||||
choices=("2.0", "2.5"),
|
|
||||||
default="2.0",
|
|
||||||
)
|
|
||||||
parser.add_argument("--model", type=str, default=None)
|
|
||||||
parser.add_argument(
|
|
||||||
"--lang",
|
|
||||||
default="zh",
|
|
||||||
help=(
|
|
||||||
"IndexTTS 2.5 language code, for example zh/en/zhen/ja/yue; "
|
|
||||||
"zhen is mixed Chinese/English and Mandarin is a vLLM-Omni alias for zh"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--no-text-normalization",
|
|
||||||
action="store_false",
|
|
||||||
dest="text_normalization",
|
|
||||||
help="Disable IndexTTS 2.5 text normalization",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--speed",
|
|
||||||
type=float,
|
|
||||||
default=1.0,
|
|
||||||
help="IndexTTS 2.5 native synthesis speed in [0.5, 2.0]; 2.0 is faster",
|
|
||||||
)
|
|
||||||
parser.add_argument("--voice", type=str, default=None, help="Uploaded voice name to use instead of --ref-audio")
|
parser.add_argument("--voice", type=str, default=None, help="Uploaded voice name to use instead of --ref-audio")
|
||||||
parser.add_argument("--output", type=str, default="output.wav")
|
parser.add_argument("--output", type=str, default="output.wav")
|
||||||
parser.add_argument("--api-base", type=str, default=DEFAULT_API_BASE)
|
parser.add_argument("--api-base", type=str, default=DEFAULT_API_BASE)
|
||||||
parser.add_argument("--api-key", type=str, default=DEFAULT_API_KEY)
|
parser.add_argument("--api-key", type=str, default=DEFAULT_API_KEY)
|
||||||
parser.add_argument("--response-format", type=str, default="wav")
|
parser.add_argument("--response-format", type=str, default="wav")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
if args.model is None:
|
|
||||||
if args.model_version == "2.5":
|
|
||||||
parser.error("--model is required for IndexTTS 2.5")
|
|
||||||
args.model = "IndexTeam/IndexTTS-2"
|
|
||||||
if args.model_version == "2.5" and not 0.5 <= args.speed <= 2.0:
|
|
||||||
parser.error("IndexTTS 2.5 --speed must be between 0.5 and 2.0")
|
|
||||||
|
|
||||||
if not args.ref_audio and not args.voice:
|
if not args.ref_audio and not args.voice:
|
||||||
parser.error("IndexTTS2 requires --ref-audio or --voice for voice cloning")
|
parser.error("IndexTTS2 requires --ref-audio or --voice for voice cloning")
|
||||||
@@ -128,10 +97,6 @@ def main() -> None:
|
|||||||
extra_params["use_emo_text"] = True
|
extra_params["use_emo_text"] = True
|
||||||
if args.use_random:
|
if args.use_random:
|
||||||
extra_params["use_random"] = True
|
extra_params["use_random"] = True
|
||||||
if args.model_version == "2.5":
|
|
||||||
payload["speed"] = args.speed
|
|
||||||
extra_params["lang"] = args.lang
|
|
||||||
extra_params["text_normalization"] = args.text_normalization
|
|
||||||
if extra_params:
|
if extra_params:
|
||||||
payload["extra_params"] = extra_params
|
payload["extra_params"] = extra_params
|
||||||
|
|
||||||
|
|||||||
@@ -84,13 +84,6 @@ forced-aligner = [
|
|||||||
"qwen-asr>=0.0.6", # faithful Japanese/Korean word segmentation for the TTS forced aligner
|
"qwen-asr>=0.0.6", # faithful Japanese/Korean word segmentation for the TTS forced aligner
|
||||||
]
|
]
|
||||||
|
|
||||||
indextts2 = [
|
|
||||||
"fugashi>=1.2.0",
|
|
||||||
"unidic-lite>=1.0.0",
|
|
||||||
"wetext>=0.0.9; sys_platform != 'linux'",
|
|
||||||
"WeTextProcessing; sys_platform == 'linux'",
|
|
||||||
]
|
|
||||||
|
|
||||||
# Local-test-only extras
|
# Local-test-only extras
|
||||||
local = [
|
local = [
|
||||||
"FlagEmbedding",
|
"FlagEmbedding",
|
||||||
@@ -334,7 +327,3 @@ byt5 = "byt5"
|
|||||||
# "seeked" is the standard HTML media element DOM event name (fires when a
|
# "seeked" is the standard HTML media element DOM event name (fires when a
|
||||||
# seek completes), used by the word-timestamps Gradio demo. Not a typo.
|
# seek completes), used by the word-timestamps Gradio demo. Not a typo.
|
||||||
seeked = "seeked"
|
seeked = "seeked"
|
||||||
# IndexTTS 2.5 language codes and speech-emotion-recognition event token.
|
|
||||||
fo = "fo"
|
|
||||||
ba = "ba"
|
|
||||||
SER = "SER"
|
|
||||||
@@ -1,308 +0,0 @@
|
|||||||
# IndexTTS-2.5 for multilingual voice-cloned TTS on 1x GPU
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
- Vendor: IndexTeam
|
|
||||||
- Model: `IndexTeam/IndexTTS-2.5`
|
|
||||||
- Task: Multilingual text-to-speech with voice cloning, emotion control, and
|
|
||||||
native speed control
|
|
||||||
- Mode: Online serving with the OpenAI-compatible `/v1/audio/speech` API, or
|
|
||||||
offline inference through `Omni`
|
|
||||||
- Maintainer: Community
|
|
||||||
|
|
||||||
## When to use this recipe
|
|
||||||
|
|
||||||
Use this recipe to serve IndexTTS-2.5 as a two-stage TTS system. Stage 0 is an
|
|
||||||
autoregressive talker; Stage 1 uses EnhancedCodec, S2Mel CFM/DiT, and BigVGAN
|
|
||||||
to produce 22.05 kHz mono speech. Each request supplies synthesis text and
|
|
||||||
reference audio, or an uploaded audio voice, for zero-shot voice cloning.
|
|
||||||
|
|
||||||
IndexTTS-2.5 adds multilingual text processing and model-native speed control
|
|
||||||
to the IndexTTS-2 serving contract.
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- Upstream model:
|
|
||||||
[IndexTeam/IndexTTS-2.5 on Hugging Face](https://huggingface.co/IndexTeam/IndexTTS-2.5)
|
|
||||||
- Online serving example:
|
|
||||||
[`examples/online_serving/text_to_speech/README.md#indextts-2-and-indextts-25`](../../examples/online_serving/text_to_speech/README.md#indextts-2-and-indextts-25)
|
|
||||||
- Offline inference example:
|
|
||||||
[`examples/offline_inference/text_to_speech/README.md#indextts-2-and-indextts-25`](../../examples/offline_inference/text_to_speech/README.md#indextts-2-and-indextts-25)
|
|
||||||
- OpenAI-compatible client:
|
|
||||||
[`examples/online_serving/text_to_speech/indextts2/speech_client.py`](../../examples/online_serving/text_to_speech/indextts2/speech_client.py)
|
|
||||||
- Standard deploy config:
|
|
||||||
[`vllm_omni/deploy/indextts2_5.yaml`](../../vllm_omni/deploy/indextts2_5.yaml)
|
|
||||||
|
|
||||||
## Hardware Support
|
|
||||||
|
|
||||||
### GPU
|
|
||||||
|
|
||||||
### 1x NVIDIA H20 96GB
|
|
||||||
|
|
||||||
#### Environment
|
|
||||||
|
|
||||||
- OS: Linux
|
|
||||||
- Python: 3.10+
|
|
||||||
- Driver / runtime: NVIDIA CUDA environment
|
|
||||||
- vLLM version: Match the repository requirements for your checkout
|
|
||||||
- vLLM-Omni version or commit: Use the commit you are deploying from
|
|
||||||
|
|
||||||
Install vLLM-Omni with the IndexTTS text-processing dependencies:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pip install 'vllm-omni[indextts2]'
|
|
||||||
```
|
|
||||||
|
|
||||||
Obtain the native IndexTTS-2.5 bundle and point `MODEL` at its root. The model
|
|
||||||
loader accepts the upstream nested `checkpoints/` layout.
|
|
||||||
|
|
||||||
#### Command
|
|
||||||
|
|
||||||
Start the standard code-only server from the repository root:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
MODEL_VERSION=2.5 \
|
|
||||||
MODEL=/path/to/indextts-2.5 \
|
|
||||||
bash examples/online_serving/text_to_speech/indextts2/run_server.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
This selects `vllm_omni/deploy/indextts2_5.yaml`, which uses
|
|
||||||
`use_gpt_latent=false`. To launch directly:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
vllm serve /path/to/indextts-2.5 \
|
|
||||||
--omni \
|
|
||||||
--trust-remote-code \
|
|
||||||
--port 8092 \
|
|
||||||
--deploy-config vllm_omni/deploy/indextts2_5.yaml
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Optional: NVIDIA MPS for higher single-GPU throughput
|
|
||||||
|
|
||||||
Stage 0 and Stage 1 run in separate processes on the same GPU. Enabling
|
|
||||||
NVIDIA Multi-Process Service (MPS) can improve steady-state throughput by
|
|
||||||
allowing work from both stages to overlap more effectively. MPS is an opt-in
|
|
||||||
deployment optimization: this recipe does not start it automatically, and the
|
|
||||||
gain depends on the request mix and concurrency, so benchmark it with the
|
|
||||||
intended workload before enabling it in production.
|
|
||||||
|
|
||||||
Pay attention to CUDA device renumbering. For example, if the MPS control
|
|
||||||
daemon is started with physical `CUDA_VISIBLE_DEVICES=1`, that GPU is exposed
|
|
||||||
to MPS clients as logical device `0`; launch the server with
|
|
||||||
`CUDA_VISIBLE_DEVICES=0`, not `1`. Use a dedicated MPS pipe/log directory when
|
|
||||||
the host is shared with other services, and stop only the MPS daemon owned by
|
|
||||||
this deployment.
|
|
||||||
|
|
||||||
#### Verification
|
|
||||||
|
|
||||||
Send a multilingual voice-cloning request with the bundled client. Local audio
|
|
||||||
paths are converted to base64 data URLs before transmission:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python examples/online_serving/text_to_speech/indextts2/speech_client.py \
|
|
||||||
--api-base http://localhost:8092 \
|
|
||||||
--model-version 2.5 \
|
|
||||||
--model /path/to/indextts-2.5 \
|
|
||||||
--lang zh \
|
|
||||||
--text "你好,这是 IndexTTS-2.5 语音合成测试。" \
|
|
||||||
--ref-audio /path/to/reference.wav \
|
|
||||||
--output indextts2_5.wav
|
|
||||||
```
|
|
||||||
|
|
||||||
##### Native speed control
|
|
||||||
|
|
||||||
The public `speed` field is handled natively. Its accepted range is
|
|
||||||
`[0.5, 2.0]`; values above `1.0` produce shorter, faster speech. The serving
|
|
||||||
adapter converts the API convention to the model-native control with
|
|
||||||
`duration_factor = 1.0 / speed`, so `speed=1.25` uses
|
|
||||||
`duration_factor=0.8`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python examples/online_serving/text_to_speech/indextts2/speech_client.py \
|
|
||||||
--api-base http://localhost:8092 \
|
|
||||||
--model-version 2.5 \
|
|
||||||
--model /path/to/indextts-2.5 \
|
|
||||||
--speed 1.25 \
|
|
||||||
--text "这是使用模型原生语速控制的测试。" \
|
|
||||||
--ref-audio /path/to/reference.wav \
|
|
||||||
--output indextts2_5_speed.wav
|
|
||||||
```
|
|
||||||
|
|
||||||
##### Upload and reuse a named voice
|
|
||||||
|
|
||||||
Upload a reference recording once when callers should reuse the same voice
|
|
||||||
without sending `ref_audio` on every request. `consent` is required by the
|
|
||||||
voice-storage API:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:8092/v1/audio/voices \
|
|
||||||
-F "audio_sample=@/path/to/reference.wav" \
|
|
||||||
-F "consent=user-consent-id" \
|
|
||||||
-F "name=indextts_demo_voice" \
|
|
||||||
-F "speaker_description=IndexTTS-2.5 demonstration voice"
|
|
||||||
```
|
|
||||||
|
|
||||||
Confirm that the voice was stored, then synthesize with its name instead of a
|
|
||||||
reference-audio payload:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl http://localhost:8092/v1/audio/voices
|
|
||||||
|
|
||||||
python examples/online_serving/text_to_speech/indextts2/speech_client.py \
|
|
||||||
--api-base http://localhost:8092 \
|
|
||||||
--model-version 2.5 \
|
|
||||||
--model /path/to/indextts-2.5 \
|
|
||||||
--voice indextts_demo_voice \
|
|
||||||
--lang zh \
|
|
||||||
--text "你好,这是复用已上传音色的语音合成测试。" \
|
|
||||||
--output indextts2_5_named_voice.wav
|
|
||||||
```
|
|
||||||
|
|
||||||
There are no built-in text-only preset voices. Each named IndexTTS voice must
|
|
||||||
first be created from an uploaded reference recording.
|
|
||||||
|
|
||||||
##### Multilingual synthesis and text normalization
|
|
||||||
|
|
||||||
The client accepts `zh` (Mandarin), `en` (English), `zhen` (mixed Chinese and
|
|
||||||
English), `ja` (Japanese), and `yue` (Cantonese). The following commands use
|
|
||||||
the same reference voice across all supported language modes:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
CLIENT=examples/online_serving/text_to_speech/indextts2/speech_client.py
|
|
||||||
MODEL=/path/to/indextts-2.5
|
|
||||||
REF_AUDIO=/path/to/reference.wav
|
|
||||||
|
|
||||||
python "$CLIENT" --model-version 2.5 --model "$MODEL" \
|
|
||||||
--ref-audio "$REF_AUDIO" --lang zh \
|
|
||||||
--text "你好,这是中文语音合成测试。" --output indextts2_5_zh.wav
|
|
||||||
|
|
||||||
python "$CLIENT" --model-version 2.5 --model "$MODEL" \
|
|
||||||
--ref-audio "$REF_AUDIO" --lang en \
|
|
||||||
--text "Hello, this is an English speech synthesis test." \
|
|
||||||
--output indextts2_5_en.wav
|
|
||||||
|
|
||||||
python "$CLIENT" --model-version 2.5 --model "$MODEL" \
|
|
||||||
--ref-audio "$REF_AUDIO" --lang zhen \
|
|
||||||
--text "Hello,欢迎使用 IndexTTS 二点五。" --output indextts2_5_zhen.wav
|
|
||||||
|
|
||||||
python "$CLIENT" --model-version 2.5 --model "$MODEL" \
|
|
||||||
--ref-audio "$REF_AUDIO" --lang ja \
|
|
||||||
--text "こんにちは、これは日本語の音声合成テストです。" \
|
|
||||||
--output indextts2_5_ja.wav
|
|
||||||
|
|
||||||
python "$CLIENT" --model-version 2.5 --model "$MODEL" \
|
|
||||||
--ref-audio "$REF_AUDIO" --lang yue \
|
|
||||||
--text "你好,呢段係粵語語音合成測試。" --output indextts2_5_yue.wav
|
|
||||||
```
|
|
||||||
|
|
||||||
Text normalization is enabled by default. Disable it only when the input is
|
|
||||||
already written exactly as it should be spoken:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python examples/online_serving/text_to_speech/indextts2/speech_client.py \
|
|
||||||
--model-version 2.5 --model /path/to/indextts-2.5 \
|
|
||||||
--ref-audio /path/to/reference.wav --lang en --no-text-normalization \
|
|
||||||
--text "The temperature is twenty five degrees." \
|
|
||||||
--output indextts2_5_pre_normalized.wav
|
|
||||||
```
|
|
||||||
|
|
||||||
For raw HTTP requests, put the same language and text-normalization controls
|
|
||||||
under `extra_params`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl http://localhost:8092/v1/audio/speech \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{
|
|
||||||
"model": "/path/to/indextts-2.5",
|
|
||||||
"input": "This is an IndexTTS-2.5 test.",
|
|
||||||
"response_format": "wav",
|
|
||||||
"speed": 1.0,
|
|
||||||
"ref_audio": "data:audio/wav;base64,<BASE64_ENCODED_AUDIO>",
|
|
||||||
"extra_params": {
|
|
||||||
"lang": "en",
|
|
||||||
"text_normalization": true
|
|
||||||
}
|
|
||||||
}' \
|
|
||||||
--output indextts2_5_en.wav
|
|
||||||
```
|
|
||||||
|
|
||||||
##### Emotion control
|
|
||||||
|
|
||||||
IndexTTS-2.5 supports an explicit eight-value emotion vector, an emotion text
|
|
||||||
description, or a separate emotion-reference recording. Use one source at a
|
|
||||||
time. Vector values are ordered as `happy`, `angry`, `sad`, `afraid`,
|
|
||||||
`disgusted`, `melancholic`, `surprised`, and `calm`; each value must be in
|
|
||||||
`[0, 1.2]`. `emo_alpha` controls the strength in `[0, 1]`.
|
|
||||||
|
|
||||||
Use an explicit emotion vector for deterministic control:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python examples/online_serving/text_to_speech/indextts2/speech_client.py \
|
|
||||||
--model-version 2.5 --model /path/to/indextts-2.5 \
|
|
||||||
--ref-audio /path/to/reference.wav --lang zh \
|
|
||||||
--text "今天真是令人开心的一天!" \
|
|
||||||
--emo-vector 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \
|
|
||||||
--emo-alpha 0.8 --output indextts2_5_happy_vector.wav
|
|
||||||
```
|
|
||||||
|
|
||||||
Infer the emotion from a natural-language description with `use_emo_text`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python examples/online_serving/text_to_speech/indextts2/speech_client.py \
|
|
||||||
--model-version 2.5 --model /path/to/indextts-2.5 \
|
|
||||||
--ref-audio /path/to/reference.wav --lang zh \
|
|
||||||
--text "我们终于完成了这项工作!" \
|
|
||||||
--use-emo-text --emo-text "开心、兴奋而且充满活力" \
|
|
||||||
--emo-alpha 0.8 --output indextts2_5_happy_text.wav
|
|
||||||
```
|
|
||||||
|
|
||||||
Transfer emotion from a recording while retaining the speaker identity from
|
|
||||||
`ref_audio`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python examples/online_serving/text_to_speech/indextts2/speech_client.py \
|
|
||||||
--model-version 2.5 --model /path/to/indextts-2.5 \
|
|
||||||
--ref-audio /path/to/reference.wav --lang zh \
|
|
||||||
--text "请用情绪参考音频中的表达方式朗读这句话。" \
|
|
||||||
--emo-audio /path/to/emotion_reference.wav \
|
|
||||||
--emo-alpha 0.8 --output indextts2_5_emotion_audio.wav
|
|
||||||
```
|
|
||||||
|
|
||||||
If multiple emotion sources are supplied, the official precedence is
|
|
||||||
`use_emo_text` > `emo_vector` > `emo_audio` > the emotion in the speaker
|
|
||||||
reference. `--use-random` is also available for exploratory random emotion
|
|
||||||
prototypes, but should not be used when reproducible conditioning is required.
|
|
||||||
|
|
||||||
For offline inference:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python examples/offline_inference/text_to_speech/indextts2/end2end.py \
|
|
||||||
--model /path/to/indextts-2.5 \
|
|
||||||
--model-version 2.5 \
|
|
||||||
--lang zh \
|
|
||||||
--speed 1.0 \
|
|
||||||
--text "你好,这是离线语音合成测试。" \
|
|
||||||
--ref-audio /path/to/reference.wav
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Notes
|
|
||||||
|
|
||||||
- Hardware scope: the standard recipe has been exercised on one NVIDIA H200.
|
|
||||||
It does not claim an unverified throughput or quality result.
|
|
||||||
- Audio output: 22.05 kHz mono WAV.
|
|
||||||
- Voice cloning: reference audio is required on the documented raw request
|
|
||||||
path. Alternatively, `voice` may name an uploaded audio voice; there is no
|
|
||||||
built-in text-only preset voice.
|
|
||||||
- Native speed: serving maps `speed` to the model's duration factor and skips
|
|
||||||
generic waveform speed adjustment, so speed is applied exactly once.
|
|
||||||
- Languages: common codes include `zh`, `en`, `zhen` (mixed Chinese/English),
|
|
||||||
`ja`, and `yue`; `Mandarin` is accepted as an alias for `zh`.
|
|
||||||
- Japanese normalization: `ja` tokenization does not automatically expand
|
|
||||||
numbers, dates, or percentages. Write these inputs as readable Japanese text
|
|
||||||
before inference.
|
|
||||||
- Emotion controls: `use_emo_text`, `emo_vector`, and `emo_audio` are
|
|
||||||
alternative conditioning modes. Their precedence is `use_emo_text` >
|
|
||||||
`emo_vector` > `emo_audio` > the speaker-reference emotion.
|
|
||||||
- Sampling difference: Stage 0 uses plain vLLM sampling, not the upstream
|
|
||||||
default `num_beams=3` beam search. Use upstream `num_beams=1` for parity
|
|
||||||
comparisons; output may differ from the official beam-search result.
|
|
||||||
@@ -1,198 +0,0 @@
|
|||||||
# MiniMax-H3 on RTX PRO 5000 Blackwell GPUs
|
|
||||||
|
|
||||||
This recipe runs MiniMax-H3 in BF16 on 72 GiB RTX PRO 5000 Blackwell GPUs. It
|
|
||||||
contains the validated two-GPU DLO configuration and the recommended resident
|
|
||||||
configurations: TP1 x Ulysses2 with 20 resident layers on two GPUs, TP2 x
|
|
||||||
Ulysses2 on four GPUs, and TP4 x Ulysses2 on eight GPUs. The four- and
|
|
||||||
eight-GPU routes do not require offload.
|
|
||||||
|
|
||||||
## Capacity requirements
|
|
||||||
|
|
||||||
| Resource | Requirement |
|
|
||||||
| --- | ---: |
|
|
||||||
| GPUs | 2, 4, or 8 x RTX PRO 5000 Blackwell |
|
|
||||||
| GPU HBM | 72 GiB per GPU |
|
|
||||||
| Checkpoint storage | 135 GiB per partition |
|
|
||||||
| Available system RAM | 200 GiB minimum |
|
|
||||||
| Recommended system RAM | 384 GiB |
|
|
||||||
|
|
||||||
`FL2VA` and `Ref2VA` are separate checkpoint partitions. Start one server at a
|
|
||||||
time on a host sized for the minimum system-memory requirement.
|
|
||||||
|
|
||||||
## PCIe topology and GPU order
|
|
||||||
|
|
||||||
RTX PRO 5000 does not provide NVLink. Before starting the server, identify the
|
|
||||||
closest PCIe pairs and their NUMA affinity:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
nvidia-smi topo -m
|
|
||||||
nvidia-smi nvlink -s
|
|
||||||
```
|
|
||||||
|
|
||||||
For two GPUs, select the closest PCIe pair on one NUMA node. For four GPUs,
|
|
||||||
select all cards from one NUMA node. On the validated host,
|
|
||||||
physical GPUs `(0,1)` and `(2,3)` are the two `PXB` pairs on NUMA node 0. The
|
|
||||||
order `CUDA_VISIBLE_DEVICES=0,2,1,3` maps the Ulysses groups to those local
|
|
||||||
pairs. For eight GPUs, the validated order is `0,4,1,5,2,6,3,7`, with host
|
|
||||||
memory interleaved across NUMA nodes 0 and 1. Do not copy these IDs blindly:
|
|
||||||
reproduce the same PCIe and NUMA relationships on the target host.
|
|
||||||
|
|
||||||
## Recommended serving configurations
|
|
||||||
|
|
||||||
### Two GPUs
|
|
||||||
|
|
||||||
Two 72 GiB cards require distributed layerwise offload. The validated route
|
|
||||||
uses TP1 x Ulysses2, keeps 20 leading DiT layers resident, and streams
|
|
||||||
rank-local weights without AllGather. Eager execution avoids regional-compile
|
|
||||||
instability on this offload path.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export MODEL_ROOT=/path/to/MiniMax-H3
|
|
||||||
export MODEL="${MODEL_ROOT}/FL2VA"
|
|
||||||
export PORT=8091
|
|
||||||
|
|
||||||
CUDA_VISIBLE_DEVICES=0,1 \
|
|
||||||
VLLM_WORKER_MULTIPROC_METHOD=spawn \
|
|
||||||
VLLM_OMNI_VIDEO_SYNC_TIMEOUT=1800 \
|
|
||||||
numactl --cpunodebind=0 --membind=0 \
|
|
||||||
vllm serve "${MODEL}" \
|
|
||||||
--omni \
|
|
||||||
--host 0.0.0.0 \
|
|
||||||
--port "${PORT}" \
|
|
||||||
--trust-remote-code \
|
|
||||||
--num-gpus 2 \
|
|
||||||
--tensor-parallel-size 1 \
|
|
||||||
--usp 2 \
|
|
||||||
--ring 1 \
|
|
||||||
--text-encoder-tp-size 2 \
|
|
||||||
--vae-patch-parallel-size 2 \
|
|
||||||
--vae-parallel-mode tile \
|
|
||||||
--vae-use-tiling \
|
|
||||||
--diffusion-attention-backend CUDNN_ATTN \
|
|
||||||
--enable-distributed-layerwise-offload \
|
|
||||||
--dlo-no-use-allgather \
|
|
||||||
--dlo-resident-layers 20 \
|
|
||||||
--enforce-eager
|
|
||||||
```
|
|
||||||
|
|
||||||
### Four GPUs
|
|
||||||
|
|
||||||
The validated baseline uses TP2 x Ulysses2, text-encoder TP4, VAE patch
|
|
||||||
parallelism 4, and explicit cuDNN BF16 attention. Selecting the backend
|
|
||||||
explicitly keeps the recipe independent of platform-default backend changes.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export MODEL_ROOT=/path/to/MiniMax-H3
|
|
||||||
export MODEL="${MODEL_ROOT}/FL2VA"
|
|
||||||
export PORT=8091
|
|
||||||
|
|
||||||
CUDA_VISIBLE_DEVICES=0,2,1,3 \
|
|
||||||
VLLM_WORKER_MULTIPROC_METHOD=spawn \
|
|
||||||
VLLM_OMNI_VIDEO_SYNC_TIMEOUT=1800 \
|
|
||||||
numactl --cpunodebind=0 --membind=0 \
|
|
||||||
vllm serve "${MODEL}" \
|
|
||||||
--omni \
|
|
||||||
--host 0.0.0.0 \
|
|
||||||
--port "${PORT}" \
|
|
||||||
--trust-remote-code \
|
|
||||||
--num-gpus 4 \
|
|
||||||
--tensor-parallel-size 2 \
|
|
||||||
--usp 2 \
|
|
||||||
--ring 1 \
|
|
||||||
--text-encoder-tp-size 4 \
|
|
||||||
--vae-patch-parallel-size 4 \
|
|
||||||
--vae-parallel-mode tile \
|
|
||||||
--vae-use-tiling \
|
|
||||||
--diffusion-attention-backend CUDNN_ATTN
|
|
||||||
```
|
|
||||||
|
|
||||||
### Eight GPUs
|
|
||||||
|
|
||||||
The recommended eight-GPU route uses TP4 x Ulysses2, text-encoder TP8, VAE
|
|
||||||
patch parallelism 8, and host-memory interleaving across both NUMA nodes.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export MODEL_ROOT=/path/to/MiniMax-H3
|
|
||||||
export MODEL="${MODEL_ROOT}/FL2VA"
|
|
||||||
export PORT=8091
|
|
||||||
|
|
||||||
CUDA_VISIBLE_DEVICES=0,4,1,5,2,6,3,7 \
|
|
||||||
VLLM_WORKER_MULTIPROC_METHOD=spawn \
|
|
||||||
VLLM_OMNI_VIDEO_SYNC_TIMEOUT=1800 \
|
|
||||||
numactl --interleave=0,1 \
|
|
||||||
vllm serve "${MODEL}" \
|
|
||||||
--omni \
|
|
||||||
--host 0.0.0.0 \
|
|
||||||
--port "${PORT}" \
|
|
||||||
--trust-remote-code \
|
|
||||||
--num-gpus 8 \
|
|
||||||
--tensor-parallel-size 4 \
|
|
||||||
--usp 2 \
|
|
||||||
--ring 1 \
|
|
||||||
--text-encoder-tp-size 8 \
|
|
||||||
--vae-patch-parallel-size 8 \
|
|
||||||
--vae-parallel-mode tile \
|
|
||||||
--vae-use-tiling \
|
|
||||||
--diffusion-attention-backend CUDNN_ATTN
|
|
||||||
```
|
|
||||||
|
|
||||||
For the four- and eight-GPU resident routes, do not add `--enforce-eager`.
|
|
||||||
Warm the server once before measuring so regional compilation is outside the
|
|
||||||
measured request. The two-GPU DLO route intentionally remains eager.
|
|
||||||
|
|
||||||
For Ref2VA, stop the FL2VA server and restart the same command with
|
|
||||||
`MODEL="${MODEL_ROOT}/Ref2VA"`.
|
|
||||||
|
|
||||||
## Target-hardware validation
|
|
||||||
|
|
||||||
All three configurations were exercised on a PCIe-only, dual-socket host with
|
|
||||||
eight RTX PRO 5000 GPUs. The run used PyTorch 2.11.0+cu130, CUDA 13.0, driver
|
|
||||||
580.95.05, 1344x768 output, 124 frames, and two warmups.
|
|
||||||
|
|
||||||
The four-GPU route also received a five-step profiling validation for GPU
|
|
||||||
balance:
|
|
||||||
|
|
||||||
| Measurement | Result |
|
|
||||||
| --- | ---: |
|
|
||||||
| Maximum externally sampled peak | 69,219 MiB (67.60 GiB) per GPU |
|
|
||||||
| T2VA worker-reported peak | 65,314 MiB |
|
|
||||||
| First-frame FL2VA worker-reported peak | 66,780 MiB |
|
|
||||||
| T2VA maximum GPU kernel-time deviation | 0.56% |
|
|
||||||
| First-frame FL2VA maximum GPU kernel-time deviation | 0.09% |
|
|
||||||
|
|
||||||
The recommended routes produced the following 50-step results. MiniMax-H3
|
|
||||||
requested 50 denoise steps and executed 49 denoise updates, so per-step latency
|
|
||||||
is `denoise / 49`.
|
|
||||||
|
|
||||||
| GPUs | Workload | Parallelism | E2E (s) | Text encode (s) | Denoise (s) | VAE decode (s) | Per step (ms) | Peak memory (GiB) |
|
|
||||||
| ---: | --- | --- | ---: | ---: | ---: | ---: | ---: | ---: |
|
|
||||||
| 2 | T2VA | TP1 x Ulysses2, DLO resident 20 | 515.57 | 1.19 | 504.69 | 8.79 | 10,300 | 36.38 |
|
|
||||||
| 2 | FL2VA first-frame I2VA | TP1 x Ulysses2, DLO resident 20 | 553.45 | 1.27 | 541.90 | 8.76 | 11,059 | 36.38 |
|
|
||||||
| 4 | T2VA | TP2 x Ulysses2 | 284.01 | 0.03 | 278.73 | 4.36 | 5,688 | 67.58 |
|
|
||||||
| 4 | FL2VA first-frame I2VA | TP2 x Ulysses2 | 305.72 | 0.28 | 299.85 | 4.36 | 6,119 | 67.58 |
|
|
||||||
| 8 | T2VA | TP4 x Ulysses2 | 163.20 | 0.04 | 159.40 | 2.58 | 3,253 | 45.83 |
|
|
||||||
| 8 | FL2VA first-frame I2VA | TP4 x Ulysses2 | 171.62 | 0.27 | 167.25 | 2.57 | 3,413 | 45.83 |
|
|
||||||
|
|
||||||
Peak memory is the maximum per-GPU value sampled externally with
|
|
||||||
`nvidia-smi`. The two-GPU route leaves about 34.7 GiB below the reported
|
|
||||||
73,415 MiB device capacity, the four-GPU route leaves about 4.1 GiB, and the
|
|
||||||
eight-GPU route leaves about 25.9 GiB. Re-measure memory for longer reference
|
|
||||||
inputs, concurrency greater than one, or a different output shape.
|
|
||||||
|
|
||||||
## T2VA request example
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export API_URL="http://127.0.0.1:${PORT}/v1/videos/sync"
|
|
||||||
|
|
||||||
curl -sS --max-time 1800 -X POST "${API_URL}" \
|
|
||||||
-F 'prompt=At night, three cats march into a bedroom playing tiny brass instruments, then abruptly file out, with synchronized room ambience.' \
|
|
||||||
-F 'width=1344' \
|
|
||||||
-F 'height=768' \
|
|
||||||
-F 'aspect_ratio=16:9' \
|
|
||||||
-F 'fps=24' \
|
|
||||||
-F 'num_inference_steps=50' \
|
|
||||||
-F 'flow_shift=12' \
|
|
||||||
-F 'seed=1101' \
|
|
||||||
-F 'extra_params={"task":"t2va","duration":5.0,"audio_flow_shift":3.0}' \
|
|
||||||
-o t2va.mp4
|
|
||||||
```
|
|
||||||
@@ -50,7 +50,6 @@ recipes/
|
|||||||
| [`inclusionAI/Ming-flash-omni-2.0.md`](./inclusionAI/Ming-flash-omni-2.0.md) | Online serving for multimodal chat + standalone TTS | 4x H100 / 1x H100 80GB |
|
| [`inclusionAI/Ming-flash-omni-2.0.md`](./inclusionAI/Ming-flash-omni-2.0.md) | Online serving for multimodal chat + standalone TTS | 4x H100 / 1x H100 80GB |
|
||||||
| [`inclusionAI/Ming-omni-tts.md`](./inclusionAI/Ming-omni-tts.md) | Offline + online dense Ming TTS/audio generation | 1x H100 80GB / 1x AMD MI300X (ROCm 7.2) |
|
| [`inclusionAI/Ming-omni-tts.md`](./inclusionAI/Ming-omni-tts.md) | Offline + online dense Ming TTS/audio generation | 1x H100 80GB / 1x AMD MI300X (ROCm 7.2) |
|
||||||
| [`IndexTeam/IndexTTS-2.md`](./IndexTeam/IndexTTS-2.md) | Online serving for voice-cloned TTS with optional emotion control | 1x L4 24GB or larger CUDA GPU |
|
| [`IndexTeam/IndexTTS-2.md`](./IndexTeam/IndexTTS-2.md) | Online serving for voice-cloned TTS with optional emotion control | 1x L4 24GB or larger CUDA GPU |
|
||||||
| [`IndexTeam/IndexTTS-2_5.md`](./IndexTeam/IndexTTS-2_5.md) | Online + offline multilingual voice-cloned TTS with native speed and emotion control | 1x NVIDIA H20 96GB |
|
|
||||||
| [`MiniMaxAI/MiniMax-H3.md`](./MiniMaxAI/MiniMax-H3.md) | T2VA, FL2VA, image+audio Ref2VA, and multi-video Ref2VA serving | 1x GPU with CPU offload / 4x B300 |
|
| [`MiniMaxAI/MiniMax-H3.md`](./MiniMaxAI/MiniMax-H3.md) | T2VA, FL2VA, image+audio Ref2VA, and multi-video Ref2VA serving | 1x GPU with CPU offload / 4x B300 |
|
||||||
| [`MiniMaxAI/MiniMax-H3-MUSA.md`](./MiniMaxAI/MiniMax-H3-MUSA.md) | MiniMax H3 T2VA, FL2VA, and Ref2VA on MUSA | 4x MTT S5000 (TP4 Ref2VA profile) |
|
| [`MiniMaxAI/MiniMax-H3-MUSA.md`](./MiniMaxAI/MiniMax-H3-MUSA.md) | MiniMax H3 T2VA, FL2VA, and Ref2VA on MUSA | 4x MTT S5000 (TP4 Ref2VA profile) |
|
||||||
| [`krea/Krea-2.md`](./krea/Krea-2.md) | Text-to-image (Turbo + Raw), offline + online, with LoRA | 1x H100 80GB |
|
| [`krea/Krea-2.md`](./krea/Krea-2.md) | Text-to-image (Turbo + Raw), offline + online, with LoRA | 1x H100 80GB |
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ example and the DiT sections below.
|
|||||||
- Optional environment variables:
|
- Optional environment variables:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export CACHE_DIT_VERSION=1.3.0
|
export CACHE_DIT_VERSION=1.5.0
|
||||||
```
|
```
|
||||||
|
|
||||||
HunyuanImage-3.0 sets the diffusion attention backend to `TORCH_SDPA`
|
HunyuanImage-3.0 sets the diffusion attention backend to `TORCH_SDPA`
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ diffusers==0.38.0
|
|||||||
safetensors>=0.8.0
|
safetensors>=0.8.0
|
||||||
accelerate==1.12.0
|
accelerate==1.12.0
|
||||||
soundfile>=0.13.1
|
soundfile>=0.13.1
|
||||||
cache-dit==1.3.0
|
cache-dit==1.5.0
|
||||||
tqdm>=4.66.0
|
tqdm>=4.66.0
|
||||||
torchsde>=0.2.6
|
torchsde>=0.2.6
|
||||||
openai-whisper>=20250625
|
openai-whisper>=20250625
|
||||||
|
|||||||
@@ -51,14 +51,6 @@ def test_load_model_configs(model_configs_path: Path) -> None:
|
|||||||
assert configs["test/ModelA"]["supported_tasks"] == ["voice_clone", "default_voice"]
|
assert configs["test/ModelA"]["supported_tasks"] == ["voice_clone", "default_voice"]
|
||||||
|
|
||||||
|
|
||||||
def test_indextts25_is_registered_in_shared_model_configs() -> None:
|
|
||||||
config_path = Path(bench_tts.__file__).with_name("model_configs.yaml")
|
|
||||||
config = bench_tts.load_model_configs(config_path)["IndexTeam/IndexTTS-2.5"]
|
|
||||||
|
|
||||||
assert config["supported_tasks"] == ["voice_clone"]
|
|
||||||
assert config["task_extra_body"]["voice_clone"]["extra_params"]["lang"] == "en"
|
|
||||||
|
|
||||||
|
|
||||||
def test_build_bench_args_voice_clone(model_configs_path: Path) -> None:
|
def test_build_bench_args_voice_clone(model_configs_path: Path) -> None:
|
||||||
configs = bench_tts.load_model_configs(model_configs_path)
|
configs = bench_tts.load_model_configs(model_configs_path)
|
||||||
cmd = bench_tts.build_bench_args(
|
cmd = bench_tts.build_bench_args(
|
||||||
@@ -128,34 +120,6 @@ def test_build_bench_args_wer_eval_adds_flag(model_configs_path: Path) -> None:
|
|||||||
assert "--seed-tts-wer-eval" in cmd
|
assert "--seed-tts-wer-eval" in cmd
|
||||||
|
|
||||||
|
|
||||||
def test_build_bench_args_supports_local_model_and_shared_sweep_options(model_configs_path: Path) -> None:
|
|
||||||
configs = bench_tts.load_model_configs(model_configs_path)
|
|
||||||
cmd = bench_tts.build_bench_args(
|
|
||||||
host="localhost",
|
|
||||||
port=8092,
|
|
||||||
model="test/ModelA",
|
|
||||||
served_model_name="/models/indextts25",
|
|
||||||
task="voice_clone",
|
|
||||||
model_cfg=configs["test/ModelA"],
|
|
||||||
locale="en",
|
|
||||||
num_prompts=500,
|
|
||||||
num_warmups=5,
|
|
||||||
request_seed=42,
|
|
||||||
concurrency=8,
|
|
||||||
dataset_path="/data/seed-tts",
|
|
||||||
wer_eval=False,
|
|
||||||
output_dir=None,
|
|
||||||
result_filename=None,
|
|
||||||
extra_cli_args=["--", "--tokenizer", "/models/indextts25/qwen0.6bemo4-merge"],
|
|
||||||
)
|
|
||||||
|
|
||||||
assert cmd[cmd.index("--model") + 1] == "/models/indextts25"
|
|
||||||
assert cmd[cmd.index("--num-warmups") + 1] == "5"
|
|
||||||
assert cmd[-2:] == ["--tokenizer", "/models/indextts25/qwen0.6bemo4-merge"]
|
|
||||||
extra_body = json.loads(cmd[cmd.index("--extra-body") + 1])
|
|
||||||
assert extra_body == {"task_type": "Base", "seed": 42}
|
|
||||||
|
|
||||||
|
|
||||||
def test_unsupported_task_exits(model_configs_path: Path, capsys: pytest.CaptureFixture, mocker) -> None:
|
def test_unsupported_task_exits(model_configs_path: Path, capsys: pytest.CaptureFixture, mocker) -> None:
|
||||||
# ModelB does not support voice_design
|
# ModelB does not support voice_design
|
||||||
mocker.patch.object(
|
mocker.patch.object(
|
||||||
|
|||||||
@@ -2276,16 +2276,6 @@ class TestPlatformOverrides:
|
|||||||
# Stage 2 unaffected fields stay at base
|
# Stage 2 unaffected fields stay at base
|
||||||
assert deploy.stages[2].enforce_eager is False
|
assert deploy.stages[2].enforce_eager is False
|
||||||
|
|
||||||
def test_qwen2_5_omni_xpu_uses_eager_ar_stages(self):
|
|
||||||
deploy_path = Path(get_deploy_config_path("qwen2_5_omni.yaml"))
|
|
||||||
|
|
||||||
deploy = load_deploy_config(deploy_path)
|
|
||||||
deploy = _apply_platform_overrides(deploy, platform="xpu")
|
|
||||||
|
|
||||||
assert deploy.stages[0].enforce_eager is True
|
|
||||||
assert deploy.stages[1].enforce_eager is True
|
|
||||||
assert deploy.stages[2].enforce_eager is True
|
|
||||||
|
|
||||||
def test_xpu_overrides(self):
|
def test_xpu_overrides(self):
|
||||||
deploy_path = Path(get_deploy_config_path("qwen3_omni_moe.yaml"))
|
deploy_path = Path(get_deploy_config_path("qwen3_omni_moe.yaml"))
|
||||||
if not deploy_path.exists():
|
if not deploy_path.exists():
|
||||||
|
|||||||
@@ -92,9 +92,6 @@ class _Request:
|
|||||||
self.num_computed_tokens = 0
|
self.num_computed_tokens = 0
|
||||||
self.num_in_flight_tokens = 0
|
self.num_in_flight_tokens = 0
|
||||||
self.num_output_placeholders = 0
|
self.num_output_placeholders = 0
|
||||||
# vLLM 0.27 (a0c092ee72): Request gained num_stale_output_tokens to
|
|
||||||
# track in-flight outputs discarded at preemption/streaming-stop.
|
|
||||||
self.num_stale_output_tokens = 0
|
|
||||||
self.has_encoder_inputs = False
|
self.has_encoder_inputs = False
|
||||||
self.pooling_params = None
|
self.pooling_params = None
|
||||||
self.resumable = True
|
self.resumable = True
|
||||||
|
|||||||
@@ -1,223 +0,0 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
||||||
|
|
||||||
"""Investigation tests for the num_stale_output_tokens drain (59b9e719).
|
|
||||||
|
|
||||||
The seed-TTS WER regression appeared with the vLLM 0.27 rebase (nightly
|
|
||||||
2946 -> 2949: mean WER 0.2695 -> 0.3936, median 0.0 in both), whose prime
|
|
||||||
suspect is the reimplemented async-token discard: vLLM 0.26 drained
|
|
||||||
``async_tokens_to_discard`` one arriving frame at a time and suppressed
|
|
||||||
only the token append; the 0.27 adaptation seeds
|
|
||||||
``num_stale_output_tokens`` with the discarded *placeholder count* and
|
|
||||||
drains it by each arriving frame's ``num_tokens_scheduled``, dropping the
|
|
||||||
whole per-request update. Whenever seed and arrivals disagree, the counter
|
|
||||||
outlives the old segment and the drain consumes *valid* frames of the new
|
|
||||||
segment — a hole in the talker's codec stream (garbled audio, text/audio
|
|
||||||
mismatch) — or underflows its own assert on the new segment's prefill
|
|
||||||
frame (engine-core death mid-step).
|
|
||||||
|
|
||||||
The fix seeds the counter from ``num_in_flight_tokens`` — the same
|
|
||||||
scheduled-token units the drain subtracts — so every pre-discard frame
|
|
||||||
drains exactly its own contribution and new-segment frames can never be
|
|
||||||
swallowed. These tests pin that contract one frame at a time; the
|
|
||||||
delivered / swallowed observable is whether ``_update_request_with_output``
|
|
||||||
runs for the frame.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from types import SimpleNamespace
|
|
||||||
from unittest.mock import MagicMock
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
# Imports must run in this order: vllm_omni applies patches to vllm.v1.request
|
|
||||||
# before Request / StreamingUpdate are bound in this module.
|
|
||||||
# isort: off
|
|
||||||
import vllm_omni # noqa: F401 - import for side effects (patch vLLM)
|
|
||||||
from vllm.sampling_params import SamplingParams
|
|
||||||
from vllm.v1.core.sched.output import SchedulerOutput
|
|
||||||
from vllm.v1.outputs import ModelRunnerOutput
|
|
||||||
from vllm.v1.request import Request, RequestStatus, StreamingUpdate
|
|
||||||
from vllm_omni.core.sched.omni_ar_scheduler import OmniARScheduler
|
|
||||||
|
|
||||||
# isort: on
|
|
||||||
|
|
||||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
|
||||||
|
|
||||||
|
|
||||||
def _make_session() -> Request:
|
|
||||||
session = Request(
|
|
||||||
request_id="req-stale-drain-test",
|
|
||||||
prompt_token_ids=[1, 2, 3],
|
|
||||||
sampling_params=SamplingParams(max_tokens=32),
|
|
||||||
pooling_params=None,
|
|
||||||
arrival_time=100.0,
|
|
||||||
block_hasher=None,
|
|
||||||
)
|
|
||||||
session.status = RequestStatus.RUNNING
|
|
||||||
if not hasattr(session, "num_stale_output_tokens"):
|
|
||||||
# vLLM 0.27 defines this Request field; 0.26 (local dev) does not.
|
|
||||||
session.num_stale_output_tokens = 0
|
|
||||||
if not hasattr(session, "num_in_flight_tokens"):
|
|
||||||
session.num_in_flight_tokens = 0
|
|
||||||
return session
|
|
||||||
|
|
||||||
|
|
||||||
def _replace_streaming_session(session: Request) -> None:
|
|
||||||
"""Run the real stage-0 streaming replacement (the discard/seed site)."""
|
|
||||||
sched = OmniARScheduler.__new__(OmniARScheduler)
|
|
||||||
sched._new_prompt_len_snapshot = {}
|
|
||||||
sched.vllm_config = SimpleNamespace(model_config=SimpleNamespace(stage_id=0))
|
|
||||||
sched.num_waiting_for_streaming_input = 0
|
|
||||||
sched.log_stats = False
|
|
||||||
sched.chunk_transfer_adapter = None
|
|
||||||
sched.skipped_waiting = set()
|
|
||||||
session.status = RequestStatus.WAITING_FOR_STREAMING_REQ
|
|
||||||
update = StreamingUpdate(
|
|
||||||
mm_features=None,
|
|
||||||
prompt_token_ids=[10, 20],
|
|
||||||
max_tokens=32,
|
|
||||||
arrival_time=200.0,
|
|
||||||
sampling_params=SamplingParams(max_tokens=16),
|
|
||||||
)
|
|
||||||
sched._update_request_as_session(session, update)
|
|
||||||
session.status = RequestStatus.RUNNING
|
|
||||||
|
|
||||||
|
|
||||||
def _make_drain_sched(session: Request) -> MagicMock:
|
|
||||||
sched = MagicMock()
|
|
||||||
sched.requests = {session.request_id: session}
|
|
||||||
sched.perf_metrics = None
|
|
||||||
sched.structured_output_manager.should_advance.return_value = False
|
|
||||||
sched._update_request_with_output.return_value = ([42], False)
|
|
||||||
sched._process_kv_transfer_trigger.return_value = False
|
|
||||||
sched.chunk_transfer_adapter = MagicMock()
|
|
||||||
sched.running = [session]
|
|
||||||
sched.waiting_for_transfer_free = set()
|
|
||||||
sched.transfer_triggered_requests = set()
|
|
||||||
sched.active_kv_transfers = set()
|
|
||||||
sched.pending_stop_after_extraction = set()
|
|
||||||
sched.connector = None
|
|
||||||
sched.kv_cache_manager.take_events.return_value = None
|
|
||||||
sched.finished_req_ids_dict = {}
|
|
||||||
sched.make_stats.return_value = None
|
|
||||||
return sched
|
|
||||||
|
|
||||||
|
|
||||||
def _run_step(sched: MagicMock, session: Request, *, num_scheduled: int, token: int) -> bool:
|
|
||||||
"""Feed one model-runner frame through update_from_output.
|
|
||||||
|
|
||||||
Returns True when the frame's tokens were delivered (the append ran),
|
|
||||||
False when the frame was swallowed by the stale drain.
|
|
||||||
"""
|
|
||||||
scheduler_output = MagicMock(spec=SchedulerOutput)
|
|
||||||
scheduler_output.num_scheduled_tokens = {session.request_id: num_scheduled}
|
|
||||||
scheduler_output.scheduled_spec_decode_tokens = {}
|
|
||||||
scheduler_output.num_invalid_spec_tokens = 0
|
|
||||||
|
|
||||||
model_runner_output = MagicMock(spec=ModelRunnerOutput)
|
|
||||||
model_runner_output.sampled_token_ids = [[token]]
|
|
||||||
model_runner_output.logprobs = None
|
|
||||||
model_runner_output.prompt_logprobs_dict = {}
|
|
||||||
model_runner_output.pooler_output = None
|
|
||||||
model_runner_output.num_nans_in_logits = None
|
|
||||||
model_runner_output.kv_connector_output = None
|
|
||||||
model_runner_output.cudagraph_stats = None
|
|
||||||
model_runner_output.req_id_to_index = {session.request_id: 0}
|
|
||||||
model_runner_output.routed_experts = None
|
|
||||||
model_runner_output.inter_stage_outputs = None
|
|
||||||
|
|
||||||
sched._update_request_with_output.reset_mock()
|
|
||||||
OmniARScheduler.update_from_output(sched, scheduler_output, model_runner_output)
|
|
||||||
return sched._update_request_with_output.called
|
|
||||||
|
|
||||||
|
|
||||||
def test_exact_drain_delivers_new_segment_frame() -> None:
|
|
||||||
"""One in-flight decode frame at replacement: its late output drains the
|
|
||||||
counter exactly and the new segment's first frame is delivered."""
|
|
||||||
session = _make_session()
|
|
||||||
session.append_output_token_ids([7, 8, 9])
|
|
||||||
session.num_computed_tokens = 6
|
|
||||||
session.num_output_placeholders = 1
|
|
||||||
session.num_in_flight_tokens = 1
|
|
||||||
|
|
||||||
_replace_streaming_session(session)
|
|
||||||
assert session.num_stale_output_tokens == 1
|
|
||||||
|
|
||||||
sched = _make_drain_sched(session)
|
|
||||||
assert _run_step(sched, session, num_scheduled=1, token=42) is False # late frame dropped
|
|
||||||
assert session.num_stale_output_tokens == 0
|
|
||||||
assert _run_step(sched, session, num_scheduled=1, token=43) is True # new segment survives
|
|
||||||
|
|
||||||
|
|
||||||
def test_two_in_flight_frames_drain_without_swallowing() -> None:
|
|
||||||
"""Two in-flight decode frames: both late frames drop, the new segment's
|
|
||||||
first valid frame is delivered. A placeholder-based seed swallowed it
|
|
||||||
whenever fewer frames arrived than placeholders counted (the driver of
|
|
||||||
the seed-TTS WER regression, nightly 2946 -> 2949: 0.2695 -> 0.3936)."""
|
|
||||||
session = _make_session()
|
|
||||||
session.append_output_token_ids([7, 8, 9])
|
|
||||||
session.num_computed_tokens = 7
|
|
||||||
session.num_output_placeholders = 2
|
|
||||||
session.num_in_flight_tokens = 2
|
|
||||||
|
|
||||||
_replace_streaming_session(session)
|
|
||||||
assert session.num_stale_output_tokens == 2
|
|
||||||
|
|
||||||
sched = _make_drain_sched(session)
|
|
||||||
assert _run_step(sched, session, num_scheduled=1, token=42) is False # late frame 1
|
|
||||||
assert _run_step(sched, session, num_scheduled=1, token=44) is False # late frame 2
|
|
||||||
assert session.num_stale_output_tokens == 0
|
|
||||||
|
|
||||||
delivered = _run_step(sched, session, num_scheduled=1, token=43) # new segment, frame 1
|
|
||||||
assert delivered is True, (
|
|
||||||
"valid new-segment frame was swallowed by leftover stale count "
|
|
||||||
f"(num_stale_output_tokens left at {session.num_stale_output_tokens})"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_spec_placeholder_asymmetry_does_not_swallow_valid_frame() -> None:
|
|
||||||
"""Placeholder counts can exceed scheduled counts (e.g. a draft
|
|
||||||
invalidated before execution): 1 in-flight MTP frame scheduled 2 tokens
|
|
||||||
but contributed 3 placeholders. Seeding from num_in_flight_tokens keeps
|
|
||||||
seed == drain, so the new segment's valid frame is delivered."""
|
|
||||||
session = _make_session()
|
|
||||||
session.append_output_token_ids([7])
|
|
||||||
session.num_computed_tokens = 7
|
|
||||||
session.num_output_placeholders = 3 # 1 sampled + 2 spec placeholders
|
|
||||||
session.num_in_flight_tokens = 2 # the frame actually scheduled 1 + 1 tokens
|
|
||||||
|
|
||||||
_replace_streaming_session(session)
|
|
||||||
assert session.num_stale_output_tokens == 2
|
|
||||||
|
|
||||||
sched = _make_drain_sched(session)
|
|
||||||
assert _run_step(sched, session, num_scheduled=2, token=42) is False # late MTP frame
|
|
||||||
assert session.num_stale_output_tokens == 0
|
|
||||||
delivered = _run_step(sched, session, num_scheduled=1, token=43) # new segment, frame 1
|
|
||||||
assert delivered is True, (
|
|
||||||
"valid new-segment frame swallowed after spec placeholder asymmetry "
|
|
||||||
f"(num_stale_output_tokens left at {session.num_stale_output_tokens})"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_in_flight_prefill_chunk_drains_exactly_without_underflow() -> None:
|
|
||||||
"""An in-flight prefill chunk carries no placeholders but 5 scheduled
|
|
||||||
tokens. The placeholder-based seed missed it entirely (its late output
|
|
||||||
was appended to the rolled-back session) and any leftover counter
|
|
||||||
underflowed 'assert num_stale_output_tokens >= 0' on the chunk's arrival,
|
|
||||||
killing the engine-core step. Seeding from num_in_flight_tokens drops the
|
|
||||||
late chunk and drains to exactly zero."""
|
|
||||||
session = _make_session()
|
|
||||||
session.append_output_token_ids([7, 8, 9])
|
|
||||||
session.num_computed_tokens = 7
|
|
||||||
session.num_output_placeholders = 0 # prefill chunk: no placeholders
|
|
||||||
session.num_in_flight_tokens = 5 # the chunk's scheduled tokens
|
|
||||||
|
|
||||||
_replace_streaming_session(session)
|
|
||||||
assert session.num_stale_output_tokens == 5
|
|
||||||
|
|
||||||
sched = _make_drain_sched(session)
|
|
||||||
assert _run_step(sched, session, num_scheduled=5, token=42) is False # late prefill chunk dropped
|
|
||||||
assert session.num_stale_output_tokens == 0
|
|
||||||
assert _run_step(sched, session, num_scheduled=1, token=43) is True # new segment survives
|
|
||||||
@@ -126,7 +126,6 @@ class TestFullPayloadCoordinatorSelection(unittest.TestCase):
|
|||||||
("AudexCode2Wav", "audex_code2wav"),
|
("AudexCode2Wav", "audex_code2wav"),
|
||||||
("AudexXCodec1", "audex_xcodec"),
|
("AudexXCodec1", "audex_xcodec"),
|
||||||
("IndexTTS2S2MelDecoder", "indextts2_s2mel_decoder"),
|
("IndexTTS2S2MelDecoder", "indextts2_s2mel_decoder"),
|
||||||
("IndexTTS25S2MelDecoder", "indextts2_5_s2mel_decoder"),
|
|
||||||
("DyninOmniForConditionalGeneration", "token2image"),
|
("DyninOmniForConditionalGeneration", "token2image"),
|
||||||
("DyninOmniForConditionalGeneration", "token2audio"),
|
("DyninOmniForConditionalGeneration", "token2audio"),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,10 +17,6 @@ from vllm_omni.diffusion.data import (
|
|||||||
OmniDiffusionConfig,
|
OmniDiffusionConfig,
|
||||||
)
|
)
|
||||||
from vllm_omni.diffusion.models.diffusers_adapter import DiffusersAdapterPipeline
|
from vllm_omni.diffusion.models.diffusers_adapter import DiffusersAdapterPipeline
|
||||||
from vllm_omni.diffusion.models.diffusers_adapter.pipeline_utils import (
|
|
||||||
LTX25_DISTILLED_SIGMAS,
|
|
||||||
get_pipeline_utils,
|
|
||||||
)
|
|
||||||
from vllm_omni.diffusion.request import OmniDiffusionRequest
|
from vllm_omni.diffusion.request import OmniDiffusionRequest
|
||||||
from vllm_omni.diffusion.worker.request_batch import DiffusionRequestBatch
|
from vllm_omni.diffusion.worker.request_batch import DiffusionRequestBatch
|
||||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||||
@@ -184,32 +180,6 @@ class TestPipelineArgumentsHandling:
|
|||||||
with pytest.raises(NotImplementedError):
|
with pytest.raises(NotImplementedError):
|
||||||
DiffusersAdapterPipeline(od_config=od_config)
|
DiffusersAdapterPipeline(od_config=od_config)
|
||||||
|
|
||||||
def test_ltx25_original_repo_rejected_before_loading(self, mocker):
|
|
||||||
mock_from_pretrained = mocker.patch(
|
|
||||||
"vllm_omni.diffusion.models.diffusers_adapter.pipeline_diffusers_adapter.DiffusionPipeline.from_pretrained"
|
|
||||||
)
|
|
||||||
adapter = DiffusersAdapterPipeline(od_config=_make_od_config(model="Lightricks/LTX-2.5"))
|
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="Lightricks/LTX-2.5-Diffusers"):
|
|
||||||
adapter.load_weights()
|
|
||||||
|
|
||||||
mock_from_pretrained.assert_not_called()
|
|
||||||
|
|
||||||
def test_ltx25_requires_upstream_diffusers_feature(self, mocker):
|
|
||||||
mocker.patch(
|
|
||||||
"vllm_omni.diffusion.models.diffusers_adapter.pipeline_utils._supports_ltx25_diffusers",
|
|
||||||
return_value=False,
|
|
||||||
)
|
|
||||||
mock_from_pretrained = mocker.patch(
|
|
||||||
"vllm_omni.diffusion.models.diffusers_adapter.pipeline_diffusers_adapter.DiffusionPipeline.from_pretrained"
|
|
||||||
)
|
|
||||||
adapter = DiffusersAdapterPipeline(od_config=_make_od_config(model="Lightricks/LTX-2.5-Diffusers"))
|
|
||||||
|
|
||||||
with pytest.raises(ImportError, match="7564fb016dabda0c943416190fc92398c50b1b20"):
|
|
||||||
adapter.load_weights()
|
|
||||||
|
|
||||||
mock_from_pretrained.assert_not_called()
|
|
||||||
|
|
||||||
def test_adapter_load_weights_injects_supported_quantization_config(self, mocker):
|
def test_adapter_load_weights_injects_supported_quantization_config(self, mocker):
|
||||||
(
|
(
|
||||||
FakePipelineQuantizationConfig,
|
FakePipelineQuantizationConfig,
|
||||||
@@ -404,28 +374,6 @@ class TestPipelineArgumentsHandling:
|
|||||||
assert isinstance(output, DiffusionOutput)
|
assert isinstance(output, DiffusionOutput)
|
||||||
assert output.output == raw_output
|
assert output.output == raw_output
|
||||||
|
|
||||||
def test_ltx25_preserves_video_and_singular_audio_output(self, tmp_path):
|
|
||||||
frames = torch.zeros(1, 2, 3, 4, 3)
|
|
||||||
audio = torch.zeros(1, 48000)
|
|
||||||
(tmp_path / "model_index.json").write_text(
|
|
||||||
'{"_class_name":"LTX2Pipeline","duration_head":["diffusers","LTX2DurationHead"]}',
|
|
||||||
encoding="utf-8",
|
|
||||||
)
|
|
||||||
od_config = _make_od_config(model=str(tmp_path))
|
|
||||||
adapter = DiffusersAdapterPipeline(od_config=od_config)
|
|
||||||
adapter._pipeline = SimpleNamespace(vocoder=SimpleNamespace(config=SimpleNamespace(output_sampling_rate=48000)))
|
|
||||||
adapter._pipeline_utils = get_pipeline_utils("LTX2Pipeline")
|
|
||||||
|
|
||||||
output = adapter._wrap_output(SimpleNamespace(frames=frames, audio=audio))
|
|
||||||
|
|
||||||
assert isinstance(output.output, dict)
|
|
||||||
assert output.output["payload"]["video"] is frames
|
|
||||||
assert output.output["payload"]["audio"] is audio
|
|
||||||
assert output.output["metadata"] == {"audio": {"sample_rate": 48000}}
|
|
||||||
|
|
||||||
frames_only = adapter._wrap_output(SimpleNamespace(frames=frames, audio=None))
|
|
||||||
assert frames_only.output is frames
|
|
||||||
|
|
||||||
def test_adapter_build_call_kwargs(self, mocker):
|
def test_adapter_build_call_kwargs(self, mocker):
|
||||||
class MockPipeline:
|
class MockPipeline:
|
||||||
def __call__(
|
def __call__(
|
||||||
@@ -681,97 +629,6 @@ class TestPipelineArgumentsHandling:
|
|||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
pipeline.forward(DiffusionRequestBatch(requests=[problematic_request]))
|
pipeline.forward(DiffusionRequestBatch(requests=[problematic_request]))
|
||||||
|
|
||||||
def test_ltx25_uses_distilled_sampling_defaults_from_renamed_local_directory(self, mocker, tmp_path):
|
|
||||||
class LTX2Pipeline:
|
|
||||||
def __call__(
|
|
||||||
self,
|
|
||||||
prompt=None,
|
|
||||||
num_inference_steps=None,
|
|
||||||
sigmas=None,
|
|
||||||
timesteps=None,
|
|
||||||
guidance_scale=None,
|
|
||||||
audio_guidance_scale=None,
|
|
||||||
stg_scale=None,
|
|
||||||
audio_stg_scale=None,
|
|
||||||
modality_scale=None,
|
|
||||||
audio_modality_scale=None,
|
|
||||||
guidance_rescale=None,
|
|
||||||
audio_guidance_rescale=None,
|
|
||||||
num_frames=None,
|
|
||||||
frame_rate=None,
|
|
||||||
output_type=None,
|
|
||||||
generator=None,
|
|
||||||
):
|
|
||||||
return None
|
|
||||||
|
|
||||||
def to(self, device):
|
|
||||||
return self
|
|
||||||
|
|
||||||
# Local model directories do not retain their Hub repository name.
|
|
||||||
# Detect 2.5 from its pipeline component metadata instead.
|
|
||||||
(tmp_path / "model_index.json").write_text(
|
|
||||||
'{"_class_name":"LTX2Pipeline","duration_head":["diffusers","LTX2DurationHead"]}',
|
|
||||||
encoding="utf-8",
|
|
||||||
)
|
|
||||||
enable_tiling = mocker.Mock()
|
|
||||||
loaded_pipeline = LTX2Pipeline()
|
|
||||||
loaded_pipeline.vae = SimpleNamespace(enable_tiling=enable_tiling)
|
|
||||||
|
|
||||||
mocker.patch(
|
|
||||||
"vllm_omni.diffusion.models.diffusers_adapter.pipeline_utils._supports_ltx25_diffusers",
|
|
||||||
return_value=True,
|
|
||||||
)
|
|
||||||
mocker.patch(
|
|
||||||
"vllm_omni.diffusion.models.diffusers_adapter.pipeline_diffusers_adapter.DiffusionPipeline.from_pretrained",
|
|
||||||
return_value=loaded_pipeline,
|
|
||||||
)
|
|
||||||
adapter = DiffusersAdapterPipeline(
|
|
||||||
od_config=_make_od_config(
|
|
||||||
model=str(tmp_path),
|
|
||||||
vae_use_tiling=True,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
adapter.load_weights()
|
|
||||||
enable_tiling.assert_called_once_with()
|
|
||||||
|
|
||||||
sampling = OmniDiffusionSamplingParams(
|
|
||||||
num_frames=121,
|
|
||||||
frame_rate=24.0,
|
|
||||||
seed=42,
|
|
||||||
generator_device="cpu",
|
|
||||||
)
|
|
||||||
kwargs = adapter._build_call_kwargs(DiffusionRequestBatch(requests=[_make_request(sampling_params=sampling)]))
|
|
||||||
|
|
||||||
assert kwargs["sigmas"] == LTX25_DISTILLED_SIGMAS
|
|
||||||
assert kwargs["num_inference_steps"] == 8
|
|
||||||
assert kwargs["guidance_scale"] == 1.0
|
|
||||||
assert kwargs["audio_guidance_scale"] == 1.0
|
|
||||||
assert kwargs["stg_scale"] == 0.0
|
|
||||||
assert kwargs["audio_stg_scale"] == 0.0
|
|
||||||
assert kwargs["modality_scale"] == 1.0
|
|
||||||
assert kwargs["audio_modality_scale"] == 1.0
|
|
||||||
assert kwargs["guidance_rescale"] == 0.0
|
|
||||||
assert kwargs["audio_guidance_rescale"] == 0.0
|
|
||||||
|
|
||||||
explicit_steps = OmniDiffusionSamplingParams(num_inference_steps=12, generator_device="cpu")
|
|
||||||
step_kwargs = adapter._build_call_kwargs(
|
|
||||||
DiffusionRequestBatch(requests=[_make_request(sampling_params=explicit_steps)])
|
|
||||||
)
|
|
||||||
assert step_kwargs["num_inference_steps"] == 12
|
|
||||||
assert "sigmas" not in step_kwargs
|
|
||||||
|
|
||||||
custom_sigmas = [1.0, 0.5]
|
|
||||||
explicit_schedule = OmniDiffusionSamplingParams(
|
|
||||||
num_inference_steps=2,
|
|
||||||
sigmas=custom_sigmas,
|
|
||||||
generator_device="cpu",
|
|
||||||
)
|
|
||||||
schedule_kwargs = adapter._build_call_kwargs(
|
|
||||||
DiffusionRequestBatch(requests=[_make_request(sampling_params=explicit_schedule)])
|
|
||||||
)
|
|
||||||
assert schedule_kwargs["num_inference_steps"] == 2
|
|
||||||
assert schedule_kwargs["sigmas"] is custom_sigmas
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.advanced_model
|
@pytest.mark.advanced_model
|
||||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ def test_moe_ep_maps_diffusion_sp_cfg_dp_to_vllm_groups(monkeypatch):
|
|||||||
local_rank,
|
local_rank,
|
||||||
backend,
|
backend,
|
||||||
group_name,
|
group_name,
|
||||||
use_all2all=False,
|
|
||||||
):
|
):
|
||||||
del backend
|
del backend
|
||||||
group = _FakeGroup(
|
group = _FakeGroup(
|
||||||
@@ -74,7 +73,6 @@ def test_moe_ep_maps_diffusion_sp_cfg_dp_to_vllm_groups(monkeypatch):
|
|||||||
device_communicator=object(),
|
device_communicator=object(),
|
||||||
reduce_scatter=lambda tensor, **kwargs: tensor,
|
reduce_scatter=lambda tensor, **kwargs: tensor,
|
||||||
)
|
)
|
||||||
group.use_all2all = use_all2all
|
|
||||||
created_groups.append(group)
|
created_groups.append(group)
|
||||||
return group
|
return group
|
||||||
|
|
||||||
@@ -123,9 +121,6 @@ def test_moe_ep_maps_diffusion_sp_cfg_dp_to_vllm_groups(monkeypatch):
|
|||||||
assert parallel_state.vllm_parallel_state._PCP.device_communicator is not None
|
assert parallel_state.vllm_parallel_state._PCP.device_communicator is not None
|
||||||
assert parallel_state.vllm_parallel_state._DP.device_communicator is not None
|
assert parallel_state.vllm_parallel_state._DP.device_communicator is not None
|
||||||
assert parallel_state.vllm_parallel_state._EP.device_communicator is not None
|
assert parallel_state.vllm_parallel_state._EP.device_communicator is not None
|
||||||
# vLLM 0.27's MoE oracle asserts the EP communicator has an all2all
|
|
||||||
# manager, which vLLM only builds for groups created with use_all2all.
|
|
||||||
assert parallel_state.vllm_parallel_state._EP.use_all2all is True
|
|
||||||
assert hasattr(parallel_state.vllm_parallel_state._PCP, "reduce_scatter")
|
assert hasattr(parallel_state.vllm_parallel_state._PCP, "reduce_scatter")
|
||||||
assert hasattr(parallel_state.vllm_parallel_state._DP, "reduce_scatter")
|
assert hasattr(parallel_state.vllm_parallel_state._DP, "reduce_scatter")
|
||||||
assert hasattr(parallel_state.vllm_parallel_state._EP, "reduce_scatter")
|
assert hasattr(parallel_state.vllm_parallel_state._EP, "reduce_scatter")
|
||||||
@@ -318,35 +313,3 @@ def test_non_moe_ep_fails_before_vllm_ep_remap(monkeypatch):
|
|||||||
assert parallel_state.vllm_parallel_state._PCP is None
|
assert parallel_state.vllm_parallel_state._PCP is None
|
||||||
assert parallel_state.vllm_parallel_state._EP is None
|
assert parallel_state.vllm_parallel_state._EP is None
|
||||||
assert parallel_state._EXPERT_PARALLEL_GROUP_RANKS is None
|
assert parallel_state._EXPERT_PARALLEL_GROUP_RANKS is None
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.cpu
|
|
||||||
@pytest.mark.core_model
|
|
||||||
def test_init_vllm_group_forwards_use_all2all_only_when_supported(monkeypatch):
|
|
||||||
"""use_all2all reaches vLLM only on versions whose API accepts it.
|
|
||||||
|
|
||||||
vLLM 0.27 builds the EP all2all manager only for groups created with
|
|
||||||
use_all2all=True (its MoE oracle then asserts the manager exists —
|
|
||||||
build 2953, HunyuanImage3-DIT accuracy). Older vLLM has no such kwarg,
|
|
||||||
so forwarding it unconditionally would TypeError at init.
|
|
||||||
"""
|
|
||||||
received = {}
|
|
||||||
|
|
||||||
def modern(group_ranks, local_rank, backend, group_name=None, use_device_communicator=True, use_all2all=False):
|
|
||||||
received["modern"] = use_all2all
|
|
||||||
return object()
|
|
||||||
|
|
||||||
monkeypatch.setattr(parallel_state.vllm_parallel_state, "init_model_parallel_group", modern)
|
|
||||||
parallel_state.init_vllm_model_parallel_group([[0]], 0, "gloo", "ep", use_all2all=True)
|
|
||||||
assert received["modern"] is True
|
|
||||||
|
|
||||||
parallel_state.init_vllm_model_parallel_group([[0]], 0, "gloo", "tp")
|
|
||||||
assert received["modern"] is False
|
|
||||||
|
|
||||||
def legacy(group_ranks, local_rank, backend, group_name=None, use_device_communicator=True):
|
|
||||||
received["legacy"] = True
|
|
||||||
return object()
|
|
||||||
|
|
||||||
monkeypatch.setattr(parallel_state.vllm_parallel_state, "init_model_parallel_group", legacy)
|
|
||||||
parallel_state.init_vllm_model_parallel_group([[0]], 0, "gloo", "ep", use_all2all=True)
|
|
||||||
assert received["legacy"] is True
|
|
||||||
@@ -110,7 +110,7 @@ class TestFusedMoEFactory:
|
|||||||
monkeypatch.setitem(
|
monkeypatch.setitem(
|
||||||
sys.modules,
|
sys.modules,
|
||||||
"vllm.model_executor.layers.fused_moe",
|
"vllm.model_executor.layers.fused_moe",
|
||||||
SimpleNamespace(FusedMoEFactory=mock_vllm_fused_moe),
|
SimpleNamespace(FusedMoE=mock_vllm_fused_moe),
|
||||||
)
|
)
|
||||||
|
|
||||||
result = fused_moe.FusedMoE(prefix="test", a=1, reduce_results=True)
|
result = fused_moe.FusedMoE(prefix="test", a=1, reduce_results=True)
|
||||||
@@ -139,7 +139,7 @@ class TestFusedMoEFactory:
|
|||||||
monkeypatch.setitem(
|
monkeypatch.setitem(
|
||||||
sys.modules,
|
sys.modules,
|
||||||
"vllm.model_executor.layers.fused_moe",
|
"vllm.model_executor.layers.fused_moe",
|
||||||
SimpleNamespace(FusedMoEFactory=mocker.MagicMock(return_value=mock_runner)),
|
SimpleNamespace(FusedMoE=mocker.MagicMock(return_value=mock_runner)),
|
||||||
)
|
)
|
||||||
mocker.patch.object(fused_moe, "current_omni_platform", mocker.MagicMock())
|
mocker.patch.object(fused_moe, "current_omni_platform", mocker.MagicMock())
|
||||||
mock_set_num_tokens = mocker.patch.object(fused_moe, "_set_forward_context_num_tokens")
|
mock_set_num_tokens = mocker.patch.object(fused_moe, "_set_forward_context_num_tokens")
|
||||||
@@ -169,7 +169,7 @@ class TestFusedMoEFactory:
|
|||||||
monkeypatch.setitem(
|
monkeypatch.setitem(
|
||||||
sys.modules,
|
sys.modules,
|
||||||
"vllm.model_executor.layers.fused_moe",
|
"vllm.model_executor.layers.fused_moe",
|
||||||
SimpleNamespace(FusedMoEFactory=mocker.MagicMock(return_value=mock_runner)),
|
SimpleNamespace(FusedMoE=mocker.MagicMock(return_value=mock_runner)),
|
||||||
)
|
)
|
||||||
mocker.patch.object(fused_moe, "current_omni_platform", mocker.MagicMock())
|
mocker.patch.object(fused_moe, "current_omni_platform", mocker.MagicMock())
|
||||||
mock_set_num_tokens = mocker.patch.object(fused_moe, "_set_forward_context_num_tokens")
|
mock_set_num_tokens = mocker.patch.object(fused_moe, "_set_forward_context_num_tokens")
|
||||||
|
|||||||
@@ -348,53 +348,3 @@ def test_normalize_lingbot_request_model_size_overrides_sampling_dimensions():
|
|||||||
num_frames=81,
|
num_frames=81,
|
||||||
)
|
)
|
||||||
assert (config.height, config.width) == (480, 480)
|
assert (config.height, config.width) == (480, 480)
|
||||||
|
|
||||||
|
|
||||||
def test_normalize_lingbot_request_reads_size_from_extra_args():
|
|
||||||
"""Regression: /v1/videos delivers `extra_params` via sampling.extra_args.
|
|
||||||
|
|
||||||
The nightly TI2V case sends `extra_params={"size": "320x192"}` with no
|
|
||||||
height/width form fields; the size used to be dropped and the video came
|
|
||||||
out at the 480x480 default (build 2952, LingBot Video MoE Function Test).
|
|
||||||
"""
|
|
||||||
config = _normalize(
|
|
||||||
{
|
|
||||||
"prompt": "motion",
|
|
||||||
"modalities": ["video"],
|
|
||||||
"multi_modal_data": {"image": Image.new("RGB", (320, 192))},
|
|
||||||
},
|
|
||||||
num_frames=9,
|
|
||||||
extra_args={"size": "320x192"},
|
|
||||||
)
|
|
||||||
assert config.mode is LingBotGenerationMode.TI2V
|
|
||||||
assert (config.height, config.width) == (192, 320)
|
|
||||||
|
|
||||||
|
|
||||||
def test_normalize_lingbot_request_extra_args_size_overrides_sampling_dimensions():
|
|
||||||
config = _normalize(
|
|
||||||
{"prompt": "motion", "modalities": ["video"]},
|
|
||||||
width=480,
|
|
||||||
height=480,
|
|
||||||
num_frames=81,
|
|
||||||
extra_args={"size": "320x192"},
|
|
||||||
)
|
|
||||||
assert (config.height, config.width) == (192, 320)
|
|
||||||
|
|
||||||
|
|
||||||
def test_normalize_lingbot_request_reads_width_height_from_extra_args():
|
|
||||||
# Parity with the pre-#5311 pipeline, which honored extra_args height/width.
|
|
||||||
config = _normalize(
|
|
||||||
{"prompt": "motion", "modalities": ["video"]},
|
|
||||||
num_frames=81,
|
|
||||||
extra_args={"width": 320, "height": 192},
|
|
||||||
)
|
|
||||||
assert (config.height, config.width) == (192, 320)
|
|
||||||
|
|
||||||
|
|
||||||
def test_resolve_lingbot_output_dimensions_reads_extra_fields():
|
|
||||||
assert resolve_lingbot_output_dimensions(extra_fields={"size": "320x192"}) == (320, 192)
|
|
||||||
assert resolve_lingbot_output_dimensions(
|
|
||||||
sampling_width=480,
|
|
||||||
sampling_height=480,
|
|
||||||
extra_fields={"resolution": "720p", "ratio": "16:9"},
|
|
||||||
) == (736, 1280)
|
|
||||||
@@ -515,14 +515,8 @@ def test_quantization_quality(config: QualityTestConfig):
|
|||||||
|
|
||||||
generate_fn = _generate_video if config.task == "t2v" else _generate_image
|
generate_fn = _generate_video if config.task == "t2v" else _generate_image
|
||||||
|
|
||||||
# Run both arms eager: LPIPS must measure quantization only. With compile
|
|
||||||
# on, a compiler failure in one arm (e.g. inductor's CantSplit on fp8 FLUX)
|
|
||||||
# silently drops that arm to eager while the other stays compiled, and the
|
|
||||||
# metric then includes compile-state differences (fp8_ltx2 rose from ~0.09
|
|
||||||
# to 0.1291 that way in build 2954). Mirrors
|
|
||||||
# vllm_omni/quantization/tools/compare_diffusion_trajectory_similarity.py.
|
|
||||||
# --- BF16 baseline ---
|
# --- BF16 baseline ---
|
||||||
bl_kwargs: dict = {"model": config.baseline_ref(), "enforce_eager": True}
|
bl_kwargs: dict = {"model": config.baseline_ref()}
|
||||||
if config.enable_cpu_offload:
|
if config.enable_cpu_offload:
|
||||||
bl_kwargs["enable_cpu_offload"] = True
|
bl_kwargs["enable_cpu_offload"] = True
|
||||||
omni_bl = Omni(**bl_kwargs)
|
omni_bl = Omni(**bl_kwargs)
|
||||||
@@ -534,7 +528,7 @@ def test_quantization_quality(config: QualityTestConfig):
|
|||||||
|
|
||||||
# --- Quantized ---
|
# --- Quantized ---
|
||||||
quantization = config.quantization_ref()
|
quantization = config.quantization_ref()
|
||||||
qt_kwargs: dict = {"model": config.quantized_ref(), "enforce_eager": True}
|
qt_kwargs: dict = {"model": config.quantized_ref()}
|
||||||
if config.enable_cpu_offload:
|
if config.enable_cpu_offload:
|
||||||
qt_kwargs["enable_cpu_offload"] = True
|
qt_kwargs["enable_cpu_offload"] = True
|
||||||
if quantization is None:
|
if quantization is None:
|
||||||
|
|||||||
@@ -74,37 +74,3 @@ def test_regionally_compile_does_not_partially_mutate_on_setup_failure(monkeypat
|
|||||||
regionally_compile(model, dynamic=True)
|
regionally_compile(model, dynamic=True)
|
||||||
|
|
||||||
assert [block.forward.__func__ for block in model.transformer_blocks] == original_forwards
|
assert [block.forward.__func__ for block in model.transformer_blocks] == original_forwards
|
||||||
|
|
||||||
|
|
||||||
def test_compiled_block_preserves_forward_signature_for_inspection(monkeypatch):
|
|
||||||
"""cache-dit matches blocks via inspect.signature(block.forward).
|
|
||||||
|
|
||||||
Whatever regionally_compile installs as the block forward must stay
|
|
||||||
signature-transparent: parameter names and the return annotation drive
|
|
||||||
cache-dit's ForwardPattern match (build 2954, Multi-GPU Layered job
|
|
||||||
failed when a bare *args/**kwargs wrapper hid them; torch.compile's own
|
|
||||||
wrapper preserves the signature).
|
|
||||||
"""
|
|
||||||
import inspect
|
|
||||||
|
|
||||||
import torch
|
|
||||||
|
|
||||||
class _SignatureBlock(nn.Module):
|
|
||||||
def forward(self, hidden_states, encoder_hidden_states=None) -> "torch.Tensor":
|
|
||||||
return hidden_states
|
|
||||||
|
|
||||||
class _Model(nn.Module):
|
|
||||||
_repeated_blocks = ["_SignatureBlock"]
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
|
||||||
super().__init__()
|
|
||||||
self.blocks = nn.ModuleList([_SignatureBlock()])
|
|
||||||
|
|
||||||
model = _Model()
|
|
||||||
monkeypatch.setattr(compile_module.torch, "compile", lambda fn, *a, **k: fn)
|
|
||||||
regionally_compile(model)
|
|
||||||
|
|
||||||
sig = inspect.signature(model.blocks[0].forward)
|
|
||||||
assert set(sig.parameters.keys()) == {"hidden_states", "encoder_hidden_states"}
|
|
||||||
assert "torch.Tensor" in str(sig.return_annotation)
|
|
||||||
assert model.blocks[0].forward("x") == "x"
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
||||||
"""Regression tests for the inductor factorable-divisibility patch.
|
|
||||||
|
|
||||||
torch 2.13 proves symbolic divisibility with torch's own unevaluated Mod,
|
|
||||||
so ``15360*s31 + 15360*s87`` is no longer provably divisible by
|
|
||||||
``s31 + s87`` and inductor raises CantSplit on FLUX's fp8 graph (nightly
|
|
||||||
builds 2953/2954). The patch adds an exact-polynomial-division proof that
|
|
||||||
is sound for every symbol assignment; on torch versions whose original
|
|
||||||
check already proves the case (<= 2.11, via sympy.Mod), the patch
|
|
||||||
self-extinguishes.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
import sympy
|
|
||||||
from torch._inductor.sizevars import SizeVarAllocator
|
|
||||||
|
|
||||||
import vllm_omni.patch as patch_module
|
|
||||||
|
|
||||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu, pytest.mark.diffusion]
|
|
||||||
|
|
||||||
_S1, _S2 = sympy.symbols("s31 s87", positive=True, integer=True)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
("numerator", "denominator", "expected"),
|
|
||||||
[
|
|
||||||
(15360 * _S1 + 15360 * _S2, _S1 + _S2, True), # the CantSplit expression
|
|
||||||
(7 * _S1 + 7 * _S2, _S1 + _S2, True),
|
|
||||||
(_S1 * _S2, _S2, True),
|
|
||||||
(_S1 * _S2 + _S2, _S2, True), # s2*(s1 + 1)
|
|
||||||
(3 * _S1, 2 * _S1, False),
|
|
||||||
(_S1 + 1, _S1, False),
|
|
||||||
(15360 * _S1 + 15359 * _S2, _S1 + _S2, False),
|
|
||||||
(sympy.Integer(12), sympy.Integer(4), True),
|
|
||||||
(sympy.Integer(12), sympy.Integer(5), False),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_provably_factorable_multiple(numerator, denominator, expected):
|
|
||||||
assert patch_module._provably_factorable_multiple(numerator, denominator) is expected
|
|
||||||
|
|
||||||
|
|
||||||
def test_non_polynomial_atoms_never_reach_cancel():
|
|
||||||
from torch.utils._sympy.functions import FloorDiv, ModularIndexing
|
|
||||||
|
|
||||||
assert patch_module._provably_factorable_multiple(FloorDiv(_S1, 2) * 4, _S1) is False
|
|
||||||
assert patch_module._provably_factorable_multiple(ModularIndexing(_S1, 1, 8) * _S2, _S2) is False
|
|
||||||
|
|
||||||
|
|
||||||
def test_divisibility_holds_after_patch_regardless_of_torch_version():
|
|
||||||
"""The end contract on any torch: the CantSplit expression proves divisible.
|
|
||||||
|
|
||||||
On torch <= 2.11 the original method proves it (patch self-extinguished);
|
|
||||||
on torch >= 2.13 the installed wrapper proves it via exact division.
|
|
||||||
"""
|
|
||||||
sv = SizeVarAllocator()
|
|
||||||
method = SizeVarAllocator.statically_known_multiple_of
|
|
||||||
patched = getattr(method, "_vllm_omni_factorable_divisibility", False)
|
|
||||||
assert method(sv, 15360 * _S1 + 15360 * _S2, _S1 + _S2) is True
|
|
||||||
# Negative control must stay non-divisible either way.
|
|
||||||
assert method(sv, 3 * _S1, 2 * _S1) is False
|
|
||||||
if patched:
|
|
||||||
# Wrapper installed: the original alone must have failed the probe.
|
|
||||||
assert patch_module._provably_factorable_multiple(15360 * _S1 + 15360 * _S2, _S1 + _S2) is True
|
|
||||||
|
|
||||||
|
|
||||||
def test_patch_installs_when_original_cannot_prove(monkeypatch):
|
|
||||||
"""Simulate torch 2.13's unevaluated-Mod prover: the wrapper must install."""
|
|
||||||
|
|
||||||
def weak(self, numerator, denominator):
|
|
||||||
return False
|
|
||||||
|
|
||||||
monkeypatch.setattr(SizeVarAllocator, "statically_known_multiple_of", weak)
|
|
||||||
patch_module._patch_inductor_factorable_divisibility()
|
|
||||||
method = SizeVarAllocator.statically_known_multiple_of
|
|
||||||
assert getattr(method, "_vllm_omni_factorable_divisibility", False) is True
|
|
||||||
|
|
||||||
sv = SizeVarAllocator()
|
|
||||||
assert method(sv, 15360 * _S1 + 15360 * _S2, _S1 + _S2) is True
|
|
||||||
assert method(sv, 3 * _S1, 2 * _S1) is False
|
|
||||||
# Idempotency: a second install call must not wrap the wrapper.
|
|
||||||
patch_module._patch_inductor_factorable_divisibility()
|
|
||||||
assert SizeVarAllocator.statically_known_multiple_of is method
|
|
||||||
@@ -33,19 +33,16 @@ BAGEL_CI_DEPLOY = get_deploy_config_path("ci/bagel.yaml")
|
|||||||
# Generated with seed=52, num_inference_steps=15,
|
# Generated with seed=52, num_inference_steps=15,
|
||||||
# prompt='Change the grass color to red',
|
# prompt='Change the grass color to red',
|
||||||
# input image: 2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg
|
# input image: 2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg
|
||||||
# Re-goldened from the vLLM-0.27-aligned output (nightly builds 2953/2954
|
|
||||||
# produced bit-identical pixels across two different torch/base images; only
|
|
||||||
# (512, 336) moved beyond tolerance vs the main-branch goldens from #5898).
|
|
||||||
REFERENCE_PIXELS = [
|
REFERENCE_PIXELS = [
|
||||||
{"position": (100, 100), "rgb": (157, 172, 217)},
|
{"position": (100, 100), "rgb": (156, 172, 217)},
|
||||||
{"position": (400, 50), "rgb": (105, 144, 217)},
|
{"position": (400, 50), "rgb": (105, 144, 217)},
|
||||||
{"position": (700, 100), "rgb": (118, 159, 232)},
|
{"position": (700, 100), "rgb": (118, 159, 232)},
|
||||||
{"position": (150, 400), "rgb": (185, 21, 55)},
|
{"position": (150, 400), "rgb": (180, 22, 52)},
|
||||||
{"position": (512, 336), "rgb": (205, 215, 191)},
|
{"position": (512, 336), "rgb": (221, 211, 194)},
|
||||||
{"position": (700, 400), "rgb": (193, 11, 48)},
|
{"position": (700, 400), "rgb": (192, 10, 46)},
|
||||||
{"position": (100, 600), "rgb": (103, 14, 25)},
|
{"position": (100, 600), "rgb": (102, 12, 22)},
|
||||||
{"position": (400, 600), "rgb": (159, 28, 48)},
|
{"position": (400, 600), "rgb": (161, 28, 47)},
|
||||||
{"position": (700, 600), "rgb": (101, 87, 94)},
|
{"position": (700, 600), "rgb": (100, 87, 94)},
|
||||||
{"position": (256, 256), "rgb": (181, 201, 221)},
|
{"position": (256, 256), "rgb": (181, 201, 221)},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -144,24 +141,13 @@ def _validate_pixels(
|
|||||||
Raises:
|
Raises:
|
||||||
AssertionError: If any pixel differs beyond tolerance.
|
AssertionError: If any pixel differs beyond tolerance.
|
||||||
"""
|
"""
|
||||||
mismatches = []
|
|
||||||
probes = []
|
|
||||||
for ref in reference_pixels:
|
for ref in reference_pixels:
|
||||||
x, y = ref["position"]
|
x, y = ref["position"]
|
||||||
expected = ref["rgb"]
|
expected = ref["rgb"]
|
||||||
actual = image.getpixel((x, y))[:3]
|
actual = image.getpixel((x, y))[:3]
|
||||||
probes.append(f' {{"position": ({x}, {y}), "rgb": {actual}}},')
|
assert all(abs(a - e) <= tolerance for a, e in zip(actual, expected)), (
|
||||||
if not all(abs(a - e) <= tolerance for a, e in zip(actual, expected)):
|
f"Pixel mismatch at ({x}, {y}): expected {expected}, got {actual}"
|
||||||
mismatches.append(f"({x}, {y}): expected {expected}, got {actual}")
|
)
|
||||||
# Report every probe, not just the first mismatch: CI does not upload the
|
|
||||||
# generated image, so this failure message is the only place the full
|
|
||||||
# actual-value table exists when the goldens need to be regenerated.
|
|
||||||
assert not mismatches, (
|
|
||||||
f"Pixel mismatch at {len(mismatches)} probe(s) (tolerance ±{tolerance}):\n "
|
|
||||||
+ "\n ".join(mismatches)
|
|
||||||
+ "\nActual values at all probes (paste as the new reference table if re-goldening):\n"
|
|
||||||
+ "\n".join(probes)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _generate_bagel_img2img(
|
def _generate_bagel_img2img(
|
||||||
|
|||||||
@@ -1094,12 +1094,6 @@ class _HashableRequest(SimpleNamespace):
|
|||||||
# request; real Requests initialise it to 0 (vllm/v1/request.py).
|
# request; real Requests initialise it to 0 (vllm/v1/request.py).
|
||||||
num_in_flight_tokens = 0
|
num_in_flight_tokens = 0
|
||||||
|
|
||||||
# vLLM 0.27 (a0c092ee72): the stale-output counter that replaced
|
|
||||||
# async_tokens_to_discard. update_from_output reads it for every scheduled
|
|
||||||
# request, and real Requests initialise it to 0 (vllm/v1/request.py), so the
|
|
||||||
# double needs it too.
|
|
||||||
num_stale_output_tokens = 0
|
|
||||||
|
|
||||||
def __hash__(self):
|
def __hash__(self):
|
||||||
return hash(self.request_id)
|
return hash(self.request_id)
|
||||||
|
|
||||||
|
|||||||
@@ -42,31 +42,10 @@ from tests.e2e.accuracy.helpers import (
|
|||||||
from tests.helpers.env import run_post_test_cleanup, run_pre_test_cleanup
|
from tests.helpers.env import run_post_test_cleanup, run_pre_test_cleanup
|
||||||
from tests.helpers.mark import hardware_test
|
from tests.helpers.mark import hardware_test
|
||||||
from tests.helpers.runtime import OmniServer, OpenAIClientHandler
|
from tests.helpers.runtime import OmniServer, OpenAIClientHandler
|
||||||
from vllm_omni.diffusion.models.diffusers_adapter.pipeline_diffusers_adapter import (
|
|
||||||
CUDA_FLASH_ATTENTION_BACKEND_ATTEMPTS,
|
|
||||||
)
|
|
||||||
|
|
||||||
pytestmark = [pytest.mark.full_model, pytest.mark.diffusion]
|
pytestmark = [pytest.mark.full_model, pytest.mark.diffusion]
|
||||||
|
|
||||||
|
|
||||||
def _set_matched_attention_backend(pipe: DiffusionPipeline) -> None:
|
|
||||||
"""Walk the same backend chain the omni server's diffusers adapter walks.
|
|
||||||
|
|
||||||
The server side (--diffusion-load-format diffusers) resolves its attention
|
|
||||||
backend through the adapter's preference chain, skipping backends that are
|
|
||||||
unavailable on the image (e.g. the FA3 hub kernel has no build variant for
|
|
||||||
the image's torch — build 2953). The reference run must resolve to the
|
|
||||||
same backend or the similarity comparison measures kernel differences.
|
|
||||||
"""
|
|
||||||
for backend in CUDA_FLASH_ATTENTION_BACKEND_ATTEMPTS:
|
|
||||||
try:
|
|
||||||
pipe.transformer.set_attention_backend(backend)
|
|
||||||
return
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
pipe.transformer.set_attention_backend("native")
|
|
||||||
|
|
||||||
|
|
||||||
PROMPT = "A photo of a cat sitting on a laptop keyboard, digital art style."
|
PROMPT = "A photo of a cat sitting on a laptop keyboard, digital art style."
|
||||||
NEGATIVE_PROMPT = "blurry, low quality"
|
NEGATIVE_PROMPT = "blurry, low quality"
|
||||||
WIDTH = 512
|
WIDTH = 512
|
||||||
@@ -256,7 +235,7 @@ def _run_diffusers_qwen_image(*, model: str, output_path: Path) -> tuple[Image.I
|
|||||||
torch_dtype=torch.bfloat16,
|
torch_dtype=torch.bfloat16,
|
||||||
trust_remote_code=True,
|
trust_remote_code=True,
|
||||||
).to("cuda")
|
).to("cuda")
|
||||||
_set_matched_attention_backend(pipe)
|
pipe.transformer.set_attention_backend("_flash_3_hub")
|
||||||
_diffusers_dummy_run(pipe)
|
_diffusers_dummy_run(pipe)
|
||||||
|
|
||||||
latencies: list[float] = []
|
latencies: list[float] = []
|
||||||
|
|||||||
@@ -30,12 +30,6 @@ NUM_INFERENCE_STEPS = 20
|
|||||||
TRUE_CFG_SCALE = 4.0
|
TRUE_CFG_SCALE = 4.0
|
||||||
SEED = 42
|
SEED = 42
|
||||||
SSIM_THRESHOLD = 0.97
|
SSIM_THRESHOLD = 0.97
|
||||||
# The FA3-hub-matched comparison clears 0.97 with margin. The SDPA-matched
|
|
||||||
# fallback (hub kernel unavailable for the image's torch) landed at 0.9687 in
|
|
||||||
# build 2954 — the attention math matches but the surrounding stacks (vLLM
|
|
||||||
# compiled path vs eager diffusers) still differ slightly. Keep the strict
|
|
||||||
# gate for the FA3 path and a slightly looser one for the fallback only.
|
|
||||||
SSIM_THRESHOLD_SDPA_FALLBACK = 0.96
|
|
||||||
PSNR_THRESHOLD = 30.0
|
PSNR_THRESHOLD = 30.0
|
||||||
|
|
||||||
MODEL_2512_ID = "Qwen/Qwen-Image-2512"
|
MODEL_2512_ID = "Qwen/Qwen-Image-2512"
|
||||||
@@ -62,44 +56,6 @@ MEAN_ABS_DIFF_THRESHOLD_2512 = 3e-2
|
|||||||
P99_ABS_DIFF_THRESHOLD_2512 = 4e-1
|
P99_ABS_DIFF_THRESHOLD_2512 = 4e-1
|
||||||
|
|
||||||
|
|
||||||
_FLASH_ATTN3_HUB_AVAILABLE: bool | None = None
|
|
||||||
|
|
||||||
|
|
||||||
def _flash_attn3_hub_available() -> bool:
|
|
||||||
"""Whether kernels-community/flash-attn3 has a build for this torch/CUDA.
|
|
||||||
|
|
||||||
The hub repo only ships prebuilt variants for specific torch versions;
|
|
||||||
a base-image torch bump can outrun it (build 2952 failed with "Cannot
|
|
||||||
find a build variant for this system"). When it is unavailable, both
|
|
||||||
sides of the comparison fall back to torch SDPA together — the test
|
|
||||||
compares implementations, so the attention math must stay matched.
|
|
||||||
"""
|
|
||||||
global _FLASH_ATTN3_HUB_AVAILABLE
|
|
||||||
if _FLASH_ATTN3_HUB_AVAILABLE is None:
|
|
||||||
try:
|
|
||||||
from kernels import get_kernel
|
|
||||||
|
|
||||||
get_kernel("kernels-community/flash-attn3")
|
|
||||||
_FLASH_ATTN3_HUB_AVAILABLE = True
|
|
||||||
except Exception as exc:
|
|
||||||
print(f"kernels-community/flash-attn3 unavailable ({exc}); using matched torch SDPA on both sides.")
|
|
||||||
_FLASH_ATTN3_HUB_AVAILABLE = False
|
|
||||||
return _FLASH_ATTN3_HUB_AVAILABLE
|
|
||||||
|
|
||||||
|
|
||||||
def _omni_server_env() -> dict[str, str] | None:
|
|
||||||
if _flash_attn3_hub_available():
|
|
||||||
return None
|
|
||||||
return {"DIFFUSION_ATTENTION_BACKEND": "TORCH_SDPA"}
|
|
||||||
|
|
||||||
|
|
||||||
def _set_reference_attention_backend(pipe: DiffusionPipeline) -> None:
|
|
||||||
if _flash_attn3_hub_available():
|
|
||||||
pipe.transformer.set_attention_backend("_flash_3_hub")
|
|
||||||
else:
|
|
||||||
pipe.transformer.set_attention_backend("native")
|
|
||||||
|
|
||||||
|
|
||||||
def _model_name() -> str:
|
def _model_name() -> str:
|
||||||
return os.environ.get(MODEL_ENV_VAR, MODEL_ID)
|
return os.environ.get(MODEL_ENV_VAR, MODEL_ID)
|
||||||
|
|
||||||
@@ -122,7 +78,7 @@ def _run_vllm_omni_qwen_image(*, model: str, output_path: Path) -> Image.Image:
|
|||||||
"900",
|
"900",
|
||||||
"--fa-deterministic",
|
"--fa-deterministic",
|
||||||
]
|
]
|
||||||
with OmniServer(model, server_args, use_omni=True, env_dict=_omni_server_env()) as omni_server:
|
with OmniServer(model, server_args, use_omni=True) as omni_server:
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
f"http://{omni_server.host}:{omni_server.port}/v1/images/generations",
|
f"http://{omni_server.host}:{omni_server.port}/v1/images/generations",
|
||||||
json={
|
json={
|
||||||
@@ -158,7 +114,7 @@ def _run_diffusers_qwen_image(*, model: str, output_path: Path) -> Image.Image:
|
|||||||
trust_remote_code=True,
|
trust_remote_code=True,
|
||||||
local_files_only=_local_files_only(model),
|
local_files_only=_local_files_only(model),
|
||||||
).to("cuda")
|
).to("cuda")
|
||||||
_set_reference_attention_backend(pipe)
|
pipe.transformer.set_attention_backend("_flash_3_hub")
|
||||||
generator = torch.Generator(device="cuda").manual_seed(SEED)
|
generator = torch.Generator(device="cuda").manual_seed(SEED)
|
||||||
result = pipe( # pyright: ignore[reportCallIssue]
|
result = pipe( # pyright: ignore[reportCallIssue]
|
||||||
prompt=PROMPT,
|
prompt=PROMPT,
|
||||||
@@ -184,7 +140,7 @@ def _run_diffusers_qwen_image(*, model: str, output_path: Path) -> Image.Image:
|
|||||||
|
|
||||||
def _run_vllm_omni_qwen_image_2512(*, model: str, output_path: Path) -> Image.Image:
|
def _run_vllm_omni_qwen_image_2512(*, model: str, output_path: Path) -> Image.Image:
|
||||||
server_args = ["--num-gpus", "1", "--stage-init-timeout", "300", "--init-timeout", "900"]
|
server_args = ["--num-gpus", "1", "--stage-init-timeout", "300", "--init-timeout", "900"]
|
||||||
with OmniServer(model, server_args, use_omni=True, env_dict=_omni_server_env()) as omni_server:
|
with OmniServer(model, server_args, use_omni=True) as omni_server:
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
f"http://{omni_server.host}:{omni_server.port}/v1/images/generations",
|
f"http://{omni_server.host}:{omni_server.port}/v1/images/generations",
|
||||||
json={
|
json={
|
||||||
@@ -220,7 +176,7 @@ def _run_diffusers_qwen_image_2512(*, model: str, output_path: Path) -> Image.Im
|
|||||||
trust_remote_code=True,
|
trust_remote_code=True,
|
||||||
local_files_only=_local_files_only(model),
|
local_files_only=_local_files_only(model),
|
||||||
).to("cuda")
|
).to("cuda")
|
||||||
_set_reference_attention_backend(pipe)
|
pipe.transformer.set_attention_backend("_flash_3_hub")
|
||||||
generator = torch.Generator(device="cuda").manual_seed(SEED_2512)
|
generator = torch.Generator(device="cuda").manual_seed(SEED_2512)
|
||||||
result = pipe( # pyright: ignore[reportCallIssue]
|
result = pipe( # pyright: ignore[reportCallIssue]
|
||||||
prompt=PROMPT_2512,
|
prompt=PROMPT_2512,
|
||||||
@@ -259,7 +215,7 @@ def test_qwen_image_matches_diffusers(accuracy_artifact_root: Path) -> None:
|
|||||||
diffusers_image=diffusers_output,
|
diffusers_image=diffusers_output,
|
||||||
width=WIDTH,
|
width=WIDTH,
|
||||||
height=HEIGHT,
|
height=HEIGHT,
|
||||||
ssim_threshold=SSIM_THRESHOLD if _flash_attn3_hub_available() else SSIM_THRESHOLD_SDPA_FALLBACK,
|
ssim_threshold=SSIM_THRESHOLD,
|
||||||
psnr_threshold=PSNR_THRESHOLD,
|
psnr_threshold=PSNR_THRESHOLD,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -118,17 +118,6 @@ def test_speech_instructions(omni_server, openai_client) -> None:
|
|||||||
"response_format": "wav",
|
"response_format": "wav",
|
||||||
"timeout": 120.0,
|
"timeout": 120.0,
|
||||||
"instructions": instruction,
|
"instructions": instruction,
|
||||||
# Re-grade a below-threshold result with whisper large-v3 before
|
|
||||||
# failing, so a miss is attributable rather than ambiguous. Same
|
|
||||||
# opt-in as test_higgs_audio_v3.py.
|
|
||||||
#
|
|
||||||
# Note: for this input it does NOT rescue the check — small and
|
|
||||||
# large-v3 both transcribe "The boy was there when sun rose."
|
|
||||||
# (sim=0.87 each) against an input of "...when the sun rose.",
|
|
||||||
# i.e. the TTS really does drop the article. That is the value
|
|
||||||
# here: two independent ASR models agreeing rules out a grader
|
|
||||||
# flake and points at the model.
|
|
||||||
"transcript_escalation_model": "large-v3",
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,6 @@ explicitly patch values that differ from vLLM.
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import inspect
|
import inspect
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from unittest.mock import Mock
|
from unittest.mock import Mock
|
||||||
|
|
||||||
@@ -197,155 +194,6 @@ def test_qwen3_tts_code2wav_injects_max_position_embeddings(monkeypatch):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_patch_missing_local_hf_config(tmp_path):
|
|
||||||
"""Native model bundles may only contain a checkpoints/ directory."""
|
|
||||||
(tmp_path / "checkpoints").mkdir()
|
|
||||||
args = object.__new__(OmniEngineArgs)
|
|
||||||
args.model = str(tmp_path)
|
|
||||||
args.model_arch = "IndexTTS25TalkerForConditionalGeneration"
|
|
||||||
args.hf_config_path = None
|
|
||||||
|
|
||||||
try:
|
|
||||||
args._patch_empty_hf_config("indextts2_5")
|
|
||||||
|
|
||||||
assert args.hf_config_path is not None
|
|
||||||
config_path = args.hf_config_path + "/config.json"
|
|
||||||
with open(config_path, encoding="utf-8") as config_file:
|
|
||||||
config = json.load(config_file)
|
|
||||||
assert config == {
|
|
||||||
"model_type": "indextts2_5",
|
|
||||||
"architectures": ["IndexTTS25TalkerForConditionalGeneration"],
|
|
||||||
}
|
|
||||||
finally:
|
|
||||||
if hasattr(args, "_temp_config_dir"):
|
|
||||||
shutil.rmtree(args._temp_config_dir, ignore_errors=True)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"config_entry",
|
|
||||||
["malformed", "directory", "broken_symlink", "permission_error"],
|
|
||||||
)
|
|
||||||
def test_non_missing_local_hf_config_error_reaches_parent_loader(tmp_path, monkeypatch, config_entry):
|
|
||||||
"""Non-missing config errors must reach vLLM's normal loader."""
|
|
||||||
config_path = tmp_path / "config.json"
|
|
||||||
if config_entry == "malformed":
|
|
||||||
config_path.write_text("{not valid json", encoding="utf-8")
|
|
||||||
loader_error = json.JSONDecodeError("invalid config", "{not valid json", 1)
|
|
||||||
elif config_entry == "directory":
|
|
||||||
config_path.mkdir()
|
|
||||||
loader_error = IsADirectoryError(str(config_path))
|
|
||||||
elif config_entry == "broken_symlink":
|
|
||||||
config_path.symlink_to(tmp_path / "missing-target.json")
|
|
||||||
loader_error = FileNotFoundError(str(config_path))
|
|
||||||
else:
|
|
||||||
config_path.write_text("{}", encoding="utf-8")
|
|
||||||
loader_error = PermissionError(str(config_path))
|
|
||||||
|
|
||||||
parent_inputs = {}
|
|
||||||
|
|
||||||
def controlled_get_config_dict(cls, model, **kwargs):
|
|
||||||
if model == str(tmp_path):
|
|
||||||
raise loader_error
|
|
||||||
with open(os.path.join(model, "config.json"), encoding="utf-8") as config_file:
|
|
||||||
return json.load(config_file), {}
|
|
||||||
|
|
||||||
def fake_parent_create_model_config(self):
|
|
||||||
parent_inputs["called"] = True
|
|
||||||
parent_inputs["model"] = self.model
|
|
||||||
parent_inputs["hf_config_path"] = self.hf_config_path
|
|
||||||
config_root = self.hf_config_path or self.model
|
|
||||||
parent_inputs["config_root"] = config_root
|
|
||||||
return PretrainedConfig.get_config_dict(config_root)[0]
|
|
||||||
|
|
||||||
monkeypatch.setattr(PretrainedConfig, "get_config_dict", classmethod(controlled_get_config_dict))
|
|
||||||
monkeypatch.setattr(OmniEngineArgs, "_ensure_omni_models_registered", lambda self: True)
|
|
||||||
monkeypatch.setattr(EngineArgs, "create_model_config", fake_parent_create_model_config)
|
|
||||||
monkeypatch.setattr(
|
|
||||||
OmniModelConfig,
|
|
||||||
"from_vllm_model_config",
|
|
||||||
classmethod(lambda cls, model_config, **omni_kwargs: model_config),
|
|
||||||
)
|
|
||||||
|
|
||||||
args = OmniEngineArgs(
|
|
||||||
model=str(tmp_path),
|
|
||||||
model_arch="IndexTTS25TalkerForConditionalGeneration",
|
|
||||||
)
|
|
||||||
|
|
||||||
with pytest.raises(type(loader_error)) as exc_info:
|
|
||||||
args.create_model_config()
|
|
||||||
|
|
||||||
assert str(exc_info.value) == str(loader_error)
|
|
||||||
assert parent_inputs["called"] is True
|
|
||||||
assert parent_inputs["model"] == str(tmp_path)
|
|
||||||
assert parent_inputs["config_root"] == str(tmp_path)
|
|
||||||
assert parent_inputs["hf_config_path"] is None
|
|
||||||
assert args.model == str(tmp_path)
|
|
||||||
assert args.hf_config_path is None
|
|
||||||
assert not hasattr(args, "_temp_config_dir")
|
|
||||||
|
|
||||||
|
|
||||||
def test_patch_valid_hf_config_without_model_type_preserves_keys(tmp_path):
|
|
||||||
"""A valid partial config should be copied before required keys are added."""
|
|
||||||
original_config = {
|
|
||||||
"hidden_size": 1024,
|
|
||||||
"custom_key": {"nested": True},
|
|
||||||
}
|
|
||||||
(tmp_path / "config.json").write_text(json.dumps(original_config), encoding="utf-8")
|
|
||||||
args = object.__new__(OmniEngineArgs)
|
|
||||||
args.model = str(tmp_path)
|
|
||||||
args.model_arch = "IndexTTS25TalkerForConditionalGeneration"
|
|
||||||
args.hf_config_path = None
|
|
||||||
|
|
||||||
try:
|
|
||||||
args._patch_empty_hf_config("indextts2_5")
|
|
||||||
|
|
||||||
assert args.hf_config_path is not None
|
|
||||||
with open(os.path.join(args.hf_config_path, "config.json"), encoding="utf-8") as config_file:
|
|
||||||
patched_config = json.load(config_file)
|
|
||||||
assert patched_config["hidden_size"] == original_config["hidden_size"]
|
|
||||||
assert patched_config["custom_key"] == original_config["custom_key"]
|
|
||||||
assert patched_config["model_type"] == "indextts2_5"
|
|
||||||
assert patched_config["architectures"] == ["IndexTTS25TalkerForConditionalGeneration"]
|
|
||||||
finally:
|
|
||||||
if hasattr(args, "_temp_config_dir"):
|
|
||||||
shutil.rmtree(args._temp_config_dir, ignore_errors=True)
|
|
||||||
|
|
||||||
|
|
||||||
def test_remote_hf_config_error_reaches_parent_loader(monkeypatch):
|
|
||||||
"""Remote resolution failures must stay on vLLM's normal error path."""
|
|
||||||
loader_error = OSError("remote config resolution failed")
|
|
||||||
parent_inputs = {}
|
|
||||||
|
|
||||||
def controlled_get_config_dict(cls, model, **kwargs):
|
|
||||||
raise loader_error
|
|
||||||
|
|
||||||
def fake_parent_create_model_config(self):
|
|
||||||
parent_inputs["called"] = True
|
|
||||||
parent_inputs["model"] = self.model
|
|
||||||
parent_inputs["hf_config_path"] = self.hf_config_path
|
|
||||||
return PretrainedConfig.get_config_dict(self.model)[0]
|
|
||||||
|
|
||||||
monkeypatch.setattr(PretrainedConfig, "get_config_dict", classmethod(controlled_get_config_dict))
|
|
||||||
monkeypatch.setattr(OmniEngineArgs, "_ensure_omni_models_registered", lambda self: True)
|
|
||||||
monkeypatch.setattr(EngineArgs, "create_model_config", fake_parent_create_model_config)
|
|
||||||
|
|
||||||
args = OmniEngineArgs(
|
|
||||||
model="remote/model",
|
|
||||||
model_arch="IndexTTS25TalkerForConditionalGeneration",
|
|
||||||
)
|
|
||||||
|
|
||||||
with pytest.raises(OSError) as exc_info:
|
|
||||||
args.create_model_config()
|
|
||||||
|
|
||||||
assert str(exc_info.value) == str(loader_error)
|
|
||||||
assert parent_inputs["called"] is True
|
|
||||||
assert parent_inputs["model"] == "remote/model"
|
|
||||||
assert parent_inputs["hf_config_path"] is None
|
|
||||||
assert args.model == "remote/model"
|
|
||||||
assert args.hf_config_path is None
|
|
||||||
assert not hasattr(args, "_temp_config_dir")
|
|
||||||
|
|
||||||
|
|
||||||
def test_stage_specific_text_config_override():
|
def test_stage_specific_text_config_override():
|
||||||
"""Stage swap must refresh hf_text_config, dependent attrs, and model_arch_config."""
|
"""Stage swap must refresh hf_text_config, dependent attrs, and model_arch_config."""
|
||||||
vllm_config = EngineArgs().create_model_config()
|
vllm_config = EngineArgs().create_model_config()
|
||||||
|
|||||||
@@ -412,7 +412,7 @@ def test_stage_runtime_passes_log_stats_to_llm_replica_launch(monkeypatch):
|
|||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
runtime_mod.StageEngineCoreClientBase,
|
runtime_mod.StageEngineCoreClientBase,
|
||||||
"make_async_mp_client",
|
"make_async_mp_client",
|
||||||
lambda **kwargs: captured.__setitem__("client_log_stats", kwargs["log_stats"]) or stage_client,
|
lambda **kwargs: (captured.__setitem__("client_log_stats", kwargs["log_stats"]) or stage_client),
|
||||||
)
|
)
|
||||||
|
|
||||||
assert runtime._initialize_local_llm_replica(plan, stage_init_timeout=1) is stage_client
|
assert runtime._initialize_local_llm_replica(plan, stage_init_timeout=1) is stage_client
|
||||||
@@ -807,9 +807,9 @@ def test_build_stage0_input_processor_uses_omni_input_preprocessor(monkeypatch):
|
|||||||
import vllm_omni.engine.stage_init_utils as init_mod
|
import vllm_omni.engine.stage_init_utils as init_mod
|
||||||
|
|
||||||
class DummyInputProcessor:
|
class DummyInputProcessor:
|
||||||
def __init__(self, vllm_config, renderer=None):
|
def __init__(self, vllm_config):
|
||||||
self.vllm_config = vllm_config
|
self.vllm_config = vllm_config
|
||||||
self.renderer = renderer or object()
|
self.renderer = object()
|
||||||
self.input_preprocessor = None
|
self.input_preprocessor = None
|
||||||
|
|
||||||
class DummyOmniInputPreprocessor:
|
class DummyOmniInputPreprocessor:
|
||||||
@@ -828,50 +828,6 @@ def test_build_stage0_input_processor_uses_omni_input_preprocessor(monkeypatch):
|
|||||||
assert input_processor.input_preprocessor.renderer is input_processor.renderer
|
assert input_processor.input_preprocessor.renderer is input_processor.renderer
|
||||||
|
|
||||||
|
|
||||||
def test_build_stage0_input_processor_does_not_resolve_tokenizer_when_skipped(
|
|
||||||
monkeypatch,
|
|
||||||
):
|
|
||||||
import vllm_omni.engine.stage_init_utils as init_mod
|
|
||||||
|
|
||||||
token_only_renderer = object()
|
|
||||||
seen = {}
|
|
||||||
|
|
||||||
class DummyInputProcessor:
|
|
||||||
def __init__(self, vllm_config, renderer=None):
|
|
||||||
if renderer is None:
|
|
||||||
raise AssertionError("skip_tokenizer_init must supply a renderer")
|
|
||||||
seen["renderer"] = renderer
|
|
||||||
self.renderer = renderer
|
|
||||||
self.input_preprocessor = None
|
|
||||||
|
|
||||||
class DummyOmniInputPreprocessor:
|
|
||||||
def __init__(self, vllm_config, renderer=None):
|
|
||||||
seen["preprocessor_renderer"] = renderer
|
|
||||||
|
|
||||||
monkeypatch.setattr(init_mod, "InputProcessor", DummyInputProcessor)
|
|
||||||
monkeypatch.setattr(
|
|
||||||
init_mod,
|
|
||||||
"_build_token_only_renderer",
|
|
||||||
lambda _config: token_only_renderer,
|
|
||||||
)
|
|
||||||
monkeypatch.setattr(
|
|
||||||
init_mod,
|
|
||||||
"OmniInputPreprocessor",
|
|
||||||
DummyOmniInputPreprocessor,
|
|
||||||
)
|
|
||||||
|
|
||||||
config = types.SimpleNamespace(
|
|
||||||
model_config=types.SimpleNamespace(
|
|
||||||
skip_tokenizer_init=True,
|
|
||||||
try_get_generation_config=lambda: {},
|
|
||||||
)
|
|
||||||
)
|
|
||||||
build_stage0_input_processor(config)
|
|
||||||
|
|
||||||
assert seen["renderer"] is token_only_renderer
|
|
||||||
assert seen["preprocessor_renderer"] is token_only_renderer
|
|
||||||
|
|
||||||
|
|
||||||
def test_inject_kv_stage_info_infers_sender_tp_topology():
|
def test_inject_kv_stage_info_infers_sender_tp_topology():
|
||||||
from vllm_omni.engine.stage_init_utils import inject_kv_stage_info
|
from vllm_omni.engine.stage_init_utils import inject_kv_stage_info
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ def test_positional_construction_matches_base_request():
|
|||||||
assert req.external_req_id is None
|
assert req.external_req_id is None
|
||||||
assert req.additional_information is None
|
assert req.additional_information is None
|
||||||
assert req.prompt_embeds_payload is None
|
assert req.prompt_embeds_payload is None
|
||||||
assert req.num_stale_output_tokens == 0
|
|
||||||
|
|
||||||
|
|
||||||
def test_positional_prompt_embeds_does_not_collide():
|
def test_positional_prompt_embeds_does_not_collide():
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
||||||
"""Regression tests for per-replica compile-cache isolation at spawn time.
|
|
||||||
|
|
||||||
Replicas of the same stage compile the same model concurrently, and vLLM
|
|
||||||
0.27's AOT compile (TritonBundler) races on the shared VLLM_CACHE_ROOT —
|
|
||||||
a replica then dies with 'Cubin file saved by TritonBundler not found'
|
|
||||||
(build 2954, Omni Perf Multi-Replica). Replica 0 must keep the default
|
|
||||||
shared cache; additional replicas must spawn with an isolated cache dir,
|
|
||||||
and the parent env must be restored afterwards.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from vllm_omni.engine.stage_engine_startup import scoped_spawn_device_env
|
|
||||||
|
|
||||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
|
||||||
|
|
||||||
|
|
||||||
def test_replica_zero_keeps_shared_cache_root(monkeypatch):
|
|
||||||
monkeypatch.delenv("VLLM_CACHE_ROOT", raising=False)
|
|
||||||
with scoped_spawn_device_env(None, None, stage_id=1, replica_id=0):
|
|
||||||
assert "VLLM_CACHE_ROOT" not in os.environ
|
|
||||||
assert "VLLM_CACHE_ROOT" not in os.environ
|
|
||||||
|
|
||||||
|
|
||||||
def test_additional_replica_gets_isolated_cache_root(monkeypatch):
|
|
||||||
monkeypatch.setenv("VLLM_CACHE_ROOT", "/tmp/base-cache")
|
|
||||||
with scoped_spawn_device_env(None, None, stage_id=2, replica_id=1):
|
|
||||||
assert os.environ["VLLM_CACHE_ROOT"] == os.path.join("/tmp/base-cache", "omni_stage2_replica1")
|
|
||||||
assert os.environ["VLLM_CACHE_ROOT"] == "/tmp/base-cache"
|
|
||||||
|
|
||||||
|
|
||||||
def test_isolated_cache_root_defaults_without_env(monkeypatch):
|
|
||||||
monkeypatch.delenv("VLLM_CACHE_ROOT", raising=False)
|
|
||||||
with scoped_spawn_device_env(None, None, stage_id=0, replica_id=3):
|
|
||||||
assert os.environ["VLLM_CACHE_ROOT"].endswith("omni_stage0_replica3")
|
|
||||||
assert os.environ["VLLM_CACHE_ROOT"].startswith(os.path.expanduser("~/.cache/vllm"))
|
|
||||||
assert "VLLM_CACHE_ROOT" not in os.environ
|
|
||||||
|
|
||||||
|
|
||||||
def test_missing_ids_leave_env_untouched(monkeypatch):
|
|
||||||
monkeypatch.delenv("VLLM_CACHE_ROOT", raising=False)
|
|
||||||
with scoped_spawn_device_env(None, None):
|
|
||||||
assert "VLLM_CACHE_ROOT" not in os.environ
|
|
||||||
@@ -743,48 +743,6 @@ class TestTTSMethods:
|
|||||||
assert speech_server._is_tts is False
|
assert speech_server._is_tts is False
|
||||||
assert speech_server._tts_stage is None
|
assert speech_server._tts_stage is None
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
("model_type", "expected_speed"),
|
|
||||||
[("indextts2_5", 1.0), ("qwen3_tts", 2.0)],
|
|
||||||
)
|
|
||||||
def test_audio_encode_speed_respects_adapter_native_control(
|
|
||||||
self,
|
|
||||||
speech_server,
|
|
||||||
model_type,
|
|
||||||
expected_speed,
|
|
||||||
):
|
|
||||||
speech_server._tts_model_type = model_type
|
|
||||||
request = OpenAICreateSpeechRequest(input="Hello", speed=2.0)
|
|
||||||
|
|
||||||
assert speech_server._audio_encode_speed(request) == expected_speed
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_non_streaming_native_speed_skips_generic_audio_adjustment(
|
|
||||||
self,
|
|
||||||
speech_server,
|
|
||||||
mocker: MockerFixture,
|
|
||||||
):
|
|
||||||
async def mock_generate():
|
|
||||||
yield create_mock_audio_output_for_test()
|
|
||||||
|
|
||||||
speech_server._tts_model_type = "indextts2_5"
|
|
||||||
mocker.patch.object(
|
|
||||||
speech_server,
|
|
||||||
"_prepare_speech_generation",
|
|
||||||
new=mocker.AsyncMock(return_value=("speech-native-speed", mock_generate(), {})),
|
|
||||||
)
|
|
||||||
create_audio = mocker.patch.object(
|
|
||||||
speech_server,
|
|
||||||
"create_audio",
|
|
||||||
return_value=SimpleNamespace(audio_data=b"dummy", media_type="audio/wav"),
|
|
||||||
)
|
|
||||||
|
|
||||||
await speech_server._generate_audio_bytes(OpenAICreateSpeechRequest(input="Hello", speed=2.0))
|
|
||||||
|
|
||||||
audio_obj = create_audio.call_args.args[0]
|
|
||||||
assert isinstance(audio_obj, CreateAudio)
|
|
||||||
assert audio_obj.speed == 1.0
|
|
||||||
|
|
||||||
def test_is_tts_detection_with_tts_stage(self, mocker: MockerFixture):
|
def test_is_tts_detection_with_tts_stage(self, mocker: MockerFixture):
|
||||||
"""Test TTS model detection when TTS stage exists."""
|
"""Test TTS model detection when TTS stage exists."""
|
||||||
mock_engine_client = mocker.MagicMock()
|
mock_engine_client = mocker.MagicMock()
|
||||||
@@ -2220,17 +2178,17 @@ class TestStreamingProtocolValidation:
|
|||||||
assert req.is_streaming() is False
|
assert req.is_streaming() is False
|
||||||
|
|
||||||
def test_stream_validation_errors(self):
|
def test_stream_validation_errors(self):
|
||||||
"""The request schema validates formats; model-aware speed checks happen in serving."""
|
"""stream=True requires response_format in ('pcm', 'wav') and speed=1.0."""
|
||||||
with pytest.raises(ValidationError, match="requires response_format='pcm' or 'wav'"):
|
with pytest.raises(ValidationError, match="requires response_format='pcm' or 'wav'"):
|
||||||
OpenAICreateSpeechRequest(input="Hello", stream=True, response_format="mp3")
|
OpenAICreateSpeechRequest(input="Hello", stream=True, response_format="mp3")
|
||||||
request = OpenAICreateSpeechRequest(input="Hello", stream=True, response_format="pcm", speed=2.0)
|
with pytest.raises(ValidationError, match="Speed adjustment is not supported"):
|
||||||
assert request.speed == 2.0
|
OpenAICreateSpeechRequest(input="Hello", stream=True, response_format="pcm", speed=2.0)
|
||||||
|
|
||||||
def test_stream_format_audio_validation_errors(self):
|
def test_stream_format_audio_validation_errors(self):
|
||||||
with pytest.raises(ValidationError, match="requires response_format='pcm' or 'wav'"):
|
with pytest.raises(ValidationError, match="requires response_format='pcm' or 'wav'"):
|
||||||
OpenAICreateSpeechRequest(input="Hello", stream_format="audio", response_format="mp3")
|
OpenAICreateSpeechRequest(input="Hello", stream_format="audio", response_format="mp3")
|
||||||
request = OpenAICreateSpeechRequest(input="Hello", stream_format="audio", response_format="pcm", speed=2.0)
|
with pytest.raises(ValidationError, match="Speed adjustment is not supported"):
|
||||||
assert request.speed == 2.0
|
OpenAICreateSpeechRequest(input="Hello", stream_format="audio", response_format="pcm", speed=2.0)
|
||||||
|
|
||||||
def test_stream_valid(self):
|
def test_stream_valid(self):
|
||||||
"""stream=True + response_format in ('pcm', 'wav') + speed=1.0 is accepted as SSE."""
|
"""stream=True + response_format in ('pcm', 'wav') + speed=1.0 is accepted as SSE."""
|
||||||
@@ -2313,18 +2271,6 @@ class TestStreamingResponse:
|
|||||||
models=mock_models,
|
models=mock_models,
|
||||||
request_logger=mocker.MagicMock(),
|
request_logger=mocker.MagicMock(),
|
||||||
)
|
)
|
||||||
speech_server._tts_model_type = None
|
|
||||||
speech_server._adapter = None
|
|
||||||
speech_server._diffusion_mode = False
|
|
||||||
mocker.patch.object(speech_server, "_uses_native_speed_control", return_value=False)
|
|
||||||
mocker.patch.object(
|
|
||||||
speech_server,
|
|
||||||
"create_error_response",
|
|
||||||
side_effect=lambda message, **_: JSONResponse(
|
|
||||||
status_code=400,
|
|
||||||
content={"error": {"message": message}},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
original_create_speech = speech_server.create_speech
|
original_create_speech = speech_server.create_speech
|
||||||
sig = signature(original_create_speech)
|
sig = signature(original_create_speech)
|
||||||
@@ -2339,7 +2285,6 @@ class TestStreamingResponse:
|
|||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
app.add_api_route("/v1/audio/speech", speech_server.create_speech, methods=["POST"], response_model=None)
|
app.add_api_route("/v1/audio/speech", speech_server.create_speech, methods=["POST"], response_model=None)
|
||||||
app.state.speech_server = speech_server
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -2448,39 +2393,6 @@ class TestStreamingResponse:
|
|||||||
assert response.status_code in (400, 422)
|
assert response.status_code in (400, 422)
|
||||||
assert "audio/" not in response.headers.get("content-type", "")
|
assert "audio/" not in response.headers.get("content-type", "")
|
||||||
|
|
||||||
@pytest.mark.parametrize("stream_format", ["sse", "audio"])
|
|
||||||
def test_native_speed_control_accepts_streaming_speed(
|
|
||||||
self,
|
|
||||||
streaming_app,
|
|
||||||
mocker: MockerFixture,
|
|
||||||
stream_format: str,
|
|
||||||
):
|
|
||||||
speech_server = streaming_app.state.speech_server
|
|
||||||
speech_server._tts_model_type = "indextts2_5"
|
|
||||||
speech_server._uses_native_speed_control.return_value = True
|
|
||||||
|
|
||||||
async def prepare(request, request_id=None, **kwargs):
|
|
||||||
del kwargs
|
|
||||||
generator = speech_server.engine_client.generate(prompt={}, request_id=request_id)
|
|
||||||
return request_id, generator, {"duration_factor": [1.0 / request.speed]}
|
|
||||||
|
|
||||||
mocker.patch.object(speech_server, "_prepare_speech_generation", side_effect=prepare)
|
|
||||||
|
|
||||||
client = TestClient(streaming_app)
|
|
||||||
response = client.post(
|
|
||||||
"/v1/audio/speech",
|
|
||||||
json={
|
|
||||||
"input": "Hello",
|
|
||||||
"stream_format": stream_format,
|
|
||||||
"response_format": "pcm",
|
|
||||||
"speed": 2.0,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 200
|
|
||||||
expected_media_type = "text/event-stream" if stream_format == "sse" else "audio/pcm"
|
|
||||||
assert expected_media_type in response.headers["content-type"]
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def erroring_streaming_app(self, mocker: MockerFixture):
|
def erroring_streaming_app(self, mocker: MockerFixture):
|
||||||
"""Test app whose mock engine raises mid-stream, to exercise the SSE error event."""
|
"""Test app whose mock engine raises mid-stream, to exercise the SSE error event."""
|
||||||
@@ -2704,18 +2616,6 @@ def test_streaming_speech_session_config_accepts_non_streaming_mode():
|
|||||||
assert config.non_streaming_mode is True
|
assert config.non_streaming_mode is True
|
||||||
|
|
||||||
|
|
||||||
def test_streaming_speech_session_config_scopes_native_speed_to_http():
|
|
||||||
with pytest.raises(
|
|
||||||
ValidationError,
|
|
||||||
match="native speed control is only available through HTTP streaming",
|
|
||||||
):
|
|
||||||
StreamingSpeechSessionConfig(
|
|
||||||
stream_audio=True,
|
|
||||||
response_format="pcm",
|
|
||||||
speed=1.5,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TestAsyncOmniSupportedTasks:
|
class TestAsyncOmniSupportedTasks:
|
||||||
"""Test that AsyncOmni reports correct supported tasks based on output modalities."""
|
"""Test that AsyncOmni reports correct supported tasks based on output modalities."""
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,6 @@
|
|||||||
Pure-Python registry/resolution logic; no model or GPU resources are loaded.
|
Pure-Python registry/resolution logic; no model or GPU resources are loaded.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
|
||||||
from types import SimpleNamespace
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from vllm_omni.entrypoints.openai.tts_adapters import (
|
from vllm_omni.entrypoints.openai.tts_adapters import (
|
||||||
@@ -14,19 +11,9 @@ from vllm_omni.entrypoints.openai.tts_adapters import (
|
|||||||
ARTTSAdapter,
|
ARTTSAdapter,
|
||||||
DiffusionTTSAdapter,
|
DiffusionTTSAdapter,
|
||||||
all_tts_model_types,
|
all_tts_model_types,
|
||||||
detect_tts_model_type,
|
|
||||||
resolve_adapter,
|
resolve_adapter,
|
||||||
)
|
)
|
||||||
from vllm_omni.entrypoints.openai.tts_adapters.indextts2 import (
|
|
||||||
IndexTTS2Adapter,
|
|
||||||
IndexTTS25Adapter,
|
|
||||||
indextts2_conditioning_cache_salt,
|
|
||||||
)
|
|
||||||
from vllm_omni.entrypoints.openai.tts_adapters.qwen3_tts import Qwen3TTSAdapter
|
from vllm_omni.entrypoints.openai.tts_adapters.qwen3_tts import Qwen3TTSAdapter
|
||||||
from vllm_omni.model_executor.models.indextts2 import prompt_utils
|
|
||||||
from vllm_omni.model_executor.models.indextts2.tokenizer_v2_5 import (
|
|
||||||
INDEXTTS25_TOKENIZER_FILE,
|
|
||||||
)
|
|
||||||
|
|
||||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||||
|
|
||||||
@@ -48,8 +35,6 @@ EXPECTED_MODEL_TYPES = {
|
|||||||
"higgs_audio_v3",
|
"higgs_audio_v3",
|
||||||
"glm_tts",
|
"glm_tts",
|
||||||
"step_audio2",
|
"step_audio2",
|
||||||
"indextts2",
|
|
||||||
"indextts2_5",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -99,113 +84,6 @@ def test_qwen3_tts_metadata():
|
|||||||
assert issubclass(Qwen3TTSAdapter, ARTTSAdapter)
|
assert issubclass(Qwen3TTSAdapter, ARTTSAdapter)
|
||||||
|
|
||||||
|
|
||||||
def test_indextts_adapters_are_versioned():
|
|
||||||
assert resolve_adapter("indextts2") is IndexTTS2Adapter
|
|
||||||
assert resolve_adapter("indextts2_5") is IndexTTS25Adapter
|
|
||||||
assert IndexTTS25Adapter.stage_keys == frozenset({"indextts2_5_talker"})
|
|
||||||
assert detect_tts_model_type("indextts2_5_talker", None) == "indextts2_5"
|
|
||||||
|
|
||||||
|
|
||||||
def test_indextts25_validates_explicit_language():
|
|
||||||
adapter = IndexTTS25Adapter(type("Context", (), {"server": object()})())
|
|
||||||
|
|
||||||
assert adapter._validate_extra_params({"lang": "ja"}) is None
|
|
||||||
assert "Unsupported IndexTTS 2.5 language" in adapter._validate_extra_params({"lang": "xx-invalid"})
|
|
||||||
|
|
||||||
|
|
||||||
def _indextts25_adapter_and_request(*, speed: float):
|
|
||||||
server = SimpleNamespace(
|
|
||||||
uploaded_speakers={},
|
|
||||||
_validate_ref_audio_format=lambda ref_audio: None,
|
|
||||||
)
|
|
||||||
adapter = IndexTTS25Adapter(SimpleNamespace(server=server))
|
|
||||||
request = SimpleNamespace(
|
|
||||||
input="hello",
|
|
||||||
voice="alloy",
|
|
||||||
ref_audio=object(),
|
|
||||||
max_new_tokens=None,
|
|
||||||
extra_params=None,
|
|
||||||
speed=speed,
|
|
||||||
)
|
|
||||||
return adapter, request
|
|
||||||
|
|
||||||
|
|
||||||
def test_indextts25_uses_native_speed_control_duration_factor():
|
|
||||||
adapter, fast_request = _indextts25_adapter_and_request(speed=2.0)
|
|
||||||
_, slow_request = _indextts25_adapter_and_request(speed=0.5)
|
|
||||||
|
|
||||||
assert adapter.native_speed_control is True
|
|
||||||
assert adapter.validate(fast_request) is None
|
|
||||||
assert adapter.validate(slow_request) is None
|
|
||||||
assert asyncio.run(adapter._build_params(fast_request))["duration_factor"] == [0.5]
|
|
||||||
assert asyncio.run(adapter._build_params(slow_request))["duration_factor"] == [2.0]
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("speed", [0.49, 2.01])
|
|
||||||
def test_indextts25_rejects_out_of_range_native_speed(speed):
|
|
||||||
adapter, request = _indextts25_adapter_and_request(speed=speed)
|
|
||||||
|
|
||||||
assert adapter.validate(request) == "IndexTTS 2.5 speed must be between 0.5 and 2.0"
|
|
||||||
|
|
||||||
|
|
||||||
def test_indextts25_speed_does_not_change_conditioning_cache_salt():
|
|
||||||
adapter, fast_request = _indextts25_adapter_and_request(speed=2.0)
|
|
||||||
_, slow_request = _indextts25_adapter_and_request(speed=0.5)
|
|
||||||
slow_request.ref_audio = fast_request.ref_audio
|
|
||||||
|
|
||||||
fast_params = asyncio.run(adapter._build_params(fast_request))
|
|
||||||
slow_params = asyncio.run(adapter._build_params(slow_request))
|
|
||||||
|
|
||||||
assert indextts2_conditioning_cache_salt(
|
|
||||||
fast_request,
|
|
||||||
fast_params,
|
|
||||||
) == indextts2_conditioning_cache_salt(slow_request, slow_params)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
("hf_config", "expected_tokenizer_file"),
|
|
||||||
[
|
|
||||||
(SimpleNamespace(tokenizer_file="custom-tokenizer.tiktoken"), "custom-tokenizer.tiktoken"),
|
|
||||||
(SimpleNamespace(), INDEXTTS25_TOKENIZER_FILE),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_indextts25_build_uses_configured_tokenizer_file(
|
|
||||||
monkeypatch,
|
|
||||||
hf_config,
|
|
||||||
expected_tokenizer_file,
|
|
||||||
):
|
|
||||||
captured = {}
|
|
||||||
|
|
||||||
def fake_estimate(*args, **kwargs):
|
|
||||||
captured.update(kwargs)
|
|
||||||
return 4
|
|
||||||
|
|
||||||
async def fake_build_params(request):
|
|
||||||
return {"lang": ["en"], "text_normalization": [True]}
|
|
||||||
|
|
||||||
monkeypatch.setattr(
|
|
||||||
prompt_utils,
|
|
||||||
"estimate_indextts2_prefill_prompt_len",
|
|
||||||
fake_estimate,
|
|
||||||
)
|
|
||||||
server = SimpleNamespace(
|
|
||||||
engine_client=SimpleNamespace(
|
|
||||||
model_config=SimpleNamespace(
|
|
||||||
model="/model",
|
|
||||||
hf_config=hf_config,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
adapter = IndexTTS25Adapter(SimpleNamespace(server=server))
|
|
||||||
monkeypatch.setattr(adapter, "_build_params", fake_build_params)
|
|
||||||
request = SimpleNamespace(input="hello", ref_audio=None)
|
|
||||||
|
|
||||||
prepared = asyncio.run(adapter.build(request, [], False))
|
|
||||||
|
|
||||||
assert prepared.prompt["prompt_token_ids"] == [1] * 4
|
|
||||||
assert captured["tokenizer_file"] == expected_tokenizer_file
|
|
||||||
|
|
||||||
|
|
||||||
def test_diffusion_adapter_extra_body_params_fallback():
|
def test_diffusion_adapter_extra_body_params_fallback():
|
||||||
class _DiffAdapter(DiffusionTTSAdapter):
|
class _DiffAdapter(DiffusionTTSAdapter):
|
||||||
name = "diff_probe"
|
name = "diff_probe"
|
||||||
|
|||||||
@@ -149,7 +149,6 @@ _PIPELINE_STAGES = [
|
|||||||
"glm_tts_dit",
|
"glm_tts_dit",
|
||||||
"higgs_audio_v2",
|
"higgs_audio_v2",
|
||||||
"higgs_audio_v3",
|
"higgs_audio_v3",
|
||||||
"indextts2_5_talker",
|
|
||||||
"indextts2_s2mel_decoder",
|
"indextts2_s2mel_decoder",
|
||||||
"indextts2_talker",
|
"indextts2_talker",
|
||||||
"latent_generator",
|
"latent_generator",
|
||||||
@@ -214,23 +213,18 @@ _CLAIMED_ARCH_CASES = [
|
|||||||
("fused_thinker_talker", "Qwen3TTSForConditionalGeneration"),
|
("fused_thinker_talker", "Qwen3TTSForConditionalGeneration"),
|
||||||
]
|
]
|
||||||
|
|
||||||
# The frozen ladder can only be compared over stages it knew about. New
|
_REACHABLE = list(itertools.product(_STAGES, _UNCLAIMED_ARCHS)) + _CLAIMED_ARCH_CASES
|
||||||
# adapter-owned stages are covered by their adapter tests and by the registry
|
|
||||||
# completeness checks below.
|
|
||||||
_NEW_ADAPTER_STAGE_KEYS = all_tts_stage_keys() - _LEGACY_TTS_MODEL_STAGES
|
|
||||||
_LEGACY_REACHABLE_STAGES = [stage for stage in _STAGES if stage not in _NEW_ADAPTER_STAGE_KEYS]
|
|
||||||
_REACHABLE = list(itertools.product(_LEGACY_REACHABLE_STAGES, _UNCLAIMED_ARCHS)) + _CLAIMED_ARCH_CASES
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(("model_stage", "model_arch"), _REACHABLE)
|
@pytest.mark.parametrize(("model_stage", "model_arch"), _REACHABLE)
|
||||||
def test_detection_matches_legacy_ladder(model_stage, model_arch):
|
def test_detection_matches_legacy_ladder(model_stage, model_arch):
|
||||||
"""Registry detection is identical to the ladder it replaced.
|
"""Registry detection is identical to the ladder it replaced.
|
||||||
|
|
||||||
Covers every stage the frozen ladder knew about, plus non-TTS pipeline
|
Covers every ``model_stage`` any in-tree pipeline emits (``_PIPELINE_STAGES``,
|
||||||
stages and negatives, against every architecture no adapter claims. New
|
plus negatives) against every architecture no adapter claims, and each
|
||||||
adapter-owned stages are tested by their adapter suites. Pairing a claimed
|
claimed architecture against the stage keys it really ships with. Pairing a
|
||||||
architecture with an unrelated model's stage key is excluded because no
|
claimed architecture with an unrelated model's stage key is excluded because
|
||||||
pipeline can produce it, and is covered separately by
|
no pipeline can produce it, and is covered separately by
|
||||||
``test_arch_matching_is_a_fallback_not_an_override``.
|
``test_arch_matching_is_a_fallback_not_an_override``.
|
||||||
"""
|
"""
|
||||||
assert detect_tts_model_type(model_stage, model_arch) == _legacy_detect(model_stage, model_arch)
|
assert detect_tts_model_type(model_stage, model_arch) == _legacy_detect(model_stage, model_arch)
|
||||||
@@ -257,7 +251,7 @@ def test_arch_matching_is_a_fallback_not_an_override():
|
|||||||
|
|
||||||
def test_stage_keys_cover_legacy_stage_set():
|
def test_stage_keys_cover_legacy_stage_set():
|
||||||
"""No stage key was dropped when the module constants were deleted."""
|
"""No stage key was dropped when the module constants were deleted."""
|
||||||
assert _LEGACY_TTS_MODEL_STAGES <= all_tts_stage_keys()
|
assert all_tts_stage_keys() == frozenset(_LEGACY_TTS_MODEL_STAGES)
|
||||||
|
|
||||||
|
|
||||||
def test_pipeline_stage_list_is_complete():
|
def test_pipeline_stage_list_is_complete():
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from PIL import Image
|
|
||||||
|
|
||||||
from examples.offline_inference.image_to_video.image_to_video import build_image_to_video_prompt
|
|
||||||
from examples.offline_inference.text_to_image.text_to_image import build_text_to_image_prompt
|
|
||||||
|
|
||||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
("negative_prompt", "expected_negative_prompt"),
|
|
||||||
[(None, None), ("", ""), ("blurry", "blurry")],
|
|
||||||
)
|
|
||||||
def test_text_to_image_builds_canonical_prompt(
|
|
||||||
negative_prompt: str | None,
|
|
||||||
expected_negative_prompt: str | None,
|
|
||||||
) -> None:
|
|
||||||
result = build_text_to_image_prompt("a red fox", negative_prompt)
|
|
||||||
|
|
||||||
assert result["prompt"] == "a red fox"
|
|
||||||
assert result["modalities"] == ["image"]
|
|
||||||
if expected_negative_prompt is None:
|
|
||||||
assert "negative_prompt" not in result
|
|
||||||
else:
|
|
||||||
assert result["negative_prompt"] == expected_negative_prompt
|
|
||||||
|
|
||||||
|
|
||||||
def test_image_to_video_builds_canonical_prompt() -> None:
|
|
||||||
image = Image.new("RGB", (32, 16), "red")
|
|
||||||
|
|
||||||
result = build_image_to_video_prompt(
|
|
||||||
prompt="the fox turns toward the camera",
|
|
||||||
negative_prompt="flicker",
|
|
||||||
media_inputs={"image": image},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result == {
|
|
||||||
"prompt": "the fox turns toward the camera",
|
|
||||||
"modalities": ["video"],
|
|
||||||
"multi_modal_data": {"image": image},
|
|
||||||
"negative_prompt": "flicker",
|
|
||||||
}
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
import os
|
|
||||||
import re
|
|
||||||
import subprocess
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[3]
|
|
||||||
SCRIPT = ROOT / "examples/online_serving/text_to_speech/indextts2/run_server.sh"
|
|
||||||
README = ROOT / "examples/online_serving/text_to_speech/README.md"
|
|
||||||
MACHINE_SPECIFIC_HOME_PATH = re.compile(r"(?:/home|/Users)/[^/\s`]+/")
|
|
||||||
|
|
||||||
|
|
||||||
def run_server(
|
|
||||||
tmp_path: Path,
|
|
||||||
*,
|
|
||||||
model_version: str | None = None,
|
|
||||||
model: str | None = None,
|
|
||||||
) -> tuple[subprocess.CompletedProcess[str], Path]:
|
|
||||||
argv_file = tmp_path / "vllm.argv"
|
|
||||||
stub = tmp_path / "vllm"
|
|
||||||
stub.write_text(
|
|
||||||
'#!/bin/bash\nprintf "%s\\0" "$@" > "$VLLM_ARGV_FILE"\n',
|
|
||||||
encoding="utf-8",
|
|
||||||
)
|
|
||||||
stub.chmod(0o755)
|
|
||||||
|
|
||||||
env = os.environ.copy()
|
|
||||||
for name in ("MODEL_VERSION", "MODEL", "DEPLOY_CONFIG", "PORT"):
|
|
||||||
env.pop(name, None)
|
|
||||||
env["PATH"] = f"{tmp_path}{os.pathsep}{env['PATH']}"
|
|
||||||
env["VLLM_ARGV_FILE"] = str(argv_file)
|
|
||||||
|
|
||||||
if model_version is not None:
|
|
||||||
env["MODEL_VERSION"] = model_version
|
|
||||||
if model is not None:
|
|
||||||
env["MODEL"] = model
|
|
||||||
result = subprocess.run(
|
|
||||||
["bash", str(SCRIPT)],
|
|
||||||
cwd=ROOT,
|
|
||||||
env=env,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
check=False,
|
|
||||||
)
|
|
||||||
return result, argv_file
|
|
||||||
|
|
||||||
|
|
||||||
def read_argv(argv_file: Path) -> list[str]:
|
|
||||||
return argv_file.read_bytes().decode().rstrip("\0").split("\0")
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("model", [None, ""])
|
|
||||||
def test_version_2_5_requires_model_before_invoking_vllm(
|
|
||||||
tmp_path: Path,
|
|
||||||
model: str | None,
|
|
||||||
) -> None:
|
|
||||||
result, argv_file = run_server(tmp_path, model_version="2.5", model=model)
|
|
||||||
|
|
||||||
assert result.returncode == 2
|
|
||||||
assert "MODEL=/path/to/native/bundle" in result.stderr
|
|
||||||
assert not argv_file.exists()
|
|
||||||
|
|
||||||
|
|
||||||
def test_version_2_5_selects_deploy_config(
|
|
||||||
tmp_path: Path,
|
|
||||||
) -> None:
|
|
||||||
model = "/models/indextts-2.5"
|
|
||||||
result, argv_file = run_server(
|
|
||||||
tmp_path,
|
|
||||||
model_version="2.5",
|
|
||||||
model=model,
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result.returncode == 0, result.stderr
|
|
||||||
argv = read_argv(argv_file)
|
|
||||||
assert argv[:2] == ["serve", model]
|
|
||||||
deploy_config_index = argv.index("--deploy-config") + 1
|
|
||||||
assert Path(argv[deploy_config_index]) == ROOT / "vllm_omni/deploy/indextts2_5.yaml"
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("model_version", [None, "2.0"])
|
|
||||||
def test_default_and_version_2_0_use_huggingface_model(
|
|
||||||
tmp_path: Path,
|
|
||||||
model_version: str | None,
|
|
||||||
) -> None:
|
|
||||||
result, argv_file = run_server(tmp_path, model_version=model_version)
|
|
||||||
|
|
||||||
assert result.returncode == 0, result.stderr
|
|
||||||
argv = read_argv(argv_file)
|
|
||||||
assert argv[:2] == ["serve", "IndexTeam/IndexTTS-2"]
|
|
||||||
deploy_config_index = argv.index("--deploy-config") + 1
|
|
||||||
assert Path(argv[deploy_config_index]) == ROOT / "vllm_omni/deploy/indextts2.yaml"
|
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_model_version_exits_before_invoking_vllm(tmp_path: Path) -> None:
|
|
||||||
result, argv_file = run_server(tmp_path, model_version="3.0")
|
|
||||||
|
|
||||||
assert result.returncode == 2
|
|
||||||
assert "MODEL_VERSION must be 2.0 or 2.5" in result.stderr
|
|
||||||
assert not argv_file.exists()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("path", [SCRIPT, README])
|
|
||||||
def test_public_example_does_not_contain_machine_specific_home_path(path: Path) -> None:
|
|
||||||
assert MACHINE_SPECIFIC_HOME_PATH.search(path.read_text(encoding="utf-8")) is None
|
|
||||||
Whitespace-only changes.
@@ -1,89 +0,0 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import yaml
|
|
||||||
|
|
||||||
from vllm_omni.config.pipeline_registry import OMNI_PIPELINES
|
|
||||||
from vllm_omni.model_executor.models.indextts2.configuration_indextts2 import (
|
|
||||||
IndexTTS2Config,
|
|
||||||
IndexTTS25Config,
|
|
||||||
)
|
|
||||||
from vllm_omni.model_executor.models.indextts2.pipeline import INDEXTTS25_PIPELINE
|
|
||||||
|
|
||||||
|
|
||||||
def test_indextts25_defaults_are_distinct_from_v2():
|
|
||||||
v2 = IndexTTS2Config()
|
|
||||||
v25 = IndexTTS25Config()
|
|
||||||
|
|
||||||
assert v2.model_type == "indextts2"
|
|
||||||
assert v2.gpt["number_text_tokens"] == 12000
|
|
||||||
assert v2.gpt["condition_num_latent"] == 32
|
|
||||||
assert v2.use_gpt_latent is True
|
|
||||||
assert v2.semantic_codec_type == "repcodec"
|
|
||||||
assert v2.vocab_size == v2.gpt["number_mel_codes"]
|
|
||||||
|
|
||||||
assert v25.model_type == "indextts2_5"
|
|
||||||
assert v25.gpt["number_text_tokens"] == 60509
|
|
||||||
assert v25.gpt["condition_num_latent"] == 32
|
|
||||||
assert v25.use_gpt_latent is False
|
|
||||||
assert v25.semantic_codec_type == "enhanced"
|
|
||||||
assert v25.semantic_codec_checkpoint == "codec.pth"
|
|
||||||
assert v25.tokenizer_file == "multilingual_zh_ja_yue_char_del.tiktoken"
|
|
||||||
assert v25.vocab_size == v25.gpt["number_mel_codes"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_indextts25_explicit_overrides_win_over_defaults():
|
|
||||||
config = IndexTTS25Config(
|
|
||||||
use_gpt_latent=True,
|
|
||||||
gpt={
|
|
||||||
"number_text_tokens": 70000,
|
|
||||||
"condition_num_latent": 2,
|
|
||||||
},
|
|
||||||
vocab_size=9000,
|
|
||||||
)
|
|
||||||
|
|
||||||
assert config.use_gpt_latent is True
|
|
||||||
assert config.gpt["number_text_tokens"] == 70000
|
|
||||||
assert config.gpt["condition_num_latent"] == 2
|
|
||||||
assert config.gpt["model_dim"] == config.hidden_size
|
|
||||||
assert config.vocab_size == 9000
|
|
||||||
|
|
||||||
|
|
||||||
def test_indextts25_pipeline_is_registered_with_two_distinct_stages():
|
|
||||||
assert OMNI_PIPELINES["indextts2_5"] is INDEXTTS25_PIPELINE
|
|
||||||
assert INDEXTTS25_PIPELINE.default_deploy_config_name == "indextts2_5.yaml"
|
|
||||||
assert [stage.model_stage for stage in INDEXTTS25_PIPELINE.stages] == [
|
|
||||||
"indextts2_5_talker",
|
|
||||||
"indextts2_5_s2mel_decoder",
|
|
||||||
]
|
|
||||||
assert [stage.model_arch for stage in INDEXTTS25_PIPELINE.stages] == [
|
|
||||||
None,
|
|
||||||
"IndexTTS25S2MelDecoder",
|
|
||||||
]
|
|
||||||
assert all("tokenizer" not in stage.extras for stage in INDEXTTS25_PIPELINE.stages)
|
|
||||||
|
|
||||||
|
|
||||||
def test_indextts25_default_deploy_selects_validated_triton_backend():
|
|
||||||
deploy_path = Path(__file__).parents[4] / "vllm_omni" / "deploy" / "indextts2_5.yaml"
|
|
||||||
config = yaml.safe_load(deploy_path.read_text(encoding="utf-8"))
|
|
||||||
stage0 = next(stage for stage in config["stages"] if stage["stage_id"] == 0)
|
|
||||||
|
|
||||||
assert stage0["attention_backend"] == "TRITON_ATTN"
|
|
||||||
assert stage0["enable_chunked_prefill"] is False
|
|
||||||
|
|
||||||
|
|
||||||
def test_indextts25_deploy_configs_keep_latent_policy_in_sync():
|
|
||||||
deploy_dir = Path(__file__).parents[4] / "vllm_omni" / "deploy"
|
|
||||||
expected = {"indextts2_5.yaml": False}
|
|
||||||
for filename, use_gpt_latent in expected.items():
|
|
||||||
with open(deploy_dir / filename, encoding="utf-8") as deploy_file:
|
|
||||||
config = yaml.safe_load(deploy_file)
|
|
||||||
assert config["pipeline"] == "indextts2_5"
|
|
||||||
assert config["async_chunk"] is False
|
|
||||||
assert all("tokenizer" not in stage for stage in config["stages"])
|
|
||||||
assert [stage["hf_overrides"]["use_gpt_latent"] for stage in config["stages"]] == [
|
|
||||||
use_gpt_latent,
|
|
||||||
use_gpt_latent,
|
|
||||||
]
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
||||||
|
|
||||||
from types import SimpleNamespace
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from vllm_omni.model_executor.models.indextts2.dit_cuda_graph import (
|
|
||||||
CUDAGraphDiTRunner,
|
|
||||||
)
|
|
||||||
|
|
||||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
|
||||||
|
|
||||||
|
|
||||||
class _CopySpy:
|
|
||||||
def __init__(self):
|
|
||||||
self.sources = []
|
|
||||||
|
|
||||||
def copy_(self, source):
|
|
||||||
self.sources.append(source)
|
|
||||||
return self
|
|
||||||
|
|
||||||
|
|
||||||
class _GraphSpy:
|
|
||||||
def __init__(self):
|
|
||||||
self.replays = 0
|
|
||||||
|
|
||||||
def replay(self):
|
|
||||||
self.replays += 1
|
|
||||||
|
|
||||||
|
|
||||||
def test_replay_only_updates_dynamic_inputs_and_reuses_static_output():
|
|
||||||
runner = CUDAGraphDiTRunner(transformer=object())
|
|
||||||
graph = _GraphSpy()
|
|
||||||
static_x = _CopySpy()
|
|
||||||
static_c = _CopySpy()
|
|
||||||
static_pos = _CopySpy()
|
|
||||||
static_mask = _CopySpy()
|
|
||||||
static_out = object()
|
|
||||||
entry = SimpleNamespace(
|
|
||||||
graph=graph,
|
|
||||||
static_x=static_x,
|
|
||||||
static_c=static_c,
|
|
||||||
static_pos=static_pos,
|
|
||||||
static_mask=static_mask,
|
|
||||||
static_out=static_out,
|
|
||||||
)
|
|
||||||
x_in = object()
|
|
||||||
c = object()
|
|
||||||
|
|
||||||
output = runner._replay(entry, x_in=x_in, c=c)
|
|
||||||
|
|
||||||
assert static_x.sources == [x_in]
|
|
||||||
assert static_c.sources == [c]
|
|
||||||
assert static_pos.sources == []
|
|
||||||
assert static_mask.sources == []
|
|
||||||
assert graph.replays == 1
|
|
||||||
assert output is static_out
|
|
||||||
|
|
||||||
|
|
||||||
def test_graph_stats_start_empty_and_remain_bounded():
|
|
||||||
runner = CUDAGraphDiTRunner(transformer=object(), max_graphs=4)
|
|
||||||
|
|
||||||
stats = runner.stats_snapshot()
|
|
||||||
stats["hits"] = 99
|
|
||||||
|
|
||||||
assert runner.stats_snapshot() == {
|
|
||||||
"calls": 0,
|
|
||||||
"hits": 0,
|
|
||||||
"captures": 0,
|
|
||||||
"capture_failures": 0,
|
|
||||||
"evictions": 0,
|
|
||||||
"eager": 0,
|
|
||||||
"cache_size": 0,
|
|
||||||
}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
||||||
|
|
||||||
import torch
|
|
||||||
|
|
||||||
from vllm_omni.model_executor.models.indextts2 import codec as codec_module
|
|
||||||
from vllm_omni.model_executor.models.indextts2 import preprocess_utils
|
|
||||||
from vllm_omni.model_executor.models.indextts2.codec import EnhancedCodec
|
|
||||||
|
|
||||||
|
|
||||||
def test_enhanced_codec_decode_restores_hidden_size_and_doubles_time():
|
|
||||||
codec = EnhancedCodec(
|
|
||||||
codebook_size=8,
|
|
||||||
hidden_size=4,
|
|
||||||
codebook_dim=2,
|
|
||||||
vocos_dim=4,
|
|
||||||
vocos_intermediate_dim=8,
|
|
||||||
vocos_num_layers=1,
|
|
||||||
downsample_scale=2,
|
|
||||||
).eval()
|
|
||||||
|
|
||||||
decoded = codec.decode(torch.tensor([[0, 1, 2]]))
|
|
||||||
|
|
||||||
assert decoded.shape == (1, 6, 4)
|
|
||||||
|
|
||||||
|
|
||||||
def test_enhanced_codec_inference_module_does_not_allocate_unused_encoder():
|
|
||||||
codec = EnhancedCodec(
|
|
||||||
codebook_size=8,
|
|
||||||
hidden_size=4,
|
|
||||||
codebook_dim=2,
|
|
||||||
vocos_dim=4,
|
|
||||||
vocos_intermediate_dim=8,
|
|
||||||
vocos_num_layers=1,
|
|
||||||
)
|
|
||||||
|
|
||||||
parameter_names = set(dict(codec.named_parameters()))
|
|
||||||
assert not any(name.startswith("encoder.") for name in parameter_names)
|
|
||||||
assert not any(name.startswith("down.") for name in parameter_names)
|
|
||||||
assert any(name.startswith("decoder.") for name in parameter_names)
|
|
||||||
assert any(name.startswith("quantizer.") for name in parameter_names)
|
|
||||||
|
|
||||||
|
|
||||||
def test_semantic_codec_loader_selects_enhanced_checkpoint(tmp_path, monkeypatch):
|
|
||||||
checkpoint = tmp_path / "codec.pth"
|
|
||||||
checkpoint.touch()
|
|
||||||
seen = {}
|
|
||||||
|
|
||||||
class FakeCodec:
|
|
||||||
def load_checkpoint(self, path):
|
|
||||||
seen["path"] = path
|
|
||||||
|
|
||||||
def to(self, **kwargs):
|
|
||||||
seen["to"] = kwargs
|
|
||||||
return self
|
|
||||||
|
|
||||||
def eval(self):
|
|
||||||
return self
|
|
||||||
|
|
||||||
def parameters(self):
|
|
||||||
return ()
|
|
||||||
|
|
||||||
monkeypatch.setattr(codec_module, "EnhancedCodec", lambda **kwargs: FakeCodec())
|
|
||||||
preprocess_utils._semantic_codec_cache.clear()
|
|
||||||
|
|
||||||
codec = preprocess_utils.load_semantic_codec(
|
|
||||||
str(tmp_path),
|
|
||||||
{"codebook_size": 8192},
|
|
||||||
torch.device("cpu"),
|
|
||||||
codec_type="enhanced",
|
|
||||||
checkpoint_name="codec.pth",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert isinstance(codec, FakeCodec)
|
|
||||||
assert seen["path"] == str(checkpoint)
|
|
||||||
assert seen["to"] == {"device": torch.device("cpu"), "dtype": torch.float32}
|
|
||||||
|
|
||||||
|
|
||||||
def test_model_asset_resolver_supports_native_checkpoints_layout(tmp_path):
|
|
||||||
checkpoint = tmp_path / "checkpoints" / "codec.pth"
|
|
||||||
checkpoint.parent.mkdir()
|
|
||||||
checkpoint.touch()
|
|
||||||
|
|
||||||
assert preprocess_utils.resolve_model_file(str(tmp_path), "codec.pth") == str(checkpoint)
|
|
||||||
@@ -1,202 +0,0 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
||||||
|
|
||||||
import importlib.util
|
|
||||||
from pathlib import Path
|
|
||||||
from types import SimpleNamespace
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from vllm_omni.model_executor.models.indextts2 import prompt_utils
|
|
||||||
from vllm_omni.model_executor.models.indextts2.tokenizer_v2_5 import (
|
|
||||||
INDEXTTS25_TOKENIZER_FILE,
|
|
||||||
)
|
|
||||||
|
|
||||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
|
||||||
|
|
||||||
|
|
||||||
def _load_indextts2_offline_example():
|
|
||||||
example_path = (
|
|
||||||
Path(__file__).resolve().parents[4]
|
|
||||||
/ "examples"
|
|
||||||
/ "offline_inference"
|
|
||||||
/ "text_to_speech"
|
|
||||||
/ "indextts2"
|
|
||||||
/ "end2end.py"
|
|
||||||
)
|
|
||||||
spec = importlib.util.spec_from_file_location(
|
|
||||||
"indextts2_offline_example_test",
|
|
||||||
example_path,
|
|
||||||
)
|
|
||||||
assert spec is not None and spec.loader is not None
|
|
||||||
module = importlib.util.module_from_spec(spec)
|
|
||||||
spec.loader.exec_module(module)
|
|
||||||
return module
|
|
||||||
|
|
||||||
|
|
||||||
def test_offline_main_uses_resolved_stage0_tokenizer_file(monkeypatch, tmp_path):
|
|
||||||
monkeypatch.setenv("VLLM_WORKER_MULTIPROC_METHOD", "spawn")
|
|
||||||
example = _load_indextts2_offline_example()
|
|
||||||
stage0 = SimpleNamespace(engine_args=SimpleNamespace(hf_overrides={"tokenizer_file": "custom-tokenizer.tiktoken"}))
|
|
||||||
fake_omni = SimpleNamespace(
|
|
||||||
stage_configs=[stage0],
|
|
||||||
generate=lambda *args, **kwargs: [],
|
|
||||||
)
|
|
||||||
captured = {}
|
|
||||||
|
|
||||||
monkeypatch.setattr(example, "Omni", lambda **kwargs: fake_omni)
|
|
||||||
monkeypatch.setattr(example, "SamplingParams", lambda **kwargs: kwargs)
|
|
||||||
monkeypatch.setattr(
|
|
||||||
example,
|
|
||||||
"build_request",
|
|
||||||
lambda **kwargs: captured.update(kwargs) or {"additional_information": {}},
|
|
||||||
)
|
|
||||||
|
|
||||||
example.main(
|
|
||||||
SimpleNamespace(
|
|
||||||
model="/model",
|
|
||||||
model_version="2.5",
|
|
||||||
deploy_config="/deploy/custom.yaml",
|
|
||||||
use_gpt_latent=False,
|
|
||||||
stage_init_timeout=300,
|
|
||||||
seed=42,
|
|
||||||
output_dir=str(tmp_path),
|
|
||||||
text="hello",
|
|
||||||
ref_audio=None,
|
|
||||||
emo_audio=None,
|
|
||||||
emo_text=None,
|
|
||||||
emo_vector=None,
|
|
||||||
emo_alpha=None,
|
|
||||||
use_emo_text=False,
|
|
||||||
use_random=False,
|
|
||||||
lang="en",
|
|
||||||
text_normalization=True,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
assert captured["tokenizer_file"] == "custom-tokenizer.tiktoken"
|
|
||||||
|
|
||||||
|
|
||||||
def test_indextts25_prompt_len_uses_three_conditioning_tokens(monkeypatch):
|
|
||||||
captured = {}
|
|
||||||
|
|
||||||
def fake_prepare(*args, **kwargs):
|
|
||||||
captured.update(kwargs)
|
|
||||||
return [11, 12, 13, 14], 0
|
|
||||||
|
|
||||||
monkeypatch.setattr(
|
|
||||||
prompt_utils,
|
|
||||||
"prepare_indextts25_text",
|
|
||||||
fake_prepare,
|
|
||||||
)
|
|
||||||
|
|
||||||
prompt_len = prompt_utils.estimate_indextts2_prefill_prompt_len(
|
|
||||||
"/model",
|
|
||||||
"hello",
|
|
||||||
model_type="indextts2_5",
|
|
||||||
lang="en",
|
|
||||||
text_normalization=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Official prepare_gpt_inputs wraps the language-prefixed tokenizer IDs
|
|
||||||
# with start_text=0 and stop_text=1 before appending start_mel.
|
|
||||||
assert prompt_len == 3 + (4 + 2) + 1
|
|
||||||
assert captured["text_normalization"] is False
|
|
||||||
assert captured["tokenizer_file"] == INDEXTTS25_TOKENIZER_FILE
|
|
||||||
|
|
||||||
|
|
||||||
def test_indextts25_prompt_len_uses_custom_tokenizer_file(monkeypatch):
|
|
||||||
captured = {}
|
|
||||||
|
|
||||||
def fake_prepare(*args, **kwargs):
|
|
||||||
captured.update(kwargs)
|
|
||||||
return [11, 12], 0
|
|
||||||
|
|
||||||
monkeypatch.setattr(
|
|
||||||
prompt_utils,
|
|
||||||
"prepare_indextts25_text",
|
|
||||||
fake_prepare,
|
|
||||||
)
|
|
||||||
|
|
||||||
prompt_utils.estimate_indextts2_prefill_prompt_len(
|
|
||||||
"/model",
|
|
||||||
"hello",
|
|
||||||
model_type="indextts2_5",
|
|
||||||
tokenizer_file="custom-tokenizer.tiktoken",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert captured["tokenizer_file"] == "custom-tokenizer.tiktoken"
|
|
||||||
|
|
||||||
|
|
||||||
def test_indextts25_prompt_len_filters_existing_text_wrapper_ids(monkeypatch):
|
|
||||||
monkeypatch.setattr(
|
|
||||||
prompt_utils,
|
|
||||||
"prepare_indextts25_text",
|
|
||||||
lambda *args, **kwargs: ([0, 58838, 1, 42, 0], 0),
|
|
||||||
)
|
|
||||||
|
|
||||||
prompt_len = prompt_utils.estimate_indextts2_prefill_prompt_len(
|
|
||||||
"/model",
|
|
||||||
"hello",
|
|
||||||
model_type="indextts2_5",
|
|
||||||
lang="en",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert prompt_len == 3 + (2 + 2) + 1
|
|
||||||
|
|
||||||
|
|
||||||
def test_indextts2_prompt_len_ignores_v25_tokenizer_file(monkeypatch):
|
|
||||||
class FakeTokenizer:
|
|
||||||
def encode(self, text, *, add_special_tokens):
|
|
||||||
assert text == "hello"
|
|
||||||
assert add_special_tokens is False
|
|
||||||
return [11, 12]
|
|
||||||
|
|
||||||
def fail_prepare(*args, **kwargs):
|
|
||||||
raise AssertionError("IndexTTS 2 must not use the IndexTTS 2.5 tokenizer")
|
|
||||||
|
|
||||||
monkeypatch.setattr(
|
|
||||||
prompt_utils,
|
|
||||||
"_get_text_tokenizer",
|
|
||||||
lambda model_id_or_path: FakeTokenizer(),
|
|
||||||
)
|
|
||||||
monkeypatch.setattr(
|
|
||||||
prompt_utils,
|
|
||||||
"prepare_indextts25_text",
|
|
||||||
fail_prepare,
|
|
||||||
)
|
|
||||||
|
|
||||||
prompt_len = prompt_utils.estimate_indextts2_prefill_prompt_len(
|
|
||||||
"/model",
|
|
||||||
"hello",
|
|
||||||
model_type="indextts2",
|
|
||||||
tokenizer_file="custom.tiktoken",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert prompt_len == 34 + (2 + 2) + 1
|
|
||||||
|
|
||||||
|
|
||||||
def test_indextts25_prompt_ids_preserve_placeholder_value(monkeypatch):
|
|
||||||
captured = {}
|
|
||||||
|
|
||||||
def fake_estimate(*args, **kwargs):
|
|
||||||
captured.update(kwargs)
|
|
||||||
return 10
|
|
||||||
|
|
||||||
monkeypatch.setattr(
|
|
||||||
prompt_utils,
|
|
||||||
"estimate_indextts2_prefill_prompt_len",
|
|
||||||
fake_estimate,
|
|
||||||
)
|
|
||||||
|
|
||||||
prompt_ids = prompt_utils.build_indextts2_prefill_prompt_ids(
|
|
||||||
"/model",
|
|
||||||
"hello",
|
|
||||||
model_type="indextts2_5",
|
|
||||||
lang="en",
|
|
||||||
tokenizer_file="custom-tokenizer.tiktoken",
|
|
||||||
placeholder_token_id=17,
|
|
||||||
)
|
|
||||||
|
|
||||||
assert prompt_ids == [17] * 10
|
|
||||||
assert captured["tokenizer_file"] == "custom-tokenizer.tiktoken"
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import yaml
|
|
||||||
|
|
||||||
|
|
||||||
def test_indextts25_default_recipe_enables_validated_cfm_batching():
|
|
||||||
recipe_path = Path(__file__).parents[4] / "vllm_omni" / "deploy" / "indextts2_5.yaml"
|
|
||||||
recipe = yaml.safe_load(recipe_path.read_text(encoding="utf-8"))
|
|
||||||
stage1 = next(stage for stage in recipe["stages"] if stage["stage_id"] == 1)
|
|
||||||
|
|
||||||
assert stage1["hf_overrides"]["s2mel_cfm_batch_size"] == 4
|
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -1,53 +0,0 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
||||||
|
|
||||||
from types import SimpleNamespace
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
import torch
|
|
||||||
|
|
||||||
from vllm_omni.model_executor.stage_input_processors.indextts2 import (
|
|
||||||
STOP_MEL_TOKEN,
|
|
||||||
talker2s2mel_full_payload,
|
|
||||||
)
|
|
||||||
|
|
||||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
|
||||||
|
|
||||||
|
|
||||||
def test_no_latent_full_payload_uses_sampled_output_ids_and_strips_stop():
|
|
||||||
request = SimpleNamespace(
|
|
||||||
request_id="req-1",
|
|
||||||
output_token_ids=[17, 23, STOP_MEL_TOKEN],
|
|
||||||
sampling_params=SimpleNamespace(seed=42),
|
|
||||||
)
|
|
||||||
payload = {
|
|
||||||
"meta": {
|
|
||||||
"use_gpt_latent": False,
|
|
||||||
"S_ref": torch.ones(1, 4),
|
|
||||||
"ref_mel": torch.ones(1, 80, 3),
|
|
||||||
"style": torch.ones(1, 192),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
result = talker2s2mel_full_payload(None, payload, request)
|
|
||||||
|
|
||||||
assert result is not None
|
|
||||||
assert result["mel_codes"].tolist() == [[17, 23]]
|
|
||||||
assert result["code_lens"].tolist() == [2]
|
|
||||||
assert result["use_gpt_latent"] is False
|
|
||||||
assert result["seed"] == 42
|
|
||||||
|
|
||||||
|
|
||||||
def test_no_latent_full_payload_rejects_legacy_payload_codes():
|
|
||||||
request = SimpleNamespace(
|
|
||||||
request_id="legacy",
|
|
||||||
output_token_ids=[],
|
|
||||||
sampling_params=SimpleNamespace(seed=None),
|
|
||||||
)
|
|
||||||
payload = {
|
|
||||||
"codes": {"mel": torch.tensor([31, 37, STOP_MEL_TOKEN])},
|
|
||||||
"meta": {"use_gpt_latent": False},
|
|
||||||
}
|
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="no completed output_token_ids"):
|
|
||||||
talker2s2mel_full_payload(None, payload, request)
|
|
||||||
@@ -1,476 +0,0 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
||||||
|
|
||||||
from types import SimpleNamespace
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
import torch
|
|
||||||
from vllm.config import CompilationMode
|
|
||||||
|
|
||||||
from vllm_omni.model_executor.models.indextts2.indextts2_talker import (
|
|
||||||
IndexTTS2TalkerForConditionalGeneration,
|
|
||||||
_normalize_indextts_checkpoint_key,
|
|
||||||
_require_exact_indextts_prefill_span,
|
|
||||||
resolve_indextts_conditioning_policy,
|
|
||||||
)
|
|
||||||
|
|
||||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
|
||||||
|
|
||||||
|
|
||||||
def test_v25_conditioning_policy():
|
|
||||||
policy = resolve_indextts_conditioning_policy("indextts2_5")
|
|
||||||
|
|
||||||
assert policy.prefix_tokens == 3
|
|
||||||
assert policy.uses_conformer_perceiver is False
|
|
||||||
assert policy.uses_campplus_projection is True
|
|
||||||
assert policy.uses_language_embedding is True
|
|
||||||
|
|
||||||
|
|
||||||
def test_v2_conditioning_policy_is_unchanged():
|
|
||||||
policy = resolve_indextts_conditioning_policy("indextts2")
|
|
||||||
|
|
||||||
assert policy.prefix_tokens == 34
|
|
||||||
assert policy.uses_conformer_perceiver is True
|
|
||||||
assert policy.uses_campplus_projection is False
|
|
||||||
assert policy.uses_language_embedding is False
|
|
||||||
|
|
||||||
|
|
||||||
def test_v25_text_tokens_wrap_language_prefixed_ids_with_start_and_stop(
|
|
||||||
monkeypatch,
|
|
||||||
):
|
|
||||||
import vllm_omni.model_executor.models.indextts2.text_processing_v2_5 as text_processing
|
|
||||||
|
|
||||||
monkeypatch.setattr(
|
|
||||||
text_processing,
|
|
||||||
"prepare_indextts25_text",
|
|
||||||
lambda *args, **kwargs: ([58838, 42], 7),
|
|
||||||
)
|
|
||||||
talker = object.__new__(IndexTTS2TalkerForConditionalGeneration)
|
|
||||||
talker.conditioning_policy = resolve_indextts_conditioning_policy("indextts2_5")
|
|
||||||
talker.model_path = "/model"
|
|
||||||
talker.config = SimpleNamespace(tokenizer_file="tokenizer.tiktoken")
|
|
||||||
|
|
||||||
token_ids, language_id = talker._tokenize_text(
|
|
||||||
"hello",
|
|
||||||
torch.device("cpu"),
|
|
||||||
lang="ja",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert token_ids.tolist() == [[0, 58838, 42, 1]]
|
|
||||||
assert language_id.tolist() == [7]
|
|
||||||
|
|
||||||
|
|
||||||
def test_v25_text_tokens_filter_existing_start_and_stop_ids(monkeypatch):
|
|
||||||
import vllm_omni.model_executor.models.indextts2.text_processing_v2_5 as text_processing
|
|
||||||
|
|
||||||
monkeypatch.setattr(
|
|
||||||
text_processing,
|
|
||||||
"prepare_indextts25_text",
|
|
||||||
lambda *args, **kwargs: ([0, 58838, 1, 42, 0], 7),
|
|
||||||
)
|
|
||||||
talker = object.__new__(IndexTTS2TalkerForConditionalGeneration)
|
|
||||||
talker.conditioning_policy = resolve_indextts_conditioning_policy("indextts2_5")
|
|
||||||
talker.model_path = "/model"
|
|
||||||
talker.config = SimpleNamespace(tokenizer_file="tokenizer.tiktoken")
|
|
||||||
|
|
||||||
token_ids, language_id = talker._tokenize_text(
|
|
||||||
"hello",
|
|
||||||
torch.device("cpu"),
|
|
||||||
lang="en",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert token_ids.tolist() == [[0, 58838, 42, 1]]
|
|
||||||
assert language_id.tolist() == [7]
|
|
||||||
|
|
||||||
|
|
||||||
def test_native_fsdp_checkpoint_keys_are_normalized_at_every_nesting_level():
|
|
||||||
assert _normalize_indextts_checkpoint_key("_fsdp_wrapped_module.spk_emb_proj.weight") == "spk_emb_proj.weight"
|
|
||||||
assert (
|
|
||||||
_normalize_indextts_checkpoint_key("_fsdp_wrapped_module.mel_embedding._fsdp_wrapped_module.weight")
|
|
||||||
== "mel_embedding.weight"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_prefill_span_mismatch_fails_instead_of_padding_or_truncating():
|
|
||||||
for actual_span_len in (9, 11):
|
|
||||||
with pytest.raises(
|
|
||||||
ValueError,
|
|
||||||
match="model_type=indextts2_5.*lang=es.*text_token_count=6",
|
|
||||||
):
|
|
||||||
_require_exact_indextts_prefill_span(
|
|
||||||
actual_span_len=actual_span_len,
|
|
||||||
scheduled_span_len=10,
|
|
||||||
model_type="indextts2_5",
|
|
||||||
lang="es",
|
|
||||||
text_token_count=6,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_matching_prefill_span_is_accepted():
|
|
||||||
_require_exact_indextts_prefill_span(
|
|
||||||
actual_span_len=10,
|
|
||||||
scheduled_span_len=10,
|
|
||||||
model_type="indextts2_5",
|
|
||||||
lang="en",
|
|
||||||
text_token_count=6,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _make_minimal_talker(
|
|
||||||
monkeypatch,
|
|
||||||
*,
|
|
||||||
model_type,
|
|
||||||
use_gpt_latent,
|
|
||||||
):
|
|
||||||
import vllm_omni.model_executor.models.indextts2.indextts2_talker as talker_module
|
|
||||||
|
|
||||||
seen = {}
|
|
||||||
|
|
||||||
def fake_parallel_lm_head(vocab_size, hidden_size, *, bias=False, **_kwargs):
|
|
||||||
seen["bias"] = bias
|
|
||||||
return torch.nn.Linear(hidden_size, vocab_size, bias=bias)
|
|
||||||
|
|
||||||
monkeypatch.setattr(talker_module, "ParallelLMHead", fake_parallel_lm_head)
|
|
||||||
monkeypatch.setattr(
|
|
||||||
talker_module,
|
|
||||||
"make_layers",
|
|
||||||
lambda *_args, **_kwargs: (
|
|
||||||
0,
|
|
||||||
1,
|
|
||||||
torch.nn.ModuleList([torch.nn.Linear(8, 8)]),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
monkeypatch.setattr(
|
|
||||||
talker_module,
|
|
||||||
"get_pp_group",
|
|
||||||
lambda: SimpleNamespace(is_last_rank=lambda: True),
|
|
||||||
)
|
|
||||||
monkeypatch.setattr(talker_module, "get_speaker_cache", object)
|
|
||||||
monkeypatch.setattr(
|
|
||||||
talker_module,
|
|
||||||
"LogitsProcessor",
|
|
||||||
lambda *_args, **_kwargs: object(),
|
|
||||||
)
|
|
||||||
monkeypatch.setattr(
|
|
||||||
talker_module,
|
|
||||||
"ConformerEncoder",
|
|
||||||
lambda **_kwargs: torch.nn.Linear(1, 1),
|
|
||||||
)
|
|
||||||
monkeypatch.setattr(
|
|
||||||
talker_module,
|
|
||||||
"PerceiverResampler",
|
|
||||||
lambda *_args, **_kwargs: torch.nn.Linear(1, 1),
|
|
||||||
)
|
|
||||||
|
|
||||||
gpt = {
|
|
||||||
"model_dim": 8,
|
|
||||||
"layers": 0,
|
|
||||||
"heads": 1,
|
|
||||||
"max_mel_tokens": 4,
|
|
||||||
"max_text_tokens": 4,
|
|
||||||
"number_mel_codes": 16,
|
|
||||||
"start_mel_token": 14,
|
|
||||||
"stop_mel_token": 15,
|
|
||||||
"number_text_tokens": 32,
|
|
||||||
"condition_num_latent": 1,
|
|
||||||
"condition_module": {},
|
|
||||||
"emo_condition_module": {},
|
|
||||||
}
|
|
||||||
config = SimpleNamespace(
|
|
||||||
gpt=gpt,
|
|
||||||
model_type=model_type,
|
|
||||||
use_gpt_latent=use_gpt_latent,
|
|
||||||
gpt_checkpoint="gpt.pth",
|
|
||||||
)
|
|
||||||
vllm_config = SimpleNamespace(
|
|
||||||
model_config=SimpleNamespace(model="/model", hf_config=config),
|
|
||||||
cache_config=None,
|
|
||||||
quant_config=None,
|
|
||||||
compilation_config=SimpleNamespace(mode=CompilationMode.NONE),
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
IndexTTS2TalkerForConditionalGeneration(vllm_config=vllm_config),
|
|
||||||
seen,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
("model_type", "use_gpt_latent"),
|
|
||||||
[
|
|
||||||
("indextts2", True),
|
|
||||||
("indextts2_5", False),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_mel_head_preserves_official_checkpoint_bias_and_hidden_payload_policy(
|
|
||||||
monkeypatch,
|
|
||||||
model_type,
|
|
||||||
use_gpt_latent,
|
|
||||||
):
|
|
||||||
talker, seen = _make_minimal_talker(
|
|
||||||
monkeypatch,
|
|
||||||
model_type=model_type,
|
|
||||||
use_gpt_latent=use_gpt_latent,
|
|
||||||
)
|
|
||||||
|
|
||||||
assert seen["bias"] is True
|
|
||||||
assert talker.mel_head.bias is not None
|
|
||||||
assert talker.omni_pooler_payload_include_hidden is use_gpt_latent
|
|
||||||
assert talker.omni_payload_at_request_end is (model_type == "indextts2_5")
|
|
||||||
|
|
||||||
|
|
||||||
def test_v2_load_weights_loads_exact_mel_head_bias_parameter(monkeypatch):
|
|
||||||
import vllm_omni.model_executor.models.indextts2.indextts2_talker as talker_module
|
|
||||||
|
|
||||||
talker, _ = _make_minimal_talker(
|
|
||||||
monkeypatch,
|
|
||||||
model_type="indextts2",
|
|
||||||
use_gpt_latent=True,
|
|
||||||
)
|
|
||||||
checkpoint = {
|
|
||||||
name: parameter.detach().clone() for name, parameter in talker.named_parameters(remove_duplicate=False)
|
|
||||||
}
|
|
||||||
expected_bias = torch.arange(
|
|
||||||
talker.number_mel_codes,
|
|
||||||
dtype=talker.mel_head.bias.dtype,
|
|
||||||
)
|
|
||||||
checkpoint["mel_head.bias"] = expected_bias
|
|
||||||
with torch.no_grad():
|
|
||||||
talker.mel_head.bias.zero_()
|
|
||||||
|
|
||||||
monkeypatch.setattr(
|
|
||||||
talker_module,
|
|
||||||
"resolve_model_file",
|
|
||||||
lambda *_args, **_kwargs: "/model/gpt.pth",
|
|
||||||
)
|
|
||||||
monkeypatch.setattr(
|
|
||||||
talker_module.torch,
|
|
||||||
"load",
|
|
||||||
lambda *_args, **_kwargs: checkpoint,
|
|
||||||
)
|
|
||||||
monkeypatch.setattr(
|
|
||||||
talker_module,
|
|
||||||
"is_pp_missing_parameter",
|
|
||||||
lambda *_args, **_kwargs: False,
|
|
||||||
)
|
|
||||||
monkeypatch.setattr(
|
|
||||||
talker,
|
|
||||||
"_warmup_external_models",
|
|
||||||
lambda _device: None,
|
|
||||||
)
|
|
||||||
|
|
||||||
loaded = talker.load_weights([])
|
|
||||||
|
|
||||||
assert "mel_head.bias" in loaded
|
|
||||||
torch.testing.assert_close(talker.mel_head.bias, expected_bias)
|
|
||||||
|
|
||||||
|
|
||||||
def test_compute_logits_passes_mel_head_bias_to_logits_processor():
|
|
||||||
talker = object.__new__(IndexTTS2TalkerForConditionalGeneration)
|
|
||||||
torch.nn.Module.__init__(talker)
|
|
||||||
talker.mel_head = torch.nn.Linear(3, 4, bias=True)
|
|
||||||
hidden_states = torch.zeros(1, 3)
|
|
||||||
seen = {}
|
|
||||||
|
|
||||||
def fake_logits_processor(head, hidden, **kwargs):
|
|
||||||
seen["head"] = head
|
|
||||||
seen["hidden"] = hidden
|
|
||||||
seen.update(kwargs)
|
|
||||||
return hidden
|
|
||||||
|
|
||||||
talker.logits_processor = fake_logits_processor
|
|
||||||
|
|
||||||
result = talker.compute_logits(hidden_states)
|
|
||||||
|
|
||||||
assert result is hidden_states
|
|
||||||
assert seen["head"] is talker.mel_head
|
|
||||||
assert seen["hidden"] is hidden_states
|
|
||||||
assert seen["embedding_bias"] is talker.mel_head.bias
|
|
||||||
|
|
||||||
|
|
||||||
def _make_cached_v25_prefill_talker(monkeypatch):
|
|
||||||
talker, _ = _make_minimal_talker(
|
|
||||||
monkeypatch,
|
|
||||||
model_type="indextts2_5",
|
|
||||||
use_gpt_latent=False,
|
|
||||||
)
|
|
||||||
talker._speaker_cache = SimpleNamespace(
|
|
||||||
make_cache_key=lambda *_args: "cached-speaker",
|
|
||||||
get=lambda _key: {
|
|
||||||
"S_ref": torch.ones(1, 3, 8),
|
|
||||||
"ref_mel": torch.ones(1, 80, 3),
|
|
||||||
"style": torch.ones(1, 192),
|
|
||||||
"spk_cond_emb": torch.ones(1, 3, 8),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
monkeypatch.setattr(
|
|
||||||
talker,
|
|
||||||
"_compute_emotion_vector",
|
|
||||||
lambda **_kwargs: torch.zeros(1, talker.model_dim),
|
|
||||||
)
|
|
||||||
monkeypatch.setattr(
|
|
||||||
talker,
|
|
||||||
"_tokenize_text",
|
|
||||||
lambda *_args, **_kwargs: (torch.tensor([[1]]), None),
|
|
||||||
)
|
|
||||||
return talker
|
|
||||||
|
|
||||||
|
|
||||||
def test_prefill_preserves_duration_factor_in_metadata(monkeypatch):
|
|
||||||
talker = _make_cached_v25_prefill_talker(monkeypatch)
|
|
||||||
|
|
||||||
_, _, info_update = talker._preprocess_prefill(
|
|
||||||
torch.zeros(5, dtype=torch.long),
|
|
||||||
None,
|
|
||||||
{
|
|
||||||
"text": ["hello"],
|
|
||||||
"voice": ["/speaker.wav"],
|
|
||||||
"voice_name": ["cached"],
|
|
||||||
"duration_factor": [0.5],
|
|
||||||
},
|
|
||||||
span_len=5,
|
|
||||||
)
|
|
||||||
|
|
||||||
assert info_update["meta"]["duration_factor"] == 0.5
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("duration_factor", [0.49, 2.01])
|
|
||||||
def test_prefill_rejects_duration_factor_outside_supported_range(monkeypatch, duration_factor):
|
|
||||||
talker = _make_cached_v25_prefill_talker(monkeypatch)
|
|
||||||
|
|
||||||
with pytest.raises(
|
|
||||||
ValueError,
|
|
||||||
match="^IndexTTS 2.5 duration_factor must be between 0.5 and 2.0$",
|
|
||||||
):
|
|
||||||
talker._preprocess_prefill(
|
|
||||||
torch.zeros(5, dtype=torch.long),
|
|
||||||
None,
|
|
||||||
{
|
|
||||||
"text": ["hello"],
|
|
||||||
"voice": ["/speaker.wav"],
|
|
||||||
"voice_name": ["cached"],
|
|
||||||
"duration_factor": [duration_factor],
|
|
||||||
},
|
|
||||||
span_len=5,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_no_latent_sampled_token_payload_emits_only_first_step_metadata():
|
|
||||||
talker = object.__new__(IndexTTS2TalkerForConditionalGeneration)
|
|
||||||
talker.use_gpt_latent = False
|
|
||||||
hidden = torch.zeros(1, 8)
|
|
||||||
info = {
|
|
||||||
"codes": {"mel": torch.tensor([17])},
|
|
||||||
"meta": {
|
|
||||||
"mel_code_count": 1,
|
|
||||||
"S_ref": torch.ones(1, 4),
|
|
||||||
"ref_mel": torch.ones(1, 80, 3),
|
|
||||||
"style": torch.ones(1, 192),
|
|
||||||
"duration_factor": 0.5,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
first_decode_output = talker.make_omni_output(
|
|
||||||
hidden,
|
|
||||||
model_intermediate_buffer=[info],
|
|
||||||
)
|
|
||||||
|
|
||||||
assert "codes" not in first_decode_output.multimodal_outputs
|
|
||||||
assert "hidden_states" not in first_decode_output.multimodal_outputs
|
|
||||||
assert first_decode_output.multimodal_outputs["meta"][0]["S_ref"] is info["meta"]["S_ref"]
|
|
||||||
assert first_decode_output.multimodal_outputs["meta"][0]["duration_factor"] == 0.5
|
|
||||||
|
|
||||||
later = talker.make_omni_output(
|
|
||||||
hidden,
|
|
||||||
model_intermediate_buffer=[
|
|
||||||
{
|
|
||||||
"codes": {"mel": torch.tensor([23])},
|
|
||||||
"meta": {"mel_code_count": 2},
|
|
||||||
}
|
|
||||||
],
|
|
||||||
)
|
|
||||||
assert later.multimodal_outputs == {"meta": [{"use_gpt_latent": False}]}
|
|
||||||
|
|
||||||
|
|
||||||
def test_first_decode_payload_defaults_missing_duration_factor_to_one():
|
|
||||||
talker = object.__new__(IndexTTS2TalkerForConditionalGeneration)
|
|
||||||
talker.use_gpt_latent = False
|
|
||||||
hidden = torch.zeros(1, 8)
|
|
||||||
info = {
|
|
||||||
"codes": {"mel": torch.tensor([17])},
|
|
||||||
"meta": {
|
|
||||||
"mel_code_count": 1,
|
|
||||||
"S_ref": torch.ones(1, 4),
|
|
||||||
"ref_mel": torch.ones(1, 80, 3),
|
|
||||||
"style": torch.ones(1, 192),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
output = talker.make_omni_output(
|
|
||||||
hidden,
|
|
||||||
model_intermediate_buffer=[info],
|
|
||||||
)
|
|
||||||
|
|
||||||
assert output.multimodal_outputs["meta"][0]["duration_factor"] == 1.0
|
|
||||||
|
|
||||||
|
|
||||||
def test_latent_sampled_token_payload_keeps_aligned_gpu_deltas():
|
|
||||||
talker = object.__new__(IndexTTS2TalkerForConditionalGeneration)
|
|
||||||
talker.use_gpt_latent = True
|
|
||||||
hidden = torch.zeros(1, 8)
|
|
||||||
info = {
|
|
||||||
"codes": {"mel": torch.tensor([17])},
|
|
||||||
"meta": {
|
|
||||||
"mel_code_count": 1,
|
|
||||||
"latent_acc": torch.arange(8, dtype=torch.float32).reshape(1, 8),
|
|
||||||
"duration_factor": 1.0,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
output = talker.make_omni_output(
|
|
||||||
hidden,
|
|
||||||
model_intermediate_buffer=[info],
|
|
||||||
)
|
|
||||||
|
|
||||||
assert output.multimodal_outputs["codes"]["mel"][0].tolist() == [[17]]
|
|
||||||
assert output.multimodal_outputs["hidden_states"]["latent"][0].tolist() == [list(range(8))]
|
|
||||||
|
|
||||||
|
|
||||||
def test_v25_default_emotion_reuses_speaker_w2v_features(monkeypatch):
|
|
||||||
talker = object.__new__(IndexTTS2TalkerForConditionalGeneration)
|
|
||||||
speaker_features = torch.zeros(1, 4, 1024)
|
|
||||||
expected = torch.ones(1, 1280)
|
|
||||||
seen = {}
|
|
||||||
|
|
||||||
def fake_merge(spk_cond_emb, emo_cond_emb, alpha):
|
|
||||||
seen["spk"] = spk_cond_emb
|
|
||||||
seen["emo"] = emo_cond_emb
|
|
||||||
seen["alpha"] = alpha
|
|
||||||
return expected
|
|
||||||
|
|
||||||
monkeypatch.setattr(talker, "_merge_emovec", fake_merge)
|
|
||||||
|
|
||||||
result = talker._compute_emotion_vector(
|
|
||||||
wav_16k=None,
|
|
||||||
speaker_audio_path="/speaker.wav",
|
|
||||||
emo_audio_path=None,
|
|
||||||
emo_voice_name=None,
|
|
||||||
main_text="hello",
|
|
||||||
use_emo_text=False,
|
|
||||||
emo_text=None,
|
|
||||||
emo_vector=None,
|
|
||||||
emo_alpha=0.25,
|
|
||||||
use_random=False,
|
|
||||||
style=torch.zeros(1, 192),
|
|
||||||
spk_cond_emb=speaker_features,
|
|
||||||
w2v_model=None,
|
|
||||||
w2v_proc=None,
|
|
||||||
device=torch.device("cpu"),
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result is expected
|
|
||||||
assert seen == {
|
|
||||||
"spk": speaker_features,
|
|
||||||
"emo": speaker_features,
|
|
||||||
"alpha": 1.0,
|
|
||||||
}
|
|
||||||
@@ -1,244 +0,0 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
||||||
|
|
||||||
import builtins
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from vllm_omni.model_executor.models.indextts2 import text_processing_v2_5
|
|
||||||
from vllm_omni.model_executor.models.indextts2.text_processing_v2_5 import (
|
|
||||||
apply_pronunciation_annotations,
|
|
||||||
clean_indextts25_text,
|
|
||||||
prepare_indextts25_text,
|
|
||||||
)
|
|
||||||
from vllm_omni.model_executor.models.indextts2.tokenizer_v2_5 import (
|
|
||||||
INDEXTTS25_SPECIAL_TOKENS,
|
|
||||||
INDEXTTS25_VOCAB_SIZE,
|
|
||||||
LANGUAGE_DICT,
|
|
||||||
lang_to_token,
|
|
||||||
normalize_language_code,
|
|
||||||
resolve_indextts25_tokenizer_file,
|
|
||||||
)
|
|
||||||
|
|
||||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
|
||||||
|
|
||||||
|
|
||||||
def test_special_token_layout_matches_official_checkpoint_vocab():
|
|
||||||
assert len(INDEXTTS25_SPECIAL_TOKENS) == 1673
|
|
||||||
assert len(set(INDEXTTS25_SPECIAL_TOKENS)) == 1673
|
|
||||||
assert 58836 + len(INDEXTTS25_SPECIAL_TOKENS) == INDEXTTS25_VOCAB_SIZE
|
|
||||||
assert INDEXTTS25_VOCAB_SIZE == 60509
|
|
||||||
|
|
||||||
|
|
||||||
def test_pronunciation_annotations_select_english_and_chinese_markers():
|
|
||||||
assert apply_pronunciation_annotations("<going|G OW1 . IH0 NG>") == (
|
|
||||||
"<|SPECIAL_TOKEN_1|>G OW1 . IH0 NG<|SPECIAL_TOKEN_1|>"
|
|
||||||
)
|
|
||||||
assert apply_pronunciation_annotations("<行|xing2>") == ("<|SPECIAL_TOKEN_2|>XING2<|SPECIAL_TOKEN_2|>")
|
|
||||||
|
|
||||||
|
|
||||||
def test_prepare_text_applies_language_prefix_case_and_annotation(monkeypatch):
|
|
||||||
captured = {}
|
|
||||||
|
|
||||||
def fake_encode(text, *, model_dir, tokenizer_file):
|
|
||||||
captured["text"] = text
|
|
||||||
captured["model_dir"] = model_dir
|
|
||||||
captured["tokenizer_file"] = tokenizer_file
|
|
||||||
return [7, 8, 9]
|
|
||||||
|
|
||||||
monkeypatch.setattr(text_processing_v2_5, "encode_indextts25_text", fake_encode)
|
|
||||||
|
|
||||||
token_ids, lang_id = prepare_indextts25_text(
|
|
||||||
"HELLO <行|xing2>",
|
|
||||||
lang="zh",
|
|
||||||
model_dir="/model",
|
|
||||||
text_normalization=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
assert token_ids == [7, 8, 9]
|
|
||||||
assert lang_id == LANGUAGE_DICT["zh"]
|
|
||||||
assert captured == {
|
|
||||||
"text": "<|zh|> hello <|SPECIAL_TOKEN_2|>XING2<|SPECIAL_TOKEN_2|>",
|
|
||||||
"model_dir": "/model",
|
|
||||||
"tokenizer_file": "multilingual_zh_ja_yue_char_del.tiktoken",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_mandarin_alias_is_an_intentional_vllm_omni_convenience():
|
|
||||||
assert normalize_language_code("Mandarin") == "zh"
|
|
||||||
assert lang_to_token("mandarin") == LANGUAGE_DICT["zh"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_language_does_not_fall_back_to_common():
|
|
||||||
with pytest.raises(ValueError, match="Unsupported IndexTTS 2.5 language"):
|
|
||||||
lang_to_token("xx-invalid")
|
|
||||||
|
|
||||||
|
|
||||||
def test_zhen_uses_mixed_normalization():
|
|
||||||
calls = []
|
|
||||||
|
|
||||||
def fake_mixed_normalizer(text):
|
|
||||||
calls.append(text)
|
|
||||||
return "MIXED"
|
|
||||||
|
|
||||||
original = text_processing_v2_5._normalize_zh_or_en
|
|
||||||
text_processing_v2_5._normalize_zh_or_en = fake_mixed_normalizer
|
|
||||||
try:
|
|
||||||
result = text_processing_v2_5._normalize_with_official_backend(
|
|
||||||
"中文 and English",
|
|
||||||
"zhen",
|
|
||||||
)
|
|
||||||
finally:
|
|
||||||
text_processing_v2_5._normalize_zh_or_en = original
|
|
||||||
|
|
||||||
assert calls == ["中文 and English"]
|
|
||||||
assert result == "MIXED"
|
|
||||||
|
|
||||||
|
|
||||||
def test_spanish_normalization_does_not_hide_broken_dependencies(monkeypatch):
|
|
||||||
original_import = builtins.__import__
|
|
||||||
|
|
||||||
def controlled_import(name, *args, **kwargs):
|
|
||||||
if name == "nemo_text_processing.text_normalization.normalize":
|
|
||||||
raise RuntimeError("broken NeMo grammar")
|
|
||||||
return original_import(name, *args, **kwargs)
|
|
||||||
|
|
||||||
monkeypatch.setattr(builtins, "__import__", controlled_import)
|
|
||||||
|
|
||||||
with pytest.raises(RuntimeError, match="broken NeMo grammar"):
|
|
||||||
text_processing_v2_5._normalize_with_official_backend("hola", "es")
|
|
||||||
|
|
||||||
|
|
||||||
def test_zhen_keeps_literal_prefix_and_uses_common_embedding(monkeypatch):
|
|
||||||
captured = {}
|
|
||||||
|
|
||||||
def fake_encode(text, *, model_dir, tokenizer_file):
|
|
||||||
captured["text"] = text
|
|
||||||
return [7, 8, 9]
|
|
||||||
|
|
||||||
monkeypatch.setattr(text_processing_v2_5, "encode_indextts25_text", fake_encode)
|
|
||||||
|
|
||||||
token_ids, lang_id = prepare_indextts25_text(
|
|
||||||
"HELLO 中文",
|
|
||||||
lang="zhen",
|
|
||||||
model_dir="/model",
|
|
||||||
text_normalization=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
assert token_ids == [7, 8, 9]
|
|
||||||
assert captured["text"] == "<|zhen|> hello 中文"
|
|
||||||
assert lang_id == LANGUAGE_DICT["common"]
|
|
||||||
assert "<|zhen|>" not in INDEXTTS25_SPECIAL_TOKENS
|
|
||||||
assert "<|common|>" not in INDEXTTS25_SPECIAL_TOKENS
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
("text", "expected"),
|
|
||||||
[
|
|
||||||
(",,,", "…"),
|
|
||||||
("测试,,,结束", "测试…结束"),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_repeated_full_width_commas_use_longest_first_cleanup(text, expected):
|
|
||||||
assert clean_indextts25_text(text) == expected
|
|
||||||
|
|
||||||
|
|
||||||
def test_default_text_normalizer_is_shared_by_prompt_and_talker(monkeypatch):
|
|
||||||
calls = []
|
|
||||||
|
|
||||||
def fake_normalizer(text, lang):
|
|
||||||
calls.append((text, lang))
|
|
||||||
return "twenty five percent"
|
|
||||||
|
|
||||||
monkeypatch.setattr(
|
|
||||||
text_processing_v2_5,
|
|
||||||
"_normalize_with_official_backend",
|
|
||||||
fake_normalizer,
|
|
||||||
)
|
|
||||||
|
|
||||||
result = text_processing_v2_5.normalize_indextts25_text(
|
|
||||||
"25%",
|
|
||||||
lang="en",
|
|
||||||
text_normalization=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
assert calls == [("25%", "en")]
|
|
||||||
assert result == "twenty five percent"
|
|
||||||
|
|
||||||
|
|
||||||
def test_mixed_text_uses_official_content_routing_and_protection(monkeypatch):
|
|
||||||
calls = []
|
|
||||||
|
|
||||||
class FakeNormalizer:
|
|
||||||
def __init__(self, lang):
|
|
||||||
self.lang = lang
|
|
||||||
|
|
||||||
def normalize(self, text):
|
|
||||||
calls.append((self.lang, text))
|
|
||||||
return text.replace("<H>", " <H> ").replace("5", "five")
|
|
||||||
|
|
||||||
monkeypatch.setattr(
|
|
||||||
text_processing_v2_5,
|
|
||||||
"_load_wetext_normalizer",
|
|
||||||
lambda lang: FakeNormalizer(lang),
|
|
||||||
)
|
|
||||||
|
|
||||||
result = text_processing_v2_5._normalize_with_official_backend(
|
|
||||||
"GPT-5-nano 中文 xuan4",
|
|
||||||
"en",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert calls[0][0] == "zh"
|
|
||||||
assert result == "GPT-five-nano 中文 XVAN4"
|
|
||||||
|
|
||||||
|
|
||||||
def test_tokenizer_resolves_native_checkpoints_subdirectory(tmp_path):
|
|
||||||
tokenizer = tmp_path / "checkpoints" / "multilingual_zh_ja_yue_char_del.tiktoken"
|
|
||||||
tokenizer.parent.mkdir()
|
|
||||||
tokenizer.touch()
|
|
||||||
|
|
||||||
assert resolve_indextts25_tokenizer_file(str(tmp_path)) == str(tokenizer)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
("lang", "text", "expected"),
|
|
||||||
[
|
|
||||||
(
|
|
||||||
"es",
|
|
||||||
"En la actualidad se continúa utilizando una corona abierta.",
|
|
||||||
"<|es|> EN LA ACTUALIDAD SE CONTINÚA UTILIZANDO UNA CORONA ABIERTA.",
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"ar",
|
|
||||||
"فكلُّها خياراتٌ اتَّخذتَها بنفسِك.",
|
|
||||||
"<|ar|> فكلُّها خياراتٌ اتَّخذتَها بنفسِك.",
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_official_spanish_and_arabic_cases_preserve_expected_text_contract(
|
|
||||||
monkeypatch,
|
|
||||||
lang,
|
|
||||||
text,
|
|
||||||
expected,
|
|
||||||
):
|
|
||||||
captured = {}
|
|
||||||
|
|
||||||
def fake_encode(value, *, model_dir, tokenizer_file):
|
|
||||||
captured["text"] = value
|
|
||||||
return [1]
|
|
||||||
|
|
||||||
monkeypatch.setattr(
|
|
||||||
text_processing_v2_5,
|
|
||||||
"encode_indextts25_text",
|
|
||||||
fake_encode,
|
|
||||||
)
|
|
||||||
|
|
||||||
_, lang_id = prepare_indextts25_text(
|
|
||||||
text,
|
|
||||||
lang=lang,
|
|
||||||
model_dir="/model",
|
|
||||||
text_normalization=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
assert captured["text"] == expected
|
|
||||||
assert lang_id == LANGUAGE_DICT[lang]
|
|
||||||
@@ -1,196 +0,0 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
||||||
|
|
||||||
from types import SimpleNamespace
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
import torch
|
|
||||||
|
|
||||||
from vllm_omni.model_executor.models.indextts2.indextts2_s2mel_decoder import (
|
|
||||||
IndexTTS2S2MelDecoder,
|
|
||||||
)
|
|
||||||
from vllm_omni.model_executor.models.indextts2.s2mel.modules.wavenet import WN
|
|
||||||
|
|
||||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
|
||||||
|
|
||||||
|
|
||||||
def _make_wavenet() -> WN:
|
|
||||||
torch.manual_seed(0)
|
|
||||||
return WN(
|
|
||||||
hidden_channels=2,
|
|
||||||
kernel_size=5,
|
|
||||||
dilation_rate=1,
|
|
||||||
n_layers=8,
|
|
||||||
gin_channels=2,
|
|
||||||
p_dropout=0.0,
|
|
||||||
causal=False,
|
|
||||||
).eval()
|
|
||||||
|
|
||||||
|
|
||||||
def _sequence_mask(lengths: torch.Tensor, max_length: int) -> torch.Tensor:
|
|
||||||
positions = torch.arange(max_length, device=lengths.device)
|
|
||||||
return (positions.unsqueeze(0) < lengths.unsqueeze(1)).unsqueeze(1)
|
|
||||||
|
|
||||||
|
|
||||||
def _singleton_outputs(
|
|
||||||
wavenet: WN,
|
|
||||||
inputs: torch.Tensor,
|
|
||||||
lengths: list[int],
|
|
||||||
conditioning: torch.Tensor,
|
|
||||||
) -> list[torch.Tensor]:
|
|
||||||
# This is the independent oracle: every request ends at the physical tensor
|
|
||||||
# boundary and therefore uses SConv1d's original reflect-padding behavior.
|
|
||||||
wavenet._assume_full_mask = True
|
|
||||||
return [
|
|
||||||
wavenet(
|
|
||||||
inputs[row : row + 1, :, :length],
|
|
||||||
torch.ones(1, 1, length, dtype=torch.bool),
|
|
||||||
g=conditioning[row : row + 1],
|
|
||||||
)
|
|
||||||
for row, length in enumerate(lengths)
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def test_ragged_wavenet_matches_original_singleton_reflect_boundaries():
|
|
||||||
wavenet = _make_wavenet()
|
|
||||||
lengths = [6, 5, 4, 3]
|
|
||||||
inputs = torch.randn(len(lengths), 2, max(lengths))
|
|
||||||
conditioning = torch.randn(len(lengths), 2, 1)
|
|
||||||
expected = _singleton_outputs(wavenet, inputs, lengths, conditioning)
|
|
||||||
|
|
||||||
mask = _sequence_mask(torch.tensor(lengths), inputs.shape[-1])
|
|
||||||
wavenet._assume_full_mask = False
|
|
||||||
# Keep deliberately non-zero invalid tails. The first WaveNet layer sees
|
|
||||||
# transformer output before the recurrent mask has cleared those values.
|
|
||||||
actual = wavenet(inputs, mask, g=conditioning)
|
|
||||||
|
|
||||||
for row, (length, singleton) in enumerate(zip(lengths, expected)):
|
|
||||||
torch.testing.assert_close(
|
|
||||||
actual[row : row + 1, :, :length],
|
|
||||||
singleton,
|
|
||||||
rtol=1e-5,
|
|
||||||
atol=1e-6,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_wavenet_exposes_minimum_safe_ragged_length():
|
|
||||||
# kernel=5, dilation=1 has two right-reflect ghost positions, so a
|
|
||||||
# logical row must contain at least three frames before it can use the
|
|
||||||
# vectorized ragged path. Shorter rows stay on original singleton SConv1d.
|
|
||||||
assert _make_wavenet().ragged_min_length == 3
|
|
||||||
|
|
||||||
|
|
||||||
def test_cfm_grouping_isolates_rows_too_short_for_logical_reflect():
|
|
||||||
groups = IndexTTS2S2MelDecoder._cfm_group_indices(
|
|
||||||
sorted_indices=[0, 1, 2, 3, 4],
|
|
||||||
total_lengths=[0, 1, 2, 3, 6],
|
|
||||||
max_group_size=2,
|
|
||||||
min_ragged_length=3,
|
|
||||||
)
|
|
||||||
|
|
||||||
assert groups == [[0], [1], [2], [3, 4]]
|
|
||||||
|
|
||||||
|
|
||||||
def test_ragged_wavenet_preserves_cfg_cond_uncond_row_order():
|
|
||||||
wavenet = _make_wavenet()
|
|
||||||
base_lengths = [3, 6]
|
|
||||||
conditional = torch.randn(2, 2, 6)
|
|
||||||
unconditional = torch.randn(2, 2, 6)
|
|
||||||
cfg_inputs = torch.cat([conditional, unconditional], dim=0)
|
|
||||||
cfg_lengths = base_lengths + base_lengths
|
|
||||||
cfg_conditioning = torch.randn(4, 2, 1)
|
|
||||||
expected = _singleton_outputs(
|
|
||||||
wavenet,
|
|
||||||
cfg_inputs,
|
|
||||||
cfg_lengths,
|
|
||||||
cfg_conditioning,
|
|
||||||
)
|
|
||||||
|
|
||||||
mask = _sequence_mask(torch.tensor(cfg_lengths), cfg_inputs.shape[-1])
|
|
||||||
wavenet._assume_full_mask = False
|
|
||||||
actual = wavenet(cfg_inputs, mask, g=cfg_conditioning)
|
|
||||||
|
|
||||||
for row, (length, singleton) in enumerate(zip(cfg_lengths, expected)):
|
|
||||||
torch.testing.assert_close(
|
|
||||||
actual[row : row + 1, :, :length],
|
|
||||||
singleton,
|
|
||||||
rtol=1e-5,
|
|
||||||
atol=1e-6,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_full_mask_wavenet_path_is_bit_exact_with_original_path():
|
|
||||||
wavenet = _make_wavenet()
|
|
||||||
inputs = torch.randn(3, 2, 6)
|
|
||||||
mask = torch.ones(3, 1, 6, dtype=torch.bool)
|
|
||||||
conditioning = torch.randn(3, 2, 1)
|
|
||||||
|
|
||||||
if hasattr(wavenet, "_assume_full_mask"):
|
|
||||||
del wavenet._assume_full_mask
|
|
||||||
expected = wavenet(inputs, mask, g=conditioning)
|
|
||||||
|
|
||||||
wavenet._assume_full_mask = True
|
|
||||||
actual = wavenet(inputs, mask, g=conditioning)
|
|
||||||
|
|
||||||
assert torch.equal(actual, expected)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(not hasattr(torch, "compile"), reason="torch.compile unavailable")
|
|
||||||
def test_ragged_wavenet_is_one_dynamic_fullgraph():
|
|
||||||
wavenet = _make_wavenet()
|
|
||||||
lengths = [3, 6]
|
|
||||||
inputs = torch.randn(2, 2, 6)
|
|
||||||
conditioning = torch.randn(2, 2, 1)
|
|
||||||
mask = _sequence_mask(torch.tensor(lengths), inputs.shape[-1])
|
|
||||||
expected = _singleton_outputs(wavenet, inputs, lengths, conditioning)
|
|
||||||
wavenet._assume_full_mask = False
|
|
||||||
|
|
||||||
compiled = torch.compile(
|
|
||||||
wavenet,
|
|
||||||
backend="eager",
|
|
||||||
fullgraph=True,
|
|
||||||
dynamic=True,
|
|
||||||
)
|
|
||||||
actual = compiled(inputs, mask, g=conditioning)
|
|
||||||
|
|
||||||
for row, (length, singleton) in enumerate(zip(lengths, expected)):
|
|
||||||
torch.testing.assert_close(
|
|
||||||
actual[row : row + 1, :, :length],
|
|
||||||
singleton,
|
|
||||||
rtol=1e-5,
|
|
||||||
atol=1e-6,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_decoder_full_mask_switch_also_controls_wavenet_boundary_path():
|
|
||||||
decoder = object.__new__(IndexTTS2S2MelDecoder)
|
|
||||||
attention_layers = [SimpleNamespace(attention=SimpleNamespace()) for _ in range(2)]
|
|
||||||
estimator = SimpleNamespace(
|
|
||||||
transformer=SimpleNamespace(layers=attention_layers),
|
|
||||||
wavenet=SimpleNamespace(),
|
|
||||||
)
|
|
||||||
|
|
||||||
decoder._set_dit_full_mask_fast_path(estimator, enabled=False)
|
|
||||||
|
|
||||||
assert all(layer.attention._assume_full_mask is False for layer in attention_layers)
|
|
||||||
assert estimator.wavenet._assume_full_mask is False
|
|
||||||
|
|
||||||
decoder._set_dit_full_mask_fast_path(estimator, enabled=True)
|
|
||||||
|
|
||||||
assert all(layer.attention._assume_full_mask is True for layer in attention_layers)
|
|
||||||
assert estimator.wavenet._assume_full_mask is True
|
|
||||||
|
|
||||||
|
|
||||||
def test_decoder_full_mask_switch_unwraps_compiled_estimator():
|
|
||||||
decoder = object.__new__(IndexTTS2S2MelDecoder)
|
|
||||||
attention_layers = [SimpleNamespace(attention=SimpleNamespace()) for _ in range(2)]
|
|
||||||
eager_estimator = SimpleNamespace(
|
|
||||||
transformer=SimpleNamespace(layers=attention_layers),
|
|
||||||
wavenet=SimpleNamespace(),
|
|
||||||
)
|
|
||||||
compiled_estimator = SimpleNamespace(_orig_mod=eager_estimator)
|
|
||||||
|
|
||||||
decoder._set_dit_full_mask_fast_path(compiled_estimator, enabled=False)
|
|
||||||
|
|
||||||
assert all(layer.attention._assume_full_mask is False for layer in attention_layers)
|
|
||||||
assert eager_estimator.wavenet._assume_full_mask is False
|
|
||||||
@@ -285,17 +285,7 @@ def test_cached_apply_hf_processor_expands_pair_miss_for_audio_hit_video_miss():
|
|||||||
captured["mm_cache_keys"] = kwargs["mm_hashes"]
|
captured["mm_cache_keys"] = kwargs["mm_hashes"]
|
||||||
return MagicMock(name="mm_kwargs"), {"video": [[]], "audio": [[]]}
|
return MagicMock(name="mm_kwargs"), {"video": [[]], "audio": [[]]}
|
||||||
|
|
||||||
fake_self = SimpleNamespace(
|
fake_self = SimpleNamespace(cache=cache, info=SimpleNamespace(model_id="m"))
|
||||||
cache=cache,
|
|
||||||
info=SimpleNamespace(
|
|
||||||
model_id="m",
|
|
||||||
ctx=SimpleNamespace(
|
|
||||||
get_mm_config=lambda: SimpleNamespace(
|
|
||||||
mm_hasher_algorithm="sha256",
|
|
||||||
)
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
fake_self._get_hf_mm_data = MagicMock(return_value=({}, {}))
|
fake_self._get_hf_mm_data = MagicMock(return_value=({}, {}))
|
||||||
# Bound methods called via self inside _cached_apply_hf_processor.
|
# Bound methods called via self inside _cached_apply_hf_processor.
|
||||||
fake_self._get_audio_in_video_pairs = lambda mm_data_items, hf_processor_mm_kwargs: (
|
fake_self._get_audio_in_video_pairs = lambda mm_data_items, hf_processor_mm_kwargs: (
|
||||||
|
|||||||
@@ -1,175 +0,0 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
||||||
"""Regression tests for the Qwen3-Omni thinker forward return contract.
|
|
||||||
|
|
||||||
Background
|
|
||||||
----------
|
|
||||||
Since omni architectures unconditionally override upstream's registry entries,
|
|
||||||
``Qwen3OmniMoeForConditionalGeneration`` (and the standalone thinker) are also
|
|
||||||
constructed by plain, non-staged vLLM runs — e.g. ``vllm serve
|
|
||||||
Qwen/Qwen3-Omni-30B-A3B-Instruct`` in the nightly vLLM-text perf job. Stock
|
|
||||||
vLLM's ``GPUModelRunner`` indexes the forward output directly
|
|
||||||
(``hidden_states[logit_indices]``), so a thinker forward that always returns a
|
|
||||||
``(hidden_states, captured_hidden_states)`` tuple kills the engine during
|
|
||||||
``profile_run`` with ``TypeError: only integer tensors of a single element can
|
|
||||||
be converted to an index`` (Buildkite build 2952).
|
|
||||||
|
|
||||||
Contract under test:
|
|
||||||
|
|
||||||
- No capture requested → the forward returns a **bare tensor** (stock-runner
|
|
||||||
compatible).
|
|
||||||
- Capture requested (``capture_layer_indices`` + ``return_hidden_states``,
|
|
||||||
staged runs with a talker downstream) → the forward returns the
|
|
||||||
``(hidden_states, captured_layer_dict)`` tuple omni's runner consumes via
|
|
||||||
``make_omni_output``.
|
|
||||||
- The combined model requests capture only in staged runs
|
|
||||||
(``model_config.model_stage`` injected), never in plain vLLM runs.
|
|
||||||
- ``make_omni_output`` tolerates both return shapes.
|
|
||||||
|
|
||||||
Pure shape/contract tests: CPU-only, no weights, no GPU.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from types import SimpleNamespace
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
import torch
|
|
||||||
from torch import nn
|
|
||||||
from vllm.sequence import IntermediateTensors
|
|
||||||
|
|
||||||
from vllm_omni.model_executor.models.output_templates import OmniOutput
|
|
||||||
from vllm_omni.model_executor.models.qwen3_omni.qwen3_omni import (
|
|
||||||
Qwen3OmniMoeForConditionalGeneration,
|
|
||||||
)
|
|
||||||
from vllm_omni.model_executor.models.qwen3_omni.qwen3_omni_moe_thinker import (
|
|
||||||
Qwen3OmniMoeThinkerForConditionalGeneration,
|
|
||||||
)
|
|
||||||
|
|
||||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
|
||||||
|
|
||||||
_HIDDEN = 8
|
|
||||||
_TOKENS = 4
|
|
||||||
|
|
||||||
|
|
||||||
def _hidden_states() -> torch.Tensor:
|
|
||||||
return torch.zeros(_TOKENS, _HIDDEN)
|
|
||||||
|
|
||||||
|
|
||||||
class _InnerModelStub:
|
|
||||||
"""Stands in for ``Qwen3MoeLLMModel``: always returns the internal 2-tuple."""
|
|
||||||
|
|
||||||
def __init__(self, output=None):
|
|
||||||
self.calls: list[dict] = []
|
|
||||||
self._output = output
|
|
||||||
|
|
||||||
def __call__(self, input_ids, positions, intermediate_tensors, **kwargs):
|
|
||||||
self.calls.append(kwargs)
|
|
||||||
if self._output is not None:
|
|
||||||
return self._output
|
|
||||||
if kwargs.get("return_hidden_states"):
|
|
||||||
return _hidden_states(), {"hidden_states": {"layers": {0: _hidden_states()}}}
|
|
||||||
return _hidden_states(), None
|
|
||||||
|
|
||||||
|
|
||||||
def _make_thinker(inner_output=None) -> tuple[Qwen3OmniMoeThinkerForConditionalGeneration, _InnerModelStub]:
|
|
||||||
thinker = object.__new__(Qwen3OmniMoeThinkerForConditionalGeneration)
|
|
||||||
nn.Module.__init__(thinker)
|
|
||||||
inner = _InnerModelStub(output=inner_output)
|
|
||||||
thinker.use_deepstack = False
|
|
||||||
thinker.language_model = SimpleNamespace(model=inner)
|
|
||||||
return thinker, inner
|
|
||||||
|
|
||||||
|
|
||||||
def _forward_args():
|
|
||||||
return {
|
|
||||||
"input_ids": torch.zeros(_TOKENS, dtype=torch.long),
|
|
||||||
"positions": torch.arange(_TOKENS),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_thinker_forward_returns_bare_tensor_without_capture():
|
|
||||||
thinker, _ = _make_thinker()
|
|
||||||
out = thinker.forward(**_forward_args())
|
|
||||||
assert isinstance(out, torch.Tensor)
|
|
||||||
assert out.shape == (_TOKENS, _HIDDEN)
|
|
||||||
|
|
||||||
|
|
||||||
def test_thinker_forward_returns_tuple_when_capturing():
|
|
||||||
thinker, _ = _make_thinker()
|
|
||||||
out = thinker.forward(
|
|
||||||
**_forward_args(),
|
|
||||||
capture_layer_indices=[0, 2],
|
|
||||||
return_hidden_states=True,
|
|
||||||
)
|
|
||||||
assert isinstance(out, tuple)
|
|
||||||
hidden, captured = out
|
|
||||||
assert isinstance(hidden, torch.Tensor)
|
|
||||||
assert "hidden_states" in captured
|
|
||||||
|
|
||||||
|
|
||||||
def test_thinker_forward_passes_through_intermediate_tensors():
|
|
||||||
it = IntermediateTensors({"hidden_states": _hidden_states(), "residual": _hidden_states()})
|
|
||||||
thinker, _ = _make_thinker(inner_output=it)
|
|
||||||
out = thinker.forward(**_forward_args())
|
|
||||||
assert out is it
|
|
||||||
|
|
||||||
|
|
||||||
class _ThinkerStub(nn.Module):
|
|
||||||
"""Records capture kwargs; mirrors the fixed thinker return contract."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
self.received: dict = {}
|
|
||||||
|
|
||||||
def forward(self, **kwargs):
|
|
||||||
self.received = kwargs
|
|
||||||
if kwargs.get("return_hidden_states"):
|
|
||||||
return _hidden_states(), {"hidden_states": {"layers": {}}}
|
|
||||||
return _hidden_states()
|
|
||||||
|
|
||||||
|
|
||||||
def _make_combined(*, is_staged_run: bool, accept_hidden_layer=6) -> Qwen3OmniMoeForConditionalGeneration:
|
|
||||||
model = object.__new__(Qwen3OmniMoeForConditionalGeneration)
|
|
||||||
nn.Module.__init__(model)
|
|
||||||
model.model_stage = "thinker"
|
|
||||||
model.is_staged_run = is_staged_run
|
|
||||||
model.talker_config = SimpleNamespace(accept_hidden_layer=accept_hidden_layer)
|
|
||||||
model.thinker = _ThinkerStub()
|
|
||||||
return model
|
|
||||||
|
|
||||||
|
|
||||||
def test_combined_forward_plain_run_skips_capture_and_returns_tensor():
|
|
||||||
model = _make_combined(is_staged_run=False)
|
|
||||||
out = model.forward(**_forward_args())
|
|
||||||
assert "capture_layer_indices" not in model.thinker.received
|
|
||||||
assert isinstance(out, torch.Tensor)
|
|
||||||
|
|
||||||
|
|
||||||
def test_combined_forward_staged_run_captures_and_returns_tuple():
|
|
||||||
model = _make_combined(is_staged_run=True)
|
|
||||||
out = model.forward(**_forward_args())
|
|
||||||
assert model.thinker.received["capture_layer_indices"] == [0, 6]
|
|
||||||
assert model.thinker.received["return_hidden_states"] is True
|
|
||||||
assert isinstance(out, tuple)
|
|
||||||
|
|
||||||
|
|
||||||
def test_combined_forward_staged_run_without_accept_layer_returns_tensor():
|
|
||||||
model = _make_combined(is_staged_run=True, accept_hidden_layer=None)
|
|
||||||
out = model.forward(**_forward_args())
|
|
||||||
assert "capture_layer_indices" not in model.thinker.received
|
|
||||||
assert isinstance(out, torch.Tensor)
|
|
||||||
|
|
||||||
|
|
||||||
def test_make_omni_output_accepts_bare_tensor():
|
|
||||||
model = _make_combined(is_staged_run=False)
|
|
||||||
out = model.make_omni_output(_hidden_states())
|
|
||||||
assert isinstance(out, OmniOutput)
|
|
||||||
assert out.text_hidden_states.shape == (_TOKENS, _HIDDEN)
|
|
||||||
assert out.multimodal_outputs == {}
|
|
||||||
|
|
||||||
|
|
||||||
def test_make_omni_output_accepts_capture_tuple():
|
|
||||||
model = _make_combined(is_staged_run=True)
|
|
||||||
captured = {"hidden_states": {"layers": {}}}
|
|
||||||
out = model.make_omni_output((_hidden_states(), captured))
|
|
||||||
assert isinstance(out, OmniOutput)
|
|
||||||
assert out.multimodal_outputs == captured
|
|
||||||
@@ -57,7 +57,6 @@ def _build_mock_modules(mocker: MockerFixture) -> dict[str, object]:
|
|||||||
platforms_mock = mocker.MagicMock()
|
platforms_mock = mocker.MagicMock()
|
||||||
platforms_mock.current_omni_platform.supports_torch_inductor.return_value = False
|
platforms_mock.current_omni_platform.supports_torch_inductor.return_value = False
|
||||||
platforms_mock.current_omni_platform.is_npu.return_value = False
|
platforms_mock.current_omni_platform.is_npu.return_value = False
|
||||||
platforms_mock.current_omni_platform.is_xpu.return_value = False
|
|
||||||
|
|
||||||
logger_mock = mocker.MagicMock()
|
logger_mock = mocker.MagicMock()
|
||||||
logger_mock.init_logger = lambda name: mocker.MagicMock()
|
logger_mock.init_logger = lambda name: mocker.MagicMock()
|
||||||
@@ -285,28 +284,6 @@ class TestCodePredictorDtypeAlignment:
|
|||||||
predictor._setup_compile()
|
predictor._setup_compile()
|
||||||
assert predictor._model_dtype == torch.float16
|
assert predictor._model_dtype == torch.float16
|
||||||
|
|
||||||
def test_setup_compile_uses_eager_on_xpu(self, mocker: MockerFixture, loaded_target_classes) -> None:
|
|
||||||
"""XPU should avoid the torch.compile path used by CUDA."""
|
|
||||||
_, _, code_predictor_wrapper, _, _ = loaded_target_classes
|
|
||||||
cp_config, talker_config = _make_tiny_config(loaded_target_classes)
|
|
||||||
vllm_config = _make_vllm_config(mocker, max_num_seqs=2)
|
|
||||||
|
|
||||||
predictor = code_predictor_wrapper(
|
|
||||||
vllm_config=vllm_config,
|
|
||||||
config=cp_config,
|
|
||||||
talker_config=talker_config,
|
|
||||||
)
|
|
||||||
|
|
||||||
common_mod = sys.modules["vllm_omni.model_executor.models.common.qwen3_code_predictor"]
|
|
||||||
mocker.patch.object(common_mod.current_omni_platform, "supports_torch_inductor", return_value=True)
|
|
||||||
mocker.patch.object(common_mod.current_omni_platform, "is_xpu", return_value=True)
|
|
||||||
compile_mock = mocker.patch.object(common_mod.torch, "compile")
|
|
||||||
|
|
||||||
predictor._setup_compile()
|
|
||||||
|
|
||||||
compile_mock.assert_not_called()
|
|
||||||
assert predictor._compiled_model_fwd == predictor.model.forward
|
|
||||||
|
|
||||||
def test_forward_with_mismatched_input_dtype(self, mocker: MockerFixture, loaded_target_classes) -> None:
|
def test_forward_with_mismatched_input_dtype(self, mocker: MockerFixture, loaded_target_classes) -> None:
|
||||||
"""forward() should not crash when inputs are float32 but model is float16."""
|
"""forward() should not crash when inputs are float32 but model is float16."""
|
||||||
_, _, code_predictor_wrapper, _, _ = loaded_target_classes
|
_, _, code_predictor_wrapper, _, _ = loaded_target_classes
|
||||||
|
|||||||
@@ -116,7 +116,6 @@ def test_strip_stop_token_all_stop():
|
|||||||
|
|
||||||
def _conditioning_tensors():
|
def _conditioning_tensors():
|
||||||
return {
|
return {
|
||||||
"use_gpt_latent": True,
|
|
||||||
"S_ref": torch.randn(1, 3, LATENT_DIM),
|
"S_ref": torch.randn(1, 3, LATENT_DIM),
|
||||||
"ref_mel": torch.randn(1, 80, 5),
|
"ref_mel": torch.randn(1, 80, 5),
|
||||||
"style": torch.randn(1, 4),
|
"style": torch.randn(1, 4),
|
||||||
@@ -144,8 +143,6 @@ def test_talker2s2mel_full_payload_flat_keys_builds_s2mel_contract():
|
|||||||
"meta.S_ref": cond["S_ref"],
|
"meta.S_ref": cond["S_ref"],
|
||||||
"meta.ref_mel": cond["ref_mel"],
|
"meta.ref_mel": cond["ref_mel"],
|
||||||
"meta.style": cond["style"],
|
"meta.style": cond["style"],
|
||||||
"meta.use_gpt_latent": True,
|
|
||||||
"meta.duration_factor": 0.5,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
result = talker2s2mel_full_payload(None, payload, SimpleNamespace(request_id="r-flat"))
|
result = talker2s2mel_full_payload(None, payload, SimpleNamespace(request_id="r-flat"))
|
||||||
@@ -159,45 +156,6 @@ def test_talker2s2mel_full_payload_flat_keys_builds_s2mel_contract():
|
|||||||
assert result["ref_mel"].device.type == "cpu"
|
assert result["ref_mel"].device.type == "cpu"
|
||||||
assert result["style"].device.type == "cpu"
|
assert result["style"].device.type == "cpu"
|
||||||
assert result["S_ref"].data_ptr() != cond["S_ref"].data_ptr()
|
assert result["S_ref"].data_ptr() != cond["S_ref"].data_ptr()
|
||||||
assert result["duration_factor"] == 0.5
|
|
||||||
|
|
||||||
|
|
||||||
def test_talker2s2mel_full_payload_defaults_missing_duration_factor_to_one():
|
|
||||||
cond = _conditioning_tensors()
|
|
||||||
payload = {
|
|
||||||
"codes.mel": torch.tensor([[10], [20]]),
|
|
||||||
"hidden_states.latent": torch.randn(2, LATENT_DIM),
|
|
||||||
"meta.S_ref": cond["S_ref"],
|
|
||||||
"meta.ref_mel": cond["ref_mel"],
|
|
||||||
"meta.style": cond["style"],
|
|
||||||
"meta.use_gpt_latent": True,
|
|
||||||
}
|
|
||||||
|
|
||||||
result = talker2s2mel_full_payload(None, payload, SimpleNamespace(request_id="r-default-duration"))
|
|
||||||
|
|
||||||
assert result is not None
|
|
||||||
assert result["duration_factor"] == 1.0
|
|
||||||
|
|
||||||
|
|
||||||
def test_talker2s2mel_full_payload_preserves_explicit_request_seed():
|
|
||||||
cond = _conditioning_tensors()
|
|
||||||
payload = {
|
|
||||||
"codes.mel": torch.tensor([[10], [20]]),
|
|
||||||
"hidden_states.latent": torch.randn(2, LATENT_DIM),
|
|
||||||
"meta.S_ref": cond["S_ref"],
|
|
||||||
"meta.ref_mel": cond["ref_mel"],
|
|
||||||
"meta.style": cond["style"],
|
|
||||||
"meta.use_gpt_latent": True,
|
|
||||||
}
|
|
||||||
request = SimpleNamespace(
|
|
||||||
request_id="r-seeded",
|
|
||||||
sampling_params=SimpleNamespace(seed=0),
|
|
||||||
)
|
|
||||||
|
|
||||||
result = talker2s2mel_full_payload(None, payload, request)
|
|
||||||
|
|
||||||
assert result is not None
|
|
||||||
assert result["seed"] == 0
|
|
||||||
|
|
||||||
|
|
||||||
def test_talker2s2mel_full_payload_nested_fallback_input():
|
def test_talker2s2mel_full_payload_nested_fallback_input():
|
||||||
@@ -248,7 +206,7 @@ def test_talker2s2mel_full_payload_trims_stop_token_and_matching_latent():
|
|||||||
assert result["code_lens"].tolist() == [2]
|
assert result["code_lens"].tolist() == [2]
|
||||||
|
|
||||||
|
|
||||||
def test_talker2s2mel_full_payload_rejects_mel_latent_length_mismatch():
|
def test_talker2s2mel_full_payload_crops_mel_latent_to_common_length():
|
||||||
cond = _conditioning_tensors()
|
cond = _conditioning_tensors()
|
||||||
payload = {
|
payload = {
|
||||||
"codes.mel": torch.tensor([[1], [2], [3], [4]]),
|
"codes.mel": torch.tensor([[1], [2], [3], [4]]),
|
||||||
@@ -256,44 +214,14 @@ def test_talker2s2mel_full_payload_rejects_mel_latent_length_mismatch():
|
|||||||
"meta": cond,
|
"meta": cond,
|
||||||
}
|
}
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="mel/latent length mismatch"):
|
result = talker2s2mel_full_payload(None, payload, SimpleNamespace(request_id="r-crop"))
|
||||||
talker2s2mel_full_payload(None, payload, SimpleNamespace(request_id="r-crop"))
|
|
||||||
|
|
||||||
|
|
||||||
def test_talker2s2mel_full_payload_v25_code_only_does_not_invent_latent():
|
|
||||||
cond = _conditioning_tensors()
|
|
||||||
payload = {
|
|
||||||
"meta.use_gpt_latent": False,
|
|
||||||
"meta.S_ref": cond["S_ref"],
|
|
||||||
"meta.ref_mel": cond["ref_mel"],
|
|
||||||
"meta.style": cond["style"],
|
|
||||||
}
|
|
||||||
|
|
||||||
request = SimpleNamespace(
|
|
||||||
request_id="r-v25",
|
|
||||||
output_token_ids=[3, 4, STOP_MEL_TOKEN],
|
|
||||||
)
|
|
||||||
result = talker2s2mel_full_payload(None, payload, request)
|
|
||||||
|
|
||||||
assert result is not None
|
assert result is not None
|
||||||
assert result["mel_codes"].tolist() == [[3, 4]]
|
assert result["mel_codes"].tolist() == [[1, 2]]
|
||||||
|
assert result["latent"].shape == (1, 2, LATENT_DIM)
|
||||||
assert result["code_lens"].tolist() == [2]
|
assert result["code_lens"].tolist() == [2]
|
||||||
assert result["use_gpt_latent"] is False
|
|
||||||
assert "latent" not in result
|
|
||||||
|
|
||||||
|
|
||||||
def test_talker2s2mel_full_payload_latent_mode_rejects_missing_latent():
|
def test_talker2s2mel_full_payload_missing_required_fields_returns_none():
|
||||||
payload = {
|
assert talker2s2mel_full_payload(None, {"hidden_states.latent": torch.randn(1, LATENT_DIM)}, None) is None
|
||||||
"codes.mel": torch.tensor([[3], [4], [STOP_MEL_TOKEN]]),
|
assert talker2s2mel_full_payload(None, {"codes.mel": torch.tensor([[1]])}, None) is None
|
||||||
"meta.use_gpt_latent": True,
|
|
||||||
}
|
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="missing hidden_states.latent"):
|
|
||||||
talker2s2mel_full_payload(None, payload, SimpleNamespace(request_id="r-latent"))
|
|
||||||
|
|
||||||
|
|
||||||
def test_talker2s2mel_full_payload_missing_required_fields_raises():
|
|
||||||
with pytest.raises(ValueError, match="missing meta.use_gpt_latent"):
|
|
||||||
talker2s2mel_full_payload(None, {"hidden_states.latent": torch.randn(1, LATENT_DIM)}, None)
|
|
||||||
with pytest.raises(ValueError, match="missing meta.use_gpt_latent"):
|
|
||||||
talker2s2mel_full_payload(None, {"codes.mel": torch.tensor([[1]])}, None)
|
|
||||||
@@ -3,8 +3,6 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
@@ -137,6 +135,67 @@ def test_lingbot_extra_registry_declares_request_params() -> None:
|
|||||||
assert should_init_extra_args_for_non_diffusion_stages("LingBotVideoPipeline") is False
|
assert should_init_extra_args_for_non_diffusion_stages("LingBotVideoPipeline") is False
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.core_model
|
||||||
|
@pytest.mark.cpu
|
||||||
|
def test_lingbot_text_to_image_prompt_builder_preserves_empty_negative_prompt() -> None:
|
||||||
|
assert build_text_to_image_prompt(
|
||||||
|
"LingBotVideoPipeline",
|
||||||
|
prompt="a red fox in fresh snow",
|
||||||
|
negative_prompt="",
|
||||||
|
height=192,
|
||||||
|
width=320,
|
||||||
|
) == {
|
||||||
|
"prompt": "a red fox in fresh snow",
|
||||||
|
"modalities": ["image"],
|
||||||
|
"negative_prompt": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.core_model
|
||||||
|
@pytest.mark.cpu
|
||||||
|
def test_lingbot_image_to_video_prompt_builder() -> None:
|
||||||
|
image = Image.new("RGB", (320, 192), "red")
|
||||||
|
result = build_image_to_video_prompt(
|
||||||
|
"LingBotVideoPipeline",
|
||||||
|
prompt="the fox looks toward the camera",
|
||||||
|
negative_prompt=None,
|
||||||
|
media_inputs={"image": image},
|
||||||
|
height=192,
|
||||||
|
width=320,
|
||||||
|
num_frames=9,
|
||||||
|
)
|
||||||
|
assert result == {
|
||||||
|
"prompt": "the fox looks toward the camera",
|
||||||
|
"modalities": ["video"],
|
||||||
|
"multi_modal_data": {"image": image},
|
||||||
|
"num_frames": 9,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.core_model
|
||||||
|
@pytest.mark.cpu
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"media_inputs",
|
||||||
|
[
|
||||||
|
{},
|
||||||
|
{"image": "input.png"},
|
||||||
|
{"image": [Image.new("RGB", (16, 16))]},
|
||||||
|
{"image": Image.new("RGB", (16, 16)), "video": b"video"},
|
||||||
|
],
|
||||||
|
ids=["missing", "path", "multiple", "extra-modality"],
|
||||||
|
)
|
||||||
|
def test_lingbot_image_to_video_prompt_builder_rejects_invalid_media(
|
||||||
|
media_inputs: dict[str, object],
|
||||||
|
) -> None:
|
||||||
|
with pytest.raises(ValueError, match="exactly one PIL image"):
|
||||||
|
build_image_to_video_prompt(
|
||||||
|
"LingBotVideoPipeline",
|
||||||
|
prompt="move",
|
||||||
|
negative_prompt=None,
|
||||||
|
media_inputs=media_inputs,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.core_model
|
@pytest.mark.core_model
|
||||||
@pytest.mark.cpu
|
@pytest.mark.cpu
|
||||||
@pytest.mark.parametrize("pipeline_name", ["Cosmos3OmniDiffusersPipeline", "Cosmos3OmniPipeline"])
|
@pytest.mark.parametrize("pipeline_name", ["Cosmos3OmniDiffusersPipeline", "Cosmos3OmniPipeline"])
|
||||||
@@ -283,11 +342,8 @@ def test_ming_flash_omni_declared_extra_args_route_into_sampling_params() -> Non
|
|||||||
def test_ming_flash_omni_text_to_image_prompt_builder() -> None:
|
def test_ming_flash_omni_text_to_image_prompt_builder() -> None:
|
||||||
assert build_text_to_image_prompt(
|
assert build_text_to_image_prompt(
|
||||||
"MingImagePipeline",
|
"MingImagePipeline",
|
||||||
{
|
prompt="Please draw a cute cat.",
|
||||||
"prompt": "Please draw a cute cat.",
|
negative_prompt="ugly, blurry",
|
||||||
"modalities": ["image"],
|
|
||||||
"negative_prompt": "ugly, blurry",
|
|
||||||
},
|
|
||||||
height=512,
|
height=512,
|
||||||
width=768,
|
width=768,
|
||||||
) == {
|
) == {
|
||||||
@@ -304,7 +360,8 @@ def test_ming_flash_omni_text_to_image_prompt_builder() -> None:
|
|||||||
# pipeline's 1024x1024 default still applies.
|
# pipeline's 1024x1024 default still applies.
|
||||||
assert build_text_to_image_prompt(
|
assert build_text_to_image_prompt(
|
||||||
"MingImagePipeline",
|
"MingImagePipeline",
|
||||||
{"prompt": "Draw a poster.", "modalities": ["image"]},
|
prompt="Draw a poster.",
|
||||||
|
negative_prompt=None,
|
||||||
) == {
|
) == {
|
||||||
"prompt": "Draw a poster.",
|
"prompt": "Draw a poster.",
|
||||||
"modalities": ["image"],
|
"modalities": ["image"],
|
||||||
@@ -334,6 +391,28 @@ def test_ming_flash_omni_image_to_image_prompt_builder() -> None:
|
|||||||
assert "negative_prompt" not in result
|
assert "negative_prompt" not in result
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.core_model
|
||||||
|
@pytest.mark.cpu
|
||||||
|
@pytest.mark.parametrize("pipeline_name", ["Cosmos3OmniDiffusersPipeline", "Cosmos3OmniPipeline"])
|
||||||
|
def test_cosmos3_text_to_image_prompt_builder_selects_image_modality(pipeline_name: str) -> None:
|
||||||
|
assert build_text_to_image_prompt(
|
||||||
|
pipeline_name,
|
||||||
|
prompt="a red sports car at golden hour",
|
||||||
|
negative_prompt="blurry, distorted",
|
||||||
|
height=1024,
|
||||||
|
width=1024,
|
||||||
|
) == {
|
||||||
|
"prompt": "a red sports car at golden hour",
|
||||||
|
"modalities": ["image"],
|
||||||
|
"negative_prompt": "blurry, distorted",
|
||||||
|
}
|
||||||
|
assert build_text_to_image_prompt(
|
||||||
|
pipeline_name,
|
||||||
|
prompt="a red sports car",
|
||||||
|
negative_prompt=None,
|
||||||
|
) == {"prompt": "a red sports car", "modalities": ["image"]}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.core_model
|
@pytest.mark.core_model
|
||||||
@pytest.mark.cpu
|
@pytest.mark.cpu
|
||||||
def test_audiox_extra_registry_declares_request_and_response_params() -> None:
|
def test_audiox_extra_registry_declares_request_and_response_params() -> None:
|
||||||
@@ -426,11 +505,8 @@ def test_unknown_pipeline_has_empty_extra_registry() -> None:
|
|||||||
def test_bagel_text_to_image_prompt_builder() -> None:
|
def test_bagel_text_to_image_prompt_builder() -> None:
|
||||||
assert build_text_to_image_prompt(
|
assert build_text_to_image_prompt(
|
||||||
"BagelPipeline",
|
"BagelPipeline",
|
||||||
{
|
prompt="a cat",
|
||||||
"prompt": "a cat",
|
negative_prompt="blurry",
|
||||||
"modalities": ["image"],
|
|
||||||
"negative_prompt": "blurry",
|
|
||||||
},
|
|
||||||
height=512,
|
height=512,
|
||||||
width=768,
|
width=768,
|
||||||
) == {
|
) == {
|
||||||
@@ -467,17 +543,14 @@ def test_bagel_image_to_image_prompt_builder() -> None:
|
|||||||
|
|
||||||
@pytest.mark.core_model
|
@pytest.mark.core_model
|
||||||
@pytest.mark.cpu
|
@pytest.mark.cpu
|
||||||
def test_unknown_pipeline_preserves_canonical_text_to_image_prompt() -> None:
|
def test_unknown_pipeline_uses_default_text_to_image_prompt() -> None:
|
||||||
canonical_prompt = {"prompt": "a cat", "modalities": ["image"]}
|
assert build_text_to_image_prompt(
|
||||||
assert (
|
"UnknownPipeline",
|
||||||
build_text_to_image_prompt(
|
prompt="a cat",
|
||||||
"UnknownPipeline",
|
negative_prompt=None,
|
||||||
canonical_prompt,
|
height=512,
|
||||||
height=512,
|
width=512,
|
||||||
width=512,
|
) == {"prompt": "a cat"}
|
||||||
)
|
|
||||||
is canonical_prompt
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.core_model
|
@pytest.mark.core_model
|
||||||
@@ -496,14 +569,12 @@ def test_unknown_pipeline_uses_default_image_to_image_prompt() -> None:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _build_vace_prompt(media_inputs: dict[str, Any], *, num_frames: int = 5) -> dict:
|
def _build_vace_prompt(media_inputs: dict[str, object], *, num_frames: int = 5) -> dict:
|
||||||
return build_image_to_video_prompt(
|
return build_image_to_video_prompt(
|
||||||
"WanVACEPipeline",
|
"WanVACEPipeline",
|
||||||
{
|
prompt="a bird flying",
|
||||||
"prompt": "a bird flying",
|
negative_prompt=None,
|
||||||
"modalities": ["video"],
|
media_inputs=media_inputs,
|
||||||
"multi_modal_data": media_inputs,
|
|
||||||
},
|
|
||||||
height=16,
|
height=16,
|
||||||
width=320,
|
width=320,
|
||||||
num_frames=num_frames,
|
num_frames=num_frames,
|
||||||
@@ -526,7 +597,7 @@ def _build_vace_prompt(media_inputs: dict[str, Any], *, num_frames: int = 5) ->
|
|||||||
],
|
],
|
||||||
ids=["i2v", "v2lf", "flf2v", "inpaint", "r2v"],
|
ids=["i2v", "v2lf", "flf2v", "inpaint", "r2v"],
|
||||||
)
|
)
|
||||||
def test_vace_image_to_video_prompt_builder(media_inputs: dict[str, Any]) -> None:
|
def test_vace_image_to_video_prompt_builder(media_inputs: dict[str, object]) -> None:
|
||||||
result = _build_vace_prompt(media_inputs)
|
result = _build_vace_prompt(media_inputs)
|
||||||
mmd = result["multi_modal_data"]
|
mmd = result["multi_modal_data"]
|
||||||
if "reference_images" in mmd:
|
if "reference_images" in mmd:
|
||||||
@@ -545,7 +616,7 @@ def test_vace_image_to_video_prompt_builder(media_inputs: dict[str, Any]) -> Non
|
|||||||
({"control_image": Image.new("RGB", (320, 16))}, "Unsupported VACE media input"),
|
({"control_image": Image.new("RGB", (320, 16))}, "Unsupported VACE media input"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_vace_rejects_invalid_media_combinations(media_inputs: dict[str, Any], message: str) -> None:
|
def test_vace_rejects_invalid_media_combinations(media_inputs: dict[str, object], message: str) -> None:
|
||||||
with pytest.raises(ValueError, match=message):
|
with pytest.raises(ValueError, match=message):
|
||||||
_build_vace_prompt(media_inputs)
|
_build_vace_prompt(media_inputs)
|
||||||
|
|
||||||
@@ -593,7 +664,8 @@ def test_mammothmoda2_extra_registry_declares_request_and_response_params() -> N
|
|||||||
|
|
||||||
wrapper_prompt = build_text_to_image_prompt(
|
wrapper_prompt = build_text_to_image_prompt(
|
||||||
"MammothModa2ForConditionalGeneration",
|
"MammothModa2ForConditionalGeneration",
|
||||||
{"prompt": "a cat", "modalities": ["image"]},
|
prompt="a cat",
|
||||||
|
negative_prompt=None,
|
||||||
height=256,
|
height=256,
|
||||||
width=256,
|
width=256,
|
||||||
)
|
)
|
||||||
@@ -607,11 +679,8 @@ def test_mammothmoda2_text_to_image_prompt_builder() -> None:
|
|||||||
# prompt is ignored (MammothModa2 t2i uses CFG, not an explicit negative path).
|
# prompt is ignored (MammothModa2 t2i uses CFG, not an explicit negative path).
|
||||||
assert build_text_to_image_prompt(
|
assert build_text_to_image_prompt(
|
||||||
"MammothModa2DiTPipeline",
|
"MammothModa2DiTPipeline",
|
||||||
{
|
prompt="a cat",
|
||||||
"prompt": "a cat",
|
negative_prompt="blurry",
|
||||||
"modalities": ["image"],
|
|
||||||
"negative_prompt": "blurry",
|
|
||||||
},
|
|
||||||
height=512,
|
height=512,
|
||||||
width=768,
|
width=768,
|
||||||
) == {
|
) == {
|
||||||
|
|||||||
@@ -1,155 +0,0 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
||||||
|
|
||||||
import builtins
|
|
||||||
import importlib.util
|
|
||||||
import platform
|
|
||||||
import sys
|
|
||||||
import types
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
import tomllib
|
|
||||||
from packaging.requirements import Requirement
|
|
||||||
|
|
||||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
|
||||||
|
|
||||||
ROOT = Path(__file__).parents[1]
|
|
||||||
TEXT_PROCESSING_PATH = ROOT / "vllm_omni" / "model_executor" / "models" / "indextts2" / "text_processing_v2_5.py"
|
|
||||||
INDEXTTS2_FRONTEND_PACKAGES = {
|
|
||||||
"fugashi",
|
|
||||||
"unidic-lite",
|
|
||||||
"wetext",
|
|
||||||
"wetextprocessing",
|
|
||||||
}
|
|
||||||
INSTALL_COMMAND = "pip install 'vllm-omni[indextts2]'"
|
|
||||||
|
|
||||||
|
|
||||||
def _load_text_processing_module(monkeypatch: pytest.MonkeyPatch):
|
|
||||||
package_name = "_indextts2_optional_dependencies_test"
|
|
||||||
package = types.ModuleType(package_name)
|
|
||||||
package.__path__ = [str(TEXT_PROCESSING_PATH.parent)]
|
|
||||||
tokenizer = types.ModuleType(f"{package_name}.tokenizer_v2_5")
|
|
||||||
tokenizer.INDEXTTS25_TOKENIZER_FILE = "tokenizer.tiktoken"
|
|
||||||
tokenizer.encode_indextts25_text = lambda *args, **kwargs: []
|
|
||||||
tokenizer.lang_to_token = lambda lang: 0
|
|
||||||
tokenizer.normalize_language_code = lambda lang: lang
|
|
||||||
monkeypatch.setitem(sys.modules, package_name, package)
|
|
||||||
monkeypatch.setitem(sys.modules, tokenizer.__name__, tokenizer)
|
|
||||||
|
|
||||||
module_name = f"{package_name}.text_processing_v2_5"
|
|
||||||
spec = importlib.util.spec_from_file_location(module_name, TEXT_PROCESSING_PATH)
|
|
||||||
assert spec is not None and spec.loader is not None
|
|
||||||
module = importlib.util.module_from_spec(spec)
|
|
||||||
monkeypatch.setitem(sys.modules, module_name, module)
|
|
||||||
spec.loader.exec_module(module)
|
|
||||||
return module
|
|
||||||
|
|
||||||
|
|
||||||
def _requirements_from_file(path: Path) -> list[Requirement]:
|
|
||||||
requirements = []
|
|
||||||
for raw_line in path.read_text().splitlines():
|
|
||||||
line = raw_line.split("#", 1)[0].strip()
|
|
||||||
if not line:
|
|
||||||
continue
|
|
||||||
if line.startswith(("-r ", "--requirement ")):
|
|
||||||
included = line.split(maxsplit=1)[1]
|
|
||||||
requirements.extend(_requirements_from_file(path.parent / included))
|
|
||||||
continue
|
|
||||||
requirements.append(Requirement(line))
|
|
||||||
return requirements
|
|
||||||
|
|
||||||
|
|
||||||
def _block_import(monkeypatch: pytest.MonkeyPatch, package: str) -> None:
|
|
||||||
original_import = builtins.__import__
|
|
||||||
|
|
||||||
def controlled_import(name, *args, **kwargs):
|
|
||||||
if name == package or name.startswith(f"{package}."):
|
|
||||||
raise ModuleNotFoundError(f"No module named {package!r}", name=package)
|
|
||||||
return original_import(name, *args, **kwargs)
|
|
||||||
|
|
||||||
monkeypatch.setattr(builtins, "__import__", controlled_import)
|
|
||||||
|
|
||||||
|
|
||||||
def test_indextts2_extra_declares_platform_specific_frontend_dependencies():
|
|
||||||
project = tomllib.loads((ROOT / "pyproject.toml").read_text())
|
|
||||||
requirements = [Requirement(value) for value in project["project"]["optional-dependencies"]["indextts2"]]
|
|
||||||
|
|
||||||
assert {
|
|
||||||
(requirement.name.lower(), str(requirement.specifier), str(requirement.marker) if requirement.marker else None)
|
|
||||||
for requirement in requirements
|
|
||||||
} == {
|
|
||||||
("fugashi", ">=1.2.0", None),
|
|
||||||
("unidic-lite", ">=1.0.0", None),
|
|
||||||
("wetext", ">=0.0.9", 'sys_platform != "linux"'),
|
|
||||||
("wetextprocessing", "", 'sys_platform == "linux"'),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_common_requirements_exclude_indextts2_frontend_dependencies():
|
|
||||||
requirements = _requirements_from_file(ROOT / "requirements" / "common.txt")
|
|
||||||
|
|
||||||
assert not ({requirement.name.lower() for requirement in requirements} & INDEXTTS2_FRONTEND_PACKAGES)
|
|
||||||
|
|
||||||
|
|
||||||
def test_wetext_normalizer_constructor_error_propagates(monkeypatch):
|
|
||||||
text_processing_v2_5 = _load_text_processing_module(monkeypatch)
|
|
||||||
sentinel = RuntimeError("wetext constructor sentinel")
|
|
||||||
|
|
||||||
class FailingNormalizer:
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
raise sentinel
|
|
||||||
|
|
||||||
wetext = types.ModuleType("wetext")
|
|
||||||
wetext.Normalizer = FailingNormalizer
|
|
||||||
monkeypatch.setitem(sys.modules, "wetext", wetext)
|
|
||||||
monkeypatch.setattr(platform, "system", lambda: "Darwin")
|
|
||||||
|
|
||||||
with pytest.raises(RuntimeError) as exc_info:
|
|
||||||
text_processing_v2_5._load_wetext_normalizer("zh")
|
|
||||||
|
|
||||||
assert exc_info.value is sentinel
|
|
||||||
|
|
||||||
|
|
||||||
def test_japanese_tagger_constructor_error_propagates(monkeypatch):
|
|
||||||
text_processing_v2_5 = _load_text_processing_module(monkeypatch)
|
|
||||||
sentinel = RuntimeError("fugashi constructor sentinel")
|
|
||||||
|
|
||||||
fugashi = types.ModuleType("fugashi")
|
|
||||||
|
|
||||||
def failing_tagger():
|
|
||||||
raise sentinel
|
|
||||||
|
|
||||||
fugashi.Tagger = failing_tagger
|
|
||||||
monkeypatch.setitem(sys.modules, "fugashi", fugashi)
|
|
||||||
monkeypatch.setitem(sys.modules, "unidic_lite", types.ModuleType("unidic_lite"))
|
|
||||||
|
|
||||||
with pytest.raises(RuntimeError) as exc_info:
|
|
||||||
text_processing_v2_5._load_japanese_tagger()
|
|
||||||
|
|
||||||
assert exc_info.value is sentinel
|
|
||||||
|
|
||||||
|
|
||||||
def test_missing_wetext_backend_reports_indextts2_extra(monkeypatch):
|
|
||||||
text_processing_v2_5 = _load_text_processing_module(monkeypatch)
|
|
||||||
text_processing_v2_5._load_wetext_normalizer.cache_clear()
|
|
||||||
monkeypatch.setattr(platform, "system", lambda: "Darwin")
|
|
||||||
_block_import(monkeypatch, "wetext")
|
|
||||||
|
|
||||||
with pytest.raises(RuntimeError, match="IndexTTS 2.5 text normalization") as exc_info:
|
|
||||||
text_processing_v2_5._load_wetext_normalizer("zh")
|
|
||||||
|
|
||||||
assert INSTALL_COMMAND in str(exc_info.value)
|
|
||||||
text_processing_v2_5._load_wetext_normalizer.cache_clear()
|
|
||||||
|
|
||||||
|
|
||||||
def test_missing_japanese_tagger_reports_indextts2_extra(monkeypatch):
|
|
||||||
text_processing_v2_5 = _load_text_processing_module(monkeypatch)
|
|
||||||
text_processing_v2_5._load_japanese_tagger.cache_clear()
|
|
||||||
_block_import(monkeypatch, "fugashi")
|
|
||||||
|
|
||||||
with pytest.raises(RuntimeError, match="IndexTTS 2.5 Japanese preprocessing") as exc_info:
|
|
||||||
text_processing_v2_5._load_japanese_tagger()
|
|
||||||
|
|
||||||
assert INSTALL_COMMAND in str(exc_info.value)
|
|
||||||
text_processing_v2_5._load_japanese_tagger.cache_clear()
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import pytest
|
import pytest
|
||||||
import torch
|
import torch
|
||||||
|
|
||||||
import vllm_omni.utils.mm_outputs as mm_outputs
|
|
||||||
from vllm_omni.utils.mm_outputs import partition_flat_payload, partition_payload_list
|
from vllm_omni.utils.mm_outputs import partition_flat_payload, partition_payload_list
|
||||||
|
|
||||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||||
@@ -70,33 +69,3 @@ def test_partition_duplex_audio_transcript_metadata_to_client_mm():
|
|||||||
assert "meta.turn_end" in client
|
assert "meta.turn_end" in client
|
||||||
assert "meta.native_duplex_segment_text" not in client
|
assert "meta.native_duplex_segment_text" not in client
|
||||||
assert "meta.native_duplex_segment_text" in inter
|
assert "meta.native_duplex_segment_text" in inter
|
||||||
|
|
||||||
|
|
||||||
def test_snapshot_mm_payload_coalesces_compatible_tensor_list(monkeypatch):
|
|
||||||
chunks = [
|
|
||||||
torch.tensor([[7]], dtype=torch.long),
|
|
||||||
torch.empty(0, 1, dtype=torch.long),
|
|
||||||
torch.tensor([[9], [10]], dtype=torch.long),
|
|
||||||
]
|
|
||||||
real_cat = torch.cat
|
|
||||||
cat_shapes = []
|
|
||||||
|
|
||||||
def recording_cat(tensors, *args, **kwargs):
|
|
||||||
cat_shapes.append([tuple(tensor.shape) for tensor in tensors])
|
|
||||||
return real_cat(tensors, *args, **kwargs)
|
|
||||||
|
|
||||||
monkeypatch.setattr(torch, "cat", recording_cat)
|
|
||||||
|
|
||||||
result = mm_outputs.snapshot_mm_payload({"codes.mel": chunks})
|
|
||||||
|
|
||||||
assert cat_shapes == [[(1, 1), (0, 1), (2, 1)]]
|
|
||||||
output_chunks = result["codes.mel"]
|
|
||||||
assert [tuple(chunk.shape) for chunk in output_chunks] == [
|
|
||||||
(1, 1),
|
|
||||||
(0, 1),
|
|
||||||
(2, 1),
|
|
||||||
]
|
|
||||||
torch.testing.assert_close(output_chunks[0], chunks[0])
|
|
||||||
torch.testing.assert_close(output_chunks[1], chunks[1])
|
|
||||||
torch.testing.assert_close(output_chunks[2], chunks[2])
|
|
||||||
assert output_chunks[0].untyped_storage().data_ptr() == output_chunks[2].untyped_storage().data_ptr()
|
|
||||||
@@ -5,7 +5,6 @@ import pytest
|
|||||||
import torch
|
import torch
|
||||||
from vllm.v1.cudagraph_dispatcher import CUDAGraphMode
|
from vllm.v1.cudagraph_dispatcher import CUDAGraphMode
|
||||||
|
|
||||||
from vllm_omni.worker.gpu_ar_model_runner import GPUARModelRunner
|
|
||||||
from vllm_omni.worker.gpu_model_runner import (
|
from vllm_omni.worker.gpu_model_runner import (
|
||||||
OmniGPUModelRunner,
|
OmniGPUModelRunner,
|
||||||
_filter_mrope_kwargs_for_model,
|
_filter_mrope_kwargs_for_model,
|
||||||
@@ -692,49 +691,6 @@ def test_full_payload_output_accumulation_hook_matrix():
|
|||||||
assert not runner._should_accumulate_full_payload_output()
|
assert not runner._should_accumulate_full_payload_output()
|
||||||
|
|
||||||
|
|
||||||
def _make_request_end_payload_runner(*, enabled=True, prefix_cache=None):
|
|
||||||
runner = object.__new__(GPUARModelRunner)
|
|
||||||
runner.model = SimpleNamespace(omni_payload_at_request_end=enabled)
|
|
||||||
runner.omni_prefix_cache = prefix_cache
|
|
||||||
runner.model_config = SimpleNamespace(
|
|
||||||
model_arch="IndexTTS25TalkerForConditionalGeneration",
|
|
||||||
model_stage="indextts2_5_talker",
|
|
||||||
async_chunk=False,
|
|
||||||
final_output=False,
|
|
||||||
custom_process_next_stage_input_func="module.full_payload",
|
|
||||||
)
|
|
||||||
runner._custom_process_func = object()
|
|
||||||
runner._pending_full_payload_send = {}
|
|
||||||
runner._stage_id = 0
|
|
||||||
runner._omni_connector = object()
|
|
||||||
return runner
|
|
||||||
|
|
||||||
|
|
||||||
def test_request_end_payload_d2h_gate_requires_opt_in_and_no_prefix_cache():
|
|
||||||
assert _make_request_end_payload_runner()._should_defer_full_payload_d2h()
|
|
||||||
assert not _make_request_end_payload_runner(enabled=False)._should_defer_full_payload_d2h()
|
|
||||||
assert not _make_request_end_payload_runner(prefix_cache=object())._should_defer_full_payload_d2h()
|
|
||||||
|
|
||||||
|
|
||||||
def test_request_end_payload_suppresses_per_step_multimodal_outputs():
|
|
||||||
runner = _make_request_end_payload_runner()
|
|
||||||
|
|
||||||
def unexpected_build(_payload):
|
|
||||||
raise AssertionError("request-end payloads must stay inside the GPU accumulator")
|
|
||||||
|
|
||||||
runner._build_multimodal_outputs = unexpected_build
|
|
||||||
pooler_inter = [{"codes.mel": torch.tensor([[7]])}]
|
|
||||||
|
|
||||||
inter_stage, client = runner._build_omni_step_outputs(
|
|
||||||
pooler_inter,
|
|
||||||
pooler_inter,
|
|
||||||
defer_full_payload_d2h=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
assert inter_stage is None
|
|
||||||
assert client is None
|
|
||||||
|
|
||||||
|
|
||||||
def test_sync_local_stage_payloads_retains_payload_until_request_is_active():
|
def test_sync_local_stage_payloads_retains_payload_until_request_is_active():
|
||||||
runner = object.__new__(OmniGPUModelRunner)
|
runner = object.__new__(OmniGPUModelRunner)
|
||||||
payload = {"codes": {"audio": [1, 2, 3]}}
|
payload = {"codes": {"audio": [1, 2, 3]}}
|
||||||
|
|||||||
@@ -30,12 +30,9 @@ def _fake_memory_profiling(*, non_torch: int, torch_peak: int):
|
|||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def _mp(snapshot, weights_memory): # noqa: ARG001 - signature parity only
|
def _mp(snapshot, weights_memory): # noqa: ARG001 - signature parity only
|
||||||
# total_consumed mirrors upstream vllm.utils.mem_utils.MemoryProfilingResult
|
|
||||||
# (added by upstream 58b2012aa2) and is read by OmniGPUWorkerBase.
|
|
||||||
yield SimpleNamespace(
|
yield SimpleNamespace(
|
||||||
non_torch_increase=non_torch,
|
non_torch_increase=non_torch,
|
||||||
torch_peak_increase=torch_peak,
|
torch_peak_increase=torch_peak,
|
||||||
total_consumed=torch_peak + non_torch,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return _mp
|
return _mp
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import traceback
|
|||||||
import uuid
|
import uuid
|
||||||
import wave
|
import wave
|
||||||
from collections.abc import Iterable
|
from collections.abc import Iterable
|
||||||
from dataclasses import dataclass, replace
|
from dataclasses import dataclass
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any, Literal
|
from typing import Any, Literal
|
||||||
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
|
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
|
||||||
@@ -1542,7 +1542,6 @@ async def benchmark(
|
|||||||
ready_check_timeout_sec: int = 600,
|
ready_check_timeout_sec: int = 600,
|
||||||
ssl_context: ssl.SSLContext | bool | None = None,
|
ssl_context: ssl.SSLContext | bool | None = None,
|
||||||
self_timed: bool = False,
|
self_timed: bool = False,
|
||||||
probe_request_rate: float = 0.0,
|
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
request_func = ASYNC_REQUEST_FUNCS[endpoint_type]
|
request_func = ASYNC_REQUEST_FUNCS[endpoint_type]
|
||||||
@@ -1688,31 +1687,6 @@ async def benchmark(
|
|||||||
async with semaphore:
|
async with semaphore:
|
||||||
return await request_func(request_func_input=request_func_input, session=session, pbar=pbar)
|
return await request_func(request_func_input=request_func_input, session=session, pbar=pbar)
|
||||||
|
|
||||||
# Ported from upstream vLLM v0.27.0 (vllm/benchmarks/serve.py), which added
|
|
||||||
# the probe_request_rate background probe; this file is the patched copy of
|
|
||||||
# that module and tracks its upstream base.
|
|
||||||
probe_outputs: list[MixRequestFuncOutput] = []
|
|
||||||
probe_stop = asyncio.Event()
|
|
||||||
|
|
||||||
async def probe_loop():
|
|
||||||
probe_input = replace(
|
|
||||||
test_input,
|
|
||||||
prompt="Hi",
|
|
||||||
prompt_len=1,
|
|
||||||
output_len=1,
|
|
||||||
multi_modal_content=None,
|
|
||||||
chat_messages=None,
|
|
||||||
)
|
|
||||||
interval = 1 / probe_request_rate
|
|
||||||
while not probe_stop.is_set():
|
|
||||||
probe_outputs.append(await request_func(request_func_input=probe_input, session=session))
|
|
||||||
await asyncio.sleep(interval)
|
|
||||||
|
|
||||||
probe_task: asyncio.Task | None = None
|
|
||||||
if probe_request_rate > 0:
|
|
||||||
print(f"Probe request rate: {probe_request_rate} req/s")
|
|
||||||
probe_task = asyncio.create_task(probe_loop())
|
|
||||||
|
|
||||||
benchmark_start_time = time.perf_counter()
|
benchmark_start_time = time.perf_counter()
|
||||||
tasks: list[asyncio.Task] = []
|
tasks: list[asyncio.Task] = []
|
||||||
|
|
||||||
@@ -1778,10 +1752,6 @@ async def benchmark(
|
|||||||
)
|
)
|
||||||
outputs: list[MixRequestFuncOutput] = await asyncio.gather(*tasks)
|
outputs: list[MixRequestFuncOutput] = await asyncio.gather(*tasks)
|
||||||
|
|
||||||
if probe_task is not None:
|
|
||||||
probe_stop.set()
|
|
||||||
await probe_task
|
|
||||||
|
|
||||||
if pbar is not None:
|
if pbar is not None:
|
||||||
pbar.close()
|
pbar.close()
|
||||||
|
|
||||||
@@ -1849,19 +1819,11 @@ async def benchmark(
|
|||||||
"input_lens": [output.prompt_len for output in outputs],
|
"input_lens": [output.prompt_len for output in outputs],
|
||||||
"errors": [output.error for output in outputs],
|
"errors": [output.error for output in outputs],
|
||||||
}
|
}
|
||||||
# Plain-vLLM backends (e.g. the vLLM-text perf config) return upstream
|
duplex_request_metrics = [metric for output in outputs for metric in (output.duplex_request_metrics or [])]
|
||||||
# RequestFuncOutput objects without the Mix duplex fields; read them
|
|
||||||
# tolerantly or the whole benchmark result is discarded ("fallback to
|
|
||||||
# template", completed=0) after every request already succeeded.
|
|
||||||
duplex_request_metrics = [
|
|
||||||
metric for output in outputs for metric in (getattr(output, "duplex_request_metrics", None) or [])
|
|
||||||
]
|
|
||||||
if duplex_request_metrics:
|
if duplex_request_metrics:
|
||||||
result["duplex_request_metrics"] = duplex_request_metrics
|
result["duplex_request_metrics"] = duplex_request_metrics
|
||||||
duplex_session_metrics = [
|
duplex_session_metrics = [
|
||||||
session_metrics
|
output.duplex_session_metrics for output in outputs if output.duplex_session_metrics is not None
|
||||||
for output in outputs
|
|
||||||
if (session_metrics := getattr(output, "duplex_session_metrics", None)) is not None
|
|
||||||
]
|
]
|
||||||
if duplex_session_metrics:
|
if duplex_session_metrics:
|
||||||
result["duplex_session_metrics"] = duplex_session_metrics
|
result["duplex_session_metrics"] = duplex_session_metrics
|
||||||
|
|||||||
@@ -61,10 +61,7 @@ from vllm_omni.model_executor.models.hunyuan_image3.pipeline import (
|
|||||||
HUNYUAN_IMAGE3_PIPELINE,
|
HUNYUAN_IMAGE3_PIPELINE,
|
||||||
)
|
)
|
||||||
from vllm_omni.model_executor.models.hunyuan_video.pipeline import HUNYUAN_VIDEO_15_PIPELINE
|
from vllm_omni.model_executor.models.hunyuan_video.pipeline import HUNYUAN_VIDEO_15_PIPELINE
|
||||||
from vllm_omni.model_executor.models.indextts2.pipeline import (
|
from vllm_omni.model_executor.models.indextts2.pipeline import INDEXTTS2_PIPELINE
|
||||||
INDEXTTS2_PIPELINE,
|
|
||||||
INDEXTTS25_PIPELINE,
|
|
||||||
)
|
|
||||||
from vllm_omni.model_executor.models.lance.pipeline import LANCE_PIPELINE
|
from vllm_omni.model_executor.models.lance.pipeline import LANCE_PIPELINE
|
||||||
from vllm_omni.model_executor.models.mammoth_moda2.pipeline import (
|
from vllm_omni.model_executor.models.mammoth_moda2.pipeline import (
|
||||||
MAMMOTH_MODA2_AR_PIPELINE,
|
MAMMOTH_MODA2_AR_PIPELINE,
|
||||||
@@ -167,7 +164,6 @@ OMNI_PIPELINES: dict[str, PipelineConfig | PipelineResolverFunc] = {
|
|||||||
"higgs_multimodal_qwen3": HIGGS_AUDIO_V3_PIPELINE,
|
"higgs_multimodal_qwen3": HIGGS_AUDIO_V3_PIPELINE,
|
||||||
"dynin_omni": DYNIN_OMNI_PIPELINE,
|
"dynin_omni": DYNIN_OMNI_PIPELINE,
|
||||||
"indextts2": INDEXTTS2_PIPELINE,
|
"indextts2": INDEXTTS2_PIPELINE,
|
||||||
"indextts2_5": INDEXTTS25_PIPELINE,
|
|
||||||
"soulxsinger_svc": SOULXSINGER_SVC_PIPELINE,
|
"soulxsinger_svc": SOULXSINGER_SVC_PIPELINE,
|
||||||
"soulxsinger_svs": SOULXSINGER_SVS_PIPELINE,
|
"soulxsinger_svs": SOULXSINGER_SVS_PIPELINE,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -325,18 +325,6 @@ class OmniARScheduler(OmniSchedulerMixin, VLLMScheduler):
|
|||||||
# max(0, computed - in_flight), so a leaked counter silently
|
# max(0, computed - in_flight), so a leaked counter silently
|
||||||
# freezes sliding-window block freeing.
|
# freezes sliding-window block freeing.
|
||||||
request.num_in_flight_tokens -= num_tokens_scheduled
|
request.num_in_flight_tokens -= num_tokens_scheduled
|
||||||
# vLLM 0.27 (a0c092ee72) removed the async_tokens_to_discard
|
|
||||||
# handling from the upstream scheduler and replaced it with the
|
|
||||||
# num_stale_output_tokens/is_stale mechanism. Omni's discard
|
|
||||||
# sites (segment stop, streaming-session replacement) record the
|
|
||||||
# in-flight share here; the delayed outputs are dropped below
|
|
||||||
# instead of decrementing num_output_placeholders (which the
|
|
||||||
# discard zeroed) and underflowing the upstream assert.
|
|
||||||
output_is_stale = False
|
|
||||||
if request is not None and request.num_stale_output_tokens > 0:
|
|
||||||
output_is_stale = True
|
|
||||||
request.num_stale_output_tokens -= num_tokens_scheduled
|
|
||||||
assert request.num_stale_output_tokens >= 0
|
|
||||||
if failed_kv_load_req_ids and req_id in failed_kv_load_req_ids:
|
if failed_kv_load_req_ids and req_id in failed_kv_load_req_ids:
|
||||||
# Skip requests that were recovered from KV load failure
|
# Skip requests that were recovered from KV load failure
|
||||||
continue
|
continue
|
||||||
@@ -345,12 +333,6 @@ class OmniARScheduler(OmniSchedulerMixin, VLLMScheduler):
|
|||||||
# request is aborted while the model is executing it (e.g.,
|
# request is aborted while the model is executing it (e.g.,
|
||||||
# in pipeline parallelism or async scheduling).
|
# in pipeline parallelism or async scheduling).
|
||||||
continue
|
continue
|
||||||
if output_is_stale:
|
|
||||||
# Output of a step scheduled before the request's in-flight
|
|
||||||
# tokens were discarded (segment stop / session replacement).
|
|
||||||
# num_computed_tokens was rolled back at the discard site, so
|
|
||||||
# this output must not be appended or emitted.
|
|
||||||
continue
|
|
||||||
|
|
||||||
req_index = model_runner_output.req_id_to_index[req_id]
|
req_index = model_runner_output.req_id_to_index[req_id]
|
||||||
generated_token_ids = sampled_token_ids[req_index] if sampled_token_ids else []
|
generated_token_ids = sampled_token_ids[req_index] if sampled_token_ids else []
|
||||||
@@ -470,28 +452,10 @@ class OmniARScheduler(OmniSchedulerMixin, VLLMScheduler):
|
|||||||
# connector. This update only resumes polling for the next segment.
|
# connector. This update only resumes polling for the next segment.
|
||||||
self.chunk_transfer_adapter.segment_finished_requests.discard(request.request_id)
|
self.chunk_transfer_adapter.segment_finished_requests.discard(request.request_id)
|
||||||
outstanding_async_tokens = request.num_output_placeholders
|
outstanding_async_tokens = request.num_output_placeholders
|
||||||
# Always record the discard signal (0 when nothing is in
|
|
||||||
# flight). Upstream a0c092ee72 removed the
|
|
||||||
# `async_tokens_to_discard` default from `Request`; it
|
|
||||||
# remains an omni-only signal set here on every segment
|
|
||||||
# stop, so a stop with no outstanding placeholders
|
|
||||||
# explicitly records 0.
|
|
||||||
request.async_tokens_to_discard = outstanding_async_tokens
|
|
||||||
# Seed the stale share in SCHEDULED-token units:
|
|
||||||
# num_in_flight_tokens is exactly the unreported steps'
|
|
||||||
# num_tokens_scheduled sum (settled per frame at the top
|
|
||||||
# of this loop), and the drain subtracts each arriving
|
|
||||||
# frame's num_tokens_scheduled — commensurable by
|
|
||||||
# construction, so pre-discard frames drain to exactly
|
|
||||||
# zero. Seeding from num_output_placeholders swallowed
|
|
||||||
# valid new-segment frames or underflowed the drain
|
|
||||||
# assert whenever placeholder counts diverged from
|
|
||||||
# scheduled counts (spec drafts, in-flight prefill).
|
|
||||||
if request.num_in_flight_tokens > 0:
|
|
||||||
request.num_stale_output_tokens += request.num_in_flight_tokens
|
|
||||||
if outstanding_async_tokens > 0:
|
if outstanding_async_tokens > 0:
|
||||||
# Discard only outputs that are already in flight and
|
# Discard only outputs that are already in flight and
|
||||||
# roll back their optimistic computed-token accounting.
|
# roll back their optimistic computed-token accounting.
|
||||||
|
request.async_tokens_to_discard = outstanding_async_tokens
|
||||||
request.num_computed_tokens -= outstanding_async_tokens
|
request.num_computed_tokens -= outstanding_async_tokens
|
||||||
request.num_output_placeholders = 0
|
request.num_output_placeholders = 0
|
||||||
request.spec_token_ids = []
|
request.spec_token_ids = []
|
||||||
@@ -618,15 +582,6 @@ class OmniARScheduler(OmniSchedulerMixin, VLLMScheduler):
|
|||||||
req_id = session.request_id
|
req_id = session.request_id
|
||||||
self._new_prompt_len_snapshot[req_id] = len(update.prompt_token_ids)
|
self._new_prompt_len_snapshot[req_id] = len(update.prompt_token_ids)
|
||||||
outstanding_async_tokens = getattr(session, "num_output_placeholders", 0)
|
outstanding_async_tokens = getattr(session, "num_output_placeholders", 0)
|
||||||
# Seed the stale share in SCHEDULED-token units (see the segment-stop
|
|
||||||
# site in update_from_output): num_in_flight_tokens matches what each
|
|
||||||
# pre-replacement frame will drain, so the counter reaches exactly
|
|
||||||
# zero and the new segment's frames are never swallowed. This also
|
|
||||||
# covers an in-flight prefill chunk, which carries no placeholders
|
|
||||||
# but must still have its late output dropped.
|
|
||||||
in_flight_tokens = int(getattr(session, "num_in_flight_tokens", 0) or 0)
|
|
||||||
if in_flight_tokens > 0:
|
|
||||||
session.num_stale_output_tokens += in_flight_tokens
|
|
||||||
if outstanding_async_tokens > 0:
|
if outstanding_async_tokens > 0:
|
||||||
# Async scheduling may already have sampled the previous
|
# Async scheduling may already have sampled the previous
|
||||||
# segment's next token. Drop that late token instead of
|
# segment's next token. Drop that late token instead of
|
||||||
|
|||||||
@@ -374,18 +374,6 @@ class OmniGenerationScheduler(OmniSchedulerMixin, VLLMScheduler):
|
|||||||
# max(0, computed - in_flight), so a leaked counter silently
|
# max(0, computed - in_flight), so a leaked counter silently
|
||||||
# freezes sliding-window block freeing.
|
# freezes sliding-window block freeing.
|
||||||
request.num_in_flight_tokens -= num_tokens_scheduled
|
request.num_in_flight_tokens -= num_tokens_scheduled
|
||||||
# vLLM 0.27 (a0c092ee72) removed the async_tokens_to_discard
|
|
||||||
# handling from the upstream scheduler and replaced it with the
|
|
||||||
# num_stale_output_tokens/is_stale mechanism. Omni's discard
|
|
||||||
# sites (segment stop, streaming-session replacement) record the
|
|
||||||
# in-flight share here; the delayed outputs are dropped below
|
|
||||||
# instead of decrementing num_output_placeholders (which the
|
|
||||||
# discard zeroed) and underflowing the upstream assert.
|
|
||||||
output_is_stale = False
|
|
||||||
if request is not None and request.num_stale_output_tokens > 0:
|
|
||||||
output_is_stale = True
|
|
||||||
request.num_stale_output_tokens -= num_tokens_scheduled
|
|
||||||
assert request.num_stale_output_tokens >= 0
|
|
||||||
if failed_kv_load_req_ids and req_id in failed_kv_load_req_ids:
|
if failed_kv_load_req_ids and req_id in failed_kv_load_req_ids:
|
||||||
# Skip requests that were recovered from KV load failure
|
# Skip requests that were recovered from KV load failure
|
||||||
continue
|
continue
|
||||||
@@ -393,12 +381,6 @@ class OmniGenerationScheduler(OmniSchedulerMixin, VLLMScheduler):
|
|||||||
# Request may already be finished (e.g., aborted during
|
# Request may already be finished (e.g., aborted during
|
||||||
# execution / pipeline parallelism / async scheduling).
|
# execution / pipeline parallelism / async scheduling).
|
||||||
continue
|
continue
|
||||||
if output_is_stale:
|
|
||||||
# Output of a step scheduled before the request's in-flight
|
|
||||||
# tokens were discarded (segment stop / session replacement).
|
|
||||||
# num_computed_tokens was rolled back at the discard site, so
|
|
||||||
# this output must not be appended or emitted.
|
|
||||||
continue
|
|
||||||
|
|
||||||
req_index = model_runner_output.req_id_to_index[req_id]
|
req_index = model_runner_output.req_id_to_index[req_id]
|
||||||
generated_token_ids = sampled_token_ids[req_index] if sampled_token_ids else []
|
generated_token_ids = sampled_token_ids[req_index] if sampled_token_ids else []
|
||||||
|
|||||||
@@ -94,17 +94,6 @@ class OmniSchedulerMixin:
|
|||||||
adapter.segment_finished_requests.discard(session.request_id)
|
adapter.segment_finished_requests.discard(session.request_id)
|
||||||
session._output_token_ids.clear()
|
session._output_token_ids.clear()
|
||||||
session._all_token_ids.clear()
|
session._all_token_ids.clear()
|
||||||
# In-flight outputs from the previous segment were optimistically
|
|
||||||
# scheduled (async lookahead). Mark them stale so update_from_output
|
|
||||||
# drops them instead of underflowing num_output_placeholders
|
|
||||||
# (vLLM 0.27 a0c092ee72 removed async_tokens_to_discard). Seed in
|
|
||||||
# SCHEDULED-token units — num_in_flight_tokens matches what each
|
|
||||||
# pre-replacement frame will drain, so the counter reaches exactly
|
|
||||||
# zero; a placeholder-based seed swallowed valid new-segment frames
|
|
||||||
# whenever placeholder counts diverged from scheduled counts.
|
|
||||||
session.num_stale_output_tokens += int(getattr(session, "num_in_flight_tokens", 0) or 0)
|
|
||||||
session.num_output_placeholders = 0
|
|
||||||
session.spec_token_ids = []
|
|
||||||
new_prompt = update.prompt_token_ids or ()
|
new_prompt = update.prompt_token_ids or ()
|
||||||
session._all_token_ids.extend(new_prompt)
|
session._all_token_ids.extend(new_prompt)
|
||||||
session.num_computed_tokens = 0
|
session.num_computed_tokens = 0
|
||||||
|
|||||||
@@ -57,10 +57,9 @@ _FULL_PAYLOAD_INPUT_STAGES: frozenset[tuple[str, str]] = frozenset(
|
|||||||
# audex TTA: tta thinker (Stage 0) -> XCodec1 (Stage 1, always sync
|
# audex TTA: tta thinker (Stage 0) -> XCodec1 (Stage 1, always sync
|
||||||
# full-payload — CNN codec decoded over the full sequence).
|
# full-payload — CNN codec decoded over the full sequence).
|
||||||
("AudexXCodec1", "audex_xcodec"),
|
("AudexXCodec1", "audex_xcodec"),
|
||||||
# indextts2 / indextts2_5: talker (Stage 0) -> s2mel decoder
|
# indextts2: indextts2_talker (Stage 0) -> indextts2_s2mel_decoder
|
||||||
# (Stage 1). Stage 1 consumes the complete mel/optional-latent payload.
|
# (Stage 1). Stage 1 consumes the complete mel/latent payload.
|
||||||
("IndexTTS2S2MelDecoder", "indextts2_s2mel_decoder"),
|
("IndexTTS2S2MelDecoder", "indextts2_s2mel_decoder"),
|
||||||
("IndexTTS25S2MelDecoder", "indextts2_5_s2mel_decoder"),
|
|
||||||
# dynin: token2text (Stage 0) -> token2image (Stage 1) ->
|
# dynin: token2text (Stage 0) -> token2image (Stage 1) ->
|
||||||
# token2audio (Stage 2). Producer wires via
|
# token2audio (Stage 2). Producer wires via
|
||||||
# custom_process_next_stage_input_func: *_full_payload in deploy yaml.
|
# custom_process_next_stage_input_func: *_full_payload in deploy yaml.
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
async_chunk: false
|
|
||||||
trust_remote_code: true
|
|
||||||
pipeline: indextts2_5
|
|
||||||
connectors:
|
|
||||||
connector_of_shared_memory:
|
|
||||||
name: SharedMemoryConnector
|
|
||||||
extra:
|
|
||||||
codec_streaming: false
|
|
||||||
connector_get_sleep_s: 0.01
|
|
||||||
connector_get_max_wait_first_chunk: 5000
|
|
||||||
connector_get_max_wait: 500
|
|
||||||
stages:
|
|
||||||
- stage_id: 0
|
|
||||||
max_num_seqs: 4
|
|
||||||
# Triton matches the native GPT-2 logits for this non-RoPE model.
|
|
||||||
# FlashInfer produces materially different semantic-token logits.
|
|
||||||
attention_backend: TRITON_ATTN
|
|
||||||
gpu_memory_utilization: 0.4
|
|
||||||
enable_prefix_caching: false
|
|
||||||
enable_chunked_prefill: false
|
|
||||||
skip_tokenizer_init: true
|
|
||||||
async_scheduling: true
|
|
||||||
max_num_batched_tokens: 2560
|
|
||||||
max_model_len: 2560
|
|
||||||
devices: "0"
|
|
||||||
output_connectors:
|
|
||||||
to_stage_1: connector_of_shared_memory
|
|
||||||
hf_overrides:
|
|
||||||
use_gpt_latent: false
|
|
||||||
# Stage 0 uses plain vLLM sampling, not the upstream default
|
|
||||||
# num_beams=3 beam search. Use upstream num_beams=1 for parity comparisons.
|
|
||||||
default_sampling_params:
|
|
||||||
temperature: 0.8
|
|
||||||
top_p: 0.8
|
|
||||||
top_k: 30
|
|
||||||
max_tokens: 1500
|
|
||||||
repetition_penalty: 10.0
|
|
||||||
stop_token_ids: [8193]
|
|
||||||
detokenize: false
|
|
||||||
- stage_id: 1
|
|
||||||
max_num_seqs: 4
|
|
||||||
gpu_memory_utilization: 0.4
|
|
||||||
enable_prefix_caching: false
|
|
||||||
skip_tokenizer_init: true
|
|
||||||
enforce_eager: true
|
|
||||||
async_scheduling: true
|
|
||||||
max_num_batched_tokens: 8192
|
|
||||||
max_model_len: 32768
|
|
||||||
devices: "0"
|
|
||||||
input_connectors:
|
|
||||||
from_stage_0: connector_of_shared_memory
|
|
||||||
hf_overrides:
|
|
||||||
use_gpt_latent: false
|
|
||||||
diffusion_steps: 25
|
|
||||||
s2mel_dit_bf16: true
|
|
||||||
s2mel_cfm_batch_size: 4
|
|
||||||
inference_cfg_rate: 0.7
|
|
||||||
s2mel_dit_cuda_graph: false
|
|
||||||
s2mel_dit_torch_compile: true
|
|
||||||
s2mel_dit_torch_compile_mode: default
|
|
||||||
s2mel_vocoder_torch_compile: false
|
|
||||||
default_sampling_params:
|
|
||||||
temperature: 0.0
|
|
||||||
max_tokens: 32767
|
|
||||||
@@ -13,8 +13,8 @@
|
|||||||
# the custom generation loop — set explicitly everywhere).
|
# the custom generation loop — set explicitly everywhere).
|
||||||
# * AR stages (thinker, talker): model-dependent. Qwen2.5-Omni runs eager
|
# * AR stages (thinker, talker): model-dependent. Qwen2.5-Omni runs eager
|
||||||
# on CUDA (thinker uses custom ops that don't trace cleanly); NPU / XPU
|
# on CUDA (thinker uses custom ops that don't trace cleanly); NPU / XPU
|
||||||
# overrides are documented below. Qwen3-Omni / Qwen3-TTS AR stages use the
|
# platform overrides flip back to false where cudagraph is verified.
|
||||||
# default (false = cudagraph on).
|
# Qwen3-Omni / Qwen3-TTS AR stages use the default (false = cudagraph on).
|
||||||
async_chunk: false
|
async_chunk: false
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
@@ -82,15 +82,13 @@ platforms:
|
|||||||
|
|
||||||
xpu:
|
xpu:
|
||||||
stages:
|
stages:
|
||||||
# PyTorch 2.13 XPU Dynamo raises duplicate-handler assertions while
|
# Verified on 2x Intel Arc Pro B60. Both AR stages use cudagraphs.
|
||||||
# compiling both Qwen2.5 AR stages. Keep them eager until that toolchain
|
|
||||||
# issue is fixed; stage 2 already inherits enforce_eager: true above.
|
|
||||||
- stage_id: 0
|
- stage_id: 0
|
||||||
gpu_memory_utilization: 0.9
|
gpu_memory_utilization: 0.9
|
||||||
enforce_eager: true
|
enforce_eager: false
|
||||||
- stage_id: 1
|
- stage_id: 1
|
||||||
gpu_memory_utilization: 0.5
|
gpu_memory_utilization: 0.5
|
||||||
enforce_eager: true
|
enforce_eager: false
|
||||||
- stage_id: 2
|
- stage_id: 2
|
||||||
gpu_memory_utilization: 0.3
|
gpu_memory_utilization: 0.3
|
||||||
# Stage 2 colocates with stage 1's device on XPU.
|
# Stage 2 colocates with stage 1's device on XPU.
|
||||||
|
|||||||
@@ -60,10 +60,7 @@ def regionally_compile(
|
|||||||
for name, submod in model.named_modules():
|
for name, submod in model.named_modules():
|
||||||
if _matches_repeated_block(name, submod, repeated_blocks, repeated_block_attrs):
|
if _matches_repeated_block(name, submod, repeated_blocks, repeated_block_attrs):
|
||||||
# Compile the block compute while keeping nn.Module.__call__ hooks
|
# Compile the block compute while keeping nn.Module.__call__ hooks
|
||||||
# outside the compiled graph. NOTE: anything that wraps this
|
# outside the compiled graph.
|
||||||
# callable must stay signature-transparent — cache-dit's
|
|
||||||
# BlockAdapter matches blocks by inspecting forward's parameter
|
|
||||||
# names and return annotation, which torch.compile preserves.
|
|
||||||
compiled_forwards.append(
|
compiled_forwards.append(
|
||||||
(
|
(
|
||||||
submod,
|
submod,
|
||||||
|
|||||||
@@ -28,8 +28,6 @@ If you only need to use the distributed environment without model parallelism,
|
|||||||
you can skip the model parallel initialization and destruction steps.
|
you can skip the model parallel initialization and destruction steps.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import inspect
|
|
||||||
|
|
||||||
import torch
|
import torch
|
||||||
import torch.distributed
|
import torch.distributed
|
||||||
import vllm.distributed.parallel_state as vllm_parallel_state
|
import vllm.distributed.parallel_state as vllm_parallel_state
|
||||||
@@ -526,23 +524,13 @@ def init_vllm_model_parallel_group(
|
|||||||
local_rank: int,
|
local_rank: int,
|
||||||
backend: str,
|
backend: str,
|
||||||
group_name: str,
|
group_name: str,
|
||||||
use_all2all: bool = False,
|
|
||||||
) -> vllm_parallel_state.GroupCoordinator:
|
) -> vllm_parallel_state.GroupCoordinator:
|
||||||
# vLLM 0.27's MoE oracle (make_unquantized_moe_kernel ->
|
|
||||||
# get_ep_all2all_manager) asserts that the EP group's device communicator
|
|
||||||
# carries an all2all_manager, and vLLM only builds one when the group is
|
|
||||||
# created with use_all2all=True. Older vLLM has neither the kwarg nor the
|
|
||||||
# requirement, so forward it only when supported.
|
|
||||||
kwargs = {}
|
|
||||||
if use_all2all and "use_all2all" in inspect.signature(vllm_parallel_state.init_model_parallel_group).parameters:
|
|
||||||
kwargs["use_all2all"] = True
|
|
||||||
return vllm_parallel_state.init_model_parallel_group(
|
return vllm_parallel_state.init_model_parallel_group(
|
||||||
group_ranks=group_ranks,
|
group_ranks=group_ranks,
|
||||||
local_rank=local_rank,
|
local_rank=local_rank,
|
||||||
backend=backend,
|
backend=backend,
|
||||||
group_name=group_name,
|
group_name=group_name,
|
||||||
use_device_communicator=True,
|
use_device_communicator=True,
|
||||||
**kwargs,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -966,7 +954,6 @@ def initialize_model_parallel(
|
|||||||
local_rank=get_world_group().local_rank,
|
local_rank=get_world_group().local_rank,
|
||||||
backend=backend,
|
backend=backend,
|
||||||
group_name="ep",
|
group_name="ep",
|
||||||
use_all2all=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
init_dit_group(dit_parallel_size, backend)
|
init_dit_group(dit_parallel_size, backend)
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ class FusedMoE:
|
|||||||
# Current vLLM FusedMoE handles output reduction internally.
|
# Current vLLM FusedMoE handles output reduction internally.
|
||||||
kwargs.pop("reduce_results", None)
|
kwargs.pop("reduce_results", None)
|
||||||
# FusedMoE is now a factory function; call it to get a MoERunner.
|
# FusedMoE is now a factory function; call it to get a MoERunner.
|
||||||
from vllm.model_executor.layers.fused_moe import FusedMoEFactory as _FusedMoE
|
from vllm.model_executor.layers.fused_moe import FusedMoE as _FusedMoE
|
||||||
|
|
||||||
moe_runner = _FusedMoE(prefix=prefix, **kwargs)
|
moe_runner = _FusedMoE(prefix=prefix, **kwargs)
|
||||||
|
|
||||||
|
|||||||
@@ -23,11 +23,7 @@ from diffusers.pipelines.pipeline_utils import DiffusionPipeline
|
|||||||
from torch import nn
|
from torch import nn
|
||||||
|
|
||||||
from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig
|
from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig
|
||||||
from vllm_omni.diffusion.models.diffusers_adapter.pipeline_utils import (
|
from vllm_omni.diffusion.models.diffusers_adapter.pipeline_utils import BasePipelineUtils, get_pipeline_utils
|
||||||
BasePipelineUtils,
|
|
||||||
get_pipeline_utils,
|
|
||||||
validate_model_compatibility,
|
|
||||||
)
|
|
||||||
from vllm_omni.diffusion.models.diffusers_adapter.quantization_utils import (
|
from vllm_omni.diffusion.models.diffusers_adapter.quantization_utils import (
|
||||||
apply_diffusers_quantization_config,
|
apply_diffusers_quantization_config,
|
||||||
convert_diffusers_quantization_config,
|
convert_diffusers_quantization_config,
|
||||||
@@ -40,22 +36,6 @@ from vllm_omni.platforms import current_omni_platform
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Preference chain for CUDA flash attention in diffusers, best first. Shared
|
|
||||||
# with the accuracy tests' diffusers reference runs so both sides of a
|
|
||||||
# similarity comparison resolve to the same backend on any given image
|
|
||||||
# (hub kernels may lack a build variant for the image's torch).
|
|
||||||
CUDA_FLASH_ATTENTION_BACKEND_ATTEMPTS: list[str] = [
|
|
||||||
"_flash_3_hub",
|
|
||||||
"_flash_3_varlen_hub",
|
|
||||||
"_flash_3",
|
|
||||||
"_flash_varlen_3",
|
|
||||||
"flash_hub",
|
|
||||||
"flash_varlen_hub",
|
|
||||||
"flash",
|
|
||||||
"flash_varlen",
|
|
||||||
"_native_flash",
|
|
||||||
]
|
|
||||||
|
|
||||||
_DIFFUSERS_CONFIG_LOAD_KWARGS = {
|
_DIFFUSERS_CONFIG_LOAD_KWARGS = {
|
||||||
"cache_dir",
|
"cache_dir",
|
||||||
"dduf_entries",
|
"dduf_entries",
|
||||||
@@ -114,7 +94,6 @@ class DiffusersAdapterPipeline(nn.Module, DiffusionPipelineProfilerMixin):
|
|||||||
|
|
||||||
model_id = self.od_config.model
|
model_id = self.od_config.model
|
||||||
dtype = self.od_config.dtype
|
dtype = self.od_config.dtype
|
||||||
validate_model_compatibility(self.od_config)
|
|
||||||
|
|
||||||
load_kwargs = {
|
load_kwargs = {
|
||||||
"torch_dtype": dtype,
|
"torch_dtype": dtype,
|
||||||
@@ -135,8 +114,6 @@ class DiffusersAdapterPipeline(nn.Module, DiffusionPipelineProfilerMixin):
|
|||||||
logger.debug(f"Loading diffusers pipeline with kwargs: {load_kwargs}")
|
logger.debug(f"Loading diffusers pipeline with kwargs: {load_kwargs}")
|
||||||
|
|
||||||
self._pipeline = DiffusionPipeline.from_pretrained(model_id, **load_kwargs)
|
self._pipeline = DiffusionPipeline.from_pretrained(model_id, **load_kwargs)
|
||||||
if pipeline_class_name is None:
|
|
||||||
self._pipeline_utils = get_pipeline_utils(self._pipeline.__class__.__name__)
|
|
||||||
self._pipeline_utils.apply_post_load_updates(self._pipeline, self.od_config)
|
self._pipeline_utils.apply_post_load_updates(self._pipeline, self.od_config)
|
||||||
|
|
||||||
self._pipeline.to(self.device)
|
self._pipeline.to(self.device)
|
||||||
@@ -153,14 +130,14 @@ class DiffusersAdapterPipeline(nn.Module, DiffusionPipelineProfilerMixin):
|
|||||||
# VAE slicing and tiling: try-catch because not all models have VAE
|
# VAE slicing and tiling: try-catch because not all models have VAE
|
||||||
if self.od_config.vae_use_slicing:
|
if self.od_config.vae_use_slicing:
|
||||||
try:
|
try:
|
||||||
self._pipeline_utils.enable_vae_optimization(self._pipeline, "slicing")
|
self._pipeline.enable_vae_slicing()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"Failed to enable VAE slicing for diffusers pipeline {self._pipeline.__class__.__name__}: {e}"
|
f"Failed to enable VAE slicing for diffusers pipeline {self._pipeline.__class__.__name__}: {e}"
|
||||||
)
|
)
|
||||||
if self.od_config.vae_use_tiling:
|
if self.od_config.vae_use_tiling:
|
||||||
try:
|
try:
|
||||||
self._pipeline_utils.enable_vae_optimization(self._pipeline, "tiling")
|
self._pipeline.enable_vae_tiling()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"Failed to enable VAE tiling for diffusers pipeline {self._pipeline.__class__.__name__}: {e}"
|
f"Failed to enable VAE tiling for diffusers pipeline {self._pipeline.__class__.__name__}: {e}"
|
||||||
@@ -302,7 +279,19 @@ class DiffusersAdapterPipeline(nn.Module, DiffusionPipelineProfilerMixin):
|
|||||||
)
|
)
|
||||||
attention_backend_attempts.append("native")
|
attention_backend_attempts.append("native")
|
||||||
else:
|
else:
|
||||||
attention_backend_attempts.extend(CUDA_FLASH_ATTENTION_BACKEND_ATTEMPTS)
|
attention_backend_attempts.extend(
|
||||||
|
[
|
||||||
|
"_flash_3_hub",
|
||||||
|
"_flash_3_varlen_hub",
|
||||||
|
"_flash_3",
|
||||||
|
"_flash_varlen_3",
|
||||||
|
"flash_hub",
|
||||||
|
"flash_varlen_hub",
|
||||||
|
"flash",
|
||||||
|
"flash_varlen",
|
||||||
|
"_native_flash",
|
||||||
|
]
|
||||||
|
)
|
||||||
case "SAGE_ATTN":
|
case "SAGE_ATTN":
|
||||||
attention_backend_attempts.extend(["sage_hub", "sage", "sage", "sage_varlen"])
|
attention_backend_attempts.extend(["sage_hub", "sage", "sage", "sage_varlen"])
|
||||||
case "ASCEND":
|
case "ASCEND":
|
||||||
@@ -400,8 +389,6 @@ class DiffusersAdapterPipeline(nn.Module, DiffusionPipelineProfilerMixin):
|
|||||||
else:
|
else:
|
||||||
kwargs["generator"] = torch.Generator(device=sampling.generator_device)
|
kwargs["generator"] = torch.Generator(device=sampling.generator_device)
|
||||||
|
|
||||||
self._pipeline_utils.update_call_kwargs(self.od_config, kwargs)
|
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"Calling diffusers pipeline with kwargs: %s", DiffusersAdapterPipeline._summarize_call_kwargs_value(kwargs)
|
"Calling diffusers pipeline with kwargs: %s", DiffusersAdapterPipeline._summarize_call_kwargs_value(kwargs)
|
||||||
)
|
)
|
||||||
@@ -506,8 +493,6 @@ class DiffusersAdapterPipeline(nn.Module, DiffusionPipelineProfilerMixin):
|
|||||||
"""
|
"""
|
||||||
from vllm_omni.diffusion.data import DiffusionOutput
|
from vllm_omni.diffusion.data import DiffusionOutput
|
||||||
|
|
||||||
output = self._pipeline_utils.normalize_output(self._pipeline, self.od_config, output)
|
|
||||||
|
|
||||||
if hasattr(output, "images"):
|
if hasattr(output, "images"):
|
||||||
# Preserve diffusers image format (`output_type`)
|
# Preserve diffusers image format (`output_type`)
|
||||||
return DiffusionOutput(output=output.images)
|
return DiffusionOutput(output=output.images)
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||||
|
|
||||||
import inspect
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from diffusers.pipelines.pipeline_utils import DiffusionPipeline
|
from diffusers.pipelines.pipeline_utils import DiffusionPipeline
|
||||||
@@ -12,74 +8,6 @@ from diffusers.pipelines.pipeline_utils import DiffusionPipeline
|
|||||||
from vllm_omni.diffusion.data import OmniDiffusionConfig
|
from vllm_omni.diffusion.data import OmniDiffusionConfig
|
||||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
LTX25_ORIGINAL_MODEL_ID = "Lightricks/LTX-2.5"
|
|
||||||
LTX25_DIFFUSERS_MODEL_ID = "Lightricks/LTX-2.5-Diffusers"
|
|
||||||
LTX25_DIFFUSERS_COMMIT = "7564fb016dabda0c943416190fc92398c50b1b20"
|
|
||||||
|
|
||||||
# Diffusers' public LTX-2.5 schedule intentionally excludes the scheduler's
|
|
||||||
# terminal zero. FlowMatchEulerDiscreteScheduler appends that internally.
|
|
||||||
LTX25_DISTILLED_SIGMAS = [1.0, 0.99375, 0.9875, 0.98125, 0.975, 0.909375, 0.725, 0.421875]
|
|
||||||
|
|
||||||
_LTX25_DISTILLED_GUIDANCE_DEFAULTS: dict[str, float] = {
|
|
||||||
"guidance_scale": 1.0,
|
|
||||||
"audio_guidance_scale": 1.0,
|
|
||||||
"stg_scale": 0.0,
|
|
||||||
"audio_stg_scale": 0.0,
|
|
||||||
"modality_scale": 1.0,
|
|
||||||
"audio_modality_scale": 1.0,
|
|
||||||
"guidance_rescale": 0.0,
|
|
||||||
"audio_guidance_rescale": 0.0,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _supports_ltx25_diffusers() -> bool:
|
|
||||||
"""Check the standard-pipeline capabilities added with LTX-2.5."""
|
|
||||||
try:
|
|
||||||
import diffusers
|
|
||||||
import transformers
|
|
||||||
|
|
||||||
pipeline_class = diffusers.LTX2Pipeline
|
|
||||||
init_parameters = inspect.signature(pipeline_class.__init__).parameters
|
|
||||||
return (
|
|
||||||
"duration_head" in init_parameters
|
|
||||||
and "prompt_enhancer" in init_parameters
|
|
||||||
and hasattr(transformers, "Gemma4UnifiedForConditionalGeneration")
|
|
||||||
)
|
|
||||||
except (AttributeError, ImportError, RuntimeError):
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def is_ltx25_diffusers_model(od_config: OmniDiffusionConfig) -> bool:
|
|
||||||
"""Return whether config targets the official converted LTX-2.5 repo."""
|
|
||||||
normalized_model = str(od_config.model).rstrip("/\\").replace("\\", "/").lower()
|
|
||||||
if normalized_model == LTX25_DIFFUSERS_MODEL_ID.lower():
|
|
||||||
return True
|
|
||||||
|
|
||||||
# A downloaded checkpoint may be served from any directory name. The
|
|
||||||
# duration-head component is the pipeline-level marker introduced for
|
|
||||||
# LTX-2.5; older LTX-2.0/2.3 indexes do not contain it.
|
|
||||||
model_path = Path(str(od_config.model)).expanduser()
|
|
||||||
subfolder = od_config.diffusers_load_kwargs.get("subfolder")
|
|
||||||
if subfolder is not None:
|
|
||||||
model_path /= str(subfolder)
|
|
||||||
try:
|
|
||||||
with (model_path / "model_index.json").open(encoding="utf-8") as model_index_file:
|
|
||||||
model_index = json.load(model_index_file)
|
|
||||||
except (OSError, TypeError, ValueError):
|
|
||||||
return False
|
|
||||||
if not isinstance(model_index, dict):
|
|
||||||
return False
|
|
||||||
|
|
||||||
duration_head = model_index.get("duration_head")
|
|
||||||
return (
|
|
||||||
model_index.get("_class_name") == "LTX2Pipeline"
|
|
||||||
and isinstance(duration_head, list)
|
|
||||||
and bool(duration_head)
|
|
||||||
and duration_head[0] is not None
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class BasePipelineUtils:
|
class BasePipelineUtils:
|
||||||
"""No-op hooks for pipeline-specific diffusers adapter behavior."""
|
"""No-op hooks for pipeline-specific diffusers adapter behavior."""
|
||||||
@@ -90,112 +18,9 @@ class BasePipelineUtils:
|
|||||||
def apply_post_load_updates(self, pipeline: DiffusionPipeline, od_config: OmniDiffusionConfig) -> None:
|
def apply_post_load_updates(self, pipeline: DiffusionPipeline, od_config: OmniDiffusionConfig) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def enable_vae_optimization(self, pipeline: DiffusionPipeline, optimization: str) -> None:
|
|
||||||
getattr(pipeline, f"enable_vae_{optimization}")()
|
|
||||||
|
|
||||||
def validate_runtime_sampling_params(self, sampling: OmniDiffusionSamplingParams) -> None:
|
def validate_runtime_sampling_params(self, sampling: OmniDiffusionSamplingParams) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def update_call_kwargs(
|
|
||||||
self,
|
|
||||||
od_config: OmniDiffusionConfig,
|
|
||||||
call_kwargs: dict[str, Any],
|
|
||||||
) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def normalize_output(
|
|
||||||
self,
|
|
||||||
pipeline: DiffusionPipeline,
|
|
||||||
od_config: OmniDiffusionConfig,
|
|
||||||
output: Any,
|
|
||||||
) -> Any:
|
|
||||||
return output
|
|
||||||
|
|
||||||
|
|
||||||
def validate_model_compatibility(od_config: OmniDiffusionConfig) -> None:
|
|
||||||
"""Reject known model/dependency combinations before loading weights."""
|
|
||||||
normalized_model = str(od_config.model).rstrip("/\\").replace("\\", "/").lower()
|
|
||||||
if normalized_model == LTX25_ORIGINAL_MODEL_ID.lower():
|
|
||||||
raise ValueError(
|
|
||||||
f"{LTX25_ORIGINAL_MODEL_ID} is a component-weight repository without a Diffusers "
|
|
||||||
f"model_index.json. Use the official converted repository {LTX25_DIFFUSERS_MODEL_ID} "
|
|
||||||
"with --diffusion-load-format diffusers."
|
|
||||||
)
|
|
||||||
|
|
||||||
if is_ltx25_diffusers_model(od_config) and not _supports_ltx25_diffusers():
|
|
||||||
raise ImportError(
|
|
||||||
f"{LTX25_DIFFUSERS_MODEL_ID} requires unreleased LTX-2.5 support from Diffusers and "
|
|
||||||
"Gemma 4 Unified support from Transformers. Install the source-preview dependencies with "
|
|
||||||
"`python -m pip install --upgrade 'transformers>=5.10.1' "
|
|
||||||
f"'diffusers @ git+https://github.com/huggingface/diffusers.git@{LTX25_DIFFUSERS_COMMIT}'`."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class LTX2PipelineUtils(BasePipelineUtils):
|
|
||||||
"""Diffusers call policy for the official default LTX-2.5 checkpoint."""
|
|
||||||
|
|
||||||
def enable_vae_optimization(self, pipeline: DiffusionPipeline, optimization: str) -> None:
|
|
||||||
pipeline_method = getattr(pipeline, f"enable_vae_{optimization}", None)
|
|
||||||
if callable(pipeline_method):
|
|
||||||
pipeline_method()
|
|
||||||
else:
|
|
||||||
getattr(pipeline.vae, f"enable_{optimization}")()
|
|
||||||
|
|
||||||
def update_call_kwargs(
|
|
||||||
self,
|
|
||||||
od_config: OmniDiffusionConfig,
|
|
||||||
call_kwargs: dict[str, Any],
|
|
||||||
) -> None:
|
|
||||||
if not is_ltx25_diffusers_model(od_config):
|
|
||||||
return
|
|
||||||
|
|
||||||
has_sigmas = call_kwargs.get("sigmas") is not None
|
|
||||||
has_timesteps = call_kwargs.get("timesteps") is not None
|
|
||||||
has_step_count = call_kwargs.get("num_inference_steps") is not None
|
|
||||||
if not has_sigmas and not has_timesteps:
|
|
||||||
if has_step_count:
|
|
||||||
logger.warning(
|
|
||||||
"LTX-2.5 distilled inference received num_inference_steps without sigmas. "
|
|
||||||
"Diffusers will use a generic schedule; omit num_inference_steps to use the "
|
|
||||||
"official distilled schedule."
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
call_kwargs["sigmas"] = list(LTX25_DISTILLED_SIGMAS)
|
|
||||||
call_kwargs["num_inference_steps"] = len(LTX25_DISTILLED_SIGMAS)
|
|
||||||
|
|
||||||
for key, value in _LTX25_DISTILLED_GUIDANCE_DEFAULTS.items():
|
|
||||||
if call_kwargs.get(key) is None:
|
|
||||||
call_kwargs[key] = value
|
|
||||||
|
|
||||||
def normalize_output(
|
|
||||||
self,
|
|
||||||
pipeline: DiffusionPipeline,
|
|
||||||
od_config: OmniDiffusionConfig,
|
|
||||||
output: Any,
|
|
||||||
) -> Any:
|
|
||||||
if not is_ltx25_diffusers_model(od_config):
|
|
||||||
return output
|
|
||||||
|
|
||||||
frames = getattr(output, "frames", None)
|
|
||||||
audio = getattr(output, "audio", None)
|
|
||||||
if frames is None or audio is None:
|
|
||||||
return output
|
|
||||||
|
|
||||||
envelope: dict[str, Any] = {"payload": {"video": frames, "audio": audio}}
|
|
||||||
config = getattr(getattr(pipeline, "vocoder", None), "config", None)
|
|
||||||
value = (
|
|
||||||
config.get("output_sampling_rate")
|
|
||||||
if hasattr(config, "get")
|
|
||||||
else getattr(config, "output_sampling_rate", None)
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
sample_rate = None if isinstance(value, bool) else int(value)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
sample_rate = None
|
|
||||||
if sample_rate is not None and sample_rate > 0:
|
|
||||||
envelope["metadata"] = {"audio": {"sample_rate": sample_rate}}
|
|
||||||
return envelope
|
|
||||||
|
|
||||||
|
|
||||||
class WanPipelineUtils(BasePipelineUtils):
|
class WanPipelineUtils(BasePipelineUtils):
|
||||||
def update_load_kwargs(self, od_config: OmniDiffusionConfig, load_kwargs: dict[str, Any]) -> None:
|
def update_load_kwargs(self, od_config: OmniDiffusionConfig, load_kwargs: dict[str, Any]) -> None:
|
||||||
@@ -224,7 +49,6 @@ class WanPipelineUtils(BasePipelineUtils):
|
|||||||
|
|
||||||
|
|
||||||
PIPELINE_UTILS_REGISTRY: dict[str, type[BasePipelineUtils]] = {
|
PIPELINE_UTILS_REGISTRY: dict[str, type[BasePipelineUtils]] = {
|
||||||
"LTX2Pipeline": LTX2PipelineUtils,
|
|
||||||
"WanPipeline": WanPipelineUtils,
|
"WanPipeline": WanPipelineUtils,
|
||||||
"WanImageToVideoPipeline": WanPipelineUtils,
|
"WanImageToVideoPipeline": WanPipelineUtils,
|
||||||
"WanVACEPipeline": WanPipelineUtils,
|
"WanVACEPipeline": WanPipelineUtils,
|
||||||
|
|||||||
@@ -299,21 +299,16 @@ def resolve_lingbot_output_dimensions(
|
|||||||
sampling_width: Any = None,
|
sampling_width: Any = None,
|
||||||
sampling_height: Any = None,
|
sampling_height: Any = None,
|
||||||
prompt_fields: Mapping[str, Any] | None = None,
|
prompt_fields: Mapping[str, Any] | None = None,
|
||||||
extra_fields: Mapping[str, Any] | None = None,
|
|
||||||
default_width: int = 480,
|
default_width: int = 480,
|
||||||
default_height: int = 480,
|
default_height: int = 480,
|
||||||
) -> tuple[int, int]:
|
) -> tuple[int, int]:
|
||||||
"""Resolve the ``(width, height)`` that LingBot will use for generation."""
|
"""Resolve the ``(width, height)`` that LingBot will use for generation."""
|
||||||
prompt_fields = prompt_fields or {}
|
prompt_fields = prompt_fields or {}
|
||||||
extra_fields = extra_fields or {}
|
requested_width = _pick(prompt_fields, "width")
|
||||||
# Online requests deliver `extra_params` through sampling.extra_args; the
|
requested_height = _pick(prompt_fields, "height")
|
||||||
# offline path embeds the same fields in the prompt dict. Consult both,
|
size = _pick(prompt_fields, "size")
|
||||||
# extra_args first — the same precedence fps and num_frames use.
|
resolution = _pick(prompt_fields, "resolution")
|
||||||
requested_width = _first_not_none(_pick(extra_fields, "width"), _pick(prompt_fields, "width"))
|
ratio = _pick(prompt_fields, "ratio")
|
||||||
requested_height = _first_not_none(_pick(extra_fields, "height"), _pick(prompt_fields, "height"))
|
|
||||||
size = _first_not_none(_pick(extra_fields, "size"), _pick(prompt_fields, "size"))
|
|
||||||
resolution = _first_not_none(_pick(extra_fields, "resolution"), _pick(prompt_fields, "resolution"))
|
|
||||||
ratio = _first_not_none(_pick(extra_fields, "ratio"), _pick(prompt_fields, "ratio"))
|
|
||||||
if requested_width is not None or requested_height is not None:
|
if requested_width is not None or requested_height is not None:
|
||||||
width, height = requested_width, requested_height
|
width, height = requested_width, requested_height
|
||||||
elif size is not None or resolution is not None or ratio is not None:
|
elif size is not None or resolution is not None or ratio is not None:
|
||||||
@@ -419,7 +414,6 @@ def normalize_lingbot_request(
|
|||||||
sampling_width=sampling.width,
|
sampling_width=sampling.width,
|
||||||
sampling_height=sampling.height,
|
sampling_height=sampling.height,
|
||||||
prompt_fields=prompt_fields,
|
prompt_fields=prompt_fields,
|
||||||
extra_fields=extra_args,
|
|
||||||
default_width=default_width,
|
default_width=default_width,
|
||||||
default_height=default_height,
|
default_height=default_height,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -22,8 +22,6 @@ _ARCH_TO_MODEL_TYPE: dict[str, str] = {
|
|||||||
"GLMTTSForConditionalGeneration": "glm_tts",
|
"GLMTTSForConditionalGeneration": "glm_tts",
|
||||||
"IndexTTS2S2MelDecoder": "indextts2",
|
"IndexTTS2S2MelDecoder": "indextts2",
|
||||||
"IndexTTS2TalkerForConditionalGeneration": "indextts2",
|
"IndexTTS2TalkerForConditionalGeneration": "indextts2",
|
||||||
"IndexTTS25S2MelDecoder": "indextts2_5",
|
|
||||||
"IndexTTS25TalkerForConditionalGeneration": "indextts2_5",
|
|
||||||
"OmniVoiceModel": "omnivoice",
|
"OmniVoiceModel": "omnivoice",
|
||||||
# PersonaPlex ships an empty config.json, so create_model_config() must patch
|
# PersonaPlex ships an empty config.json, so create_model_config() must patch
|
||||||
# model_type=personaplex from the arch name or the staged pipeline can't load
|
# model_type=personaplex from the arch name or the staged pipeline can't load
|
||||||
@@ -46,7 +44,6 @@ def _register_omni_hf_configs() -> None:
|
|||||||
|
|
||||||
from vllm_omni.model_executor.models.indextts2.configuration_indextts2 import (
|
from vllm_omni.model_executor.models.indextts2.configuration_indextts2 import (
|
||||||
IndexTTS2Config,
|
IndexTTS2Config,
|
||||||
IndexTTS25Config,
|
|
||||||
)
|
)
|
||||||
from vllm_omni.model_executor.models.ming_tts.config_ming_tts import (
|
from vllm_omni.model_executor.models.ming_tts.config_ming_tts import (
|
||||||
MingDenseConfig,
|
MingDenseConfig,
|
||||||
@@ -82,7 +79,6 @@ def _register_omni_hf_configs() -> None:
|
|||||||
("dense", MingDenseConfig),
|
("dense", MingDenseConfig),
|
||||||
("bailingmm", MingMoeConfig),
|
("bailingmm", MingMoeConfig),
|
||||||
("indextts2", IndexTTS2Config),
|
("indextts2", IndexTTS2Config),
|
||||||
("indextts2_5", IndexTTS25Config),
|
|
||||||
("moss_tts_local", MossTTSLocalConfig),
|
("moss_tts_local", MossTTSLocalConfig),
|
||||||
("moss_tts_realtime", MossTTSRealtimeConfig),
|
("moss_tts_realtime", MossTTSRealtimeConfig),
|
||||||
("qwen3_tts", Qwen3TTSConfig),
|
("qwen3_tts", Qwen3TTSConfig),
|
||||||
@@ -108,16 +104,10 @@ def register_omni_models_to_vllm():
|
|||||||
|
|
||||||
_register_omni_hf_configs()
|
_register_omni_hf_configs()
|
||||||
|
|
||||||
# Unconditionally (re)register every omni arch into the upstream global
|
supported_archs = ModelRegistry.get_supported_archs()
|
||||||
# ModelRegistry: vLLM 0.27 added some omni archs (e.g.
|
|
||||||
# Qwen3OmniMoeForConditionalGeneration) to its own registry, but resolves
|
|
||||||
# them to upstream classes that lack omni features (e.g.
|
|
||||||
# buffer_realtime_audio for the realtime endpoint). Registering here
|
|
||||||
# overwrites those entries with the vllm-omni classes, which is the
|
|
||||||
# behavior entrypoints resolving through the global registry rely on
|
|
||||||
# (and matches OmniModelRegistry's omni-wins ordering).
|
|
||||||
for arch, (mod_folder, mod_relname, cls_name) in _OMNI_MODELS.items():
|
for arch, (mod_folder, mod_relname, cls_name) in _OMNI_MODELS.items():
|
||||||
ModelRegistry.register_model(arch, f"vllm_omni.model_executor.models.{mod_folder}.{mod_relname}:{cls_name}")
|
if arch not in supported_archs:
|
||||||
|
ModelRegistry.register_model(arch, f"vllm_omni.model_executor.models.{mod_folder}.{mod_relname}:{cls_name}")
|
||||||
|
|
||||||
# Register omni-specific reasoning parsers (e.g., step_audio).
|
# Register omni-specific reasoning parsers (e.g., step_audio).
|
||||||
import vllm_omni.reasoning # noqa: F401
|
import vllm_omni.reasoning # noqa: F401
|
||||||
@@ -254,15 +244,7 @@ class OmniEngineArgs(EngineArgs):
|
|||||||
if config_dict.get("model_type"):
|
if config_dict.get("model_type"):
|
||||||
return # config.json already has model_type, no patching needed
|
return # config.json already has model_type, no patching needed
|
||||||
except Exception:
|
except Exception:
|
||||||
# The official IndexTTS 2.5 bundle has no HuggingFace config.json.
|
return # can't load config, let vLLM handle the error
|
||||||
# Keep this exception model-scoped so other loader failures retain
|
|
||||||
# vLLM's normal error path.
|
|
||||||
if model_type != "indextts2_5" or not os.path.isdir(self.model):
|
|
||||||
return
|
|
||||||
config_path = os.path.join(self.model, "config.json")
|
|
||||||
if os.path.lexists(config_path):
|
|
||||||
return
|
|
||||||
config_dict = {}
|
|
||||||
|
|
||||||
# Create a temp dir with a patched config.json
|
# Create a temp dir with a patched config.json
|
||||||
temp_dir = tempfile.mkdtemp(prefix="omni_hf_config_")
|
temp_dir = tempfile.mkdtemp(prefix="omni_hf_config_")
|
||||||
|
|||||||
Loaded 100 of 157 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user