Compare commits
87
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9012a1b79 | ||
|
|
7f821be57b | ||
|
|
a54c323952 | ||
|
|
0df8e80574 | ||
|
|
f266b04534 | ||
|
|
c9d7cd189f | ||
|
|
4869311f2f | ||
|
|
58efc5dde3 | ||
|
|
0aa72b952f | ||
|
|
c3894585b5 | ||
|
|
b564132e44 | ||
|
|
6d5ceaebcc | ||
|
|
72a8553310 | ||
|
|
25b08b3371 | ||
|
|
334d306d9a | ||
|
|
d2a72f380f | ||
|
|
9f552d010b | ||
|
+13 |
776c3a794e | ||
|
|
a578a376d5 | ||
|
|
8825730b98 | ||
|
|
430c60b9b5 | ||
|
|
a1c37cc6e3 | ||
|
|
3fc4f988ea | ||
|
|
5e7035e183 | ||
|
|
36c28767cd | ||
|
|
bb24e07a7a | ||
|
|
0888520875 | ||
|
|
4e23bfff9e | ||
|
|
3fb6adc40a | ||
|
|
a7f9926013 | ||
|
|
43ec7f701b | ||
|
|
35f994e255 | ||
|
|
b5cf8806f0 | ||
|
|
4203bfa350 | ||
|
|
aaea77a180 | ||
|
|
2d5faf3634 | ||
|
|
314bb4e324 | ||
|
|
1fe64e85a6 | ||
|
|
e213bdd0a4 | ||
|
|
1444e1fc45 | ||
|
|
e9a1beec77 | ||
|
|
d7cd00e26c | ||
|
|
f7611198ae | ||
|
|
8e20b33b38 | ||
|
|
9cdf592782 | ||
|
|
7e14c943d5 | ||
|
|
779f5987d9 | ||
|
|
2e13bddf87 | ||
|
|
8c7f704c23 | ||
|
|
2c92b63cce | ||
|
|
61ddeef9f9 | ||
|
|
19887997eb | ||
|
|
36c7b0148f | ||
|
|
f25cd37f93 | ||
|
|
d0836d8459 | ||
|
|
67c0a17a0e | ||
|
|
dde96c2468 | ||
|
|
0ec2da08de | ||
|
|
f3741ac2b9 | ||
|
|
9c2f7467a0 | ||
|
|
65b55d1c83 | ||
|
|
f26219162b | ||
|
|
978e3ad7d4 | ||
|
|
1467b3e2c7 | ||
|
|
c784b3e07d | ||
|
|
cddcc400d3 | ||
|
|
4c72b67444 | ||
|
|
bb13467adc | ||
|
|
8c1259383c | ||
|
|
4df794bdaa | ||
|
|
89f47c910a | ||
|
|
27b804cea7 | ||
|
|
0cb7ec62da | ||
|
|
75cdf1cb4e | ||
|
|
aaba243832 | ||
|
|
14c04d0fd4 | ||
|
|
c32235c4e0 | ||
|
|
b7e63e07d7 | ||
|
|
aa5b628b6a | ||
|
|
b60030092c | ||
|
|
695e62e2b9 | ||
|
|
c3a638e8b6 | ||
|
|
026f613e57 | ||
|
|
b414a4d15e | ||
|
|
c9a4683a1a | ||
|
|
4a26b2eca9 | ||
|
|
535b1a93f5 |
No files matched your search
+72
-3
@@ -29,7 +29,40 @@ steps:
|
||||
depends_on: image-build
|
||||
commands:
|
||||
- pytest -s -v tests/e2e/offline_inference/test_t2i_model.py
|
||||
- pytest -s -v tests/e2e/offline_inference/test_ovis_image.py
|
||||
agents:
|
||||
queue: "gpu_1_queue" # g6.4xlarge instance on AWS, has 1 L4 GPU
|
||||
plugins:
|
||||
- docker#v5.2.0:
|
||||
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
|
||||
always-pull: true
|
||||
propagate-environment: true
|
||||
environment:
|
||||
- "HF_HOME=/fsx/hf_cache"
|
||||
volumes:
|
||||
- "/fsx/hf_cache:/fsx/hf_cache"
|
||||
|
||||
- label: "Diffusion Model CPU offloading Test"
|
||||
timeout_in_minutes: 20
|
||||
depends_on: image-build
|
||||
commands:
|
||||
- pytest -s -v tests/e2e/offline_inference/test_diffusion_cpu_offload.py
|
||||
agents:
|
||||
queue: "gpu_1_queue" # g6.4xlarge instance on AWS, has 1 L4 GPU
|
||||
plugins:
|
||||
- docker#v5.2.0:
|
||||
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
|
||||
always-pull: true
|
||||
propagate-environment: true
|
||||
environment:
|
||||
- "HF_HOME=/fsx/hf_cache"
|
||||
volumes:
|
||||
- "/fsx/hf_cache:/fsx/hf_cache"
|
||||
|
||||
- label: "Audio Generation Model Test"
|
||||
timeout_in_minutes: 20
|
||||
depends_on: image-build
|
||||
commands:
|
||||
- pytest -s -v tests/e2e/offline_inference/test_stable_audio_model.py
|
||||
agents:
|
||||
queue: "gpu_1_queue" # g6.4xlarge instance on AWS, has 1 L4 GPU
|
||||
plugins:
|
||||
@@ -59,8 +92,8 @@ steps:
|
||||
volumes:
|
||||
- "/fsx/hf_cache:/fsx/hf_cache"
|
||||
|
||||
- label: "Diffusion Parallelism Test"
|
||||
timeout_in_minutes: 15
|
||||
- label: "Diffusion Sequence Parallelism Test"
|
||||
timeout_in_minutes: 20
|
||||
depends_on: image-build
|
||||
commands:
|
||||
- pytest -s -v tests/e2e/offline_inference/test_sequence_parallel.py
|
||||
@@ -77,6 +110,42 @@ steps:
|
||||
volumes:
|
||||
- "/fsx/hf_cache:/fsx/hf_cache"
|
||||
|
||||
- label: "Diffusion Tensor Parallelism Test"
|
||||
timeout_in_minutes: 20
|
||||
depends_on: image-build
|
||||
commands:
|
||||
- pytest -s -v tests/e2e/offline_inference/test_zimage_tensor_parallel.py
|
||||
agents:
|
||||
queue: "gpu_4_queue" # g6.12xlarge instance on AWS, has 4 L4 GPU
|
||||
plugins:
|
||||
- docker#v5.2.0:
|
||||
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
|
||||
always-pull: true
|
||||
propagate-environment: true
|
||||
shm-size: "8gb"
|
||||
environment:
|
||||
- "HF_HOME=/fsx/hf_cache"
|
||||
volumes:
|
||||
- "/fsx/hf_cache:/fsx/hf_cache"
|
||||
|
||||
- label: "Diffusion GPU Worker Test"
|
||||
timeout_in_minutes: 20
|
||||
depends_on: image-build
|
||||
commands:
|
||||
- pytest -s -v tests/diffusion/test_gpu_diffusion_worker.py
|
||||
agents:
|
||||
queue: "gpu_4_queue" # g6.12xlarge instance on AWS, has 4 L4 GPU
|
||||
plugins:
|
||||
- docker#v5.2.0:
|
||||
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
|
||||
always-pull: true
|
||||
propagate-environment: true
|
||||
shm-size: "8gb"
|
||||
environment:
|
||||
- "HF_HOME=/fsx/hf_cache"
|
||||
volumes:
|
||||
- "/fsx/hf_cache:/fsx/hf_cache"
|
||||
|
||||
- label: "Omni Model Test"
|
||||
timeout_in_minutes: 15
|
||||
depends_on: image-build
|
||||
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
#!/bin/bash
|
||||
# Helper function to safely login to ECR Public with per-job config isolation
|
||||
# Uses DOCKER_CONFIG environment variable to prevent race conditions
|
||||
#
|
||||
# This script prevents the "device or resource busy" error by giving each
|
||||
# Buildkite job its own isolated Docker config directory.
|
||||
#
|
||||
# Usage:
|
||||
# source docker_login_ecr_public.sh && safe_docker_login_ecr_public
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Configuration
|
||||
ECR_REGISTRY="public.ecr.aws"
|
||||
|
||||
setup_isolated_docker_config() {
|
||||
# Use BUILDKITE_JOB_ID for job-specific isolation
|
||||
# Fallback to PID if running outside Buildkite
|
||||
local job_id="${BUILDKITE_JOB_ID:-$$}"
|
||||
|
||||
# Set Docker config to job-specific directory
|
||||
export DOCKER_CONFIG="/tmp/docker-config-${job_id}"
|
||||
|
||||
# Create directory if it doesn't exist
|
||||
mkdir -p "$DOCKER_CONFIG"
|
||||
|
||||
echo "[docker-config] Using isolated Docker config: $DOCKER_CONFIG"
|
||||
}
|
||||
|
||||
check_docker_auth() {
|
||||
# Check if already authenticated to the given registry
|
||||
# Returns 0 if authenticated, 1 if not
|
||||
local registry="$1"
|
||||
|
||||
# Check if credentials exist in the isolated config
|
||||
if [[ -f "$DOCKER_CONFIG/config.json" ]]; then
|
||||
# Check if registry is present in config
|
||||
if grep -q "$registry" "$DOCKER_CONFIG/config.json" 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
safe_docker_login_ecr_public() {
|
||||
# Setup isolated config first
|
||||
setup_isolated_docker_config
|
||||
|
||||
local registry="$ECR_REGISTRY"
|
||||
|
||||
# Check if already authenticated (within this job)
|
||||
if check_docker_auth "$registry"; then
|
||||
echo "[docker-login] Already authenticated to $registry in this job"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Perform login to isolated config directory
|
||||
echo "[docker-login] Logging in to $ECR_REGISTRY (isolated config)..."
|
||||
if aws ecr-public get-login-password --region us-east-1 | docker login --username AWS --password-stdin "$ECR_REGISTRY"; then
|
||||
echo "[docker-login] Login successful (config: $DOCKER_CONFIG)"
|
||||
return 0
|
||||
else
|
||||
local exit_code=$?
|
||||
echo "[docker-login] ERROR: Login failed with exit code $exit_code" >&2
|
||||
return $exit_code
|
||||
fi
|
||||
}
|
||||
|
||||
# Execute if run as script (not sourced)
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
safe_docker_login_ecr_public
|
||||
fi
|
||||
@@ -66,6 +66,14 @@ done
|
||||
echo "--- Pulling container"
|
||||
image_name="public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:${BUILDKITE_COMMIT}-rocm-omni"
|
||||
container_name="rocm_${BUILDKITE_COMMIT}_$(tr -dc A-Za-z0-9 < /dev/urandom | head -c 10; echo)"
|
||||
|
||||
# Install AWS CLI to authenticate to ECR Public Gallery to get higher rate limit for pulling images
|
||||
sudo apt-get update && sudo apt-get install -y awscli
|
||||
# Use safe docker login helper to prevent race conditions
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/../docker_login_ecr_public.sh"
|
||||
safe_docker_login_ecr_public
|
||||
# Pull the container from ECR Public Gallery
|
||||
|
||||
docker pull "${image_name}"
|
||||
|
||||
remove_docker_container() {
|
||||
|
||||
@@ -47,8 +47,11 @@ fi
|
||||
VENV_PYTHON="${VENV_DIR}/bin/python"
|
||||
[[ -x "${VENV_PYTHON}" ]] || { echo "Python not found in ${VENV_DIR}"; exit 1; }
|
||||
|
||||
"${UV_BIN}" pip install --python "${VENV_PYTHON}" vllm==0.12.0
|
||||
"${UV_BIN}" pip install --python "${VENV_PYTHON}" vllm==0.14.0
|
||||
"${UV_BIN}" pip install --python "${VENV_PYTHON}" -e ".[dev]"
|
||||
|
||||
"${VENV_PYTHON}" -m pytest -v -s tests/entrypoints/
|
||||
"${VENV_PYTHON}" -m pytest -v -s tests/diffusion/cache/
|
||||
"${VENV_PYTHON}" -m pytest -v -s tests/model_executor/models/qwen2_5_omni/test_audio_length.py
|
||||
"${VENV_PYTHON}" -m pytest -v -s tests/worker/
|
||||
"${VENV_PYTHON}" -m pytest -v -s tests/distributed/omni_connectors/test_kv_flow.py
|
||||
@@ -1,10 +1,10 @@
|
||||
steps:
|
||||
|
||||
- label: "Diffusion Model Test"
|
||||
timeout_in_minutes: 15
|
||||
timeout_in_minutes: 20
|
||||
agent_pool: mi325_2
|
||||
depends_on: amd-build
|
||||
mirror_hardwares: [amdexperimental, amdproduction, amdtentative]
|
||||
mirror_hardwares: [amdproduction]
|
||||
grade: Blocking
|
||||
commands:
|
||||
- export GPU_ARCHS=gfx942
|
||||
@@ -20,7 +20,7 @@ steps:
|
||||
timeout_in_minutes: 15
|
||||
agent_pool: mi325_1
|
||||
depends_on: amd-build
|
||||
mirror_hardwares: [amdexperimental, amdproduction, amdtentative]
|
||||
mirror_hardwares: [amdproduction]
|
||||
grade: Blocking
|
||||
commands:
|
||||
- export GPU_ARCHS=gfx942
|
||||
@@ -34,11 +34,33 @@ steps:
|
||||
- export VLLM_ROCM_USE_AITER_RMSNORM=0
|
||||
- pytest -s -v tests/e2e/offline_inference/test_cache_dit.py tests/e2e/offline_inference/test_teacache.py
|
||||
|
||||
- label: "Diffusion Parallelism Test"
|
||||
timeout_in_minutes: 15
|
||||
agent_pool: mi325_2
|
||||
depends_on: amd-build
|
||||
mirror_hardwares: [amdproduction]
|
||||
grade: Blocking
|
||||
commands:
|
||||
- export MIOPEN_DEBUG_CONV_DIRECT=0
|
||||
- export MIOPEN_DEBUG_CONV_GEMM=0
|
||||
- pytest -s -v tests/e2e/offline_inference/test_sequence_parallel.py
|
||||
|
||||
- label: "Diffusion GPU Worker Test"
|
||||
timeout_in_minutes: 20
|
||||
agent_pool: mi325_2
|
||||
depends_on: amd-build
|
||||
mirror_hardwares: [amdproduction]
|
||||
grade: Blocking
|
||||
commands:
|
||||
- export MIOPEN_DEBUG_CONV_DIRECT=0
|
||||
- export MIOPEN_DEBUG_CONV_GEMM=0
|
||||
- pytest -s -v tests/diffusion/test_gpu_diffusion_worker.py
|
||||
|
||||
- label: "Omni Model Test Qwen2-5-Omni"
|
||||
timeout_in_minutes: 15
|
||||
agent_pool: mi325_2
|
||||
depends_on: amd-build
|
||||
mirror_hardwares: [amdexperimental, amdproduction, amdtentative]
|
||||
mirror_hardwares: [amdproduction]
|
||||
grade: Blocking
|
||||
commands:
|
||||
- export GPU_ARCHS=gfx942
|
||||
@@ -51,3 +73,34 @@ steps:
|
||||
- export VLLM_ROCM_USE_AITER_LINEAR=0
|
||||
- export VLLM_ROCM_USE_AITER_RMSNORM=0
|
||||
- pytest -s -v tests/e2e/offline_inference/test_qwen2_5_omni.py
|
||||
|
||||
- label: "Omni Model Test Qwen3-Omni"
|
||||
timeout_in_minutes: 15
|
||||
agent_pool: mi325_2
|
||||
depends_on: amd-build
|
||||
mirror_hardwares: [amdproduction]
|
||||
grade: Blocking
|
||||
commands:
|
||||
- export VLLM_LOGGING_LEVEL=DEBUG
|
||||
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
- export MIOPEN_DEBUG_CONV_DIRECT=0
|
||||
- export MIOPEN_DEBUG_CONV_GEMM=0
|
||||
- pytest -s -v tests/e2e/offline_inference/test_qwen3_omni.py tests/e2e/online_serving/test_qwen3_omni.py
|
||||
|
||||
- label: "Diffusion Image Edit Test"
|
||||
timeout_in_minutes: 15
|
||||
agent_pool: mi325_1
|
||||
depends_on: amd-build
|
||||
mirror_hardwares: [amdproduction]
|
||||
grade: Blocking
|
||||
commands:
|
||||
- export GPU_ARCHS=gfx942
|
||||
- export VLLM_LOGGING_LEVEL=DEBUG
|
||||
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
- export MIOPEN_DEBUG_CONV_DIRECT=0
|
||||
- export MIOPEN_DEBUG_CONV_GEMM=0
|
||||
- export VLLM_ROCM_USE_AITER=1
|
||||
- export VLLM_ROCM_USE_AITER_MHA=1
|
||||
- export VLLM_ROCM_USE_AITER_LINEAR=0
|
||||
- export VLLM_ROCM_USE_AITER_RMSNORM=0
|
||||
- pytest -s -v tests/e2e/online_serving/test_i2i_multi_image_input.py
|
||||
@@ -13,8 +13,8 @@
|
||||
depends_on: ~
|
||||
soft_fail: false
|
||||
commands:
|
||||
- "aws ecr-public get-login-password --region us-east-1 | docker login --username AWS --password-stdin public.ecr.aws/q9t5s3a7"
|
||||
- "docker build -f docker/Dockerfile.rocm -t {{ docker_image_amd }} --progress plain ."
|
||||
- "source .buildkite/scripts/docker_login_ecr_public.sh && safe_docker_login_ecr_public"
|
||||
- "docker build -f docker/Dockerfile.rocm -t {{ docker_image_amd }} --target final --progress plain ."
|
||||
- "docker push {{ docker_image_amd }}"
|
||||
key: "amd-build"
|
||||
env:
|
||||
|
||||
@@ -40,6 +40,31 @@ body:
|
||||
</details>
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Your code version
|
||||
description: |
|
||||
Please provide a description of which commit id or version the code is.
|
||||
|
||||
value: |
|
||||
<details>
|
||||
<summary>The commit id or version of vllm</summary>
|
||||
|
||||
```text
|
||||
|
||||
```
|
||||
</details>
|
||||
<details>
|
||||
<summary>The commit id or version of vllm-omni</summary>
|
||||
|
||||
```text
|
||||
|
||||
```
|
||||
</details>
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 🐛 Describe the bug
|
||||
|
||||
@@ -20,6 +20,8 @@ body:
|
||||
label: Proposed Change.
|
||||
description: >
|
||||
The proposed change of the RFC.
|
||||
value: |
|
||||
Please provide the detailed design document of the RFC using the [template](https://docs.google.com/document/d/12YxSsVeD1jvL-InClkeAEnZyWFDndz_65JmXvsamuV4/edit?tab=t.0#heading=h.4ef4szrsgspp).
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
|
||||
@@ -9,13 +9,13 @@ Easy, fast, and cheap omni-modality model serving for everyone
|
||||
</h3>
|
||||
|
||||
<p align="center">
|
||||
| <a href="https://vllm-omni.readthedocs.io/en/latest/"><b>Documentation</b></a> | <a href="https://discuss.vllm.ai"><b>User Forum</b></a> | <a href="https://slack.vllm.ai"><b>Developer Slack</b></a> |
|
||||
| <a href="https://vllm-omni.readthedocs.io/en/latest/"><b>Documentation</b></a> | <a href="https://discuss.vllm.ai"><b>User Forum</b></a> | <a href="https://slack.vllm.ai"><b>Developer Slack</b></a> | <a href="docs/assets/WeChat.jpg"><b>WeChat</b></a> |
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
*Latest News* 🔥
|
||||
|
||||
- [2026/01] We released [0.12.0rc1](https://github.com/vllm-project/vllm-omni/releases/tag/v0.12.0rc1) - a major RC milestone focused on maturing the diffusion stack, strengthening OpenAI-compatible serving, expanding omni-model coverage, and improving stability across platforms (GPU/NPU/ROCm), please check our latest [design](https://docs.google.com/presentation/d/1qv4qMW1rKAqDREMXiUDLIgqqHQe7TDPj/edit?usp=sharing&ouid=110473603432222024453&rtpof=true&sd=true).
|
||||
- [2025/11] vLLM community officially released [vllm-project/vllm-omni](https://github.com/vllm-project/vllm-omni) in order to support omni-modality models serving.
|
||||
|
||||
---
|
||||
@@ -69,6 +69,10 @@ Please check out [Contributing to vLLM-Omni](https://vllm-omni.readthedocs.io/en
|
||||
## Join the Community
|
||||
Feel free to ask questions, provide feedbacks and discuss with fellow users of vLLM-Omni in `#sig-omni` slack channel at [slack.vllm.ai](https://slack.vllm.ai) or vLLM user forum at [discuss.vllm.ai](https://discuss.vllm.ai).
|
||||
|
||||
## Star History
|
||||
|
||||
[](https://www.star-history.com/#vllm-project/vllm-omni&type=date&legend=top-left)
|
||||
|
||||
## License
|
||||
|
||||
Apache License 2.0, as found in the [LICENSE](./LICENSE) file.
|
||||
@@ -0,0 +1,117 @@
|
||||
|
||||
# Diffusion Serving Benchmark (Image/Video)
|
||||
|
||||
This folder contains an online-serving benchmark script for diffusion models.
|
||||
It sends requests to a vLLM OpenAI-compatible endpoint and reports throughput,
|
||||
latency percentiles, and optional SLO attainment.
|
||||
|
||||
The main entrypoint is:
|
||||
|
||||
- `benchmarks/diffusion/diffusion_benchmark_serving.py`
|
||||
|
||||
## 1. Quick Start
|
||||
|
||||
1. Start the server:
|
||||
|
||||
```bash
|
||||
vllm serve Qwen/Qwen-Image --omni --port 8099
|
||||
```
|
||||
|
||||
2. Run a minimal benchmark:
|
||||
|
||||
```bash
|
||||
python3 benchmarks/diffusion/diffusion_benchmark_serving.py \
|
||||
--base-url http://localhost:8099 \
|
||||
--model Qwen/Qwen-Image \
|
||||
--task t2i \
|
||||
--dataset vbench \
|
||||
--num-prompts 5
|
||||
```
|
||||
|
||||
**Notes**
|
||||
|
||||
- The benchmark talks to `http://<host>:<port>/v1/chat/completions`.
|
||||
- If you run the server on another host or port, pass `--base-url` accordingly.
|
||||
|
||||
## 2. Supported Datasets
|
||||
|
||||
The benchmark supports three dataset modes via `--dataset`:
|
||||
|
||||
- `vbench`: Built-in prompt/data loader.
|
||||
- `trace`: Heterogeneous request traces (each request can have different resolution/frames/steps).
|
||||
- `random`: Synthetic prompts for quick smoke tests.
|
||||
|
||||
### VBench dataset
|
||||
|
||||
If you use i2v/i2i bench datasets and need auto-download support, you may need:
|
||||
|
||||
```bash
|
||||
uv pip install gdown
|
||||
```
|
||||
|
||||
### Trace dataset
|
||||
|
||||
Use `--dataset trace` to replay a trace file. The trace can specify per-request fields such as:
|
||||
|
||||
- `width`, `height`
|
||||
- `num_frames` (video)
|
||||
- `num_inference_steps`
|
||||
- `seed`, `fps`
|
||||
- optional `slo_ms` (per-request SLO target)
|
||||
|
||||
By default (when `--dataset-path` is not provided), the script downloads a default trace from
|
||||
the HuggingFace dataset repo `asukaqaqzz/Dit_Trace`. The default filename can depend on `--task`
|
||||
(e.g., `t2v` uses a video trace).
|
||||
|
||||
Current defaults:
|
||||
|
||||
- `--task t2i` -> `sd3_trace.txt`
|
||||
- `--task t2v` -> `cogvideox_trace.txt`
|
||||
|
||||
You can point to your own trace using `--dataset-path`.
|
||||
|
||||
## 3. Benchmark Parameters
|
||||
|
||||
### Basic flags
|
||||
|
||||
- `--base-url`: Server address (the script calls `.../v1/chat/completions`).
|
||||
- `--model`: The OpenAI-compatible `model` field.
|
||||
- `--task`: Task type (e.g., `t2i`, `t2v`, `i2i`, `i2v`).
|
||||
- `--dataset`: Dataset mode (`vbench` / `trace` / `random`).
|
||||
- `--num-prompts`: Number of requests to send.
|
||||
|
||||
Common optional flags:
|
||||
|
||||
- `--output-file`: Write metrics to a JSON file.
|
||||
- `--disable-tqdm`: Disable the progress bar.
|
||||
|
||||
### Resolution / frames / steps: CLI defaults vs dataset fields
|
||||
|
||||
Related flags: `--width`, `--height`, `--num-frames`, `--fps`, `--num-inference-steps`.
|
||||
|
||||
- For `vbench` / `random`: these CLI flags act as global defaults for all generated requests.
|
||||
- For `trace`: each request can carry its own fields (e.g., `width/height/num_frames/num_inference_steps`).
|
||||
|
||||
Precedence rules for `trace` (i.e., what actually gets sent):
|
||||
|
||||
- `width/height`: if either `--width` or `--height` is explicitly set, it overrides per-request values from the trace; otherwise per-request values are used when present.
|
||||
- `num_frames`: per-request `num_frames` takes precedence; otherwise fall back to `--num-frames`.
|
||||
- `num_inference_steps`: per-request `num_inference_steps` takes precedence; otherwise fall back to `--num-inference-steps`.
|
||||
|
||||
### SLO, warmup, and max concurrency
|
||||
|
||||
Enable SLO evaluation with `--slo`.
|
||||
|
||||
- If a request in the trace already has `slo_ms`, that value is used.
|
||||
- Otherwise, the script runs warmup requests to infer a base unit time, estimates `expected_ms` by linearly scaling with area/frames/steps, and then sets `slo_ms = expected_ms * --slo-scale`.
|
||||
|
||||
Warmup flags:
|
||||
|
||||
- `--warmup-requests`: Number of warmup requests.
|
||||
- `--warmup-num-inference-steps`: Steps used during warmup.
|
||||
- For `--task t2v`: warmup requests are forced to use `num_frames=1` to make warmup faster and less noisy.
|
||||
|
||||
Traffic / concurrency flags:
|
||||
|
||||
- `--request-rate`: Target request rate (requests/second). If set to `inf`, the script sends all requests immediately.
|
||||
- `--max-concurrency`: Max number of in-flight requests (default: `1`). This can hard-cap the achieved QPS: if it is too small, requests will queue behind the semaphore, and both achieved throughput and observed SLO attainment can be skewed.
|
||||
+424
-17
@@ -9,28 +9,29 @@ If you want to use i2v, i2i dataset, you should `uv pip install gdown` first
|
||||
Usage:
|
||||
# Video
|
||||
t2v:
|
||||
python3 benchmarks/diffusion_benchmark_serving.py \
|
||||
python3 benchmarks/diffusion/diffusion_benchmark_serving.py \
|
||||
--dataset vbench --task t2v --num-prompts 10 \
|
||||
--height 480 --width 640 --fps 16 --num-frames 80
|
||||
|
||||
i2v:
|
||||
python3 benchmarks/diffusion_benchmark_serving.py \
|
||||
python3 benchmarks/diffusion/diffusion_benchmark_serving.py \
|
||||
--dataset vbench --task i2v --num-prompts 10
|
||||
|
||||
|
||||
# Image
|
||||
t2i:
|
||||
python3 benchmarks/diffusion_benchmark_serving.py \
|
||||
python3 benchmarks/diffusion/diffusion_benchmark_serving.py \
|
||||
--dataset vbench --task t2i --num-prompts 10 \
|
||||
--height 1024 --width 1024
|
||||
|
||||
i2i:
|
||||
python3 benchmarks/diffusion_benchmark_serving.py \
|
||||
python3 benchmarks/diffusion/diffusion_benchmark_serving.py \
|
||||
--dataset vbench --task i2i --num-prompts 10
|
||||
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import ast
|
||||
import asyncio
|
||||
import base64
|
||||
import glob
|
||||
@@ -40,6 +41,7 @@ import os
|
||||
import time
|
||||
import uuid
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import AsyncGenerator
|
||||
from dataclasses import dataclass, field, replace
|
||||
from typing import Any
|
||||
|
||||
@@ -60,6 +62,8 @@ class RequestFuncInput:
|
||||
num_inference_steps: int | None = None
|
||||
seed: int | None = None
|
||||
fps: int | None = None
|
||||
timestamp: float | None = None
|
||||
slo_ms: float | None = None
|
||||
extra_body: dict[str, Any] = field(default_factory=dict)
|
||||
image_paths: list[str] | None = None
|
||||
request_id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
||||
@@ -73,6 +77,7 @@ class RequestFuncOutput:
|
||||
start_time: float = 0.0
|
||||
response_body: dict[str, Any] = field(default_factory=dict)
|
||||
peak_memory_mb: float = 0.0
|
||||
slo_achieved: bool | None = None
|
||||
|
||||
|
||||
class BaseDataset(ABC):
|
||||
@@ -310,6 +315,226 @@ class VBenchDataset(BaseDataset):
|
||||
return [self[i] for i in range(len(self))]
|
||||
|
||||
|
||||
class TraceDataset(BaseDataset):
|
||||
"""Trace-based dataset loader for heterogeneous diffusion requests."""
|
||||
|
||||
DEFAULT_REPO_ID = "asukaqaqzz/Dit_Trace"
|
||||
DEFAULT_FILENAME = "sd3_trace.txt"
|
||||
DEFAULT_FILENAME_BY_TASK: dict[str, str] = {
|
||||
# Text-to-image traces (e.g., SD3)
|
||||
"t2i": "sd3_trace.txt",
|
||||
# Text-to-video traces (e.g., CogVideoX)
|
||||
"t2v": "cogvideox_trace.txt",
|
||||
}
|
||||
|
||||
def __init__(self, args, api_url: str, model: str):
|
||||
super().__init__(args, api_url, model)
|
||||
self.cache_dir = os.path.join(os.path.expanduser("~"), ".cache", "vllm-omni", "trace")
|
||||
self.default_filename = self.DEFAULT_FILENAME_BY_TASK.get(getattr(args, "task", ""), self.DEFAULT_FILENAME)
|
||||
dataset_root = args.dataset_path
|
||||
if not dataset_root:
|
||||
dataset_root = self._download_default_trace()
|
||||
self.items = self._load_items(dataset_root)
|
||||
|
||||
@staticmethod
|
||||
def _coerce_int(x: Any) -> int | None:
|
||||
if x is None:
|
||||
return None
|
||||
if isinstance(x, bool):
|
||||
return None
|
||||
if isinstance(x, int):
|
||||
return x
|
||||
try:
|
||||
s = str(x).strip()
|
||||
if not s:
|
||||
return None
|
||||
return int(float(s))
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _coerce_float(x: Any) -> float | None:
|
||||
if x is None:
|
||||
return None
|
||||
if isinstance(x, float):
|
||||
return x
|
||||
if isinstance(x, int):
|
||||
return float(x)
|
||||
try:
|
||||
s = str(x).strip()
|
||||
if not s:
|
||||
return None
|
||||
return float(s)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _download_default_trace(self) -> str:
|
||||
"""Download default trace file from HuggingFace Hub if not provided."""
|
||||
|
||||
try:
|
||||
from huggingface_hub import hf_hub_download
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"huggingface_hub is required to download the default trace dataset. "
|
||||
"Install via `pip install huggingface_hub`."
|
||||
) from exc
|
||||
|
||||
os.makedirs(self.cache_dir, exist_ok=True)
|
||||
return hf_hub_download(
|
||||
repo_id=self.DEFAULT_REPO_ID,
|
||||
filename=self.default_filename,
|
||||
repo_type="dataset",
|
||||
local_dir=self.cache_dir,
|
||||
local_dir_use_symlinks=False,
|
||||
)
|
||||
|
||||
def _expand_paths(self, dataset_path: str | None) -> list[str]:
|
||||
if not dataset_path:
|
||||
return []
|
||||
|
||||
parts = [p.strip() for p in str(dataset_path).split(",") if p.strip()]
|
||||
paths: list[str] = []
|
||||
for p in parts:
|
||||
if any(ch in p for ch in ["*", "?", "["]):
|
||||
paths.extend(sorted(glob.glob(p)))
|
||||
elif os.path.isdir(p):
|
||||
paths.extend(sorted(glob.glob(os.path.join(p, "**", "*.txt"), recursive=True)))
|
||||
else:
|
||||
paths.append(p)
|
||||
|
||||
seen = set()
|
||||
unique_paths = []
|
||||
for p in paths:
|
||||
if p not in seen:
|
||||
seen.add(p)
|
||||
unique_paths.append(p)
|
||||
return unique_paths
|
||||
|
||||
def _parse_trace_file(self, path: str) -> list[dict[str, Any]]:
|
||||
rows: list[dict[str, Any]] = []
|
||||
|
||||
def parse_request_repr_line(line: str) -> dict[str, Any] | None:
|
||||
text = line.strip()
|
||||
if not text:
|
||||
return None
|
||||
if not (text.startswith("Request(") and text.endswith(")")):
|
||||
return None
|
||||
inner = text[len("Request(") : -1]
|
||||
try:
|
||||
expr = ast.parse(f"f({inner})", mode="eval")
|
||||
if not isinstance(expr.body, ast.Call):
|
||||
return None
|
||||
call = expr.body
|
||||
out: dict[str, Any] = {}
|
||||
for kw in call.keywords:
|
||||
if kw.arg is None:
|
||||
continue
|
||||
out[kw.arg] = ast.literal_eval(kw.value)
|
||||
return out
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
# detect first non-empty line to pick parser
|
||||
first_non_empty = None
|
||||
with open(path, encoding="utf-8") as f:
|
||||
for _ in range(50):
|
||||
pos = f.tell()
|
||||
line = f.readline()
|
||||
if not line:
|
||||
break
|
||||
if line.strip():
|
||||
first_non_empty = line.strip()
|
||||
f.seek(pos)
|
||||
break
|
||||
|
||||
if first_non_empty is None:
|
||||
return rows
|
||||
|
||||
if first_non_empty.startswith("Request("):
|
||||
with open(path, encoding="utf-8") as f:
|
||||
for line in f:
|
||||
parsed = parse_request_repr_line(line)
|
||||
if isinstance(parsed, dict):
|
||||
rows.append(parsed)
|
||||
return rows
|
||||
|
||||
# txt fallback: parse Request(...) lines only
|
||||
with open(path, encoding="utf-8") as f:
|
||||
for line in f:
|
||||
parsed = parse_request_repr_line(line)
|
||||
if isinstance(parsed, dict):
|
||||
rows.append(parsed)
|
||||
return rows
|
||||
|
||||
def _load_items(self, dataset_root: str) -> list[dict[str, Any]]:
|
||||
paths = self._expand_paths(dataset_root)
|
||||
if not paths:
|
||||
raise ValueError("No trace files found. Provide --dataset-path or rely on default HuggingFace download.")
|
||||
|
||||
items: list[dict[str, Any]] = []
|
||||
for p in paths:
|
||||
if not os.path.exists(p):
|
||||
continue
|
||||
for row in self._parse_trace_file(p):
|
||||
if isinstance(row, dict):
|
||||
row = dict(row)
|
||||
row.setdefault("_source", p)
|
||||
items.append(row)
|
||||
|
||||
if not items:
|
||||
raise ValueError("Trace dataset is empty after parsing provided paths.")
|
||||
|
||||
if self.args.num_prompts is not None:
|
||||
items = items[: self.args.num_prompts]
|
||||
|
||||
return items
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self.items)
|
||||
|
||||
def __getitem__(self, idx: int) -> RequestFuncInput:
|
||||
row = self.items[idx]
|
||||
prompt = row.get("prompt") or row.get("text") or ""
|
||||
|
||||
row_height = self._coerce_int(row.get("height"))
|
||||
row_width = self._coerce_int(row.get("width"))
|
||||
num_frames = self._coerce_int(row.get("num_frames"))
|
||||
num_steps = self._coerce_int(row.get("num_inference_steps"))
|
||||
seed = self._coerce_int(row.get("seed"))
|
||||
fps = self._coerce_int(row.get("fps"))
|
||||
timestamp = self._coerce_float(row.get("timestamp"))
|
||||
slo_ms = self._coerce_float(row.get("slo_ms"))
|
||||
image_paths = row.get("image_paths")
|
||||
|
||||
override_w = self.args.width
|
||||
override_h = self.args.height
|
||||
if override_w is not None or override_h is not None:
|
||||
width = override_w
|
||||
height = override_h
|
||||
else:
|
||||
width = row_width
|
||||
height = row_height
|
||||
|
||||
return RequestFuncInput(
|
||||
prompt=str(prompt),
|
||||
api_url=self.api_url,
|
||||
model=self.model,
|
||||
width=width,
|
||||
height=height,
|
||||
num_frames=num_frames if num_frames is not None else self.args.num_frames,
|
||||
num_inference_steps=num_steps if num_steps is not None else self.args.num_inference_steps,
|
||||
seed=seed if seed is not None else self.args.seed,
|
||||
fps=fps if fps is not None else self.args.fps,
|
||||
timestamp=timestamp,
|
||||
slo_ms=slo_ms,
|
||||
image_paths=image_paths,
|
||||
request_id=str(row.get("request_id")) if row.get("request_id") is not None else str(uuid.uuid4()),
|
||||
)
|
||||
|
||||
def get_requests(self) -> list[RequestFuncInput]:
|
||||
return [self[i] for i in range(len(self))]
|
||||
|
||||
|
||||
class RandomDataset(BaseDataset):
|
||||
def __init__(self, args, api_url: str, model: str):
|
||||
self.args = args
|
||||
@@ -337,6 +562,128 @@ class RandomDataset(BaseDataset):
|
||||
return [self[i] for i in range(len(self))]
|
||||
|
||||
|
||||
def _compute_expected_latency_ms_from_base(req: RequestFuncInput, args, base_time_ms: float | None) -> float | None:
|
||||
"""Compute expected execution time (ms) based on a base per-step-per-frame unit time.
|
||||
|
||||
Assumes linear scaling with pixel area, frame count, and num_inference_steps.
|
||||
The base unit represents latency for a 16x16 resolution, single frame, single step.
|
||||
"""
|
||||
|
||||
if base_time_ms is None:
|
||||
return None
|
||||
|
||||
width = req.width if req.width is not None else args.width
|
||||
height = req.height if req.height is not None else args.height
|
||||
if width is None or height is None:
|
||||
return None
|
||||
|
||||
frames = req.num_frames if req.num_frames is not None else args.num_frames
|
||||
steps = req.num_inference_steps if req.num_inference_steps is not None else args.num_inference_steps
|
||||
|
||||
frame_scale = frames if isinstance(frames, int) and frames > 0 else 1
|
||||
step_scale = steps if isinstance(steps, int) and steps > 0 else 1
|
||||
|
||||
area_units = max((float(width) * float(height)) / float(16 * 16), 1.0)
|
||||
return float(base_time_ms) * area_units * frame_scale * step_scale
|
||||
|
||||
|
||||
def _infer_slo_base_time_ms_from_warmups(
|
||||
warmup_pairs: list[tuple[RequestFuncInput, RequestFuncOutput]],
|
||||
args,
|
||||
) -> float | None:
|
||||
"""Infer base SLO unit time from warmup requests.
|
||||
|
||||
Returns the median base latency (ms) for a 16x16 resolution, single-frame,
|
||||
single-step request. Only uses warmups that succeeded and have resolvable
|
||||
width/height.
|
||||
"""
|
||||
|
||||
candidates_ms: list[float] = []
|
||||
for req, out in warmup_pairs:
|
||||
if not out.success or out.latency <= 0:
|
||||
continue
|
||||
|
||||
width = req.width if req.width is not None else args.width
|
||||
height = req.height if req.height is not None else args.height
|
||||
if width is None or height is None:
|
||||
continue
|
||||
|
||||
frames = req.num_frames if req.num_frames is not None else args.num_frames
|
||||
steps = req.num_inference_steps if req.num_inference_steps is not None else args.num_inference_steps
|
||||
|
||||
frame_scale = int(frames) if isinstance(frames, int) and frames > 0 else 1
|
||||
step_scale = int(steps) if isinstance(steps, int) and steps > 0 else 1
|
||||
|
||||
area_units = max((float(width) * float(height)) / float(16 * 16), 1.0)
|
||||
denom = area_units * float(frame_scale) * float(step_scale)
|
||||
if denom <= 0:
|
||||
continue
|
||||
|
||||
candidates_ms.append((out.latency * 1000.0) / denom)
|
||||
|
||||
if not candidates_ms:
|
||||
return None
|
||||
return float(np.median(candidates_ms))
|
||||
|
||||
|
||||
def _populate_slo_ms_from_warmups(
|
||||
requests_list: list[RequestFuncInput],
|
||||
warmup_pairs: list[tuple[RequestFuncInput, RequestFuncOutput]],
|
||||
args,
|
||||
) -> list[RequestFuncInput]:
|
||||
"""Populate missing RequestFuncInput.slo_ms using warmup outputs.
|
||||
|
||||
- If a request already has slo_ms (e.g., trace-provided), it is kept as-is.
|
||||
- If any request has slo_ms is None and we can infer base time from warmups,
|
||||
we estimate each missing request's expected execution time and set:
|
||||
req.slo_ms = expected_latency_ms * args.slo_scale
|
||||
|
||||
Returns updated requests_list.
|
||||
"""
|
||||
|
||||
if not any(req.slo_ms is None for req in requests_list):
|
||||
return requests_list
|
||||
|
||||
base_time_ms = _infer_slo_base_time_ms_from_warmups(warmup_pairs, args)
|
||||
if base_time_ms is None:
|
||||
return requests_list
|
||||
|
||||
slo_scale = float(getattr(args, "slo_scale", 3.0))
|
||||
if slo_scale <= 0:
|
||||
raise ValueError(f"slo_scale must be positive, got {slo_scale}.")
|
||||
|
||||
updated: list[RequestFuncInput] = []
|
||||
for req in requests_list:
|
||||
if req.slo_ms is not None:
|
||||
updated.append(req)
|
||||
continue
|
||||
expected_ms = _compute_expected_latency_ms_from_base(req, args, base_time_ms)
|
||||
updated.append(replace(req, slo_ms=(expected_ms * slo_scale) if expected_ms is not None else None))
|
||||
|
||||
return updated
|
||||
|
||||
|
||||
async def iter_requests(
|
||||
requests_list: list[RequestFuncInput],
|
||||
request_rate: float,
|
||||
) -> AsyncGenerator[RequestFuncInput, None]:
|
||||
"""Yield requests using a fixed interval if request_rate is set.
|
||||
|
||||
- If request_rate is inf, all requests are yielded immediately (no sleep).
|
||||
- Otherwise, requests are emitted at a fixed cadence of 1 / request_rate seconds.
|
||||
"""
|
||||
|
||||
if request_rate != float("inf"):
|
||||
if request_rate <= 0:
|
||||
raise ValueError(f"request_rate must be positive or inf, got {request_rate}.")
|
||||
interval_s = 1.0 / float(request_rate)
|
||||
|
||||
for i, req in enumerate(requests_list):
|
||||
if request_rate != float("inf") and i > 0:
|
||||
await asyncio.sleep(interval_s)
|
||||
yield req
|
||||
|
||||
|
||||
def _guess_mime_type(path: str) -> str:
|
||||
mime, _ = mimetypes.guess_type(path)
|
||||
return mime or "application/octet-stream"
|
||||
@@ -415,12 +762,21 @@ async def async_request_chat_completions(
|
||||
|
||||
output.latency = time.perf_counter() - output.start_time
|
||||
|
||||
if output.success and input.slo_ms is not None:
|
||||
output.slo_achieved = (output.latency * 1000.0) <= float(input.slo_ms)
|
||||
|
||||
if pbar:
|
||||
pbar.update(1)
|
||||
return output
|
||||
|
||||
|
||||
def calculate_metrics(outputs: list[RequestFuncOutput], total_duration: float):
|
||||
def calculate_metrics(
|
||||
outputs: list[RequestFuncOutput],
|
||||
total_duration: float,
|
||||
requests_list: list[RequestFuncInput],
|
||||
args,
|
||||
slo_enabled: bool,
|
||||
):
|
||||
success_outputs = [o for o in outputs if o.success]
|
||||
error_outputs = [o for o in outputs if not o.success]
|
||||
|
||||
@@ -442,6 +798,29 @@ def calculate_metrics(outputs: list[RequestFuncOutput], total_duration: float):
|
||||
"peak_memory_mb_median": np.median(peak_memories) if peak_memories else 0,
|
||||
}
|
||||
|
||||
if slo_enabled:
|
||||
slo_defined_total = 0
|
||||
slo_met_success = 0
|
||||
|
||||
for req, out in zip(requests_list, outputs):
|
||||
if req.slo_ms is None:
|
||||
continue
|
||||
slo_defined_total += 1
|
||||
if out.slo_achieved is None:
|
||||
continue
|
||||
if out.slo_achieved:
|
||||
slo_met_success += 1
|
||||
|
||||
slo_attain_all = (slo_met_success / slo_defined_total) if slo_defined_total > 0 else 0.0
|
||||
|
||||
metrics.update(
|
||||
{
|
||||
"slo_attainment_rate": slo_attain_all,
|
||||
"slo_met_success": slo_met_success,
|
||||
"slo_scale": getattr(args, "slo_scale", 3.0),
|
||||
}
|
||||
)
|
||||
|
||||
return metrics
|
||||
|
||||
|
||||
@@ -475,6 +854,8 @@ async def benchmark(args):
|
||||
|
||||
if args.dataset == "vbench":
|
||||
dataset = VBenchDataset(args, api_url, args.model)
|
||||
elif args.dataset == "trace":
|
||||
dataset = TraceDataset(args, api_url, args.model)
|
||||
elif args.dataset == "random":
|
||||
dataset = RandomDataset(args, api_url, args.model)
|
||||
else:
|
||||
@@ -501,6 +882,7 @@ async def benchmark(args):
|
||||
pbar = tqdm(total=len(requests_list), disable=args.disable_tqdm)
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
warmup_pairs: list[tuple[RequestFuncInput, RequestFuncOutput]] = []
|
||||
if args.warmup_requests and requests_list:
|
||||
print(
|
||||
f"Running {args.warmup_requests} warmup request(s) \
|
||||
@@ -513,16 +895,20 @@ async def benchmark(args):
|
||||
warm_req,
|
||||
num_inference_steps=args.warmup_num_inference_steps,
|
||||
)
|
||||
await limited_request_func(warm_req, session, None)
|
||||
warm_out = await limited_request_func(warm_req, session, None)
|
||||
warmup_pairs.append((warm_req, warm_out))
|
||||
|
||||
if args.slo:
|
||||
# Prefer trace-provided per-request slo_ms. Only populate when missing.
|
||||
requests_list = _populate_slo_ms_from_warmups(
|
||||
requests_list=requests_list,
|
||||
warmup_pairs=warmup_pairs,
|
||||
args=args,
|
||||
)
|
||||
|
||||
start_time = time.perf_counter()
|
||||
tasks = []
|
||||
for req in requests_list:
|
||||
if args.request_rate != float("inf"):
|
||||
# Poisson process: inter-arrival times follow exponential distribution
|
||||
interval = np.random.exponential(1.0 / args.request_rate)
|
||||
await asyncio.sleep(interval)
|
||||
|
||||
async for req in iter_requests(requests_list=requests_list, request_rate=args.request_rate):
|
||||
task = asyncio.create_task(limited_request_func(req, session, pbar))
|
||||
tasks.append(task)
|
||||
|
||||
@@ -532,7 +918,7 @@ async def benchmark(args):
|
||||
pbar.close()
|
||||
|
||||
# Calculate metrics
|
||||
metrics = calculate_metrics(outputs, total_duration)
|
||||
metrics = calculate_metrics(outputs, total_duration, requests_list, args, args.slo)
|
||||
|
||||
print("\n{s:{c}^{n}}".format(s=" Serving Benchmark Result ", n=60, c="="))
|
||||
|
||||
@@ -561,6 +947,12 @@ async def benchmark(args):
|
||||
print("{:<40} {:<15.4f}".format("Latency Median (s):", metrics["latency_median"]))
|
||||
print("{:<40} {:<15.4f}".format("Latency P99 (s):", metrics["latency_p99"]))
|
||||
|
||||
if args.slo:
|
||||
print(f"{'-' * 50}")
|
||||
print("{:<40} {:<15.2%}".format("SLO Attainment Rate (all):", metrics.get("slo_attainment_rate", 0.0)))
|
||||
print("{:<40} {:<15}".format("SLO Met (success count):", str(metrics.get("slo_met_success", 0))))
|
||||
print("{:<40} {:<15}".format("SLO Scale:", str(metrics.get("slo_scale", 3.0))))
|
||||
|
||||
if metrics["peak_memory_mb_max"] > 0:
|
||||
print(f"{'-' * 50}")
|
||||
print("{:<40} {:<15.2f}".format("Peak Memory Max (MB):", metrics["peak_memory_mb_max"]))
|
||||
@@ -581,16 +973,16 @@ if __name__ == "__main__":
|
||||
"--base-url",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Base URL of the server (e.g., http://localhost:30000). Overrides host/port.",
|
||||
help="Base URL of the server (e.g., http://localhost:8091). Overrides host/port.",
|
||||
)
|
||||
parser.add_argument("--host", type=str, default="localhost", help="Server host.")
|
||||
parser.add_argument("--port", type=int, default=30000, help="Server port.")
|
||||
parser.add_argument("--port", type=int, default=8091, help="Server port.")
|
||||
parser.add_argument("--model", type=str, default="default", help="Model name.")
|
||||
parser.add_argument(
|
||||
"--dataset",
|
||||
type=str,
|
||||
default="vbench",
|
||||
choices=["vbench", "random"],
|
||||
choices=["vbench", "trace", "random"],
|
||||
help="Dataset to use.",
|
||||
)
|
||||
parser.add_argument(
|
||||
@@ -645,7 +1037,7 @@ if __name__ == "__main__":
|
||||
parser.add_argument(
|
||||
"--num-inference-steps",
|
||||
type=int,
|
||||
default=None,
|
||||
default=50,
|
||||
help="Number of inference steps (for diffusion models).",
|
||||
)
|
||||
parser.add_argument(
|
||||
@@ -656,6 +1048,21 @@ if __name__ == "__main__":
|
||||
)
|
||||
parser.add_argument("--fps", type=int, default=None, help="FPS (for video).")
|
||||
parser.add_argument("--output-file", type=str, default=None, help="Output JSON file for metrics.")
|
||||
parser.add_argument(
|
||||
"--slo",
|
||||
action="store_true",
|
||||
help=(
|
||||
"Enable SLO calculation and reporting. If trace provides per-request slo_ms, it is used. "
|
||||
"Otherwise, warmup request(s) are used to infer expected execution time assuming linear "
|
||||
"scaling by resolution, frames, and steps, then slo_ms = expected_time * --slo-scale."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--slo-scale",
|
||||
type=float,
|
||||
default=3.0,
|
||||
help="SLO target multiplier: slo_ms = estimated_exec_time_ms * slo_scale (default: 3).",
|
||||
)
|
||||
parser.add_argument("--disable-tqdm", action="store_true", help="Disable progress bar.")
|
||||
|
||||
args = parser.parse_args()
|
||||
@@ -57,6 +57,7 @@ SystemEnv = namedtuple(
|
||||
"cpu_info",
|
||||
"rocm_version", # vllm specific field
|
||||
"vllm_version", # vllm specific field
|
||||
"vllm_omni_version", # vllm-omni specific field
|
||||
"vllm_build_flags", # vllm specific field
|
||||
"gpu_topo", # vllm specific field
|
||||
"env_vars",
|
||||
@@ -289,6 +290,31 @@ def get_vllm_version():
|
||||
return __version__
|
||||
|
||||
|
||||
def get_vllm_omni_version(run_lambda):
|
||||
try:
|
||||
import vllm_omni
|
||||
from vllm_omni import __version__, __version_tuple__
|
||||
|
||||
version_str = __version_tuple__[-1]
|
||||
if isinstance(version_str, str) and version_str.startswith("g"):
|
||||
if "." in version_str:
|
||||
git_sha = version_str.split(".")[0][1:]
|
||||
date = version_str.split(".")[-1][1:]
|
||||
return f"{__version__} (git sha: {git_sha}, date: {date})"
|
||||
else:
|
||||
git_sha = version_str[1:]
|
||||
return f"{__version__} (git sha: {git_sha})"
|
||||
|
||||
package_dir = os.path.dirname(os.path.abspath(vllm_omni.__file__))
|
||||
git_sha = run_and_read_all(run_lambda, f"git -C {package_dir} rev-parse --short HEAD")
|
||||
if git_sha:
|
||||
return f"{__version__} (git sha: {git_sha})"
|
||||
|
||||
return __version__
|
||||
except ImportError:
|
||||
return "N/A (vllm_omni not installed)"
|
||||
|
||||
|
||||
def summarize_vllm_build_flags():
|
||||
# This could be a static method if the flags are constant, or dynamic if you need to check environment variables, etc.
|
||||
return "CUDA Archs: {}; ROCm: {}".format(
|
||||
@@ -524,6 +550,7 @@ def get_env_info():
|
||||
|
||||
rocm_version = get_rocm_version(run_lambda)
|
||||
vllm_version = get_vllm_version()
|
||||
vllm_omni_version = get_vllm_omni_version(run_lambda)
|
||||
vllm_build_flags = summarize_vllm_build_flags()
|
||||
gpu_topo = get_gpu_topo(run_lambda)
|
||||
|
||||
@@ -555,6 +582,7 @@ def get_env_info():
|
||||
cpu_info=get_cpu_info(run_lambda),
|
||||
rocm_version=rocm_version,
|
||||
vllm_version=vllm_version,
|
||||
vllm_omni_version=vllm_omni_version,
|
||||
vllm_build_flags=vllm_build_flags,
|
||||
gpu_topo=gpu_topo,
|
||||
env_vars=get_env_vars(),
|
||||
@@ -621,6 +649,7 @@ env_info_fmt += """
|
||||
==============================
|
||||
ROCM Version : {rocm_version}
|
||||
vLLM Version : {vllm_version}
|
||||
vLLM-Omni Version : {vllm_omni_version}
|
||||
vLLM Build Flags:
|
||||
{vllm_build_flags}
|
||||
GPU Topology:
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
ARG VLLM_BASE_IMAGE=vllm/vllm-openai
|
||||
ARG VLLM_BASE_TAG=v0.12.0
|
||||
ARG VLLM_BASE_TAG=v0.14.0
|
||||
FROM ${VLLM_BASE_IMAGE}:${VLLM_BASE_TAG}
|
||||
ARG APP_DIR=/workspace/vllm-omni
|
||||
WORKDIR ${APP_DIR}
|
||||
|
||||
COPY . .
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && \
|
||||
apt-get install -y ffmpeg && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install vllm-omni into the same uv-managed Python environment used by the base image.
|
||||
RUN uv pip install --python "$(python3 -c 'import sys; print(sys.executable)')" --no-cache-dir ".[dev]"
|
||||
|
||||
|
||||
+10
-24
@@ -1,9 +1,7 @@
|
||||
ARG BASE_IMAGE=rocm/vllm-dev:nightly_main_20251205
|
||||
FROM ${BASE_IMAGE}
|
||||
ARG BASE_IMAGE=vllm/vllm-openai-rocm:v0.14.0
|
||||
FROM ${BASE_IMAGE} AS final
|
||||
|
||||
ARG COMMON_WORKDIR=/app
|
||||
ARG VLLM_VERSION=v0.12.0
|
||||
ARG PYTORCH_ROCM_ARCH="gfx942;gfx950"
|
||||
|
||||
WORKDIR ${COMMON_WORKDIR}
|
||||
|
||||
@@ -13,30 +11,18 @@ RUN apt-get update && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Step 2: Reinstall vllm from source
|
||||
RUN python3 -m pip uninstall -y vllm && rm -rf vllm &&\
|
||||
git clone https://github.com/vllm-project/vllm.git && \
|
||||
cd vllm && \
|
||||
git checkout ${VLLM_VERSION} && \
|
||||
python3 -m pip install -r requirements/rocm.txt && \
|
||||
python3 setup.py clean --all && \
|
||||
PYTORCH_ROCM_ARCH=${PYTORCH_ROCM_ARCH} python3 setup.py develop && \
|
||||
cd ../ && \
|
||||
rm -rf vllm/.git
|
||||
|
||||
RUN mkdir -p ${COMMON_WORKDIR}/vllm-omni
|
||||
|
||||
# Step 3: Copy vllm-omni code and install without uv
|
||||
# Step 2: Copy vllm-omni code and install without uv
|
||||
COPY . ${COMMON_WORKDIR}/vllm-omni
|
||||
RUN cd ${COMMON_WORKDIR}/vllm-omni && python3 -m pip install --no-cache-dir ".[dev]"
|
||||
RUN cd ${COMMON_WORKDIR}/vllm-omni && uv pip install --python "$(python3 -c 'import sys; print(sys.executable)')" --no-cache-dir ".[dev]"
|
||||
|
||||
# Create python symlink
|
||||
# `GPU_ARCHS` is an environment variable that is used to set the GPU archs for the AITER.
|
||||
# This is needed to prevent the AITER automatic GPU arch detection from failing on MI325X.
|
||||
# The AITER version used in this dockerfile has issues with handling
|
||||
# the GPU archs of MI325X (CI machine) correctly. So we manually set the GPU archs here.
|
||||
# We reuse AITER_ROCM_ARCH from the base image to avoid duplication.
|
||||
ENV GPU_ARCHS=${AITER_ROCM_ARCH}
|
||||
RUN ln -sf /usr/bin/python3 /usr/bin/python
|
||||
|
||||
CMD ["/bin/bash"]
|
||||
|
||||
ENTRYPOINT []
|
||||
|
||||
#Set entrypoint for vllm-openai official images
|
||||
FROM final AS vllm-openai
|
||||
ENTRYPOINT ["vllm", "serve"]
|
||||
+11
-9
@@ -3,7 +3,7 @@ nav:
|
||||
- User Guide:
|
||||
- Getting Started:
|
||||
- getting_started/quickstart.md
|
||||
- getting_started/installation
|
||||
- getting_started/installation/*
|
||||
- Serving:
|
||||
- OpenAI-Compatible API:
|
||||
- Image Generation: serving/image_generation_api.md
|
||||
@@ -11,8 +11,10 @@ nav:
|
||||
- examples/README.md
|
||||
- Offline Inference:
|
||||
- Image-To-Image: user_guide/examples/offline_inference/image_to_image.md
|
||||
- Image-To-Video: user_guide/examples/offline_inference/image_to_video.md
|
||||
- Qwen2.5-Omni: user_guide/examples/offline_inference/qwen2_5_omni.md
|
||||
- Qwen3-Omni: user_guide/examples/offline_inference/qwen3_omni.md
|
||||
- Qwen3-TTS Offline Inference: user_guide/examples/offline_inference/qwen3_tts.md
|
||||
- Text-To-Image: user_guide/examples/offline_inference/text_to_image.md
|
||||
- Text-To-Video: user_guide/examples/offline_inference/text_to_video.md
|
||||
- Online Serving:
|
||||
@@ -25,14 +27,16 @@ nav:
|
||||
- Configuration:
|
||||
- configuration/README.md
|
||||
- configuration/*
|
||||
- Diffusion Acceleration:
|
||||
- Overview: user_guide/diffusion_acceleration.md
|
||||
- Acceleration Methods:
|
||||
- TeaCache: user_guide/acceleration/teacache.md
|
||||
- Cache-DiT: user_guide/acceleration/cache_dit_acceleration.md
|
||||
- Parallelism Acceleration: user_guide/acceleration/parallelism_acceleration.md
|
||||
- Models:
|
||||
- models/supported_models.md
|
||||
- Features:
|
||||
- Sleep Mode: features/sleep_mode.md
|
||||
- Diffusion Features:
|
||||
- Overview: user_guide/diffusion_acceleration.md
|
||||
- TeaCache: user_guide/diffusion/teacache.md
|
||||
- Cache-DiT: user_guide/diffusion/cache_dit_acceleration.md
|
||||
- Parallelism Acceleration: user_guide/diffusion/parallelism_acceleration.md
|
||||
- CPU Offloading: user_guide/diffusion/cpu_offload_diffusion.md
|
||||
- Developer Guide:
|
||||
- General:
|
||||
- contributing/README.md
|
||||
@@ -43,13 +47,11 @@ nav:
|
||||
- contributing/model/adding_omni_model.md
|
||||
- contributing/model/adding_diffusion_model.md
|
||||
- CI: contributing/ci
|
||||
- Tests: contributing/tests
|
||||
- Design Documents:
|
||||
- design/index.md
|
||||
- design/architecture_overview.md
|
||||
- Feature Design:
|
||||
- design/feature/disaggregated_inference.md
|
||||
- design/feature/multi_request_streaming.md
|
||||
- design/feature/ray_based_execution.md
|
||||
- Module Design:
|
||||
- design/module/ar_module.md
|
||||
|
||||
+1
-1
@@ -60,5 +60,5 @@ vLLM-Omni seamlessly supports most popular open-source models on HuggingFace, in
|
||||
|
||||
For more information, checkout the following:
|
||||
|
||||
- [vllm-omni architecture design and recent roadmaps](https://docs.google.com/presentation/d/1Y7t2Zm3BIISPN-_X_sDpxZMNCWir2rAx/edit?usp=drive_link&ouid=110473603432222024453&rtpof=true&sd=true)
|
||||
- [vllm-omni architecture design and recent roadmaps](https://docs.google.com/presentation/d/1qv4qMW1rKAqDREMXiUDLIgqqHQe7TDPj/edit?usp=sharing&ouid=110473603432222024453&rtpof=true&sd=true)
|
||||
- [vllm-omni announcement blogpost](https://blog.vllm.ai/2025/11/30/vllm-omni.html)
|
||||
+19
-34
@@ -36,7 +36,6 @@ Input data structures for multi-modal inputs.
|
||||
|
||||
Engine classes for offline and online inference.
|
||||
|
||||
- [vllm_omni.diffusion.diffusion_engine.BackgroundResources][]
|
||||
- [vllm_omni.diffusion.diffusion_engine.DiffusionEngine][]
|
||||
- [vllm_omni.engine.AdditionalInformationEntry][]
|
||||
- [vllm_omni.engine.AdditionalInformationPayload][]
|
||||
@@ -57,37 +56,11 @@ Core scheduling and caching components.
|
||||
- [vllm_omni.core.sched.omni_ar_scheduler.OmniARScheduler][]
|
||||
- [vllm_omni.core.sched.omni_generation_scheduler.OmniGenerationScheduler][]
|
||||
- [vllm_omni.core.sched.output.OmniNewRequestData][]
|
||||
|
||||
## Model Executor
|
||||
|
||||
Model execution components.
|
||||
|
||||
- [vllm_omni.model_executor.custom_process_mixin.CustomProcessMixin][]
|
||||
- [vllm_omni.model_executor.models.output_templates.OmniOutput][]
|
||||
- [vllm_omni.model_executor.models.qwen2_5_omni.qwen2_5_omni.Qwen2_5OmniForConditionalGeneration][]
|
||||
- [vllm_omni.model_executor.models.qwen2_5_omni.qwen2_5_omni_talker.Qwen2_5OmniTalkerForConditionalGeneration][]
|
||||
- [vllm_omni.model_executor.models.qwen2_5_omni.qwen2_5_omni_thinker.Qwen2_5OmniConditionalGenerationMixin][]
|
||||
- [vllm_omni.model_executor.models.qwen2_5_omni.qwen2_5_omni_thinker.Qwen2_5OmniThinkerForConditionalGeneration][]
|
||||
- [vllm_omni.model_executor.models.qwen2_5_omni.qwen2_5_omni_token2wav.Qwen2_5OmniToken2WavBigVGANModel][]
|
||||
- [vllm_omni.model_executor.models.qwen2_5_omni.qwen2_5_omni_token2wav.Qwen2_5OmniToken2WavDiTModel][]
|
||||
- [vllm_omni.model_executor.models.qwen2_5_omni.qwen2_5_omni_token2wav.Qwen2_5OmniToken2WavForConditionalGenerationVLLM][]
|
||||
- [vllm_omni.model_executor.models.qwen2_5_omni.qwen2_5_omni_token2wav.Qwen2_5OmniToken2WavModel][]
|
||||
- [vllm_omni.model_executor.models.qwen2_5_omni.qwen2_old.Qwen2EmbeddingModel][]
|
||||
- [vllm_omni.model_executor.models.qwen2_5_omni.qwen2_old.Qwen2ForCausalLM][]
|
||||
- [vllm_omni.model_executor.models.qwen2_5_omni.qwen2_old.Qwen2Model][]
|
||||
- [vllm_omni.model_executor.models.qwen3_omni.qwen3_moe.Qwen3MoeForCausalLM][]
|
||||
- [vllm_omni.model_executor.models.qwen3_omni.qwen3_omni.Qwen3OmniMoeForConditionalGeneration][]
|
||||
- [vllm_omni.model_executor.models.qwen3_omni.qwen3_omni_code2wav.Qwen3OmniMoeCode2Wav][]
|
||||
- [vllm_omni.model_executor.models.qwen3_omni.qwen3_omni_moe_code_predictor_mtp.Qwen3OmniCodePredictorBaseModel][]
|
||||
- [vllm_omni.model_executor.models.qwen3_omni.qwen3_omni_moe_code_predictor_mtp.Qwen3OmniMoeTalkerCodePredictor][]
|
||||
- [vllm_omni.model_executor.models.qwen3_omni.qwen3_omni_moe_talker.Qwen3OmniMoeModel][]
|
||||
- [vllm_omni.model_executor.models.qwen3_omni.qwen3_omni_moe_talker.Qwen3OmniMoeTalkerForConditionalGeneration][]
|
||||
- [vllm_omni.model_executor.models.qwen3_omni.qwen3_omni_moe_thinker.Qwen3MoeLLMForCausalLM][]
|
||||
- [vllm_omni.model_executor.models.qwen3_omni.qwen3_omni_moe_thinker.Qwen3MoeLLMModel][]
|
||||
- [vllm_omni.model_executor.models.qwen3_omni.qwen3_omni_moe_thinker.Qwen3OmniMoeConditionalGenerationMixin][]
|
||||
- [vllm_omni.model_executor.models.qwen3_omni.qwen3_omni_moe_thinker.Qwen3OmniMoeThinkerForConditionalGeneration][]
|
||||
- [vllm_omni.model_executor.models.qwen3_omni.qwen3_omni_moe_thinker.Qwen3OmniMoeThinkerMultiModalProcessor][]
|
||||
- [vllm_omni.model_executor.models.qwen3_omni.qwen3_omni_moe_thinker.Qwen3OmniMoeThinkerProcessingInfo][]
|
||||
- [vllm_omni.model_executor.models.qwen3_tts.tokenizer_25hz.vq.core_vq.DistributedGroupResidualVectorQuantization][]
|
||||
- [vllm_omni.model_executor.models.qwen3_tts.tokenizer_25hz.vq.core_vq.DistributedResidualVectorQuantization][]
|
||||
- [vllm_omni.model_executor.models.qwen3_tts.tokenizer_25hz.vq.core_vq.EuclideanCodebook][]
|
||||
- [vllm_omni.model_executor.models.qwen3_tts.tokenizer_25hz.vq.core_vq.VectorQuantization][]
|
||||
- [vllm_omni.model_executor.models.qwen3_tts.tokenizer_25hz.vq.core_vq.preprocess][]
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -97,13 +70,25 @@ Configuration classes.
|
||||
- [vllm_omni.diffusion.cache.teacache.config.TeaCacheConfig][]
|
||||
- [vllm_omni.distributed.omni_connectors.utils.config.ConnectorSpec][]
|
||||
- [vllm_omni.distributed.omni_connectors.utils.config.OmniTransferConfig][]
|
||||
- [vllm_omni.model_executor.models.qwen3_tts.configuration_qwen3_tts.Qwen3TTSConfig][]
|
||||
- [vllm_omni.model_executor.models.qwen3_tts.configuration_qwen3_tts.Qwen3TTSSpeakerEncoderConfig][]
|
||||
- [vllm_omni.model_executor.models.qwen3_tts.configuration_qwen3_tts.Qwen3TTSTalkerCodePredictorConfig][]
|
||||
- [vllm_omni.model_executor.models.qwen3_tts.configuration_qwen3_tts.Qwen3TTSTalkerConfig][]
|
||||
- [vllm_omni.model_executor.models.qwen3_tts.tokenizer_12hz.configuration_qwen3_tts_tokenizer_v2.Qwen3TTSTokenizerV2Config][]
|
||||
- [vllm_omni.model_executor.models.qwen3_tts.tokenizer_12hz.configuration_qwen3_tts_tokenizer_v2.Qwen3TTSTokenizerV2DecoderConfig][]
|
||||
- [vllm_omni.model_executor.models.qwen3_tts.tokenizer_25hz.configuration_qwen3_tts_tokenizer_v1.Qwen3TTSTokenizerV1Config][]
|
||||
- [vllm_omni.model_executor.models.qwen3_tts.tokenizer_25hz.configuration_qwen3_tts_tokenizer_v1.Qwen3TTSTokenizerV1DecoderBigVGANConfig][]
|
||||
- [vllm_omni.model_executor.models.qwen3_tts.tokenizer_25hz.configuration_qwen3_tts_tokenizer_v1.Qwen3TTSTokenizerV1DecoderConfig][]
|
||||
- [vllm_omni.model_executor.models.qwen3_tts.tokenizer_25hz.configuration_qwen3_tts_tokenizer_v1.Qwen3TTSTokenizerV1DecoderDiTConfig][]
|
||||
- [vllm_omni.model_executor.models.qwen3_tts.tokenizer_25hz.configuration_qwen3_tts_tokenizer_v1.Qwen3TTSTokenizerV1EncoderConfig][]
|
||||
|
||||
## Workers
|
||||
|
||||
Worker classes and model runners for distributed inference.
|
||||
|
||||
- [vllm_omni.diffusion.worker.gpu_worker.GPUWorker][]
|
||||
- [vllm_omni.diffusion.worker.gpu_worker.WorkerProc][]
|
||||
- [vllm_omni.diffusion.worker.gpu_diffusion_model_runner.GPUDiffusionModelRunner][]
|
||||
- [vllm_omni.diffusion.worker.gpu_diffusion_worker.GPUDiffusionWorker][]
|
||||
- [vllm_omni.diffusion.worker.gpu_diffusion_worker.WorkerProc][]
|
||||
- [vllm_omni.diffusion.worker.npu.npu_worker.NPUWorker][]
|
||||
- [vllm_omni.diffusion.worker.npu.npu_worker.NPUWorkerProc][]
|
||||
- [vllm_omni.worker.gpu_ar_model_runner.ExecuteModelState][]
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 226 KiB |
@@ -2,7 +2,7 @@
|
||||
|
||||
This section lists the most common options for running vLLM-Omni.
|
||||
|
||||
For options within a vLLM Engine. Please refer to [vLLM Configuration](https://docs.vllm.ai/en/v0.11.0/configuration/index.html)
|
||||
For options within a vLLM Engine. Please refer to [vLLM Configuration](https://docs.vllm.ai/en/v0.14.0/configuration/index.html)
|
||||
|
||||
Currently, the main options are maintained by stage configs for each model.
|
||||
|
||||
@@ -16,6 +16,6 @@ For introduction, please check [Introduction for stage config](./stage_configs.m
|
||||
|
||||
## Optimization Features
|
||||
|
||||
- **[TeaCache Configuration](../user_guide/acceleration/teacache.md)** - Enable TeaCache adaptive caching for DiT models to achieve 1.5x-2.0x speedup with minimal quality loss
|
||||
- **[Cache-DiT Configuration](../user_guide/acceleration/cache_dit_acceleration.md)** - Enable Cache-DiT as cache acceleration backends for DiT models
|
||||
- **[Parallelism Configuration](../user_guide/acceleration/parallelism_acceleration.md)** - Enable parallelism (e.g., sequence parallelism) for for DiT models
|
||||
- **[TeaCache Configuration](../user_guide/diffusion/teacache.md)** - Enable TeaCache adaptive caching for DiT models to achieve 1.5x-2.0x speedup with minimal quality loss
|
||||
- **[Cache-DiT Configuration](../user_guide/diffusion/cache_dit_acceleration.md)** - Enable Cache-DiT as cache acceleration backends for DiT models
|
||||
- **[Parallelism Configuration](../user_guide/diffusion/parallelism_acceleration.md)** - Enable parallelism (e.g., sequence parallelism) for for DiT models
|
||||
@@ -0,0 +1,160 @@
|
||||
# Markers for Tests
|
||||
|
||||
By adding markers before test functions, tests can later be executed uniformly by simply declaring the corresponding marker type.
|
||||
|
||||
## Current Markers
|
||||
Defined in `pyproject.toml`:
|
||||
|
||||
| Marker | Description |
|
||||
| ------------------ | ------------------------------------------------------- |
|
||||
| `core_model` | Core model tests (run in each PR) |
|
||||
| `diffusion` | Diffusion model tests |
|
||||
| `omni` | Omni model tests |
|
||||
| `cache` | Cache backend tests |
|
||||
| `parallel` | Parallelism/distributed tests |
|
||||
| `cpu` | Tests that run on CPU |
|
||||
| `gpu` | Tests that run on GPU (auto-added) |
|
||||
| `cuda` | Tests that run on CUDA (auto-added) |
|
||||
| `rocm` | Tests that run on AMD/ROCm (auto-added) |
|
||||
| `npu` | Tests that run on NPU/Ascend (auto-added) |
|
||||
| `H100` | Tests that require H100 GPU |
|
||||
| `L4` | Tests that require L4 GPU |
|
||||
| `MI325` | Tests that require MI325 GPU (AMD/ROCm) |
|
||||
| `A2` | Tests that require A2 NPU |
|
||||
| `A3` | Tests that require A3 NPU |
|
||||
| `distributed_cuda` | Tests that require multi cards on CUDA platform |
|
||||
| `distributed_rocm` | Tests that require multi cards on ROCm platform |
|
||||
| `distributed_npu` | Tests that require multi cards on NPU platform |
|
||||
| `skipif_cuda` | Skip if the num of CUDA cards is less than the required |
|
||||
| `skipif_rocm` | Skip if the num of ROCm cards is less than the required |
|
||||
| `skipif_npu` | Skip if the num of NPU cards is less than the required |
|
||||
| `slow` | Slow tests (may skip in quick CI) |
|
||||
| `benchmark` | Benchmark tests |
|
||||
|
||||
For those markers shown as auto-added, they will be added by the `@hardware_test` decorator.
|
||||
|
||||
### Example usage for markers
|
||||
|
||||
```python
|
||||
from tests.utils import hardware_test
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.omni
|
||||
@hardware_test(
|
||||
res={"cuda": "L4", "rocm": "MI325", "npu": "A2"},
|
||||
num_cards=2,
|
||||
)
|
||||
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
||||
def test_video_to_audio()
|
||||
...
|
||||
```
|
||||
### Decorator: `@hardware_test`
|
||||
|
||||
This decorator is intended to make hardware-aware, cross-platform test authoring easier and more robust for CI/CD environments. The `hardware_test` decorator in `vllm-omni/tests/utils.py` performs the following actions:
|
||||
|
||||
1. **Applies platform and resource markers**
|
||||
Adds the appropriate pytest markers for each specified hardware platform (e.g., `cuda`, `rocm`, `npu`) and resource type (e.g., `L4`, `H100`, `MI325`, `A2`, `A3`).
|
||||
```
|
||||
@pytest.mark.cuda
|
||||
@pytest.mark.L4
|
||||
```
|
||||
2. **Handles multi-card (distributed) scenarios**
|
||||
For tests requiring multiple cards, it automatically adds distributed markers such as `distributed_cuda`, `distributed_rocm`, or `distributed_npu`.
|
||||
```
|
||||
@pytest.mark.distributed_cuda(num_cards=num_cards)
|
||||
```
|
||||
3. **Supports flexible card requirements**
|
||||
Accepts `num_cards` as either a single integer for all platforms or as a dictionary with per-platform values. If not specified, defaults to 1 card per platform.
|
||||
|
||||
4. **Integrates resource validation**
|
||||
On CUDA, adds a skip marker (`skipif_cuda`) if the system does not have the required number of devices.
|
||||
Support for `skipif_rocm` and `skipif_npu` will be implemented later.
|
||||
|
||||
|
||||
5. **Runs each test in a new process**
|
||||
Automatically wraps the distributed test with a decorator (`@create_new_process_for_each_test`) to ensure isolation and compatibility with multi-process hardware backends.
|
||||
|
||||
6. **Works with pytest filtering**
|
||||
Allows tests to be filtered and selected at runtime using standard pytest marker expressions (e.g., `-m "distributed_cuda and L4"`).
|
||||
|
||||
#### Example usage for decorator
|
||||
- Single call for multiple platforms:
|
||||
```python
|
||||
@hardware_test(
|
||||
res={"cuda": "L4", "rocm": "MI325", "npu": "A2"},
|
||||
num_cards={"cuda": 2, "rocm": 2, "npu": 2},
|
||||
)
|
||||
```
|
||||
or
|
||||
```python
|
||||
@hardware_test(
|
||||
res={"cuda": "L4", "rocm": "MI325", "npu": "A2"},
|
||||
num_cards=2,
|
||||
)
|
||||
```
|
||||
- `res` must be a dict; supported resources: CUDA (L4/H100), ROCm (MI325), NPU (A2/A3)
|
||||
- `num_cards` can be int (all platforms) or dict (per platform); defaults to 1 when missing
|
||||
- `hardware_test` automatically applies `@create_new_process_for_each_test` for distributed tests.
|
||||
- Distributed markers (`distributed_cuda`, `distributed_rocm`, `distributed_npu`) are auto-added for multi-card cases
|
||||
- Filtering examples:
|
||||
- CUDA only: `pytest -m "distributed_cuda and L4"`
|
||||
- ROCm only: `pytest -m "distributed_rocm and MI325"`
|
||||
- NPU only: `pytest -m "distributed_npu"`
|
||||
|
||||
## Add Support for a New Platform
|
||||
|
||||
If you want to add support for a new platform (e.g., "tpu" for a new accelerator), follow these steps:
|
||||
|
||||
1. **Extend the marker list in your pytest config** so that platform/resource markers are defined:
|
||||
```toml
|
||||
# In pyproject.toml or pytest.ini
|
||||
[tool.pytest.ini_options]
|
||||
markers = [
|
||||
# ... existing markers ...
|
||||
"tpu: Tests that require TPU device",
|
||||
"TPU_V3: Tests that require TPU v3 hardware",
|
||||
"distributed_tpu: Tests that require multiple TPU devices",
|
||||
]
|
||||
```
|
||||
2. **Implement a marker construction function for your platform** in `vllm-omni/tests/utils.py`:
|
||||
```python
|
||||
# In vllm-omni/tests/utils.py
|
||||
|
||||
def tpu_marks(*, res: str, num_cards: int):
|
||||
test_platform = pytest.mark.tpu
|
||||
if res == "TPU_V3":
|
||||
test_resource = pytest.mark.TPU_V3
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Invalid TPU resource type: {res}. Supported: TPU_V3")
|
||||
|
||||
if num_cards == 1:
|
||||
return [test_platform, test_resource]
|
||||
else:
|
||||
test_distributed = pytest.mark.distributed_tpu(num_cards=num_cards)
|
||||
# Optionally: add skipif_tpu when implemented
|
||||
return [test_platform, test_resource, test_distributed]
|
||||
```
|
||||
3. **Update `hardware_test` to recognize your new platform**:
|
||||
In the relevant place (see the `hardware_test` implementation), add:
|
||||
```python
|
||||
if platform == "tpu":
|
||||
marks = tpu_marks(res=resource, num_cards=cards)
|
||||
```
|
||||
4. **(Recommended) Add a test using your new markers**:
|
||||
```python
|
||||
@hardware_test(
|
||||
res={"tpu": "TPU_V3"},
|
||||
num_cards=2,
|
||||
)
|
||||
def test_my_tpu_feature():
|
||||
...
|
||||
```
|
||||
|
||||
**Summary**:
|
||||
- Add pytest markers for your new platform/resources
|
||||
- Implement a marker function (`xxx_marks`)
|
||||
- Plug into `hardware_test`
|
||||
- You're done: tests decorated with `@hardware_test` using your platform now automatically get the correct markers, distribution, and isolation!
|
||||
|
||||
See code in `vllm-omni/tests/utils.py` for existing examples (`cuda_marks`, `rocm_marks`, `npu_marks`).
|
||||
@@ -113,7 +113,7 @@ vllm_omni/ tests/
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
- **Unit/System Tests**: Use `test_<module_name>.py` format. Example: `omni_llm.py` → `test_omni_llm.py`
|
||||
- **Unit Tests**: Use `test_<module_name>.py` format. Example: `omni_llm.py` → `test_omni_llm.py`
|
||||
|
||||
- **E2E Tests**: Place in `tests/e2e/offline_inference/` or `tests/e2e/online_serving/` with descriptive names. Example: `tests/e2e/offline_inference/test_qwen3_omni.py`, `tests/e2e/offline_inference/test_diffusion_model.py`
|
||||
|
||||
@@ -139,7 +139,7 @@ vllm_omni/ tests/
|
||||
4. **Documentation**: Add docstrings to all test functions
|
||||
5. **Environment variables**: Set uniformly in `conftest.py` or at the top of files
|
||||
6. **Type annotations**: Add type annotations to all test function parameters
|
||||
7. **Resources**, Using pytest tag to specify the computation resources the test required.
|
||||
7. **Pytest Markers**: Add necessary markers like `@pytest.mark.core_model` and use `@hardware_test` to declare hardware requirements (check detailed in [Markers for Tests](../ci/tests_markers.md)).
|
||||
|
||||
### Template
|
||||
#### E2E - Online serving
|
||||
@@ -155,6 +155,7 @@ from pathlib import Path
|
||||
import pytest
|
||||
import openai
|
||||
|
||||
from tests.utils import hardware_test
|
||||
|
||||
# Optional: set process start method for workers
|
||||
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
|
||||
@@ -184,6 +185,12 @@ def base64_encoded_video() -> str:
|
||||
def dummy_messages_from_video_data(video_data_url: str, content_text: str) -> str:
|
||||
xxx
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.omni
|
||||
@hardware_test(
|
||||
res={"cuda": "L4", "rocm": "MI325", "npu": "A2"},
|
||||
num_cards={"cuda": 2, "rocm": 2, "npu": 4},
|
||||
)
|
||||
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
||||
def test_video_to_audio(
|
||||
client: openai.OpenAI,
|
||||
@@ -226,6 +233,7 @@ from pathlib import Path
|
||||
import pytest
|
||||
from vllm.assets.video import VideoAsset
|
||||
|
||||
from tests.utils import hardware_test
|
||||
from ..multi_stages.conftest import OmniRunner
|
||||
|
||||
# Optional: set process start method for workers
|
||||
@@ -239,7 +247,12 @@ test_params = [(model, stage_config) for model in models for stage_config in sta
|
||||
|
||||
# function name: test_{input_modality}_to_{output_modality}
|
||||
# modality candidate: text, image, audio, video, mixed_modalities
|
||||
@pytest.mark.gpu_mem_high # requires high-memory GPU node
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.omni
|
||||
@hardware_test(
|
||||
res={"cuda": "L4", "rocm": "MI325", "npu": "A2"},
|
||||
num_cards=2,
|
||||
)
|
||||
@pytest.mark.parametrize("test_config", test_params)
|
||||
def test_video_to_audio(omni_runner: type[OmniRunner], model: str) -> None:
|
||||
"""Offline inference: video input, audio output."""
|
||||
@@ -263,4 +276,5 @@ def test_video_to_audio(omni_runner: type[OmniRunner], model: str) -> None:
|
||||
|
||||
1. The file is saved in an appropriate place and the file name is clear.
|
||||
2. The coding style follows the requirements outlined above.
|
||||
3. For e2e model test, please ensure the test is configured under the `./buildkite/` folder.
|
||||
3. **All test functions have appropriate pytest markers**
|
||||
4. For tests that need run in CI, please ensure the test is configured under the `./buildkite/` folder.
|
||||
@@ -140,7 +140,7 @@ Key point for writing the example:
|
||||
+ Save or display the generated results so users can validate the integration.
|
||||
|
||||
# Testing
|
||||
For comprehensive testing guidelines, please refer to the [Test File Structure and Style Guide](../tests/tests_style.md).
|
||||
For comprehensive testing guidelines, please refer to the [Test File Structure and Style Guide](../ci/tests_style.md).
|
||||
|
||||
|
||||
## Adding a Model Recipe
|
||||
|
||||
@@ -572,7 +572,7 @@ def talker2code2wav(
|
||||
|
||||
## Testing
|
||||
|
||||
For comprehensive testing guidelines, please refer to the [Test File Structure and Style Guide](../tests/tests_style.md).
|
||||
For comprehensive testing guidelines, please refer to the [Test File Structure and Style Guide](../ci/tests_style.md).
|
||||
|
||||
## Adding a Model Recipe
|
||||
|
||||
|
||||
@@ -1,3 +1,166 @@
|
||||
# Profiling vLLM-Omni
|
||||
|
||||
Profiling is only intended for vLLM-Omni developers and maintainers to understand the proportion of time spent in different parts of the codebase. **vLLM-Omni end-users should never turn on profiling** as it will significantly slow down the inference.
|
||||
> **Warning:** Profiling incurs significant overhead. Use only for development and debugging, never in production.
|
||||
|
||||
vLLM-Omni uses the PyTorch Profiler to analyze performance across both **Multi-Stage LLMs** and **Diffusion Models**.
|
||||
|
||||
### 1. Set the Output Directory
|
||||
Before running any script, set this environment variable. The system detects this and automatically saves traces here.
|
||||
|
||||
```bash
|
||||
export VLLM_TORCH_PROFILER_DIR=./profiles
|
||||
```
|
||||
|
||||
### 2. Start Profiling
|
||||
|
||||
It is best to limit profiling to one iteration to keep trace files manageable.
|
||||
|
||||
```bash
|
||||
export VLLM_PROFILER_MAX_ITERS=1
|
||||
```
|
||||
|
||||
**Selective Stage Profiling**
|
||||
The profiler is default to function across all stages. But It is highly recommended to profile specific stages by passing the stages list, preventing from producing too large trace files:
|
||||
```python
|
||||
# Profile all stages
|
||||
omni_llm.start_profile()
|
||||
|
||||
# Only profile Stage 1
|
||||
omni_llm.start_profile(stages=[1])
|
||||
```
|
||||
|
||||
```python
|
||||
# Stage 0 (Thinker) and Stage 2 (Audio Decoder) for qwen omni
|
||||
omni_llm.start_profile(stages=[0, 2])
|
||||
```
|
||||
|
||||
**Python Usage**: Wrap your generation logic with `start_profile()` and `stop_profile()`.
|
||||
|
||||
```python
|
||||
from vllm_omni import omni_llm
|
||||
|
||||
profiler_enabled = bool(os.getenv("VLLM_TORCH_PROFILER_DIR"))
|
||||
|
||||
# 1. Start profiling if enabled
|
||||
if profiler_enabled:
|
||||
omni_llm.start_profile(stages=[0])
|
||||
|
||||
# Initialize generator
|
||||
omni_generator = omni_llm.generate(prompts, sampling_params_list, py_generator=args.py_generator)
|
||||
|
||||
total_requests = len(prompts)
|
||||
processed_count = 0
|
||||
|
||||
# Main Processing Loop
|
||||
for stage_outputs in omni_generator:
|
||||
|
||||
# ... [Output processing logic for text/audio would go here] ...
|
||||
|
||||
# Update count to track when to stop profiling
|
||||
processed_count += len(stage_outputs.request_output)
|
||||
|
||||
# 2. Check if all requests are done to stop the profiler safely
|
||||
if profiler_enabled and processed_count >= total_requests:
|
||||
print(f"[Info] Processed {processed_count}/{total_requests}. Stopping profiler inside active loop...")
|
||||
|
||||
# Stop the profiler while workers are still active
|
||||
omni_llm.stop_profile()
|
||||
|
||||
# Wait for traces to flush to disk
|
||||
print("[Info] Waiting 30s for workers to write trace files to disk...")
|
||||
time.sleep(30)
|
||||
print("[Info] Trace export wait time finished.")
|
||||
|
||||
omni_llm.close()
|
||||
```
|
||||
|
||||
|
||||
**Examples**:
|
||||
|
||||
1. **Qwen-omni 2.5**: [https://github.com/vllm-project/vllm-omni/blob/main/examples/offline_inference/qwen2_5_omni/end2end.py](https://github.com/vllm-project/vllm-omni/blob/main/examples/offline_inference/qwen2_5_omni/end2end.py)
|
||||
|
||||
2. **Qwen-omni 3.0**: [https://github.com/vllm-project/vllm-omni/blob/main/examples/offline_inference/qwen3_omni/end2end.py](https://github.com/vllm-project/vllm-omni/blob/main/examples/offline_inference/qwen3_omni/end2end.py)
|
||||
|
||||
|
||||
**For Diffusion Models as a single stage**
|
||||
|
||||
Diffusion profiling is End-to-End, capturing encoding, denoising loops, and decoding.
|
||||
|
||||
**CLI Usage:**
|
||||
```python
|
||||
|
||||
python image_to_video.py \
|
||||
--model Wan-AI/Wan2.2-I2V-A14B-Diffusers \
|
||||
--image qwen-bear.png \
|
||||
--prompt "A cat playing with yarn, smooth motion" \
|
||||
\
|
||||
# Minimize Spatial Dimensions (Optional but helpful):
|
||||
# Drastically reduces memory usage so the profiler doesn't
|
||||
# crash due to overhead, though for accurate performance
|
||||
# tuning you often want target resolutions.
|
||||
--height 48 \
|
||||
--width 64 \
|
||||
\
|
||||
# Minimize Temporal Dimension (Frames):
|
||||
# Video models process 3D tensors (Time, Height, Width).
|
||||
# Reducing frames to the absolute minimum (2) keeps the
|
||||
# tensor size small, ensuring the trace file doesn't become
|
||||
# multi-gigabytes in size.
|
||||
--num_frames 2 \
|
||||
\
|
||||
# Minimize Iteration Loop (Steps):
|
||||
# This is the most critical setting for profiling.
|
||||
# Diffusion models run the same loop X times.
|
||||
# Profiling 2 steps gives you the exact same performance
|
||||
# data as 50 steps, but saves minutes of runtime and
|
||||
# prevents the trace viewer from freezing.
|
||||
--num_inference_steps 2 \
|
||||
\
|
||||
--guidance_scale 5.0 \
|
||||
--guidance_scale_high 6.0 \
|
||||
--boundary_ratio 0.875 \
|
||||
--flow_shift 12.0 \
|
||||
--fps 16 \
|
||||
--output i2v_output.mp4
|
||||
|
||||
```
|
||||
|
||||
**Examples**:
|
||||
|
||||
1. **Qwen image edit**: [https://github.com/vllm-project/vllm-omni/blob/main/examples/offline_inference/image_to_image/image_edit.py](https://github.com/vllm-project/vllm-omni/blob/main/examples/offline_inference/image_to_image/image_edit.py)
|
||||
|
||||
2. **Wan-AI/Wan2.2-I2V-A14B-Diffusers**: [https://github.com/vllm-project/vllm-omni/tree/main/examples/offline_inference/image_to_video](https://github.com/vllm-project/vllm-omni/tree/main/examples/offline_inference/image_to_video)
|
||||
|
||||
|
||||
**Online Inference(Async)**
|
||||
|
||||
For online serving using AsyncOmni, the methods are asynchronous. This allows you to toggle profiling dynamically without restarting the server.
|
||||
|
||||
```python
|
||||
from vllm_omni import AsyncOmni
|
||||
|
||||
# Inside an async function:
|
||||
async_omni = AsyncOmni.from_engine_args(engine_args)
|
||||
|
||||
await async_omni.start_profile()
|
||||
|
||||
async for output in async_omni.generate(prompt, sampling_params, request_id):
|
||||
# Process outputs...
|
||||
pass
|
||||
|
||||
await async_omni.stop_profile()
|
||||
```
|
||||
|
||||
### 3. Analyzing Omni Traces
|
||||
|
||||
Output files are saved to your configured ```VLLM_TORCH_PROFILER_DIR```.
|
||||
|
||||
**Output**
|
||||
**Chrome Trace** (```.json.gz```): Visual timeline of kernels and stages. Open in Perfetto UI.
|
||||
|
||||
**Viewing Tools:**
|
||||
|
||||
- [Perfetto](https://ui.perfetto.dev/)(recommended)
|
||||
- ```chrome://tracing```(Chrome only)
|
||||
|
||||
**Note**: vLLM-Omni reuses the PyTorch Profiler infrastructure from vLLM. See the official vLLM profiler documentation: [vLLM Profiling Guide](https://docs.vllm.ai/en/latest/dev/profiling.html)
|
||||
@@ -64,13 +64,13 @@ According to analysis for current popular open-source models, most of them have
|
||||
|
||||
## Key Components
|
||||
|
||||
| Component | Description |
|
||||
|-----------|-------------|
|
||||
| **OmniRouter** | provide an intelligent router for Omni-modality requests dispatch |
|
||||
| **EntryPoints** | define the APIs for offline/online serving (APIServer, Omni/AsyncOmni) and provide the OmniStage abstraction for different AR/DiT stages |
|
||||
| **AR** | adapted for omni-modality models while inheriting efficient features from vLLM, such as cache management |
|
||||
| **Diffusion** | natively implemented and optimized using acceleration components |
|
||||
| **OmniConnector** | supports fully disaggregation based on E/P/D/G (Encoding/Processing/Decoding/Generation) disaggregation across stages |
|
||||
| Component | Description |
|
||||
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **OmniRouter** | provide an intelligent router for Omni-modality requests dispatch |
|
||||
| **EntryPoints** | define the APIs for offline/online serving (APIServer, Omni/AsyncOmni) and provide the OmniStage abstraction for different AR/DiT stages |
|
||||
| **AR** | adapted for omni-modality models while inheriting efficient features from vLLM, such as cache management |
|
||||
| **Diffusion** | natively implemented and optimized using acceleration components |
|
||||
| **OmniConnector** | supports fully disaggregation based on E/P/D/G (Encoding/Processing/Decoding/Generation) disaggregation across stages |
|
||||
|
||||
Disaggregated stages are managed through configuration, such as in the Qwen3-Omni example, where stages like Thinker, Talker, and Code2wav are defined as separate OmniStage instances with specific resources and input/output type.
|
||||
|
||||
@@ -192,4 +192,4 @@ curl -sS -X POST http://localhost:8091/v1/chat/completions \
|
||||
}
|
||||
```
|
||||
|
||||
For more usages, please refer to [examples](../user_guide/examples/).
|
||||
For more usages, please refer to [examples](https://github.com/vllm-project/vllm-omni/tree/main/examples).
|
||||
@@ -1,47 +0,0 @@
|
||||
## Multi-Request Streaming (MRS) on a Single Machine
|
||||
|
||||
### 1. Background & Scope
|
||||
- All processing runs on a single physical machine with multi-process, per-stage workers. No proxy or network transport involved.
|
||||
- Current alignment with vllm-omni: `OmniLLM` supports multiple stages (`OmniStage`). GPU runners already expose streamable steps (prefill/decoding/diffusion), but the entry layer still collects lists and lacks intra-stage streaming and window scheduling.
|
||||
- Goal: implement multi-stage, multi-request streaming (MRS) locally. Each stage outputs segments; downstream stages stitch and trigger compute based on configured windows. Shared memory and zero-copy strategies reduce data movement overhead.
|
||||
|
||||
### 2. Key Constraints
|
||||
- Multi-process per stage: each stage is an independent process with a while loop; device visibility can be configured (`CUDA_VISIBLE_DEVICES`/`torch.cuda.set_device`).
|
||||
- Simple IPC (copy-based): use `multiprocessing.Queue`/Pipe for inter-process communication with CPU copies/serialization; do not rely on CUDA IPC/SHM zero-copy in this version.
|
||||
- Cross-stage pipeline: different stages can process different requests concurrently (e.g., stage A handles request 1 while stage B handles request 0).
|
||||
|
||||
### 3. Architecture Overview
|
||||
- Processes & IPC queues
|
||||
- Each "sub-stage" is an OS process (worker). The loop: take from input_queue → compute → put to output_queue.
|
||||
- Inter-stage connection via IPC: copy-based `multiprocessing.Queue` passing dict payloads; use shared memory for large objects.
|
||||
- Each link is SPSC (single-producer/single-consumer): the upstream is the orchestrator and the downstream is a single stage process; queues are unbounded (maxsize=0) on the orchestrator side.
|
||||
- Device visibility
|
||||
- Each stage sets `CUDA_VISIBLE_DEVICES` or calls `torch.cuda.set_device` to bind to GPU sets.
|
||||
- A stage may use multiple GPUs internally (TP/PP/DP) but presents as a single stage unit.
|
||||
- Simplified IPC: copy-based queues/pipes for data transfer; zero-copy is future work.
|
||||
- Pipeline progression: when a stage finishes a request, it enqueues outputs to the downstream stage; if downstream is idle, it starts immediately.
|
||||
- Scheduling
|
||||
- A downstream stage triggers only after the upstream completes the request.
|
||||
- Windowed segmentation/stitched triggering is not implemented; intra-stage streaming is not provided.
|
||||
|
||||
### 4. IPC Implementation (simplified: copy-based)
|
||||
- Use `multiprocessing.Queue`/Pipe for inter-process communication (control + data).
|
||||
- Data is serialized/copied via CPU; no CUDA IPC/SHM zero-copy in this version.
|
||||
- Backpressure: queues are unbounded; pressure manifests as compute-rate differences. Optional SHM reduces large-object transfer cost; RX/decoding overhead is recorded for observability.
|
||||
|
||||
### 5. Scheduling & Cancellation (simplified)
|
||||
- Pipeline: when a stage finishes a request, it enqueues to the next stage; that stage immediately pulls the next request from its input queue, enabling cross-stage concurrency.
|
||||
- Cancellation/timeout: explicit cancellation/timeouts are not provided; graceful shutdown uses a `None` sentinel sent to each stage input queue.
|
||||
|
||||
#### Short sequence example (req0/req1, stage A→B)
|
||||
1) t0: stage A handles req0
|
||||
2) t1: req0 completes on A → enters B; A immediately starts req1
|
||||
3) t2: B handles req0 while A handles req1 (parallel across stages)
|
||||
|
||||
### 6. Integration Points (by file)
|
||||
- `vllm_omni/entrypoints/omni.py` (Orchestrator)
|
||||
- Class `Omni` orchestrates multi-process stages; constructs `OmniStage` instances in parallel and spawns per-stage workers.
|
||||
- Spawns stage processes per config (set `CUDA_VISIBLE_DEVICES`/`torch.cuda.set_device`), creates control/data channels, builds simple full-trigger flow.
|
||||
- Stats/logging are disabled by default; per-stage and orchestrator stats are only written when explicitly enabled.
|
||||
- Manages process lifecycle: start/wait for readiness, graceful shutdown; forwards results between stages using copy-based IPC and optional SHM.
|
||||
- Stage readiness: each stage emits `{"type": "stage_ready"}` after initialization; the orchestrator waits for all stages or times out and logs diagnostic suggestions.
|
||||
@@ -1,14 +1,17 @@
|
||||
# Distributed utils
|
||||
|
||||
This directory (vllm_omni/distributed/ray_utils) contains utilities for distributed execution in vllm-omni, supporting both **Ray** and **Multiprocessing** backends.
|
||||
|
||||
## 1. Ray Utils
|
||||
## 1. Installation
|
||||
```bash
|
||||
pip install "ray[default]"
|
||||
```
|
||||
## 2. Ray Utils
|
||||
|
||||
The `ray_utils` module provides helper functions for managing Ray clusters and actors, which is essential for:
|
||||
* **Multi-node deployment**: Running pipeline stages across different physical machines.
|
||||
* **Resource management**: Efficient GPU/CPU allocation.
|
||||
|
||||
### 1.1 Basic Usage
|
||||
### 2.1 Basic Usage
|
||||
|
||||
To use the Ray backend, specify `worker_backend="ray"` when initializing the engine.
|
||||
|
||||
@@ -21,7 +24,7 @@ vllm serve Qwen/Qwen2.5-Omni-7B \
|
||||
--ray-address auto
|
||||
```
|
||||
|
||||
### 1.2 Cluster Setup
|
||||
### 2.2 Cluster Setup
|
||||
|
||||
**Step 1: Start Head Node**
|
||||
Run this on your primary machine:
|
||||
@@ -38,7 +41,7 @@ ray start --address=<HEAD_NODE_IP>:6399
|
||||
> **Tip**: For a complete cluster setup script, refer to the vLLM example:
|
||||
> [run_cluster.sh](https://github.com/vllm-project/vllm/blob/main/examples/online_serving/run_cluster.sh)
|
||||
|
||||
### 1.3 Distributed Connector Support
|
||||
### 2.3 Distributed Connector Support
|
||||
|
||||
When running on Ray, the system automatically adapts its communication strategy:
|
||||
|
||||
@@ -46,16 +49,11 @@ When running on Ray, the system automatically adapts its communication strategy:
|
||||
* **Same-Node**: Can still use `SharedMemoryConnector` for efficiency, or Ray's native object store (plasma).
|
||||
* **SHM threshold default differs**: when `worker_backend="ray"`, the SharedMemoryConnector default threshold is set to `sys.maxsize`, which forces payloads to go inline (no SHM). Override `shm_threshold_bytes` in the connector config if you want SHM for Ray runs.
|
||||
|
||||
### 1.4 Internal Helpers
|
||||
### 2.4 Internal Helpers
|
||||
|
||||
* **`initialize_ray_cluster`**: Connects to an existing Ray cluster or starts a local one.
|
||||
|
||||
## 2. Troubleshooting
|
||||
## 3. Troubleshooting
|
||||
|
||||
* **Connection Issues**: Ensure the Ray head node is accessible and ports (default 6399 in this example) are open.
|
||||
* **Version Mismatch**: Ensure all nodes run the same version of Ray and Python.
|
||||
|
||||
### Installation
|
||||
```bash
|
||||
pip install "ray[default]"
|
||||
```
|
||||
@@ -9,7 +9,6 @@ This section contains design documents and architecture specifications for vLLM-
|
||||
## Feature Design Documents
|
||||
|
||||
- [Disaggregated Inference](feature/disaggregated_inference.md)
|
||||
- [Multi-Request Streaming](feature/multi_request_streaming.md)
|
||||
- [Ray-based Execution](feature/ray_based_execution.md)
|
||||
|
||||
## Module Design Documents
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
toc_depth: 4
|
||||
---
|
||||
|
||||
# Diffusion Module Architecture Design
|
||||
|
||||
The vLLM-Omni diffusion module (`vllm_omni/diffusion`) is a high-performance inference engine for diffusion models, designed with a modular architecture that separates concerns across multiple components. It provides efficient execution for non-autoregressive generation tasks such as image and video generation.
|
||||
@@ -5,7 +9,7 @@ The vLLM-Omni diffusion module (`vllm_omni/diffusion`) is a high-performance inf
|
||||
This document describes the architecture design of the diffusion module, including the diffusion engine, scheduler, worker, diffusion pipeline, and acceleration components.
|
||||
|
||||
<p align="center">
|
||||
<img src="https://github.com/user-attachments/assets/cde1ebea-4006-4d30-8283-15a5721a09d2" alt="vLLM-Omni Diffusion Module Components" width="60%">
|
||||
<img src="https://github.com/user-attachments/assets/cde1ebea-4006-4d30-8283-15a5721a09d2" alt="vLLM-Omni Diffusion Module Components" width="80%">
|
||||
</p>
|
||||
<p align="center">
|
||||
<em> Main Components of the Diffusion Module </em>
|
||||
@@ -20,10 +24,10 @@ This document describes the architecture design of the diffusion module, includi
|
||||
- [Worker](#3-worker)
|
||||
- [Diffusion Pipeline](#4-diffusion-pipeline)
|
||||
- [Acceleration Components](#5-acceleration-components)
|
||||
- [Attention Backends](#51-attention-backends)
|
||||
- [Parallel Attention](#52-parallel-attention)
|
||||
- [Cache Backends](#53-cache-backends)
|
||||
- [Parallel Strategies](#54-parallel-strategies)
|
||||
- [Attention Backends](#51-attention-backends)
|
||||
- [Parallel Attention](#52-parallel-attention)
|
||||
- [Cache Backends](#53-cache-backends)
|
||||
- [Parallel Strategies](#54-parallel-strategies)
|
||||
- [Data Flow](#6-data-flow)
|
||||
|
||||
---
|
||||
@@ -65,8 +69,11 @@ class DiffusionEngine:
|
||||
```
|
||||
|
||||
**Key Features**:
|
||||
|
||||
- **Pre/Post Processing**: Registers model-specific pre-processing and post-processing functions via registry pattern
|
||||
|
||||
- **Worker Management**: Launches and manages multiple worker processes (one per GPU)
|
||||
|
||||
- **Process Isolation**: Uses multiprocessing for true parallelism
|
||||
|
||||
#### 1.2 Worker Launch Process
|
||||
@@ -86,8 +93,11 @@ def _launch_workers(self, broadcast_handle):
|
||||
```
|
||||
|
||||
**Design Decisions**:
|
||||
|
||||
- **Spawn Method**: Ensures clean state for each worker (no shared memory issues)
|
||||
|
||||
- **Pipe Communication**: Uses `mp.Pipe` for initialization handshake
|
||||
|
||||
- **Device Selection**: Each worker is assigned a specific GPU (`cuda:{rank}`)
|
||||
|
||||
#### 1.3 Request Processing Flow
|
||||
@@ -106,8 +116,11 @@ def step(self, requests: list[OmniDiffusionRequest]):
|
||||
```
|
||||
|
||||
**Flow**:
|
||||
|
||||
1. **Pre-processing**: Applies model-specific transformations
|
||||
|
||||
2. **Scheduling**: Delegates to scheduler for distribution
|
||||
|
||||
3. **Post-processing**: Converts raw outputs to final format (e.g., PIL images)
|
||||
|
||||
---
|
||||
@@ -139,8 +152,11 @@ class Scheduler:
|
||||
```
|
||||
|
||||
**Communication Pattern**:
|
||||
|
||||
- **Broadcast Queue**: One-to-many communication (scheduler → all workers)
|
||||
|
||||
- **Result Queue**: One-to-one communication (rank 0 → scheduler)
|
||||
|
||||
- **Shared Memory**: Uses `MessageQueue` (ZMQ-based) for efficient IPC
|
||||
|
||||
#### 2.2 Request Distribution
|
||||
@@ -156,8 +172,11 @@ def add_req(self, requests: list[OmniDiffusionRequest]) -> DiffusionOutput:
|
||||
```
|
||||
|
||||
**Design Features**:
|
||||
|
||||
- **Broadcast Model**: All workers receive the same request (for tensor parallelism)
|
||||
|
||||
- **Single Response**: Only rank 0 sends results back (avoids duplicate outputs)
|
||||
|
||||
- **Synchronous**: Blocks until result is received (can be made async)
|
||||
|
||||
#### 2.3 Singleton Pattern
|
||||
@@ -176,8 +195,11 @@ scheduler = Scheduler()
|
||||
```
|
||||
|
||||
**Benefits**:
|
||||
|
||||
- **Single Point of Control**: Ensures consistent state
|
||||
|
||||
- **Easy Access**: Global `scheduler` instance accessible everywhere
|
||||
|
||||
- **Resource Management**: Centralized queue management
|
||||
|
||||
---
|
||||
@@ -212,12 +234,19 @@ class WorkerProc:
|
||||
```
|
||||
|
||||
**Initialization Steps**:
|
||||
|
||||
1. **IPC Setup**: Creates ZMQ context and message queues
|
||||
|
||||
2. **Distributed Environment Setup**: Initializes PyTorch distributed communication
|
||||
- For CUDA GPUs: Uses NCCL (fast GPU communication)
|
||||
- For NPU: Uses HCCL (Huawei Collective Communications Library)
|
||||
- For other devices: Uses appropriate backend (GLOO, MCCL, etc.)
|
||||
|
||||
- For CUDA GPUs: Uses NCCL (fast GPU communication)
|
||||
|
||||
- For NPU: Uses HCCL (Huawei Collective Communications Library)
|
||||
|
||||
- For other devices: Uses appropriate backend (GLOO, MCCL, etc.)
|
||||
|
||||
3. **Model Loading**: Loads diffusion pipeline on assigned GPU
|
||||
|
||||
4. **Cache Setup**: Enables cache backend if configured.
|
||||
|
||||
#### 3.2 GPU Worker
|
||||
@@ -253,8 +282,11 @@ class GPUWorker:
|
||||
```
|
||||
|
||||
**Key Features**:
|
||||
|
||||
- **Tensor Parallelism**: Supports multi-GPU tensor parallelism via PyTorch distributed
|
||||
|
||||
- **Model Loading**: Uses `DiffusersPipelineLoader` for efficient weight loading
|
||||
|
||||
- **Cache Integration**: Enables cache backends (TeaCache, cache-dit, etc.) transparently
|
||||
|
||||
#### 3.3 Worker Busy Loop
|
||||
@@ -283,9 +315,13 @@ def worker_busy_loop(self):
|
||||
```
|
||||
|
||||
**Execution Flow**:
|
||||
|
||||
1. **Receive**: Dequeues unified messages from shared memory queue
|
||||
|
||||
2. **Route**: Handles different message types (generation, RPC, shutdown)
|
||||
|
||||
3. **Execute**: Runs forward pass through pipeline for generation requests
|
||||
|
||||
4. **Respond**: Sends results back (rank 0 for generation, specified rank for RPC)
|
||||
|
||||
#### 3.4 Model Execution
|
||||
@@ -315,10 +351,14 @@ The model execution leverages multiple parallelism strategies that are transpare
|
||||
from vllm_omni.diffusion.distributed.parallel_state import (
|
||||
get_sp_group, get_dp_group, get_cfg_group, get_pp_group
|
||||
)
|
||||
```
|
||||
|
||||
**Optimizations**:
|
||||
|
||||
- **Cache Refresh**: Clears cache state before each generation for clean state
|
||||
|
||||
- **Context Management**: Forward context ensures parallel groups are available during execution
|
||||
|
||||
- **Single Request**: Currently processes one request at a time (batching TODO)
|
||||
|
||||
---
|
||||
@@ -327,66 +367,9 @@ from vllm_omni.diffusion.distributed.parallel_state import (
|
||||
|
||||
**Location**: `vllm_omni/diffusion/models/*/pipeline_*.py`
|
||||
|
||||
### Architecture
|
||||
|
||||
The pipeline is the **model-specific implementation** that orchestrates the diffusion process. Different models (QwenImage, Wan2.2, Z-Image) have their own pipeline implementations.
|
||||
|
||||
### Key Components (QwenImage Example)
|
||||
|
||||
#### 4.1 Pipeline Structure
|
||||
|
||||
```python
|
||||
class QwenImagePipeline(nn.Module):
|
||||
def __init__(self, od_config: OmniDiffusionConfig):
|
||||
# Core components
|
||||
self.scheduler = FlowMatchEulerDiscreteScheduler.from_pretrained(...)
|
||||
self.text_encoder = Qwen2_5_VLForConditionalGeneration.from_pretrained(...)
|
||||
self.vae = AutoencoderKLQwenImage.from_pretrained(...)
|
||||
self.transformer = QwenImageTransformer2DModel(od_config=od_config)
|
||||
self.tokenizer = Qwen2Tokenizer.from_pretrained(...)
|
||||
|
||||
# Cache backend (set by worker)
|
||||
self._cache_backend = None
|
||||
```
|
||||
|
||||
**Component Responsibilities**:
|
||||
- **Text Encoder**: Encodes prompts into embeddings
|
||||
- **Transformer**: Main denoising network (DiT-style)
|
||||
- **VAE**: Encodes/decodes between pixel and latent space
|
||||
- **Scheduler**: Manages timestep scheduling
|
||||
|
||||
#### 4.2 Forward Pass
|
||||
|
||||
```python
|
||||
def forward(self, req: OmniDiffusionRequest, ...) -> DiffusionOutput:
|
||||
# 1. Encode prompts
|
||||
prompt_embeds, prompt_embeds_mask = self.encode_prompt(prompt)
|
||||
|
||||
# 2. Prepare latents
|
||||
latents = self.prepare_latents(batch_size, num_channels, height, width, ...)
|
||||
|
||||
# 3. Prepare timesteps
|
||||
timesteps, num_inference_steps = self.prepare_timesteps(...)
|
||||
|
||||
# 4. Diffusion loop
|
||||
latents = self.diffuse(
|
||||
prompt_embeds, prompt_embeds_mask,
|
||||
negative_prompt_embeds, negative_prompt_embeds_mask,
|
||||
latents, img_shapes, txt_seq_lens,
|
||||
timesteps, do_true_cfg, guidance, true_cfg_scale,
|
||||
)
|
||||
|
||||
# 5. Decode latents
|
||||
image = self.vae.decode(latents)
|
||||
|
||||
return DiffusionOutput(output=image)
|
||||
```
|
||||
|
||||
The main steps of the forward pass are referred from `diffusers`.
|
||||
|
||||
#### 4.3 Diffusion Loop
|
||||
|
||||
The multi-step diffusion loop is usually the most time-consuming part during the overall inference process.
|
||||
Most pipeline implementation are referred from `diffusers`. The multi-step diffusion loop is usually the most time-consuming part during the overall inference process, which is defined by the `diffuse` function in the pipeline class. An example is as follows:
|
||||
|
||||
```python
|
||||
def diffuse(self, ...):
|
||||
@@ -416,11 +399,14 @@ def diffuse(self, ...):
|
||||
```
|
||||
|
||||
**Key Features**:
|
||||
|
||||
- **CFG Support**: Handles classifier-free guidance with separate forward passes
|
||||
|
||||
- **Cache Branching**: Uses `cache_branch` parameter for cache-aware execution
|
||||
|
||||
- **True CFG**: Implements advanced CFG with norm preservation
|
||||
|
||||
To learn how to add a new diffusion pipeline, please view [Adding Diffusion Model](https://docs.vllm.ai/projects/vllm-omni/en/latest/contributing/model/adding_diffusion_model)
|
||||
To learn more about the diffusion pipeline and how to add a new diffusion pipeline, please view [Adding Diffusion Model](https://docs.vllm.ai/projects/vllm-omni/en/latest/contributing/model/adding_diffusion_model)
|
||||
|
||||
---
|
||||
|
||||
@@ -448,9 +434,13 @@ class Attention(nn.Module):
|
||||
```
|
||||
|
||||
**Available Backends**:
|
||||
|
||||
- **FlashAttention**: Optimized CUDA kernel (FA2/FA3) - memory efficient via tiling
|
||||
|
||||
- **SDPA**: PyTorch's scaled dot-product attention - default, cross-platform
|
||||
|
||||
- **SageAttention**: Sparse attention implementation from SageAttention library
|
||||
|
||||
- **AscendAttention**: NPU-optimized attention for Ascend hardware
|
||||
|
||||
These backends provide the **kernel implementations** for attention computation. For attention-level sequence parallelism strategies (Ring Attention, Ulysses), see [Parallel Attention](#52-parallel-attention).
|
||||
@@ -470,16 +460,25 @@ def get_attn_backend(head_size: int) -> type[AttentionBackend]:
|
||||
```
|
||||
|
||||
**Selection Priority**:
|
||||
|
||||
1. **Environment Variable**: `DIFFUSION_ATTENTION_BACKEND` for manual override
|
||||
- Valid values: `FLASH_ATTN`, `TORCH_SDPA`, `SAGE_ATTN`, `ASCEND`
|
||||
- Example: `export DIFFUSION_ATTENTION_BACKEND=SAGE_ATTN`
|
||||
|
||||
- Valid values: `FLASH_ATTN`, `TORCH_SDPA`, `SAGE_ATTN`, `ASCEND`
|
||||
|
||||
- Example: `export DIFFUSION_ATTENTION_BACKEND=SAGE_ATTN`
|
||||
|
||||
2. **Automatic Fallback**: Falls back to SDPA if selected backend unavailable
|
||||
|
||||
3. **Hardware Detection**: Can select based on device type (NPU, CUDA, etc.)
|
||||
|
||||
**Backend Availability**:
|
||||
|
||||
- **SDPA**: Always available (PyTorch built-in)
|
||||
|
||||
- **FlashAttention**: Requires `flash-attn` package installed
|
||||
|
||||
- **SageAttention**: Requires `sage-attention` package (from THU-ML GitHub)
|
||||
|
||||
- **AscendAttention**: Only available on Ascend NPU hardware
|
||||
|
||||
#### Attention Backend Registry
|
||||
@@ -563,8 +562,11 @@ Parallel attention strategies implement **Sequence Parallelism (SP) at the atten
|
||||
**Key Distinction**: Unlike AttentionBackend (which provides kernel implementations), ParallelAttentionStrategy provides communication patterns for multi-GPU attention parallelism. These strategies implement the `ParallelAttentionStrategy` interface and use AttentionBackend implementations internally.
|
||||
|
||||
Both Ring Attention and Ulysses are forms of Sequence Parallelism (SP) that:
|
||||
|
||||
- Split the sequence dimension across GPUs
|
||||
|
||||
- Contribute to `sequence_parallel_size` (via `ring_degree` and `ulysses_degree`)
|
||||
|
||||
- Work at the attention layer level (not model/pipeline level)
|
||||
|
||||
#### Ulysses Sequence Parallelism (USP)
|
||||
@@ -574,7 +576,9 @@ Both Ring Attention and Ulysses are forms of Sequence Parallelism (SP) that:
|
||||
USP is a sequence-parallel attention strategy that splits attention computation across multiple GPUs by distributing both the sequence dimension and attention heads. It uses **all-to-all communication** to efficiently parallelize attention for very long sequences. Specifically, it uses **all-to-all** collective operations to redistribute Q/K/V tensors before attention computation and gather results afterward.
|
||||
|
||||
Ulysses splits attention computation in two dimensions:
|
||||
|
||||
1. **Sequence Dimension**: Splits the sequence length across GPUs
|
||||
|
||||
2. **Head Dimension**: Splits attention heads across GPUs
|
||||
|
||||
**Configuration**: `ulysses_degree` contributes to `sequence_parallel_size`
|
||||
@@ -601,9 +605,13 @@ class RingParallelAttention:
|
||||
```
|
||||
|
||||
**Integration**:
|
||||
|
||||
- Ring Attention is activated when `ring_degree > 1` in parallel config
|
||||
|
||||
- It's selected by `build_parallel_attention_strategy()` in the attention layer
|
||||
|
||||
- The `Attention` layer routes to `_run_ring_attention()` when Ring is enabled
|
||||
|
||||
- Works alongside attention backends: Ring handles communication, backends handle computation
|
||||
|
||||
**Configuration**: `ring_degree` contributes to `sequence_parallel_size`
|
||||
@@ -611,8 +619,11 @@ class RingParallelAttention:
|
||||
#### Relationship with AttentionBackend
|
||||
|
||||
Parallel attention strategies (Ring, Ulysses) work **on top of** AttentionBackend implementations:
|
||||
|
||||
- They use AttentionBackend for the actual attention computation (FlashAttention, SDPA, etc.)
|
||||
|
||||
- They handle the multi-GPU communication/parallelization layer
|
||||
|
||||
- They implement `ParallelAttentionStrategy` interface (not `AttentionBackend`)
|
||||
|
||||
For general parallelism strategies (Data Parallelism, Tensor Parallelism, Pipeline Parallelism), see [Parallel Strategies](#54-parallel-strategies).
|
||||
@@ -651,8 +662,11 @@ class CacheBackend(ABC):
|
||||
```
|
||||
|
||||
**Design Pattern**:
|
||||
|
||||
- **Abstract Base Class**: Defines contract for all cache backends
|
||||
|
||||
- **Pipeline-based**: Works with pipeline instances (not just transformers)
|
||||
|
||||
- **State Management**: Provides refresh mechanism for clean state between generations
|
||||
|
||||
#### Available Backends
|
||||
@@ -686,9 +700,13 @@ class TeaCacheBackend(CacheBackend):
|
||||
```
|
||||
|
||||
**TeaCache Features**:
|
||||
|
||||
- **Timestep-aware**: Caches based on timestep embedding similarity
|
||||
|
||||
- **Adaptive**: Dynamically decides when to reuse cached computations
|
||||
|
||||
- **CFG-aware**: Handles positive/negative branches separately
|
||||
|
||||
- **Custom Hook System**: Uses a custom forward interception mechanism (via `HookRegistry`) that wraps the module's `forward` method, allowing transparent integration without modifying model code
|
||||
|
||||
**2. Cache-DiT Backend**
|
||||
@@ -710,9 +728,13 @@ class CacheDiTBackend(CacheBackend):
|
||||
```
|
||||
|
||||
**Cache-DiT Features**:
|
||||
|
||||
- **DBCache**: Dynamic block caching with configurable compute blocks
|
||||
|
||||
- **SCM**: Step Computation Masking for additional speedup
|
||||
|
||||
- **TaylorSeer**: Advanced calibration for cache accuracy
|
||||
|
||||
- **Dual-transformer Support**: Handles models like Wan2.2 with two transformers
|
||||
|
||||
#### Cache Backend Selector
|
||||
@@ -748,9 +770,13 @@ def get_cache_backend(
|
||||
```
|
||||
|
||||
**Usage Flow**:
|
||||
|
||||
1. **Selection**: `get_cache_backend()` returns appropriate backend instance
|
||||
|
||||
2. **Enable**: `backend.enable(pipeline)` called during worker initialization
|
||||
|
||||
3. **Refresh**: `backend.refresh(pipeline, num_inference_steps)` called before each generation
|
||||
|
||||
4. **Check**: `backend.is_enabled()` verifies cache is active
|
||||
|
||||
### 5.4 Parallel Strategies
|
||||
@@ -762,23 +788,35 @@ def get_cache_backend(
|
||||
The system supports multiple orthogonal parallelism strategies:
|
||||
|
||||
**Sequence Parallelism (SP)**
|
||||
|
||||
- **Purpose**: Split sequence dimension across GPUs
|
||||
|
||||
- **Attention-level SP**: Ring Attention and Ulysses (USP) implement SP at the attention layer level
|
||||
- See [Parallel Attention](#52-parallel-attention) for details
|
||||
- Configuration: `ulysses_degree` × `ring_degree` = `sequence_parallel_size`
|
||||
|
||||
- See [Parallel Attention](#52-parallel-attention) for details
|
||||
|
||||
- Configuration: `ulysses_degree` × `ring_degree` = `sequence_parallel_size`
|
||||
|
||||
- **Use Case**: Very long sequences (e.g., high-resolution images)
|
||||
|
||||
**Data Parallelism (DP)**
|
||||
|
||||
- **Purpose**: Replicate model across GPUs, split batch
|
||||
|
||||
- **Use Case**: Batch processing, throughput optimization
|
||||
|
||||
**Tensor Parallelism (TP)** (Experimental)
|
||||
|
||||
- **Purpose**: Split model weights across GPUs
|
||||
|
||||
- **Implementation**: Uses vLLM's tensor parallel groups
|
||||
|
||||
- **Use Case**: Large models that don't fit on single GPU
|
||||
|
||||
**CFG Parallelism** (under development)
|
||||
|
||||
- **Purpose**: Parallelize Classifier-Free Guidance (positive/negative prompts)
|
||||
|
||||
- **Infrastructure**: CFG parallel groups are initialized and available via `get_cfg_group()`
|
||||
|
||||
#### Parallel Group Management
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# Sleep Mode
|
||||
|
||||
vLLM-Omni’s **Sleep Mode** allows you to temporarily release most GPU memory used by a model—such as model weights and key-value (KV) caches (for autoregressive models)—**without stopping the server or unloading the Docker container**.
|
||||
|
||||
This feature is inherited from [vLLM’s Sleep Mode](https://blog.vllm.ai/2025/10/26/sleep-mode.html), which provides zero-reload model switching for multi-model serving.
|
||||
|
||||
It is especially useful in **RLHF**, **training**, or **cost-saving scenarios**, where GPU resources must be freed between inference workloads.
|
||||
|
||||
---
|
||||
|
||||
## Omni Model
|
||||
|
||||
Omni model inherit the feature from vLLM' Sleep Mode
|
||||
|
||||
This means:
|
||||
|
||||
- Support both Level 1 and Level 2 sleep, allow to release and reset both model weights and KV Cache
|
||||
|
||||
## Diffusion Model Extension
|
||||
|
||||
We added Sleep Mode support for **diffusion models**, which previously lacked this functionality.
|
||||
In diffusion pipelines, this currently only offloads **model weight memory**, as these models typically do not use KV caches.
|
||||
|
||||
This means:
|
||||
|
||||
- Diffusion models can now enter Level 1 sleep.
|
||||
- Pipeline states (e.g., noise schedulers, buffers) remain intact after waking.
|
||||
- Useful for releasing VRAM between image generation or training cycles.
|
||||
|
||||
---
|
||||
|
||||
## Enable sleep mode
|
||||
To enable sleep mode, set the `enable_sleep_mode` in `engine_args` to `True`
|
||||
|
||||
|
||||
Example:
|
||||
```python
|
||||
omni = Omni(model=...,enable_sleep_mode=True)
|
||||
```
|
||||
@@ -49,15 +49,18 @@ vLLM-Omni is a Python library that supports the following GPU variants. The libr
|
||||
|
||||
## Set up using Docker
|
||||
|
||||
### Pre-built images
|
||||
|
||||
=== "NVIDIA CUDA"
|
||||
|
||||
--8<-- "docs/getting_started/installation/gpu/cuda.inc.md:pre-built-images"
|
||||
|
||||
=== "AMD ROCm"
|
||||
|
||||
--8<-- "docs/getting_started/installation/gpu/rocm.inc.md:pre-built-images"
|
||||
|
||||
### Build your own docker image
|
||||
|
||||
=== "AMD ROCm"
|
||||
|
||||
--8<-- "docs/getting_started/installation/gpu/rocm.inc.md:build-docker"
|
||||
|
||||
### Pre-built images
|
||||
|
||||
=== "NVIDIA CUDA"
|
||||
|
||||
--8<-- "docs/getting_started/installation/gpu/cuda.inc.md:pre-built-images"
|
||||
@@ -17,12 +17,12 @@ Therefore, it is recommended to install vLLM and vLLM-Omni with a **fresh new**
|
||||
# --8<-- [start:pre-built-wheels]
|
||||
|
||||
#### Installation of vLLM
|
||||
Note: Pre-built wheels are currently only available for vLLM-Omni 0.11.0rc1. For the latest version, please [build from source](https://docs.vllm.ai/projects/vllm-omni/en/latest/getting_started/installation/gpu/#build-wheel-from-source).
|
||||
Note: Pre-built wheels are currently only available for vLLM-Omni 0.11.0rc1 and 0.12.0rc1. For the latest version, please [build from source](https://docs.vllm.ai/projects/vllm-omni/en/latest/getting_started/installation/gpu/#build-wheel-from-source).
|
||||
|
||||
|
||||
vLLM-Omni is built based on vLLM. Please install it with command below.
|
||||
```bash
|
||||
uv pip install vllm==0.11.0 --torch-backend=auto
|
||||
uv pip install vllm==0.14.0 --torch-backend=auto
|
||||
```
|
||||
|
||||
#### Installation of vLLM-Omni
|
||||
@@ -36,13 +36,13 @@ uv pip install vllm-omni
|
||||
# --8<-- [start:build-wheel-from-source]
|
||||
|
||||
#### Installation of vLLM
|
||||
If you do not need to modify source code of vLLM, you can directly install the stable 0.12.0 release version of the library
|
||||
If you do not need to modify source code of vLLM, you can directly install the stable 0.14.0 release version of the library
|
||||
|
||||
```bash
|
||||
uv pip install vllm==0.12.0 --torch-backend=auto
|
||||
uv pip install vllm==0.14.0 --torch-backend=auto
|
||||
```
|
||||
|
||||
The release 0.12.0 of vLLM is based on PyTorch 2.9.0 which requires CUDA 12.9 environment.
|
||||
The release 0.14.0 of vLLM is based on PyTorch 2.9.0 which requires CUDA 12.9 environment.
|
||||
|
||||
#### Installation of vLLM-Omni
|
||||
Since vllm-omni is rapidly evolving, it's recommended to install it from source
|
||||
@@ -58,11 +58,11 @@ If you want to check, modify or debug with source code of vLLM, install the libr
|
||||
```bash
|
||||
git clone https://github.com/vllm-project/vllm.git
|
||||
cd vllm
|
||||
git checkout v0.12.0
|
||||
git checkout v0.14.0
|
||||
```
|
||||
Set up environment variables to get pre-built wheels. If there are internet problems, just download the whl file manually. And set `VLLM_PRECOMPILED_WHEEL_LOCATION` as your local absolute path of whl file.
|
||||
```bash
|
||||
export VLLM_PRECOMPILED_WHEEL_LOCATION=https://github.com/vllm-project/vllm/releases/download/v0.12.0/vllm-0.12.0-cp38-abi3-manylinux_2_31_x86_64.whl
|
||||
export VLLM_PRECOMPILED_WHEEL_LOCATION=https://github.com/vllm-project/vllm/releases/download/v0.14.0/vllm-0.14.0-cp38-abi3-manylinux_2_31_x86_64.whl
|
||||
```
|
||||
Install vllm with command below (If you have no existing PyTorch).
|
||||
```bash
|
||||
@@ -93,7 +93,7 @@ docker run --runtime nvidia --gpus 2 \
|
||||
--env "HF_TOKEN=$HF_TOKEN" \
|
||||
-p 8091:8091 \
|
||||
--ipc=host \
|
||||
vllm/vllm-omni:v0.11.0rc1 \
|
||||
vllm/vllm-omni:v0.14.0 \
|
||||
--model Qwen/Qwen3-Omni-30B-A3B-Instruct --port 8091
|
||||
```
|
||||
|
||||
|
||||
@@ -34,8 +34,28 @@ DOCKER_BUILDKIT=1 docker build \
|
||||
|
||||
#### Launch the docker image
|
||||
|
||||
##### Launch with OpenAI API Server
|
||||
|
||||
```
|
||||
docker run -it \
|
||||
docker run --rm \
|
||||
--group-add=video \
|
||||
--ipc=host \
|
||||
--cap-add=SYS_PTRACE \
|
||||
--security-opt seccomp=unconfined \
|
||||
--device /dev/kfd \
|
||||
--device /dev/dri \
|
||||
-v ~/.cache/huggingface:/root/.cache/huggingface \
|
||||
--env "HF_TOKEN=$HF_TOKEN" \
|
||||
-p 8091:8091 \
|
||||
--ipc=host \
|
||||
vllm-omni-rocm \
|
||||
--model Qwen/Qwen3-Omni-30B-A3B-Instruct --port 8091
|
||||
```
|
||||
|
||||
##### Launch with interactive session for development
|
||||
|
||||
```
|
||||
docker run --rm -it \
|
||||
--network=host \
|
||||
--group-add=video \
|
||||
--ipc=host \
|
||||
@@ -44,12 +64,51 @@ docker run -it \
|
||||
--device /dev/kfd \
|
||||
--device /dev/dri \
|
||||
-v <path/to/model>:/app/model \
|
||||
vllm-omni-rocm \
|
||||
bash
|
||||
-v ~/.cache/huggingface:/root/.cache/huggingface \
|
||||
--entrypoint bash \
|
||||
vllm-omni-rocm
|
||||
```
|
||||
|
||||
# --8<-- [end:build-docker]
|
||||
|
||||
# --8<-- [start:pre-built-images]
|
||||
|
||||
vLLM-Omni offers an official docker image for deployment. These images are built on top of vLLM docker images and available on Docker Hub as [vllm/vllm-omni-rocm](https://hub.docker.com/r/vllm/vllm-omni-rocm/tags). The version of vLLM-Omni indicates which release of vLLM it is based on.
|
||||
|
||||
#### Launch vLLM-Omni Server
|
||||
Here's an example deployment command that has been verified on 2 x MI300's:
|
||||
```bash
|
||||
docker run --rm \
|
||||
--group-add=video \
|
||||
--ipc=host \
|
||||
--cap-add=SYS_PTRACE \
|
||||
--security-opt seccomp=unconfined \
|
||||
--device /dev/kfd \
|
||||
--device /dev/dri \
|
||||
-v <path/to/model>:/app/model \
|
||||
-v ~/.cache/huggingface:/root/.cache/huggingface \
|
||||
--env "HF_TOKEN=$HF_TOKEN" \
|
||||
-p 8091:8091 \
|
||||
vllm/vllm-omni-rocm:v0.14.0 \
|
||||
--model Qwen/Qwen3-Omni-30B-A3B-Instruct --omni --port 8091
|
||||
```
|
||||
|
||||
#### Launch an interactive terminal with prebuilt docker image.
|
||||
If you want to run in dev environment you can launch the docker image as follows:
|
||||
```bash
|
||||
docker run --rm -it \
|
||||
--network=host \
|
||||
--group-add=video \
|
||||
--ipc=host \
|
||||
--cap-add=SYS_PTRACE \
|
||||
--security-opt seccomp=unconfined \
|
||||
--device /dev/kfd \
|
||||
--device /dev/dri \
|
||||
-v <path/to/model>:/app/model \
|
||||
-v ~/.cache/huggingface:/root/.cache/huggingface \
|
||||
--env "HF_TOKEN=$HF_TOKEN" \
|
||||
--entrypoint bash \
|
||||
vllm/vllm-omni-rocm:v0.14.0
|
||||
```
|
||||
|
||||
# --8<-- [end:pre-built-images]
|
||||
@@ -13,10 +13,10 @@ export DEVICE0=/dev/davinci0
|
||||
export DEVICE1=/dev/davinci1
|
||||
# Update the vllm-ascend image
|
||||
# Atlas A2:
|
||||
# export IMAGE=quay.io/ascend/vllm-ascend:v0.11.0rc2
|
||||
# export IMAGE=quay.io/ascend/vllm-ascend:v0.14.0
|
||||
# Atlas A3:
|
||||
# export IMAGE=quay.io/ascend/vllm-ascend:v0.11.0rc2-a3
|
||||
export IMAGE=quay.io/ascend/vllm-ascend:v0.11.0rc2
|
||||
# export IMAGE=quay.io/ascend/vllm-ascend:v0.14.0-a3
|
||||
export IMAGE=quay.io/ascend/vllm-ascend:v0.14.0
|
||||
docker run --rm \
|
||||
--name vllm-omni-npu \
|
||||
--shm-size=1g \
|
||||
@@ -34,12 +34,22 @@ docker run --rm \
|
||||
-p 8000:8000 \
|
||||
-it $IMAGE bash
|
||||
|
||||
# Install the missing dependency of mooncake in the origin image.
|
||||
apt update
|
||||
apt install libjemalloc2
|
||||
echo "export LD_PRELOAD=/usr/lib/$(uname -m)-linux-gnu/libjemalloc.so.2:$LD_PRELOAD" >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
|
||||
# Inside the container, install vLLM-Omni from source
|
||||
cd /vllm-workspace
|
||||
git clone -b v0.11.0rc1 https://github.com/vllm-project/vllm-omni.git
|
||||
git clone -b v0.14.0 https://github.com/vllm-project/vllm-omni.git
|
||||
cd vllm-omni
|
||||
pip install -v -e .
|
||||
export VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
|
||||
# (Optional) Disable mooncake for stable capability
|
||||
mv /usr/local/Ascend/ascend-toolkit/latest/python/site-packages/mooncake \
|
||||
/usr/local/Ascend/ascend-toolkit/latest/python/site-packages/mooncake.disabled
|
||||
```
|
||||
|
||||
The default workdir is `/workspace`, with vLLM, vLLM-Ascend and vLLM-Omni code placed in `/vllm-workspace` installed in development mode.
|
||||
|
||||
@@ -17,7 +17,13 @@ For installation on GPU from source:
|
||||
```bash
|
||||
uv venv --python 3.12 --seed
|
||||
source .venv/bin/activate
|
||||
uv pip install vllm==0.12.0 --torch-backend=auto
|
||||
|
||||
# On CUDA
|
||||
uv pip install vllm==0.14.0 --torch-backend=auto
|
||||
|
||||
# On ROCm
|
||||
uv pip install vllm==0.14.0 --extra-index-url https://wheels.vllm.ai/rocm/0.14.0/rocm700
|
||||
|
||||
git clone https://github.com/vllm-project/vllm-omni.git
|
||||
cd vllm-omni
|
||||
uv pip install -e .
|
||||
|
||||
@@ -34,10 +34,10 @@ CATEGORIES = {
|
||||
"name": "Core",
|
||||
"description": "Core scheduling and caching components.",
|
||||
},
|
||||
"model_executor": {
|
||||
"name": "Model Executor",
|
||||
"description": "Model execution components.",
|
||||
},
|
||||
# "model_executor": {
|
||||
# "name": "Model Executor",
|
||||
# "description": "Model execution components.",
|
||||
# },
|
||||
"config": {
|
||||
"name": "Configuration",
|
||||
"description": "Configuration classes.",
|
||||
|
||||
@@ -21,6 +21,7 @@ th {
|
||||
| `Qwen2_5OmniForConditionalGeneration` | Qwen2.5-Omni | `Qwen/Qwen2.5-Omni-7B`, `Qwen/Qwen2.5-Omni-3B` |
|
||||
| `BagelForConditionalGeneration` | BAGEL (DiT-only) | `ByteDance-Seed/BAGEL-7B-MoT` |
|
||||
| `QwenImagePipeline` | Qwen-Image | `Qwen/Qwen-Image` |
|
||||
| `QwenImagePipeline` | Qwen-Image-2512 | `Qwen/Qwen-Image-2512` |
|
||||
| `QwenImageEditPipeline` | Qwen-Image-Edit | `Qwen/Qwen-Image-Edit` |
|
||||
| `QwenImageEditPlusPipeline` | Qwen-Image-Edit-2509 | `Qwen/Qwen-Image-Edit-2509` |
|
||||
| `QwenImageLayeredPipeline` | Qwen-Image-Layered | `Qwen/Qwen-Image-Layered` |
|
||||
@@ -31,6 +32,11 @@ th {
|
||||
|`LongcatImagePipeline` | LongCat-Image | `meituan-longcat/LongCat-Image` |
|
||||
|`LongCatImageEditPipeline` | LongCat-Image-Edit | `meituan-longcat/LongCat-Image-Edit` |
|
||||
|`StableDiffusion3Pipeline` | Stable-Diffusion-3 | `stabilityai/stable-diffusion-3.5-medium` |
|
||||
|`Flux2KleinPipeline` | FLUX.2-klein | `black-forest-labs/FLUX.2-klein-4B`, `black-forest-labs/FLUX.2-klein-9B` |
|
||||
|`StableAudioPipeline` | Stable-Audio-Open | `stabilityai/stable-audio-open-1.0` |
|
||||
|`Qwen3TTSForConditionalGeneration` | Qwen3-TTS-12Hz-1.7B-CustomVoice | `Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice` |
|
||||
|`Qwen3TTSForConditionalGeneration` | Qwen3-TTS-12Hz-1.7B-VoiceDesign | `Qwen/Qwen3-TTS-12Hz-1.7B-VoiceDesign` |
|
||||
|`Qwen3TTSForConditionalGeneration` | Qwen3-TTS-12Hz-1.7B-Base | `Qwen/Qwen3-TTS-12Hz-0.6B-Base` |
|
||||
|
||||
|
||||
## List of Supported Models for NPU
|
||||
|
||||
@@ -1,274 +0,0 @@
|
||||
# Parallelism Acceleration Guide
|
||||
|
||||
This guide includes how to use parallelism methods in vLLM-Omni to speed up diffusion model inference as well as reduce the memory requirement on each device.
|
||||
|
||||
## Overview
|
||||
|
||||
The following parallelism methods are currently supported in vLLM-Omni:
|
||||
|
||||
1. DeepSpeed Ulysses Sequence Parallel (DeepSpeed Ulysses-SP) ([arxiv paper](https://arxiv.org/pdf/2309.14509)): Ulysses-SP splits the input along the sequence dimension and uses all-to-all communication to allow each device to compute only a subset of attention heads.
|
||||
|
||||
2. [Ring-Attention](#ring-attention) - splits the input along the sequence dimension and uses ring-based P2P communication to accumulate attention results, keeping the sequence dimension sharded
|
||||
|
||||
|
||||
The following table shows which models are currently supported by parallelism method:
|
||||
|
||||
### ImageGen
|
||||
|
||||
| Model | Model Identifier | Ulysses-SP | Ring-SP |
|
||||
|-------|------------------|-----------|---------|
|
||||
| **LongCat-Image** | `meituan-longcat/LongCat-Image` | ❌ | ❌ |
|
||||
| **LongCat-Image-Edit** | `meituan-longcat/LongCat-Image-Edit` | ❌ | ❌ |
|
||||
| **Ovis-Image** | `OvisAI/Ovis-Image` | ❌ | ❌ |
|
||||
| **Qwen-Image** | `Qwen/Qwen-Image` | ✅ | ✅ |
|
||||
| **Qwen-Image-Edit** | `Qwen/Qwen-Image-Edit` | ✅ | ✅ |
|
||||
| **Qwen-Image-Edit-2509** | `Qwen/Qwen-Image-Edit-2509` | ✅ | ✅ |
|
||||
| **Qwen-Image-Layered** | `Qwen/Qwen-Image-Layered` | ✅ | ✅ |
|
||||
| **Z-Image** | `Tongyi-MAI/Z-Image-Turbo` | ❌ | ❌ |
|
||||
|
||||
### VideoGen
|
||||
|
||||
| Model | Model Identifier | Ulysses-SP | Ring-SP |
|
||||
|-------|------------------|-----------|---------|
|
||||
| **Wan2.2** | `Wan-AI/Wan2.2-T2V-A14B-Diffusers` | ❌ | ❌ |
|
||||
|
||||
### Sequence Parallelism
|
||||
|
||||
#### Ulysses-SP
|
||||
|
||||
##### Offline Inference
|
||||
|
||||
An example of offline inference script using [Ulysses-SP](https://arxiv.org/pdf/2309.14509) is shown below:
|
||||
```python
|
||||
from vllm_omni import Omni
|
||||
from vllm_omni.diffusion.data import DiffusionParallelConfig
|
||||
ulysses_degree = 2
|
||||
|
||||
omni = Omni(
|
||||
model="Qwen/Qwen-Image",
|
||||
parallel_config=DiffusionParallelConfig(ulysses_degree=2)
|
||||
)
|
||||
|
||||
outputs = omni.generate(prompt="A cat sitting on a windowsill", num_inference_steps=50, width=2048, height=2048)
|
||||
```
|
||||
|
||||
See `examples/offline_inference/text_to_image/text_to_image.py` for a complete working example.
|
||||
|
||||
##### Online Serving
|
||||
|
||||
You can enable Ulysses-SP in online serving for diffusion models via `--usp`:
|
||||
|
||||
```bash
|
||||
# Text-to-image (requires >= 2 GPUs)
|
||||
vllm serve Qwen/Qwen-Image --omni --port 8091 --usp 2
|
||||
```
|
||||
|
||||
##### Benchmarks
|
||||
!!! note "Benchmark Disclaimer"
|
||||
These benchmarks are provided for **general reference only**. The configurations shown use default or common parameter settings and have not been exhaustively optimized for maximum performance. Actual performance may vary based on:
|
||||
|
||||
- Specific model and use case
|
||||
- Hardware configuration
|
||||
- Careful parameter tuning
|
||||
- Different inference settings (e.g., number of steps, image resolution)
|
||||
|
||||
|
||||
To measure the parallelism methods, we run benchmarks with **Qwen/Qwen-Image** model generating images (**2048x2048** as long sequence input) with 50 inference steps. The hardware devices are NVIDIA H800 GPUs. `sdpa` is the attention backends.
|
||||
|
||||
| Configuration | Ulysses degree |Generation Time | Speedup |
|
||||
|---------------|----------------|---------|---------|
|
||||
| **Baseline (diffusers)** | - | 112.5s | 1.0x |
|
||||
| Ulysses-SP | 2 | 65.2s | 1.73x |
|
||||
| Ulysses-SP | 4 | 39.6s | 2.84x |
|
||||
| Ulysses-SP | 8 | 30.8s | 3.65x |
|
||||
|
||||
#### Ring-Attention
|
||||
|
||||
Ring-Attention ([arxiv paper](https://arxiv.org/abs/2310.01889)) splits the input along the sequence dimension and uses ring-based P2P communication to accumulate attention results. Unlike Ulysses-SP which uses all-to-all communication, Ring-Attention keeps the sequence dimension sharded throughout the computation and circulates Key/Value blocks through a ring topology.
|
||||
|
||||
##### Offline Inference
|
||||
|
||||
An example of offline inference script using Ring-Attention is shown below:
|
||||
```python
|
||||
from vllm_omni import Omni
|
||||
from vllm_omni.diffusion.data import DiffusionParallelConfig
|
||||
ring_degree = 2
|
||||
|
||||
omni = Omni(
|
||||
model="Qwen/Qwen-Image",
|
||||
parallel_config=DiffusionParallelConfig(ring_degree=2)
|
||||
)
|
||||
|
||||
outputs = omni.generate(prompt="A cat sitting on a windowsill", num_inference_steps=50, width=2048, height=2048)
|
||||
```
|
||||
|
||||
See `examples/offline_inference/text_to_image/text_to_image.py` for a complete working example.
|
||||
|
||||
|
||||
##### Online Serving
|
||||
|
||||
You can enable Ring-Attention in online serving for diffusion models via `--ring`:
|
||||
|
||||
```bash
|
||||
# Text-to-image (requires >= 2 GPUs)
|
||||
vllm serve Qwen/Qwen-Image --omni --port 8091 --ring 2
|
||||
```
|
||||
|
||||
##### Benchmarks
|
||||
!!! note "Benchmark Disclaimer"
|
||||
These benchmarks are provided for **general reference only**. The configurations shown use default or common parameter settings and have not been exhaustively optimized for maximum performance. Actual performance may vary based on:
|
||||
|
||||
- Specific model and use case
|
||||
- Hardware configuration
|
||||
- Careful parameter tuning
|
||||
- Different inference settings (e.g., number of steps, image resolution)
|
||||
|
||||
|
||||
To measure the parallelism methods, we run benchmarks with **Qwen/Qwen-Image** model generating images (**1024x1024** as long sequence input) with 50 inference steps. The hardware devices are NVIDIA A100 GPUs. `flash_attn` is the attention backends.
|
||||
|
||||
| Configuration | Ring degree |Generation Time | Speedup |
|
||||
|---------------|----------------|---------|---------|
|
||||
| **Baseline (diffusers)** | - | 45.2s | 1.0x |
|
||||
| Ring-Attention | 2 | 29.9s | 1.51x |
|
||||
| Ring-Attention | 4 | 23.3s | 1.94x |
|
||||
|
||||
|
||||
#### Hybrid Ulysses + Ring
|
||||
|
||||
You can combine both Ulysses-SP and Ring-Attention for larger scale parallelism. The total sequence parallel size equals `ulysses_degree × ring_degree`.
|
||||
|
||||
##### Offline Inference
|
||||
|
||||
```python
|
||||
from vllm_omni import Omni
|
||||
from vllm_omni.diffusion.data import DiffusionParallelConfig
|
||||
|
||||
# Hybrid: 2 Ulysses × 2 Ring = 4 GPUs total
|
||||
omni = Omni(
|
||||
model="Qwen/Qwen-Image",
|
||||
parallel_config=DiffusionParallelConfig(ulysses_degree=2, ring_degree=2)
|
||||
)
|
||||
|
||||
outputs = omni.generate(prompt="A cat sitting on a windowsill", num_inference_steps=50, width=2048, height=2048)
|
||||
```
|
||||
|
||||
##### Online Serving
|
||||
|
||||
```bash
|
||||
# Text-to-image (requires >= 4 GPUs)
|
||||
vllm serve Qwen/Qwen-Image --omni --port 8091 --usp 2 --ring 2
|
||||
```
|
||||
|
||||
##### Benchmarks
|
||||
!!! note "Benchmark Disclaimer"
|
||||
These benchmarks are provided for **general reference only**. The configurations shown use default or common parameter settings and have not been exhaustively optimized for maximum performance. Actual performance may vary based on:
|
||||
|
||||
- Specific model and use case
|
||||
- Hardware configuration
|
||||
- Careful parameter tuning
|
||||
- Different inference settings (e.g., number of steps, image resolution)
|
||||
|
||||
|
||||
To measure the parallelism methods, we run benchmarks with **Qwen/Qwen-Image** model generating images (**1024x1024** as long sequence input) with 50 inference steps. The hardware devices are NVIDIA A100 GPUs. `flash_attn` is the attention backends.
|
||||
|
||||
| Configuration | Ulysses degree | Ring degree | Generation Time | Speedup |
|
||||
|---------------|----------------|-------------|-----------------|---------|
|
||||
| **Baseline (diffusers)** | - | - | 45.2s | 1.0x |
|
||||
| Hybrid Ulysses + Ring | 2 | 2 | 24.3s | 1.87x |
|
||||
|
||||
|
||||
##### How to parallelize a new model
|
||||
|
||||
If a diffusion model has been deployed in vLLM-Omni and supports single-card inference, you can refer to the following instructions to parallelize it with [Ulysses-SP](https://arxiv.org/pdf/2309.14509).
|
||||
|
||||
This section uses **Qwen-Image** (`QwenImageTransformer2DModel`) as the reference implementation. Qwen-Image is a **dual-stream** transformer (text + image) that performs **joint attention** across the concatenated sequences. Because of that, when enabling sequence parallel you typically:
|
||||
|
||||
- Chunk **image tokens** (`hidden_states`) across SP ranks along the **sequence dimension**.
|
||||
- Keep **text embeddings** (`encoder_hidden_states`) **replicated** on all SP ranks for correctness (unless you implement full joint-SP semantics and explicitly split text too).
|
||||
- Chunk **image RoPE freqs** to match the chunked image tokens.
|
||||
- `all_gather` the final output back along the sequence dimension.
|
||||
|
||||
First, add the sequence-parallel helpers and (for Qwen-Image) the forward-context flag. Then, in the transformer's `forward()`:
|
||||
|
||||
- Chunk `hidden_states` by SP world size.
|
||||
- Set `get_forward_context().split_text_embed_in_sp = False` because Qwen-Image uses joint attention and we would use a full text embedding in each rank.
|
||||
- After `pos_embed`, chunk `img_freqs` on `dim=0` (token axis) to match the chunked image tokens; only chunk `txt_freqs` if you explicitly split text embeddings in SP.
|
||||
|
||||
Taking `vllm_omni/diffusion/models/qwen_image/qwen_image_transformer.py` as an example:
|
||||
|
||||
```diff
|
||||
from vllm_omni.diffusion.distributed.parallel_state import (
|
||||
get_sequence_parallel_rank,
|
||||
get_sequence_parallel_world_size,
|
||||
get_sp_group,
|
||||
)
|
||||
from vllm_omni.diffusion.forward_context import get_forward_context
|
||||
|
||||
class QwenImageTransformer2DModel(...):
|
||||
def forward(
|
||||
self,
|
||||
hidden_states: torch.Tensor,
|
||||
encoder_hidden_states: torch.Tensor = None,
|
||||
encoder_hidden_states_mask: torch.Tensor = None,
|
||||
timestep: torch.LongTensor = None,
|
||||
img_shapes: list[tuple[int, int, int]] | None = None,
|
||||
txt_seq_lens: list[int] | None = None,
|
||||
...
|
||||
):
|
||||
+ if self.parallel_config.sequence_parallel_size > 1:
|
||||
+ # Chunk image tokens along sequence dimension.
|
||||
+ hidden_states = torch.chunk(
|
||||
+ hidden_states,
|
||||
+ get_sequence_parallel_world_size(),
|
||||
+ dim=-2,
|
||||
+ )[get_sequence_parallel_rank()]
|
||||
+
|
||||
+ # Qwen-Image uses *dual-stream* (text + image) and runs *joint attention*.
|
||||
+ # Text embeddings should be replicated across SP ranks for correctness.
|
||||
+ get_forward_context().split_text_embed_in_sp = False
|
||||
|
||||
hidden_states = self.img_in(hidden_states)
|
||||
|
||||
...
|
||||
image_rotary_emb = self.pos_embed(img_shapes, txt_seq_lens, device=hidden_states.device)
|
||||
|
||||
+ def get_rotary_emb_chunk(freqs):
|
||||
+ return torch.chunk(freqs, get_sequence_parallel_world_size(), dim=0)[
|
||||
+ get_sequence_parallel_rank()
|
||||
+ ]
|
||||
+
|
||||
+ if self.parallel_config.sequence_parallel_size > 1:
|
||||
+ img_freqs, txt_freqs = image_rotary_emb
|
||||
+ img_freqs = get_rotary_emb_chunk(img_freqs)
|
||||
+ if get_forward_context().split_text_embed_in_sp:
|
||||
+ txt_freqs = get_rotary_emb_chunk(txt_freqs)
|
||||
+ image_rotary_emb = (img_freqs, txt_freqs)
|
||||
```
|
||||
|
||||
Next, at the end of the `forward()` function, call `get_sp_group().all_gather` to gather the chunked outputs across devices and concatenate them along the sequence dimension (matching the earlier `dim=-2` chunking):
|
||||
|
||||
```diff
|
||||
class QwenImageTransformer2DModel(...):
|
||||
def forward(...):
|
||||
...
|
||||
hidden_states = self.norm_out(hidden_states, temb)
|
||||
output = self.proj_out(hidden_states)
|
||||
|
||||
+ if self.parallel_config.sequence_parallel_size > 1:
|
||||
+ output = get_sp_group().all_gather(output, dim=-2)
|
||||
return Transformer2DModelOutput(sample=output)
|
||||
```
|
||||
|
||||
Finally, you can set the parallel configuration and pass it to `Omni` and start parallel inference with:
|
||||
```diff
|
||||
from vllm_omni import Omni
|
||||
+from vllm_omni.diffusion.data import DiffusionParallelConfig
|
||||
ulysses_degree = 2
|
||||
|
||||
omni = Omni(
|
||||
model="Qwen/Qwen-Image",
|
||||
+ parallel_config=DiffusionParallelConfig(ulysses_degree=2)
|
||||
)
|
||||
|
||||
outputs = omni.generate(prompt="A cat sitting on a windowsill", num_inference_steps=50)
|
||||
```
|
||||
+4
-4
@@ -69,7 +69,7 @@ vllm serve Qwen/Qwen-Image --omni --port 8091 \
|
||||
|
||||
## Acceleration Methods
|
||||
|
||||
For comprehensive illustration, please view cache-dit [User_Guide](https://github.com/vipshop/cache-dit/blob/main/docs/User_Guide.md)
|
||||
For comprehensive illustration, please view cache-dit [User_Guide](https://cache-dit.readthedocs.io/en/latest/user_guide/OVERVIEWS/)
|
||||
|
||||
### 1. DBCache (Dual Block Cache)
|
||||
|
||||
@@ -222,6 +222,6 @@ To test another model, you can modify `--model` with the target model identifier
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Cache-DiT User Guide](https://github.com/vipshop/cache-dit/blob/main/docs/User_Guide.md)
|
||||
- [Cache-DiT Benchmark](https://github.com/vipshop/cache-dit/tree/main/bench)
|
||||
- [DBCache Technical Details](https://github.com/vipshop/cache-dit/blob/main/docs/DBCache.md)
|
||||
- [Cache-DiT User Guide](https://cache-dit.readthedocs.io/en/latest/user_guide/OVERVIEWS/)
|
||||
- [Cache-DiT Benchmark](https://cache-dit.readthedocs.io/en/latest/benchmark/HYBRID_CACHE/)
|
||||
- [DBCache Technical Details](https://cache-dit.readthedocs.io/en/latest/user_guide/CACHE_API/)
|
||||
@@ -0,0 +1,29 @@
|
||||
# CPU Offloading for Diffusion Model
|
||||
|
||||
## Overview
|
||||
CPU offload lets the diffusion worker move large model components between GPU and CPU memory on demand. It keeps the DiT transformer resident on GPU only while it is actively running, and swaps it out when encoders modules need the device. This reduces peak VRAM usage so bigger checkpoints run on smaller GPUs, or multiple requests can share the same GPU.
|
||||
|
||||
## Execution Model
|
||||
1. Text encoders run on GPU while the DiT transformer is offloaded to CPU.
|
||||
2. Before denoising, weights are prefetched back to GPU, honoring pinned-memory copies for speed.
|
||||
3. After the diffusion step, the transformer returns to CPU and the process repeats as needed.
|
||||
|
||||
Transfers use pinned host buffers, and the worker coordinates swaps via mutex-style hooks so components never compete for memory.
|
||||
|
||||
## Configuration
|
||||
You can enable CPU offload in two ways:
|
||||
|
||||
- **Python API**: set `enable_cpu_offload=True`.
|
||||
|
||||
```python
|
||||
from vllm_omni import Omni
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
m = Omni(model="Qwen/Qwen-Image",enable_cpu_offload=True)
|
||||
```
|
||||
|
||||
- **CLI**: pass `--enable-cpu-offload` to the diffusion service entrypoint.
|
||||
|
||||
## Known Limitations
|
||||
- Cold start latency increases for over one minute for some models(e.g., Qwen-Image)
|
||||
@@ -0,0 +1,453 @@
|
||||
# Parallelism Acceleration Guide
|
||||
|
||||
This guide includes how to use parallelism methods in vLLM-Omni to speed up diffusion model inference as well as reduce the memory requirement on each device.
|
||||
|
||||
## Overview
|
||||
|
||||
The following parallelism methods are currently supported in vLLM-Omni:
|
||||
|
||||
1. DeepSpeed Ulysses Sequence Parallel (DeepSpeed Ulysses-SP) ([arxiv paper](https://arxiv.org/pdf/2309.14509)): Ulysses-SP splits the input along the sequence dimension and uses all-to-all communication to allow each device to compute only a subset of attention heads.
|
||||
|
||||
2. [Ring-Attention](#ring-attention) - splits the input along the sequence dimension and uses ring-based P2P communication to accumulate attention results, keeping the sequence dimension sharded
|
||||
|
||||
3. Classifier-Free-Guidance Parallel (CFG-Parallel): CFG-Parallel runs the positive/negative prompts of classifier-free guidance (CFG) on different devices, then merges on a single device to perform the scheduler step.
|
||||
|
||||
4. [Tensor Parallelism](#tensor-parallelism): Tensor parallelism shards model weights across devices. This can reduce per-GPU memory usage. Note that for diffusion models we currently shard the majority of layers within the DiT.
|
||||
|
||||
The following table shows which models are currently supported by parallelism method:
|
||||
|
||||
### ImageGen
|
||||
|
||||
| Model | Model Identifier | Ulysses-SP | Ring-SP | CFG-Parallel | Tensor-Parallel |
|
||||
|-------|------------------|------------|---------|--------------|--------------------------|
|
||||
| **LongCat-Image** | `meituan-longcat/LongCat-Image` | ✅ | ✅ | ❌ | ❌ |
|
||||
| **LongCat-Image-Edit** | `meituan-longcat/LongCat-Image-Edit` | ✅ | ✅ | ❌ | ❌ |
|
||||
| **Ovis-Image** | `OvisAI/Ovis-Image` | ❌ | ❌ | ❌ | ❌ |
|
||||
| **Qwen-Image** | `Qwen/Qwen-Image` | ✅ | ✅ | ✅ | ✅ |
|
||||
| **Qwen-Image-Edit** | `Qwen/Qwen-Image-Edit` | ✅ | ✅ | ✅ | ❌ |
|
||||
| **Qwen-Image-Edit-2509** | `Qwen/Qwen-Image-Edit-2509` | ✅ | ✅ | ✅ | ❌ |
|
||||
| **Qwen-Image-Layered** | `Qwen/Qwen-Image-Layered` | ✅ | ✅ | ✅ | ❌ |
|
||||
| **Z-Image** | `Tongyi-MAI/Z-Image-Turbo` | ✅ | ✅ | ❌ | ✅ (TP=2 only) |
|
||||
| **Stable-Diffusion3.5** | `stabilityai/stable-diffusion-3.5` | ❌ | ❌ | ❌ | ❌ |
|
||||
|
||||
|
||||
!!! note "TP Limitations for Diffusion Models"
|
||||
We currently implement Tensor Parallelism (TP) only for the DiT (Diffusion Transformer) blocks. This is because the `text_encoder` component in vLLM-Omni uses the original Transformers implementation, which does not yet support TP.
|
||||
|
||||
- Good news: The text_encoder typically has minimal impact on overall inference performance.
|
||||
- Bad news: When TP is enabled, every TP process retains a full copy of the text_encoder weights, leading to significant GPU memory waste.
|
||||
|
||||
We are actively refactoring this design to address this. For details and progress, please refer to [Issue #771](https://github.com/vllm-project/vllm-omni/issues/771).
|
||||
|
||||
|
||||
!!! note "Why Z-Image is TP=2 only"
|
||||
Z-Image Turbo is currently limited to `tensor_parallel_size` of **1 or 2** due to model shape divisibility constraints.
|
||||
For example, the model has `n_heads=30` and a final projection out dimension of `64`, so valid TP sizes must divide both 30 and 64; the only common divisors are **1 and 2**.
|
||||
|
||||
### VideoGen
|
||||
|
||||
| Model | Model Identifier | Ulysses-SP | Ring-SP | Tensor-Parallel |
|
||||
|-------|------------------|------------|---------|--------------------------|
|
||||
| **Wan2.2** | `Wan-AI/Wan2.2-T2V-A14B-Diffusers` | ❌ | ❌ | ❌ |
|
||||
|
||||
### Tensor Parallelism
|
||||
|
||||
Tensor parallelism splits model parameters across GPUs. In vLLM-Omni, tensor parallelism is configured via `DiffusionParallelConfig.tensor_parallel_size`.
|
||||
|
||||
#### Offline Inference
|
||||
|
||||
```python
|
||||
from vllm_omni import Omni
|
||||
from vllm_omni.diffusion.data import DiffusionParallelConfig
|
||||
|
||||
omni = Omni(
|
||||
model="Tongyi-MAI/Z-Image-Turbo",
|
||||
parallel_config=DiffusionParallelConfig(tensor_parallel_size=2),
|
||||
)
|
||||
|
||||
outputs = omni.generate(
|
||||
prompt="a cat reading a book",
|
||||
num_inference_steps=9,
|
||||
width=512,
|
||||
height=512,
|
||||
)
|
||||
```
|
||||
|
||||
### Sequence Parallelism
|
||||
|
||||
#### Ulysses-SP
|
||||
|
||||
##### Offline Inference
|
||||
|
||||
An example of offline inference script using [Ulysses-SP](https://arxiv.org/pdf/2309.14509) is shown below:
|
||||
```python
|
||||
from vllm_omni import Omni
|
||||
from vllm_omni.diffusion.data import DiffusionParallelConfig
|
||||
ulysses_degree = 2
|
||||
|
||||
omni = Omni(
|
||||
model="Qwen/Qwen-Image",
|
||||
parallel_config=DiffusionParallelConfig(ulysses_degree=2)
|
||||
)
|
||||
|
||||
outputs = omni.generate(prompt="A cat sitting on a windowsill", num_inference_steps=50, width=2048, height=2048)
|
||||
```
|
||||
|
||||
See `examples/offline_inference/text_to_image/text_to_image.py` for a complete working example.
|
||||
|
||||
##### Online Serving
|
||||
|
||||
You can enable Ulysses-SP in online serving for diffusion models via `--usp`:
|
||||
|
||||
```bash
|
||||
# Text-to-image (requires >= 2 GPUs)
|
||||
vllm serve Qwen/Qwen-Image --omni --port 8091 --usp 2
|
||||
```
|
||||
|
||||
##### Benchmarks
|
||||
!!! note "Benchmark Disclaimer"
|
||||
These benchmarks are provided for **general reference only**. The configurations shown use default or common parameter settings and have not been exhaustively optimized for maximum performance. Actual performance may vary based on:
|
||||
|
||||
- Specific model and use case
|
||||
- Hardware configuration
|
||||
- Careful parameter tuning
|
||||
- Different inference settings (e.g., number of steps, image resolution)
|
||||
|
||||
|
||||
To measure the parallelism methods, we run benchmarks with **Qwen/Qwen-Image** model generating images (**2048x2048** as long sequence input) with 50 inference steps. The hardware devices are NVIDIA H800 GPUs. `sdpa` is the attention backends.
|
||||
|
||||
| Configuration | Ulysses degree |Generation Time | Speedup |
|
||||
|---------------|----------------|---------|---------|
|
||||
| **Baseline (diffusers)** | - | 112.5s | 1.0x |
|
||||
| Ulysses-SP | 2 | 65.2s | 1.73x |
|
||||
| Ulysses-SP | 4 | 39.6s | 2.84x |
|
||||
| Ulysses-SP | 8 | 30.8s | 3.65x |
|
||||
|
||||
#### Ring-Attention
|
||||
|
||||
Ring-Attention ([arxiv paper](https://arxiv.org/abs/2310.01889)) splits the input along the sequence dimension and uses ring-based P2P communication to accumulate attention results. Unlike Ulysses-SP which uses all-to-all communication, Ring-Attention keeps the sequence dimension sharded throughout the computation and circulates Key/Value blocks through a ring topology.
|
||||
|
||||
##### Offline Inference
|
||||
|
||||
An example of offline inference script using Ring-Attention is shown below:
|
||||
```python
|
||||
from vllm_omni import Omni
|
||||
from vllm_omni.diffusion.data import DiffusionParallelConfig
|
||||
ring_degree = 2
|
||||
|
||||
omni = Omni(
|
||||
model="Qwen/Qwen-Image",
|
||||
parallel_config=DiffusionParallelConfig(ring_degree=2)
|
||||
)
|
||||
|
||||
outputs = omni.generate(prompt="A cat sitting on a windowsill", num_inference_steps=50, width=2048, height=2048)
|
||||
```
|
||||
|
||||
See `examples/offline_inference/text_to_image/text_to_image.py` for a complete working example.
|
||||
|
||||
|
||||
##### Online Serving
|
||||
|
||||
You can enable Ring-Attention in online serving for diffusion models via `--ring`:
|
||||
|
||||
```bash
|
||||
# Text-to-image (requires >= 2 GPUs)
|
||||
vllm serve Qwen/Qwen-Image --omni --port 8091 --ring 2
|
||||
```
|
||||
|
||||
##### Benchmarks
|
||||
!!! note "Benchmark Disclaimer"
|
||||
These benchmarks are provided for **general reference only**. The configurations shown use default or common parameter settings and have not been exhaustively optimized for maximum performance. Actual performance may vary based on:
|
||||
|
||||
- Specific model and use case
|
||||
- Hardware configuration
|
||||
- Careful parameter tuning
|
||||
- Different inference settings (e.g., number of steps, image resolution)
|
||||
|
||||
|
||||
To measure the parallelism methods, we run benchmarks with **Qwen/Qwen-Image** model generating images (**1024x1024** as long sequence input) with 50 inference steps. The hardware devices are NVIDIA A100 GPUs. `flash_attn` is the attention backends.
|
||||
|
||||
| Configuration | Ring degree |Generation Time | Speedup |
|
||||
|---------------|----------------|---------|---------|
|
||||
| **Baseline (diffusers)** | - | 45.2s | 1.0x |
|
||||
| Ring-Attention | 2 | 29.9s | 1.51x |
|
||||
| Ring-Attention | 4 | 23.3s | 1.94x |
|
||||
|
||||
|
||||
#### Hybrid Ulysses + Ring
|
||||
|
||||
You can combine both Ulysses-SP and Ring-Attention for larger scale parallelism. The total sequence parallel size equals `ulysses_degree × ring_degree`.
|
||||
|
||||
##### Offline Inference
|
||||
|
||||
```python
|
||||
from vllm_omni import Omni
|
||||
from vllm_omni.diffusion.data import DiffusionParallelConfig
|
||||
|
||||
# Hybrid: 2 Ulysses × 2 Ring = 4 GPUs total
|
||||
omni = Omni(
|
||||
model="Qwen/Qwen-Image",
|
||||
parallel_config=DiffusionParallelConfig(ulysses_degree=2, ring_degree=2)
|
||||
)
|
||||
|
||||
outputs = omni.generate(prompt="A cat sitting on a windowsill", num_inference_steps=50, width=2048, height=2048)
|
||||
```
|
||||
|
||||
##### Online Serving
|
||||
|
||||
```bash
|
||||
# Text-to-image (requires >= 4 GPUs)
|
||||
vllm serve Qwen/Qwen-Image --omni --port 8091 --usp 2 --ring 2
|
||||
```
|
||||
|
||||
##### Benchmarks
|
||||
!!! note "Benchmark Disclaimer"
|
||||
These benchmarks are provided for **general reference only**. The configurations shown use default or common parameter settings and have not been exhaustively optimized for maximum performance. Actual performance may vary based on:
|
||||
|
||||
- Specific model and use case
|
||||
- Hardware configuration
|
||||
- Careful parameter tuning
|
||||
- Different inference settings (e.g., number of steps, image resolution)
|
||||
|
||||
|
||||
To measure the parallelism methods, we run benchmarks with **Qwen/Qwen-Image** model generating images (**1024x1024** as long sequence input) with 50 inference steps. The hardware devices are NVIDIA A100 GPUs. `flash_attn` is the attention backends.
|
||||
|
||||
| Configuration | Ulysses degree | Ring degree | Generation Time | Speedup |
|
||||
|---------------|----------------|-------------|-----------------|---------|
|
||||
| **Baseline (diffusers)** | - | - | 45.2s | 1.0x |
|
||||
| Hybrid Ulysses + Ring | 2 | 2 | 24.3s | 1.87x |
|
||||
|
||||
|
||||
##### How to parallelize a new model
|
||||
|
||||
NOTE: "Terminology: SP vs CP"
|
||||
Our "Sequence Parallelism" (SP) corresponds to "Context Parallelism" (CP) in the [diffusers library](https://github.com/huggingface/diffusers/blob/main/src/diffusers/models/_modeling_parallel.py).
|
||||
We use "Sequence Parallelism" to align with vLLM-Omni's terminology.
|
||||
|
||||
---
|
||||
|
||||
###### Non-intrusive `_sp_plan` (Recommended)
|
||||
|
||||
The `_sp_plan` mechanism allows SP without modifying `forward()` logic. The framework automatically registers hooks to shard inputs and gather outputs at module boundaries.
|
||||
|
||||
**Requirements for `forward()` function:**
|
||||
|
||||
- Tensor operations that need sharding/gathering must happen at **`nn.Module` boundaries** (not inline Python operations)
|
||||
- If your `forward()` contains inline tensor operations (e.g., `torch.cat`, `pad_sequence`) that need sharding, **extract them into a submodule**
|
||||
|
||||
**When to create a submodule:**
|
||||
|
||||
```python
|
||||
# ❌ BAD: Inline operations - hooks cannot intercept
|
||||
def forward(self, x, cap_feats):
|
||||
unified = torch.cat([x, cap_feats], dim=1) # Cannot be sharded via _sp_plan
|
||||
...
|
||||
|
||||
# ✅ GOOD: Extract into a submodule
|
||||
class UnifiedPrepare(nn.Module):
|
||||
def forward(self, x, cap_feats):
|
||||
return torch.cat([x, cap_feats], dim=1) # Now can be sharded via _sp_plan
|
||||
|
||||
class MyModel(nn.Module):
|
||||
def __init__(self):
|
||||
self.unified_prepare = UnifiedPrepare() # Submodule
|
||||
|
||||
def forward(self, x, cap_feats):
|
||||
unified = self.unified_prepare(x, cap_feats) # Hook can intercept here
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
###### Defining `_sp_plan`
|
||||
|
||||
**Type definitions** (see [diffusers `_modeling_parallel.py`](https://github.com/huggingface/diffusers/blob/main/src/diffusers/models/_modeling_parallel.py) for reference):
|
||||
|
||||
```python
|
||||
from vllm_omni.diffusion.distributed.sp_plan import (
|
||||
SequenceParallelInput, # Corresponds to diffusers' ContextParallelInput
|
||||
SequenceParallelOutput, # Corresponds to diffusers' ContextParallelOutput
|
||||
)
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
|-----------|-------------|
|
||||
| `split_dim` | Dimension to split/gather (usually `1` for sequence) |
|
||||
| `expected_dims` | Expected tensor rank for validation (optional) |
|
||||
| `split_output` | `False`: shard **input** parameters; `True`: shard **output** tensors |
|
||||
| `auto_pad` | Auto-pad if sequence not divisible by world_size (Ulysses only) |
|
||||
|
||||
**Key naming convention:**
|
||||
|
||||
| Key | Meaning | Python equivalent |
|
||||
|-----|---------|-------------------|
|
||||
| `""` | Root model | `model` |
|
||||
| `"blocks.0"` | First element of ModuleList | `model.blocks[0]` |
|
||||
| `"blocks.*"` | All elements of ModuleList | `for b in model.blocks` |
|
||||
| `"outputs.main"` | ModuleDict entry | `model.outputs["main"]` |
|
||||
|
||||
**Dictionary key types:**
|
||||
|
||||
| Key type | `split_output` | Description |
|
||||
|----------|----------------|-------------|
|
||||
| `"param_name"` (str) | `False` | Shard **input parameter** by name |
|
||||
| `0`, `1` (int) | `True` | Shard **output tuple** by index |
|
||||
|
||||
**Example** (similar to [diffusers `transformer_wan.py`](https://github.com/huggingface/diffusers/blob/main/src/diffusers/models/transformers/transformer_wan.py)):
|
||||
|
||||
```python
|
||||
class MyTransformer(nn.Module):
|
||||
_sp_plan = {
|
||||
# Shard rope module OUTPUTS (returns tuple)
|
||||
"rope": {
|
||||
0: SequenceParallelInput(split_dim=1, expected_dims=4, split_output=True), # cos
|
||||
1: SequenceParallelInput(split_dim=1, expected_dims=4, split_output=True), # sin
|
||||
},
|
||||
# Shard transformer block INPUT parameter
|
||||
"blocks.0": {
|
||||
"hidden_states": SequenceParallelInput(split_dim=1, expected_dims=3),
|
||||
},
|
||||
# Gather at final projection
|
||||
"proj_out": SequenceParallelOutput(gather_dim=1, expected_dims=3),
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
###### Hook flow
|
||||
|
||||
```
|
||||
Input → [SequenceParallelSplitHook: pre_forward] → Module.forward() → [post_forward] → ...
|
||||
↓
|
||||
... → [SequenceParallelGatherHook: post_forward] → Output
|
||||
```
|
||||
|
||||
1. **SplitHook** shards tensors before/after the target module
|
||||
2. **Attention layers** handle Ulysses/Ring communication internally
|
||||
3. **GatherHook** collects sharded outputs
|
||||
|
||||
The framework automatically applies these hooks when `sequence_parallel_size > 1`.
|
||||
|
||||
---
|
||||
|
||||
###### Method 2: Intrusive modification (For complex cases)
|
||||
|
||||
For models with dynamic sharding logic that cannot be expressed via `_sp_plan`:
|
||||
|
||||
```python
|
||||
from vllm_omni.diffusion.distributed.sp_sharding import sp_shard, sp_gather
|
||||
|
||||
def forward(self, hidden_states, ...):
|
||||
if self.parallel_config.sequence_parallel_size > 1:
|
||||
hidden_states = sp_shard(hidden_states, dim=1)
|
||||
# ... computation ...
|
||||
output = sp_gather(output, dim=1)
|
||||
return output
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
###### Choosing the right approach
|
||||
|
||||
| Scenario | Approach |
|
||||
|----------|----------|
|
||||
| Standard transformer | `_sp_plan` |
|
||||
| Inline tensor ops need sharding | Extract to submodule + `_sp_plan` |
|
||||
| Dynamic/conditional sharding | Intrusive modification |
|
||||
|
||||
|
||||
### CFG-Parallel
|
||||
|
||||
##### Offline Inference
|
||||
|
||||
CFG-Parallel is enabled through `DiffusionParallelConfig(cfg_parallel_size=...)`. The recommended configuration is `cfg_parallel_size=2` (one rank for the positive branch and one rank for the negative branch).
|
||||
|
||||
An example of offline inference using CFG-Parallel (image-to-image) is shown below:
|
||||
|
||||
```python
|
||||
from vllm_omni import Omni
|
||||
from vllm_omni.diffusion.data import DiffusionParallelConfig
|
||||
|
||||
omni = Omni(
|
||||
model="Qwen/Qwen-Image-Edit",
|
||||
parallel_config=DiffusionParallelConfig(cfg_parallel_size=2),
|
||||
)
|
||||
|
||||
outputs = omni.generate(
|
||||
prompt="turn this cat to a dog",
|
||||
negative_prompt="low quality, blurry",
|
||||
true_cfg_scale=4.0,
|
||||
pil_image=input_image,
|
||||
num_inference_steps=50,
|
||||
)
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- CFG-Parallel is only effective when **true CFG** is enabled (i.e., `true_cfg_scale > 1` and a `negative_prompt` is provided).
|
||||
|
||||
#### How to parallelize a pipeline
|
||||
|
||||
This section describes how to add CFG-Parallel to a diffusion **pipeline**. We use the Qwen-Image pipeline (`vllm_omni/diffusion/models/qwen_image/pipeline_qwen_image.py`) as the reference implementation.
|
||||
|
||||
In `QwenImagePipeline`, each diffusion step runs two denoiser forward passes sequentially:
|
||||
|
||||
- positive (prompt-conditioned)
|
||||
- negative (negative-prompt-conditioned)
|
||||
|
||||
CFG-Parallel assigns these two branches to different ranks in the **CFG group** and synchronizes the results.
|
||||
|
||||
Below is an example of CFG-Parallel implementation:
|
||||
|
||||
```python
|
||||
def diffuse(
|
||||
self,
|
||||
...
|
||||
):
|
||||
# Enable CFG-parallel: rank0 computes positive, rank1 computes negative.
|
||||
cfg_parallel_ready = do_true_cfg and get_classifier_free_guidance_world_size() > 1
|
||||
|
||||
self.transformer.do_true_cfg = do_true_cfg
|
||||
|
||||
if cfg_parallel_ready:
|
||||
cfg_group = get_cfg_group()
|
||||
cfg_rank = get_classifier_free_guidance_rank()
|
||||
|
||||
if cfg_rank == 0:
|
||||
local_pred = self.transformer(
|
||||
hidden_states=latents,
|
||||
timestep=timestep / 1000,
|
||||
guidance=guidance,
|
||||
encoder_hidden_states_mask=prompt_embeds_mask,
|
||||
encoder_hidden_states=prompt_embeds,
|
||||
img_shapes=img_shapes,
|
||||
txt_seq_lens=txt_seq_lens,
|
||||
attention_kwargs=self.attention_kwargs,
|
||||
return_dict=False,
|
||||
)[0]
|
||||
else:
|
||||
local_pred = self.transformer(
|
||||
hidden_states=latents,
|
||||
timestep=timestep / 1000,
|
||||
guidance=guidance,
|
||||
encoder_hidden_states_mask=negative_prompt_embeds_mask,
|
||||
encoder_hidden_states=negative_prompt_embeds,
|
||||
img_shapes=img_shapes,
|
||||
txt_seq_lens=negative_txt_seq_lens,
|
||||
attention_kwargs=self.attention_kwargs,
|
||||
return_dict=False,
|
||||
)[0]
|
||||
|
||||
gathered = cfg_group.all_gather(local_pred, separate_tensors=True)
|
||||
if cfg_rank == 0:
|
||||
noise_pred = gathered[0]
|
||||
neg_noise_pred = gathered[1]
|
||||
comb_pred = neg_noise_pred + true_cfg_scale * (noise_pred - neg_noise_pred)
|
||||
cond_norm = torch.norm(noise_pred, dim=-1, keepdim=True)
|
||||
noise_norm = torch.norm(comb_pred, dim=-1, keepdim=True)
|
||||
noise_pred = comb_pred * (cond_norm / noise_norm)
|
||||
latents = self.scheduler.step(noise_pred, t, latents, return_dict=False)[0]
|
||||
cfg_group.broadcast(latents, src=0)
|
||||
else:
|
||||
# fallback: run positive then negative sequentially on one rank
|
||||
...
|
||||
```
|
||||
File renamed without changes.
@@ -6,18 +6,21 @@ vLLM-Omni supports various cache acceleration methods to speed up diffusion mode
|
||||
|
||||
vLLM-Omni currently supports two main cache acceleration backends:
|
||||
|
||||
1. **[TeaCache](acceleration/teacache.md)** - Hook-based adaptive caching that caches transformer computations when consecutive timesteps are similar
|
||||
2. **[Cache-DiT](acceleration/cache_dit_acceleration.md)** - Library-based acceleration using multiple techniques:
|
||||
1. **[TeaCache](diffusion/teacache.md)** - Hook-based adaptive caching that caches transformer computations when consecutive timesteps are similar
|
||||
2. **[Cache-DiT](diffusion/cache_dit_acceleration.md)** - Library-based acceleration using multiple techniques:
|
||||
- **DBCache** (Dual Block Cache): Caches intermediate transformer block outputs based on residual differences
|
||||
- **TaylorSeer**: Uses Taylor expansion-based forecasting for faster inference
|
||||
- **SCM** (Step Computation Masking): Selectively computes steps based on adaptive masking
|
||||
|
||||
Both methods can provide significant speedups (typically **1.5x-2.0x**) while maintaining high output quality.
|
||||
|
||||
vLLM-Omni also supports the sequence parallelism (SP) for the diffusion model, that includes:
|
||||
vLLM-Omni also supports parallelism methods for diffusion models, including:
|
||||
|
||||
1. [Ulysses-SP](acceleration/parallelism_acceleration.md#ulysses-sp) - splits the input along the sequence dimension and uses all-to-all communication to allow each device to compute only a subset of attention heads.
|
||||
2. [Ring-Attention](acceleration/parallelism_acceleration.md#ring-attention) - splits the input along the sequence dimension and uses ring-based P2P communication to accumulate attention results, keeping the sequence dimension sharded.
|
||||
1. [Ulysses-SP](diffusion/parallelism_acceleration.md#ulysses-sp) - splits the input along the sequence dimension and uses all-to-all communication to allow each device to compute only a subset of attention heads.
|
||||
|
||||
2. [Ring-Attention](diffusion/parallelism_acceleration.md#ring-attention) - splits the input along the sequence dimension and uses ring-based P2P communication to accumulate attention results, keeping the sequence dimension sharded.
|
||||
|
||||
3. [CFG-Parallel](diffusion/parallelism_acceleration.md#cfg-parallel) - runs the positive/negative prompts of classifier-free guidance (CFG) on different devices, then merges on a single device to perform the scheduler step.
|
||||
|
||||
## Quick Comparison
|
||||
|
||||
@@ -34,26 +37,25 @@ The following table shows which models are currently supported by each accelerat
|
||||
|
||||
### ImageGen
|
||||
|
||||
| Model | Model Identifier | TeaCache | Cache-DiT | Ulysses-SP | Ring-Attention |
|
||||
|-------|------------------|----------|-----------|-----------|----------------|
|
||||
| **LongCat-Image** | `meituan-longcat/LongCat-Image` | ❌ | ✅ | ❌ | ❌ |
|
||||
| **LongCat-Image-Edit** | `meituan-longcat/LongCat-Image-Edit` | ❌ | ✅ | ❌ | ❌ |
|
||||
| **Ovis-Image** | `OvisAI/Ovis-Image` | ❌ | ✅ | ❌ | ❌ |
|
||||
| **Qwen-Image** | `Qwen/Qwen-Image` | ✅ | ✅ | ✅ | ✅ |
|
||||
| **Qwen-Image-Edit** | `Qwen/Qwen-Image-Edit` | ✅ | ✅ | ✅ | ✅ |
|
||||
| **Qwen-Image-Edit-2509** | `Qwen/Qwen-Image-Edit-2509` | ❌ | ✅ | ✅ | ✅ |
|
||||
| **Qwen-Image-Layered** | `Qwen/Qwen-Image-Layered` | ❌ | ✅ | ✅ | ✅ |
|
||||
| **Z-Image** | `Tongyi-MAI/Z-Image-Turbo` | ❌ | ✅ |❌ | ❌ |
|
||||
| **LongCat-Image** | `meituan-longcat/LongCat-Image` | ❌ | ✅ | ❌ | ❌ |
|
||||
| **LongCat-Image-Edit** | `meituan-longcat/LongCat-Image-Edit` | ❌ | ✅ | ❌ | ❌ |
|
||||
| **Ovis-Image** | `OvisAI/Ovis-Image` | ❌ | ✅ | ❌ | ❌ |
|
||||
| **Stable-Diffusion3.5** | `stabilityai/stable-diffusion-3.5` | ❌ | ✅ | ❌ | ❌ |
|
||||
| Model | Model Identifier | TeaCache | Cache-DiT | Ulysses-SP | Ring-Attention | CFG-Parallel |
|
||||
|-------|------------------|:----------:|:-----------:|:-----------:|:----------------:|:----------------:|
|
||||
| **LongCat-Image** | `meituan-longcat/LongCat-Image` | ❌ | ✅ | ❌ | ❌ | ❌ |
|
||||
| **LongCat-Image-Edit** | `meituan-longcat/LongCat-Image-Edit` | ❌ | ✅ | ❌ | ❌ | ❌ |
|
||||
| **Ovis-Image** | `OvisAI/Ovis-Image` | ❌ | ✅ | ❌ | ❌ | ❌ |
|
||||
| **Qwen-Image** | `Qwen/Qwen-Image` | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| **Qwen-Image-2512** | `Qwen/Qwen-Image-2512` | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| **Qwen-Image-Edit** | `Qwen/Qwen-Image-Edit` | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| **Qwen-Image-Edit-2509** | `Qwen/Qwen-Image-Edit-2509` | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| **Qwen-Image-Layered** | `Qwen/Qwen-Image-Layered` | ❌ | ✅ | ✅ | ✅ | ✅ |
|
||||
| **Z-Image** | `Tongyi-MAI/Z-Image-Turbo` | ✅ | ✅ | ❌ | ❌ | ❌ |
|
||||
| **Stable-Diffusion3.5** | `stabilityai/stable-diffusion-3.5` | ❌ | ✅ | ❌ | ❌ | ❌ |
|
||||
| **Bagel** | `ByteDance-Seed/BAGEL-7B-MoT` | ❌ | ✅ | ❌ | ❌ | ❌ |
|
||||
|
||||
### VideoGen
|
||||
|
||||
| Model | Model Identifier | TeaCache | Cache-DiT | Ulysses-SP |
|
||||
|-------|------------------|----------|-----------|-----------|
|
||||
| **Wan2.2** | `Wan-AI/Wan2.2-T2V-A14B-Diffusers` | ❌ | ✅ | ❌ |
|
||||
| Model | Model Identifier | TeaCache | Cache-DiT | Ulysses-SP | Ring-Attention |
|
||||
|-------|------------------|:--------:|:---------:|:----------:|:--------------:|
|
||||
| **Wan2.2** | `Wan-AI/Wan2.2-T2V-A14B-Diffusers` | ❌ | ✅ | ❌ | ❌ |
|
||||
|
||||
|
||||
## Performance Benchmarks
|
||||
@@ -134,7 +136,7 @@ ulysses_degree = 2
|
||||
|
||||
omni = Omni(
|
||||
model="Qwen/Qwen-Image",
|
||||
parallel_config=DiffusionParallelConfig(ulysses_degree=2)
|
||||
parallel_config=DiffusionParallelConfig(ulysses_degree=ulysses_degree)
|
||||
)
|
||||
|
||||
outputs = omni.generate(prompt="A cat sitting on a windowsill", num_inference_steps=50, width=2048, height=2048)
|
||||
@@ -149,7 +151,7 @@ ulysses_degree = 2
|
||||
|
||||
omni = Omni(
|
||||
model="Qwen/Qwen-Image-Edit",
|
||||
parallel_config=DiffusionParallelConfig(ulysses_degree=2)
|
||||
parallel_config=DiffusionParallelConfig(ulysses_degree=ulysses_degree)
|
||||
)
|
||||
|
||||
outputs = omni.generate(prompt="turn this cat to a dog",
|
||||
@@ -172,10 +174,31 @@ omni = Omni(
|
||||
outputs = omni.generate(prompt="A cat sitting on a windowsill", num_inference_steps=50, width=2048, height=2048)
|
||||
```
|
||||
|
||||
### Using CFG-Parallel
|
||||
|
||||
Run image-to-image:
|
||||
|
||||
CFG-Parallel splits the CFG positive/negative branches across GPUs. Use it when you set a non-trivial `true_cfg_scale`.
|
||||
|
||||
```python
|
||||
from vllm_omni import Omni
|
||||
from vllm_omni.diffusion.data import DiffusionParallelConfig
|
||||
cfg_parallel_size = 2
|
||||
|
||||
omni = Omni(
|
||||
model="Qwen/Qwen-Image-Edit",
|
||||
parallel_config=DiffusionParallelConfig(cfg_parallel_size=cfg_parallel_size)
|
||||
)
|
||||
|
||||
outputs = omni.generate(prompt="turn this cat to a dog",
|
||||
pil_image=input_image, num_inference_steps=50, true_cfg_scale=4.0)
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
For detailed information on each acceleration method:
|
||||
|
||||
- **[TeaCache Guide](acceleration/teacache.md)** - Complete TeaCache documentation, configuration options, and best practices
|
||||
- **[Cache-DiT Acceleration Guide](acceleration/cache_dit_acceleration.md)** - Comprehensive Cache-DiT guide covering DBCache, TaylorSeer, SCM, and configuration parameters
|
||||
- **[Sequence Parallelism](acceleration/parallelism_acceleration.md#sequence-parallelism)** - Guidance on how to set sequence parallelism with configuration.
|
||||
- **[TeaCache Guide](diffusion/teacache.md)** - Complete TeaCache documentation, configuration options, and best practices
|
||||
- **[Cache-DiT Acceleration Guide](diffusion/cache_dit_acceleration.md)** - Comprehensive Cache-DiT guide covering DBCache, TaylorSeer, SCM, and configuration parameters
|
||||
- **[Sequence Parallelism](diffusion/parallelism_acceleration.md#sequence-parallelism)** - Guidance on how to set sequence parallelism with configuration.
|
||||
- **[CFG-Parallel](diffusion/parallelism_acceleration.md#cfg-parallel)** - Guidance on how to set CFG-Parallel to run positive/negative branches across ranks.
|
||||
@@ -0,0 +1,66 @@
|
||||
# Image-To-Video
|
||||
|
||||
Source <https://github.com/vllm-project/vllm-omni/tree/main/examples/offline_inference/image_to_video>.
|
||||
|
||||
|
||||
This example demonstrates how to generate videos from images using Wan2.2 Image-to-Video models with vLLM-Omni's offline inference API.
|
||||
|
||||
## Local CLI Usage
|
||||
|
||||
### Wan2.2-I2V-A14B-Diffusers (MoE)
|
||||
```bash
|
||||
python image_to_video.py \
|
||||
--model Wan-AI/Wan2.2-I2V-A14B-Diffusers \
|
||||
--image input.png \
|
||||
--prompt "A cat playing with yarn, smooth motion" \
|
||||
--negative_prompt "<optional quality filter>" \
|
||||
--height 480 \
|
||||
--width 832 \
|
||||
--num_frames 48 \
|
||||
--guidance_scale 5.0 \
|
||||
--guidance_scale_high 6.0 \
|
||||
--num_inference_steps 40 \
|
||||
--boundary_ratio 0.875 \
|
||||
--flow_shift 12.0 \
|
||||
--fps 16 \
|
||||
--output i2v_output.mp4
|
||||
```
|
||||
|
||||
### Wan2.2-TI2V-5B-Diffusers (Unified)
|
||||
```bash
|
||||
python image_to_video.py \
|
||||
--model Wan-AI/Wan2.2-TI2V-5B-Diffusers \
|
||||
--image input.png \
|
||||
--prompt "A cat playing with yarn, smooth motion" \
|
||||
--negative_prompt "<optional quality filter>" \
|
||||
--height 480 \
|
||||
--width 832 \
|
||||
--num_frames 48 \
|
||||
--guidance_scale 4.0 \
|
||||
--num_inference_steps 40 \
|
||||
--flow_shift 12.0 \
|
||||
--fps 16 \
|
||||
--output i2v_output.mp4
|
||||
```
|
||||
|
||||
Key arguments:
|
||||
|
||||
- `--model`: Model ID (I2V-A14B for MoE, TI2V-5B for unified T2V+I2V).
|
||||
- `--image`: Path to input image (required).
|
||||
- `--prompt`: Text description of desired motion/animation.
|
||||
- `--height/--width`: Output resolution (auto-calculated from image if not set). Dimensions should be multiples of 16.
|
||||
- `--num_frames`: Number of frames (default 81).
|
||||
- `--guidance_scale` and `--guidance_scale_high`: CFG scale (applied to low/high-noise stages for MoE).
|
||||
- `--negative_prompt`: Optional list of artifacts to suppress.
|
||||
- `--boundary_ratio`: Boundary split ratio for two-stage MoE models.
|
||||
- `--flow_shift`: Scheduler flow shift (5.0 for 720p, 12.0 for 480p).
|
||||
- `--num_inference_steps`: Number of denoising steps (default 50).
|
||||
- `--fps`: Frames per second for the saved MP4 (requires `diffusers` export_to_video).
|
||||
- `--output`: Path to save the generated video.
|
||||
|
||||
## Example materials
|
||||
|
||||
??? abstract "image_to_video.py"
|
||||
``````py
|
||||
--8<-- "examples/offline_inference/image_to_video/image_to_video.py"
|
||||
``````
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
Source <https://github.com/vllm-project/vllm-omni/tree/main/examples/offline_inference/qwen2_5_omni>.
|
||||
|
||||
|
||||
## Setup
|
||||
Please refer to the [stage configuration documentation](https://docs.vllm.ai/projects/vllm-omni/en/latest/configuration/stage_configs/) to configure memory allocation appropriately for your hardware setup.
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
Source <https://github.com/vllm-project/vllm-omni/tree/main/examples/offline_inference/qwen3_omni>.
|
||||
|
||||
|
||||
## Setup
|
||||
Please refer to the [stage configuration documentation](https://docs.vllm.ai/projects/vllm-omni/en/latest/configuration/stage_configs/) to configure memory allocation appropriately for your hardware setup.
|
||||
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
# Qwen3-TTS Offline Inference
|
||||
|
||||
Source <https://github.com/vllm-project/vllm-omni/tree/main/examples/offline_inference/qwen3_tts>.
|
||||
|
||||
|
||||
This directory contains an offline demo for running Qwen3 TTS models with vLLM Omni. It builds task-specific inputs and generates WAV files locally.
|
||||
|
||||
## Model Overview
|
||||
|
||||
Qwen3 TTS provides multiple task variants for speech generation:
|
||||
|
||||
- **CustomVoice**: Generate speech with a known speaker identity (speaker ID) and optional instruction.
|
||||
- **VoiceDesign**: Generate speech from text plus a descriptive instruction that designs a new voice.
|
||||
- **Base**: Voice cloning using a reference audio + reference transcript, with optional mode selection.
|
||||
|
||||
## Setup
|
||||
Please refer to the [stage configuration documentation](https://docs.vllm.ai/projects/vllm-omni/en/latest/configuration/stage_configs/) to configure memory allocation appropriately for your hardware setup.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Run a single sample for a task:
|
||||
|
||||
```
|
||||
python end2end.py --query-type CustomVoice
|
||||
```
|
||||
|
||||
Generated audio files are saved to `output_audio/` by default.
|
||||
|
||||
## Task Usage
|
||||
|
||||
### CustomVoice
|
||||
|
||||
Single sample:
|
||||
|
||||
```
|
||||
python end2end.py --query-type CustomVoice
|
||||
```
|
||||
|
||||
Batch sample (multiple prompts in one run):
|
||||
|
||||
```
|
||||
python end2end.py --query-type CustomVoice --use-batch-sample
|
||||
```
|
||||
|
||||
### VoiceDesign
|
||||
|
||||
Single sample:
|
||||
|
||||
```
|
||||
python end2end.py --query-type VoiceDesign
|
||||
```
|
||||
|
||||
Batch sample:
|
||||
|
||||
```
|
||||
python end2end.py --query-type VoiceDesign --use-batch-sample
|
||||
```
|
||||
|
||||
### Base (Voice Clone)
|
||||
|
||||
Single sample:
|
||||
|
||||
```
|
||||
python end2end.py --query-type Base
|
||||
```
|
||||
|
||||
Batch sample:
|
||||
|
||||
```
|
||||
python end2end.py --query-type Base --use-batch-sample
|
||||
```
|
||||
|
||||
Mode selection for Base:
|
||||
|
||||
- `--mode-tag icl` (default): standard mode
|
||||
- `--mode-tag xvec_only`: enable `x_vector_only_mode` in the request
|
||||
|
||||
Examples:
|
||||
|
||||
```
|
||||
python end2end.py --query-type Base --mode-tag icl
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- The script uses the model paths embedded in `end2end.py`. Update them if your local cache path differs.
|
||||
- Use `--output-dir` (preferred) or `--output-wav` to change the output folder.
|
||||
|
||||
## Example materials
|
||||
|
||||
??? abstract "end2end.py"
|
||||
``````py
|
||||
--8<-- "examples/offline_inference/qwen3_tts/end2end.py"
|
||||
``````
|
||||
@@ -1,7 +0,0 @@
|
||||
# Gradio Demo
|
||||
|
||||
Source <https://github.com/vllm-project/vllm-omni/blob/main/examples/online_serving/gradio_demo.py>.
|
||||
|
||||
``````py
|
||||
--8<-- "examples/online_serving/gradio_demo.py"
|
||||
``````
|
||||
@@ -230,6 +230,10 @@ Provide multiple images in `content` (order matters):
|
||||
``````py
|
||||
--8<-- "examples/online_serving/image_to_image/openai_chat_client.py"
|
||||
``````
|
||||
??? abstract "run_curl_image_edit.sh"
|
||||
``````sh
|
||||
--8<-- "examples/online_serving/image_to_image/run_curl_image_edit.sh"
|
||||
``````
|
||||
??? abstract "run_server.sh"
|
||||
``````sh
|
||||
--8<-- "examples/online_serving/image_to_image/run_server.sh"
|
||||
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
# OpenAI Chat Completion Client For Multimodal Generation
|
||||
|
||||
Source <https://github.com/vllm-project/vllm-omni/blob/main/examples/online_serving/openai_chat_completion_client_for_multimodal_generation.py>.
|
||||
|
||||
``````py
|
||||
--8<-- "examples/online_serving/openai_chat_completion_client_for_multimodal_generation.py"
|
||||
``````
|
||||
@@ -137,6 +137,7 @@ response = client.chat.completions.create(
|
||||
# Response contains two choices: one with text, one with audio
|
||||
print(response.choices[0].message.content) # Text response
|
||||
print(response.choices[1].message.audio) # Audio response
|
||||
```
|
||||
|
||||
## Streaming Output
|
||||
If you want to enable streaming output, please set the argument as below. The final output will be obtained just after generated by corresponding stage. Now we only support text streaming output. Other modalities can output normally.
|
||||
|
||||
@@ -145,6 +145,7 @@ response = client.chat.completions.create(
|
||||
# Response contains two choices: one with text, one with audio
|
||||
print(response.choices[0].message.content) # Text response
|
||||
print(response.choices[1].message.audio) # Audio response
|
||||
```
|
||||
|
||||
## Streaming Output
|
||||
If you want to enable streaming output, please set the argument as below. The final output will be obtained just after generated by corresponding stage. Now we only support text streaming output. Other modalities can output normally.
|
||||
|
||||
@@ -12,6 +12,8 @@ This example demonstrates how to deploy Qwen-Image model for online image genera
|
||||
```bash
|
||||
vllm serve Qwen/Qwen-Image --omni --port 8091
|
||||
```
|
||||
!!! note
|
||||
If you encounter Out-of-Memory (OOM) issues or have limited GPU memory, you can enable VAE slicing and tiling to reduce memory usage, --vae-use-slicing --vae-use-tiling
|
||||
|
||||
### Start with Parameters
|
||||
|
||||
@@ -43,7 +45,7 @@ curl -s http://localhost:8091/v1/chat/completions \
|
||||
"true_cfg_scale": 4.0,
|
||||
"seed": 42
|
||||
}
|
||||
}' | jq -r '.choices[0].message.content[0].image_url.url' | cut -d',' -f2 | base64 -d > output.png
|
||||
}' | jq -r '.choices[0].message.content[0].image_url.url' | cut -d',' -f2- | base64 -d > output.png
|
||||
```
|
||||
|
||||
### Method 2: Using Python Client
|
||||
@@ -117,6 +119,7 @@ Use `extra_body` to pass generation parameters:
|
||||
| `seed` | int | None | Random seed (reproducible) |
|
||||
| `negative_prompt` | str | None | Negative prompt |
|
||||
| `num_outputs_per_prompt` | int | 1 | Number of images to generate |
|
||||
| `--cfg-parallel-size`. | int | 1 | Number of GPUs for CFG parallelism |
|
||||
|
||||
## Response Format
|
||||
|
||||
@@ -146,7 +149,7 @@ Use `extra_body` to pass generation parameters:
|
||||
|
||||
```bash
|
||||
# Extract base64 from response and decode to image
|
||||
cat response.json | jq -r '.choices[0].message.content[0].image_url.url' | cut -d',' -f2 | base64 -d > output.png
|
||||
cat response.json | jq -r '.choices[0].message.content[0].image_url.url' | cut -d',' -f2- | base64 -d > output.png
|
||||
```
|
||||
|
||||
## File Description
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
import argparse
|
||||
import os
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"--model",
|
||||
default="ByteDance-Seed/BAGEL-7B-MoT",
|
||||
help="Path to merged model directory.",
|
||||
)
|
||||
parser.add_argument("--prompts", nargs="+", default=None, help="Input text prompts.")
|
||||
parser.add_argument(
|
||||
"--txt-prompts",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Path to a .txt file with one prompt per line (preferred).",
|
||||
)
|
||||
parser.add_argument("--prompt_type", default="text", choices=["text"])
|
||||
|
||||
parser.add_argument(
|
||||
"--modality",
|
||||
default="text2img",
|
||||
choices=["text2img", "img2img", "img2text", "text2text"],
|
||||
help="Modality mode to control stage execution.",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--image-path",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Path to input image for img2img.",
|
||||
)
|
||||
|
||||
# OmniLLM init args
|
||||
parser.add_argument("--enable-stats", action="store_true", default=False)
|
||||
parser.add_argument("--init-sleep-seconds", type=int, default=20)
|
||||
parser.add_argument("--batch-timeout", type=int, default=5)
|
||||
parser.add_argument("--init-timeout", type=int, default=300)
|
||||
parser.add_argument("--shm-threshold-bytes", type=int, default=65536)
|
||||
parser.add_argument("--worker-backend", type=str, default="process", choices=["process", "ray"])
|
||||
parser.add_argument("--ray-address", type=str, default=None)
|
||||
parser.add_argument("--stage-configs-path", type=str, default=None)
|
||||
parser.add_argument("--steps", type=int, default=50, help="Number of inference steps.")
|
||||
|
||||
args = parser.parse_args()
|
||||
return args
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
model_name = args.model
|
||||
try:
|
||||
# Preferred: load from txt file (one prompt per line)
|
||||
if getattr(args, "txt_prompts", None) and args.prompt_type == "text":
|
||||
with open(args.txt_prompts, encoding="utf-8") as f:
|
||||
lines = [ln.strip() for ln in f.readlines()]
|
||||
args.prompts = [ln for ln in lines if ln != ""]
|
||||
print(f"[Info] Loaded {len(args.prompts)} prompts from {args.txt_prompts}")
|
||||
except Exception as e:
|
||||
print(f"[Error] Failed to load prompts: {e}")
|
||||
raise
|
||||
|
||||
if args.prompts is None:
|
||||
# Default prompt for text2img test if none provided
|
||||
args.prompts = ["<|im_start|>A cute cat<|im_end|>"]
|
||||
print(f"[Info] No prompts provided, using default: {args.prompts}")
|
||||
omni_outputs = []
|
||||
|
||||
from PIL import Image
|
||||
|
||||
if args.modality == "img2img":
|
||||
from PIL import Image
|
||||
|
||||
from vllm_omni.entrypoints.omni_diffusion import OmniDiffusion
|
||||
|
||||
print("[Info] Running in img2img mode (Stage 1 only)")
|
||||
client = OmniDiffusion(model=model_name)
|
||||
|
||||
generate_kwargs = {
|
||||
"prompt": args.prompts,
|
||||
"seed": 52,
|
||||
"need_kv_receive": False,
|
||||
"num_inference_steps": args.steps,
|
||||
}
|
||||
|
||||
if args.image_path:
|
||||
if os.path.exists(args.image_path):
|
||||
loaded_image = Image.open(args.image_path).convert("RGB")
|
||||
generate_kwargs["pil_image"] = loaded_image
|
||||
else:
|
||||
print(f"[Warning] Image path {args.image_path} does not exist.")
|
||||
|
||||
result = client.generate(**generate_kwargs)
|
||||
|
||||
# Ensure result is a list for iteration
|
||||
if not isinstance(result, list):
|
||||
omni_outputs = [result]
|
||||
else:
|
||||
omni_outputs = result
|
||||
|
||||
else:
|
||||
import copy
|
||||
|
||||
from vllm_omni.entrypoints.omni import Omni
|
||||
|
||||
omni_kwargs = {}
|
||||
if args.stage_configs_path:
|
||||
omni_kwargs["stage_configs_path"] = args.stage_configs_path
|
||||
|
||||
omni_kwargs.update(
|
||||
{
|
||||
"log_stats": args.enable_stats,
|
||||
"init_sleep_seconds": args.init_sleep_seconds,
|
||||
"batch_timeout": args.batch_timeout,
|
||||
"init_timeout": args.init_timeout,
|
||||
"shm_threshold_bytes": args.shm_threshold_bytes,
|
||||
"worker_backend": args.worker_backend,
|
||||
"ray_address": args.ray_address,
|
||||
}
|
||||
)
|
||||
|
||||
omni = Omni(model=model_name, **omni_kwargs)
|
||||
|
||||
formatted_prompts = []
|
||||
for p in args.prompts:
|
||||
if args.modality == "img2text":
|
||||
if args.image_path:
|
||||
loaded_image = Image.open(args.image_path).convert("RGB")
|
||||
final_prompt_text = f"<|im_start|>user\n<|image_pad|>\n{p}<|im_end|>\n<|im_start|>assistant\n"
|
||||
prompt_dict = {
|
||||
"prompt": final_prompt_text,
|
||||
"multi_modal_data": {"image": loaded_image},
|
||||
"modalities": ["text"],
|
||||
}
|
||||
formatted_prompts.append(prompt_dict)
|
||||
elif args.modality == "text2text":
|
||||
final_prompt_text = f"<|im_start|>user\n{p}<|im_end|>\n<|im_start|>assistant\n"
|
||||
prompt_dict = {"prompt": final_prompt_text, "modalities": ["text"]}
|
||||
formatted_prompts.append(prompt_dict)
|
||||
else:
|
||||
# text2img
|
||||
final_prompt_text = f"<|im_start|>{p}<|im_end|>"
|
||||
prompt_dict = {"prompt": final_prompt_text, "modalities": ["image"]}
|
||||
formatted_prompts.append(prompt_dict)
|
||||
|
||||
params_list = copy.deepcopy(omni.default_sampling_params_list)
|
||||
if args.modality == "text2img":
|
||||
params_list[0]["max_tokens"] = 1
|
||||
if len(params_list) > 1:
|
||||
params_list[1]["num_inference_steps"] = args.steps
|
||||
|
||||
omni_outputs = list(omni.generate(prompts=formatted_prompts, sampling_params_list=params_list))
|
||||
|
||||
for i, req_output in enumerate(omni_outputs):
|
||||
images = getattr(req_output, "images", None)
|
||||
if not images and hasattr(req_output, "output"):
|
||||
if isinstance(req_output.output, list):
|
||||
images = req_output.output
|
||||
else:
|
||||
images = [req_output.output]
|
||||
|
||||
if images:
|
||||
for j, img in enumerate(images):
|
||||
img.save(f"output_{i}_{j}.png")
|
||||
|
||||
if hasattr(req_output, "request_output") and req_output.request_output:
|
||||
for stage_out in req_output.request_output:
|
||||
if hasattr(stage_out, "images") and stage_out.images:
|
||||
for k, img in enumerate(stage_out.images):
|
||||
save_path = f"output_{i}_stage_{getattr(stage_out, 'stage_id', '?')}_{k}.png"
|
||||
img.save(save_path)
|
||||
print(f"[Info] Saved stage output image to {save_path}")
|
||||
|
||||
print(omni_outputs)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,6 @@
|
||||
prompt="<|im_start|>user\nWhat is the capital of France?<|im_end|>\n<|im_start|>assistant\n"
|
||||
|
||||
python end2end.py --model ByteDance-Seed/BAGEL-7B-MoT \
|
||||
--prompt_type text \
|
||||
--init-sleep-seconds 0 \
|
||||
--prompts ${prompt}
|
||||
@@ -49,6 +49,22 @@ Usage (layered):
|
||||
--layers 4 \
|
||||
--color-format "RGBA"
|
||||
|
||||
Usage (with CFG Parallel):
|
||||
python image_edit.py \
|
||||
--image input.png \
|
||||
--prompt "Edit description" \
|
||||
--cfg_parallel_size 2 \
|
||||
--num_inference_steps 50 \
|
||||
--cfg_scale 4.0
|
||||
|
||||
Usage (disable torch.compile):
|
||||
python image_edit.py \
|
||||
--image input.png \
|
||||
--prompt "Edit description" \
|
||||
--enforce_eager \
|
||||
--num_inference_steps 50 \
|
||||
--cfg_scale 4.0
|
||||
|
||||
For more options, run:
|
||||
python image_edit.py --help
|
||||
"""
|
||||
@@ -94,7 +110,7 @@ def parse_args() -> argparse.Namespace:
|
||||
parser.add_argument(
|
||||
"--negative_prompt",
|
||||
type=str,
|
||||
default=" ",
|
||||
default=None,
|
||||
required=False,
|
||||
)
|
||||
parser.add_argument(
|
||||
@@ -165,6 +181,12 @@ def parse_args() -> argparse.Namespace:
|
||||
default=1,
|
||||
help="Number of GPUs used for ring sequence parallelism.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--tensor_parallel_size",
|
||||
type=int,
|
||||
default=1,
|
||||
help="Number of GPUs used for tensor parallelism (TP) inside the DiT.",
|
||||
)
|
||||
parser.add_argument("--layers", type=int, default=4, help="Number of layers to decompose the input image into.")
|
||||
parser.add_argument(
|
||||
"--resolution",
|
||||
@@ -245,7 +267,18 @@ def parse_args() -> argparse.Namespace:
|
||||
default=0.2,
|
||||
help="[tea_cache] Threshold for accumulated relative L1 distance.",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--cfg_parallel_size",
|
||||
type=int,
|
||||
default=1,
|
||||
choices=[1, 2],
|
||||
help="Number of GPUs used for classifier free guidance parallel size.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--enforce_eager",
|
||||
action="store_true",
|
||||
help="Disable torch.compile and force eager execution.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
@@ -273,7 +306,13 @@ def main():
|
||||
# Enable VAE memory optimizations on NPU
|
||||
vae_use_slicing = is_npu()
|
||||
vae_use_tiling = is_npu()
|
||||
parallel_config = DiffusionParallelConfig(ulysses_degree=args.ulysses_degree, ring_degree=args.ring_degree)
|
||||
parallel_config = DiffusionParallelConfig(
|
||||
ulysses_degree=args.ulysses_degree,
|
||||
ring_degree=args.ring_degree,
|
||||
cfg_parallel_size=args.cfg_parallel_size,
|
||||
tensor_parallel_size=args.tensor_parallel_size,
|
||||
)
|
||||
|
||||
# Configure cache based on backend type
|
||||
cache_config = None
|
||||
if args.cache_backend == "cache_dit":
|
||||
@@ -304,9 +343,13 @@ def main():
|
||||
cache_backend=args.cache_backend,
|
||||
cache_config=cache_config,
|
||||
parallel_config=parallel_config,
|
||||
enforce_eager=args.enforce_eager,
|
||||
)
|
||||
print("Pipeline loaded")
|
||||
|
||||
# Check if profiling is requested via environment variable
|
||||
profiler_enabled = bool(os.getenv("VLLM_TORCH_PROFILER_DIR"))
|
||||
|
||||
# Time profiling for generation
|
||||
print(f"\n{'=' * 60}")
|
||||
print("Generation Configuration:")
|
||||
@@ -319,7 +362,9 @@ def main():
|
||||
print(f" Image {idx + 1} size: {img.size}")
|
||||
else:
|
||||
print(f" Input image size: {input_image.size}")
|
||||
print(f" Parallel configuration: ulysses_degree={args.ulysses_degree}, ring_degree={args.ring_degree}")
|
||||
print(
|
||||
f" Parallel configuration: ulysses_degree={args.ulysses_degree}, ring_degree={args.ring_degree}, cfg_parallel_size={args.cfg_parallel_size}, tensor_parallel_size={args.tensor_parallel_size}"
|
||||
)
|
||||
print(f"{'=' * 60}\n")
|
||||
|
||||
generation_start = time.perf_counter()
|
||||
@@ -337,6 +382,10 @@ def main():
|
||||
"resolution": args.resolution,
|
||||
}
|
||||
|
||||
if profiler_enabled:
|
||||
print("[Profiler] Starting profiling...")
|
||||
omni.start_profile()
|
||||
|
||||
outputs = omni.generate(**generate_kwargs)
|
||||
generation_end = time.perf_counter()
|
||||
generation_time = generation_end - generation_start
|
||||
@@ -344,6 +393,23 @@ def main():
|
||||
# Print profiling results
|
||||
print(f"Total generation time: {generation_time:.4f} seconds ({generation_time * 1000:.2f} ms)")
|
||||
|
||||
if profiler_enabled:
|
||||
print("\n[Profiler] Stopping profiler and collecting results...")
|
||||
profile_results = omni.stop_profile()
|
||||
if profile_results and isinstance(profile_results, dict):
|
||||
traces = profile_results.get("traces", [])
|
||||
print("\n" + "=" * 60)
|
||||
print("PROFILING RESULTS:")
|
||||
for rank, trace in enumerate(traces):
|
||||
print(f"\nRank {rank}:")
|
||||
if trace:
|
||||
print(f" • Trace: {trace}")
|
||||
if not traces:
|
||||
print(" No traces collected.")
|
||||
print("=" * 60)
|
||||
else:
|
||||
print("[Profiler] No valid profiling data returned.")
|
||||
|
||||
if not outputs:
|
||||
raise ValueError("No output generated from omni.generate()")
|
||||
logger.info("Outputs: %s", outputs)
|
||||
|
||||
@@ -19,6 +19,7 @@ Usage:
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
@@ -95,6 +96,9 @@ def main():
|
||||
vae_use_slicing = is_npu()
|
||||
vae_use_tiling = is_npu()
|
||||
|
||||
# Check if profiling is requested via environment variable
|
||||
profiler_enabled = bool(os.getenv("VLLM_TORCH_PROFILER_DIR"))
|
||||
|
||||
omni = Omni(
|
||||
model=args.model,
|
||||
vae_use_slicing=vae_use_slicing,
|
||||
@@ -103,6 +107,10 @@ def main():
|
||||
flow_shift=args.flow_shift,
|
||||
)
|
||||
|
||||
if profiler_enabled:
|
||||
print("[Profiler] Starting profiling...")
|
||||
omni.start_profile()
|
||||
|
||||
# omni.generate() returns Generator[OmniRequestOutput, None, None]
|
||||
frames = omni.generate(
|
||||
args.prompt,
|
||||
@@ -178,6 +186,23 @@ def main():
|
||||
export_to_video(video_array, str(output_path), fps=args.fps)
|
||||
print(f"Saved generated video to {output_path}")
|
||||
|
||||
if profiler_enabled:
|
||||
print("\n[Profiler] Stopping profiler and collecting results...")
|
||||
profile_results = omni.stop_profile()
|
||||
if profile_results and isinstance(profile_results, dict):
|
||||
traces = profile_results.get("traces", [])
|
||||
print("\n" + "=" * 60)
|
||||
print("PROFILING RESULTS:")
|
||||
for rank, trace in enumerate(traces):
|
||||
print(f"\nRank {rank}:")
|
||||
if trace:
|
||||
print(f" • Trace: {trace}")
|
||||
if not traces:
|
||||
print(" No traces collected.")
|
||||
print("=" * 60)
|
||||
else:
|
||||
print("[Profiler] No valid profiling data returned.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -278,9 +278,9 @@ def get_audio_query(question: str = None, audio_path: str | None = None, samplin
|
||||
|
||||
|
||||
query_map = {
|
||||
"mixed_modalities": get_mixed_modalities_query,
|
||||
"use_mixed_modalities": get_mixed_modalities_query,
|
||||
"use_audio_in_video": get_use_audio_in_video_query,
|
||||
"multi_audios": get_multi_audios_query,
|
||||
"use_multi_audios": get_multi_audios_query,
|
||||
"use_image": get_image_query,
|
||||
"use_video": get_video_query,
|
||||
"use_audio": get_audio_query,
|
||||
@@ -434,7 +434,7 @@ def parse_args():
|
||||
"--query-type",
|
||||
"-q",
|
||||
type=str,
|
||||
default="mixed_modalities",
|
||||
default="use_mixed_modalities",
|
||||
choices=query_map.keys(),
|
||||
help="Query type.",
|
||||
)
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
python end2end.py --output-wav output_audio \
|
||||
--query-type mixed_modalities
|
||||
--query-type use_mixed_modalities
|
||||
@@ -12,6 +12,7 @@ from typing import NamedTuple
|
||||
import librosa
|
||||
import numpy as np
|
||||
import soundfile as sf
|
||||
import vllm
|
||||
from PIL import Image
|
||||
from vllm import SamplingParams
|
||||
from vllm.assets.audio import AudioAsset
|
||||
@@ -225,18 +226,76 @@ def get_multi_audios_query() -> QueryResult:
|
||||
)
|
||||
|
||||
|
||||
# def get_use_audio_in_video_query(video_path: str | None = None) -> QueryResult:
|
||||
# question = (
|
||||
# "Describe the content of the video in details, then convert what the "
|
||||
# "baby say into text."
|
||||
# )
|
||||
# prompt = (
|
||||
# f"<|im_start|>system\n{default_system}<|im_end|>\n"
|
||||
# "<|im_start|>user\n<|vision_start|><|video_pad|><|vision_end|>"
|
||||
# f"{question}<|im_end|>\n"
|
||||
# f"<|im_start|>assistant\n"
|
||||
# )
|
||||
# if video_path:
|
||||
# if not os.path.exists(video_path):
|
||||
# raise FileNotFoundError(f"Video file not found: {video_path}")
|
||||
# video_frames = video_to_ndarrays(video_path, num_frames=16)
|
||||
# else:
|
||||
# video_frames = VideoAsset(name="baby_reading", num_frames=16).np_ndarrays
|
||||
# audio = extract_video_audio(video_path, sampling_rate=16000)
|
||||
# return QueryResult(
|
||||
# inputs={
|
||||
# "prompt": prompt,
|
||||
# "multi_modal_data": {
|
||||
# "video": video_frames,
|
||||
# "audio": audio,
|
||||
# },
|
||||
# "mm_processor_kwargs": {
|
||||
# "use_audio_in_video": True,
|
||||
# },
|
||||
# },
|
||||
# limit_mm_per_prompt={"audio": 1, "video": 1},
|
||||
# )
|
||||
def get_use_audio_in_video_query() -> QueryResult:
|
||||
question = "Describe the content of the video in details, then convert what the baby say into text."
|
||||
prompt = (
|
||||
f"<|im_start|>system\n{default_system}<|im_end|>\n"
|
||||
"<|im_start|>user\n<|vision_start|><|video_pad|><|vision_end|>"
|
||||
f"{question}<|im_end|>\n"
|
||||
f"<|im_start|>assistant\n"
|
||||
)
|
||||
asset = VideoAsset(name="baby_reading", num_frames=16)
|
||||
audio = asset.get_audio(sampling_rate=16000)
|
||||
return QueryResult(
|
||||
inputs={
|
||||
"prompt": prompt,
|
||||
"multi_modal_data": {
|
||||
"video": asset.np_ndarrays,
|
||||
"audio": audio,
|
||||
},
|
||||
"mm_processor_kwargs": {
|
||||
"use_audio_in_video": True,
|
||||
},
|
||||
},
|
||||
limit_mm_per_prompt={"audio": 1, "video": 1},
|
||||
)
|
||||
|
||||
|
||||
query_map = {
|
||||
"text": get_text_query,
|
||||
"use_audio": get_audio_query,
|
||||
"use_image": get_image_query,
|
||||
"use_video": get_video_query,
|
||||
"multi_audios": get_multi_audios_query,
|
||||
"mixed_modalities": get_mixed_modalities_query,
|
||||
"use_multi_audios": get_multi_audios_query,
|
||||
"use_mixed_modalities": get_mixed_modalities_query,
|
||||
"use_audio_in_video": get_use_audio_in_video_query,
|
||||
}
|
||||
|
||||
|
||||
def main(args):
|
||||
model_name = "Qwen/Qwen3-Omni-30B-A3B-Instruct"
|
||||
print("=" * 20, "\n", f"vllm version: {vllm.__version__}", "\n", "=" * 20)
|
||||
|
||||
# Get paths from args
|
||||
video_path = getattr(args, "video_path", None)
|
||||
@@ -259,6 +318,10 @@ def main(args):
|
||||
num_frames=getattr(args, "num_frames", 16),
|
||||
sampling_rate=getattr(args, "sampling_rate", 16000),
|
||||
)
|
||||
elif args.query_type == "multi_audios":
|
||||
query_result = query_func()
|
||||
elif args.query_type == "use_audio_in_video":
|
||||
query_result = query_func()
|
||||
else:
|
||||
query_result = query_func()
|
||||
|
||||
@@ -270,7 +333,7 @@ def main(args):
|
||||
)
|
||||
|
||||
thinker_sampling_params = SamplingParams(
|
||||
temperature=0.4,
|
||||
temperature=0.9,
|
||||
top_p=0.9,
|
||||
top_k=-1,
|
||||
max_tokens=1200,
|
||||
@@ -331,6 +394,8 @@ def main(args):
|
||||
total_requests = len(prompts)
|
||||
processed_count = 0
|
||||
|
||||
print(f"query type: {args.query_type}")
|
||||
|
||||
for stage_outputs in omni_generator:
|
||||
if stage_outputs.final_output_type == "text":
|
||||
for output in stage_outputs.request_output:
|
||||
@@ -385,7 +450,7 @@ def parse_args():
|
||||
"--query-type",
|
||||
"-q",
|
||||
type=str,
|
||||
default="mixed_modalities",
|
||||
default="use_mixed_modalities",
|
||||
choices=query_map.keys(),
|
||||
help="Query type.",
|
||||
)
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
# Qwen3-TTS Offline Inference
|
||||
|
||||
This directory contains an offline demo for running Qwen3 TTS models with vLLM Omni. It builds task-specific inputs and generates WAV files locally.
|
||||
|
||||
## Model Overview
|
||||
|
||||
Qwen3 TTS provides multiple task variants for speech generation:
|
||||
|
||||
- **CustomVoice**: Generate speech with a known speaker identity (speaker ID) and optional instruction.
|
||||
- **VoiceDesign**: Generate speech from text plus a descriptive instruction that designs a new voice.
|
||||
- **Base**: Voice cloning using a reference audio + reference transcript, with optional mode selection.
|
||||
|
||||
## Setup
|
||||
Please refer to the [stage configuration documentation](https://docs.vllm.ai/projects/vllm-omni/en/latest/configuration/stage_configs/) to configure memory allocation appropriately for your hardware setup.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Run a single sample for a task:
|
||||
|
||||
```
|
||||
python end2end.py --query-type CustomVoice
|
||||
```
|
||||
|
||||
Generated audio files are saved to `output_audio/` by default.
|
||||
|
||||
## Task Usage
|
||||
|
||||
### CustomVoice
|
||||
|
||||
Single sample:
|
||||
|
||||
```
|
||||
python end2end.py --query-type CustomVoice
|
||||
```
|
||||
|
||||
Batch sample (multiple prompts in one run):
|
||||
|
||||
```
|
||||
python end2end.py --query-type CustomVoice --use-batch-sample
|
||||
```
|
||||
|
||||
### VoiceDesign
|
||||
|
||||
Single sample:
|
||||
|
||||
```
|
||||
python end2end.py --query-type VoiceDesign
|
||||
```
|
||||
|
||||
Batch sample:
|
||||
|
||||
```
|
||||
python end2end.py --query-type VoiceDesign --use-batch-sample
|
||||
```
|
||||
|
||||
### Base (Voice Clone)
|
||||
|
||||
Single sample:
|
||||
|
||||
```
|
||||
python end2end.py --query-type Base
|
||||
```
|
||||
|
||||
Batch sample:
|
||||
|
||||
```
|
||||
python end2end.py --query-type Base --use-batch-sample
|
||||
```
|
||||
|
||||
Mode selection for Base:
|
||||
|
||||
- `--mode-tag icl` (default): standard mode
|
||||
- `--mode-tag xvec_only`: enable `x_vector_only_mode` in the request
|
||||
|
||||
Examples:
|
||||
|
||||
```
|
||||
python end2end.py --query-type Base --mode-tag icl
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- The script uses the model paths embedded in `end2end.py`. Update them if your local cache path differs.
|
||||
- Use `--output-dir` (preferred) or `--output-wav` to change the output folder.
|
||||
@@ -0,0 +1,381 @@
|
||||
"""Offline inference demo for Qwen3 TTS via vLLM Omni.
|
||||
|
||||
Provides single and batch sample inputs for CustomVoice, VoiceDesign, and Base
|
||||
tasks, then runs Omni generation and saves output wav files.
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import NamedTuple
|
||||
|
||||
import soundfile as sf
|
||||
|
||||
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
|
||||
|
||||
from vllm import SamplingParams
|
||||
from vllm.utils.argparse_utils import FlexibleArgumentParser
|
||||
|
||||
from vllm_omni import Omni
|
||||
|
||||
|
||||
class QueryResult(NamedTuple):
|
||||
"""Container for a prepared Omni request."""
|
||||
|
||||
inputs: dict
|
||||
model_name: str
|
||||
|
||||
|
||||
def get_custom_voice_query(use_batch_sample: bool = False) -> QueryResult:
|
||||
"""Build CustomVoice sample inputs.
|
||||
|
||||
Args:
|
||||
use_batch_sample: When True, return a batch of prompts; otherwise a single prompt.
|
||||
|
||||
Returns:
|
||||
QueryResult with Omni inputs and the CustomVoice model path.
|
||||
"""
|
||||
task_type = "CustomVoice"
|
||||
if use_batch_sample:
|
||||
texts = ["其实我真的有发现,我是一个特别善于观察别人情绪的人。", "She said she would be here by noon."]
|
||||
instructs = ["", "Very happy."]
|
||||
languages = ["Chinese", "English"]
|
||||
speakers = ["Vivian", "Ryan"]
|
||||
inputs = []
|
||||
for text, instruct, language, speaker in zip(texts, instructs, languages, speakers):
|
||||
prompt = f"<|im_start|>assistant\n{text}<|im_end|>\n<|im_start|>assistant\n"
|
||||
inputs.append(
|
||||
{
|
||||
"prompt": prompt,
|
||||
"additional_information": {
|
||||
"task_type": [task_type],
|
||||
"text": [text],
|
||||
"instruct": [instruct],
|
||||
"language": [language],
|
||||
"speaker": [speaker],
|
||||
"max_new_tokens": [2048],
|
||||
},
|
||||
}
|
||||
)
|
||||
else:
|
||||
text = "其实我真的有发现,我是一个特别善于观察别人情绪的人。"
|
||||
language = "Chinese"
|
||||
speaker = "Vivian"
|
||||
instruct = "用特别愤怒的语气说"
|
||||
prompts = f"<|im_start|>assistant\n{text}<|im_end|>\n<|im_start|>assistant\n"
|
||||
inputs = {
|
||||
"prompt": prompts,
|
||||
"additional_information": {
|
||||
"task_type": [task_type],
|
||||
"text": [text],
|
||||
"language": [language],
|
||||
"speaker": [speaker],
|
||||
"instruct": [instruct],
|
||||
"max_new_tokens": [2048],
|
||||
},
|
||||
}
|
||||
return QueryResult(
|
||||
inputs=inputs,
|
||||
model_name="Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice",
|
||||
)
|
||||
|
||||
|
||||
def get_voice_design_query(use_batch_sample: bool = False) -> QueryResult:
|
||||
"""Build VoiceDesign sample inputs.
|
||||
|
||||
Args:
|
||||
use_batch_sample: When True, return a batch of prompts; otherwise a single prompt.
|
||||
|
||||
Returns:
|
||||
QueryResult with Omni inputs and the VoiceDesign model path.
|
||||
"""
|
||||
task_type = "VoiceDesign"
|
||||
if use_batch_sample:
|
||||
texts = [
|
||||
"哥哥,你回来啦,人家等了你好久好久了,要抱抱!",
|
||||
"It's in the top drawer... wait, it's empty? No way, that's impossible! I'm sure I put it there!",
|
||||
]
|
||||
instructs = [
|
||||
"体现撒娇稚嫩的萝莉女声,音调偏高且起伏明显,营造出黏人、做作又刻意卖萌的听觉效果。",
|
||||
"Speak in an incredulous tone, but with a hint of panic beginning to creep into your voice.",
|
||||
]
|
||||
languages = ["Chinese", "English"]
|
||||
inputs = []
|
||||
for text, instruct, language in zip(texts, instructs, languages):
|
||||
prompt = f"<|im_start|>assistant\n{text}<|im_end|>\n<|im_start|>assistant\n"
|
||||
inputs.append(
|
||||
{
|
||||
"prompt": prompt,
|
||||
"additional_information": {
|
||||
"task_type": [task_type],
|
||||
"text": [text],
|
||||
"language": [language],
|
||||
"instruct": [instruct],
|
||||
"max_new_tokens": [2048],
|
||||
"non_streaming_mode": [True],
|
||||
},
|
||||
}
|
||||
)
|
||||
else:
|
||||
text = "哥哥,你回来啦,人家等了你好久好久了,要抱抱!"
|
||||
instruct = "体现撒娇稚嫩的萝莉女声,音调偏高且起伏明显,营造出黏人、做作又刻意卖萌的听觉效果。"
|
||||
language = "Chinese"
|
||||
prompt = f"<|im_start|>assistant\n{text}<|im_end|>\n<|im_start|>assistant\n"
|
||||
inputs = {
|
||||
"prompt": prompt,
|
||||
"additional_information": {
|
||||
"task_type": [task_type],
|
||||
"text": [text],
|
||||
"language": [language],
|
||||
"instruct": [instruct],
|
||||
"max_new_tokens": [2048],
|
||||
"non_streaming_mode": [True],
|
||||
},
|
||||
}
|
||||
return QueryResult(
|
||||
inputs=inputs,
|
||||
model_name="Qwen/Qwen3-TTS-12Hz-1.7B-VoiceDesign",
|
||||
)
|
||||
|
||||
|
||||
def get_base_query(use_batch_sample: bool = False, mode_tag: str = "icl") -> QueryResult:
|
||||
"""Build Base (voice clone) sample inputs.
|
||||
|
||||
Args:
|
||||
use_batch_sample: When True, return a batch of prompts (Case 2).
|
||||
mode_tag: "icl" or "xvec_only" to control x_vector_only_mode behavior.
|
||||
|
||||
Returns:
|
||||
QueryResult with Omni inputs and the Base model path.
|
||||
"""
|
||||
task_type = "Base"
|
||||
ref_audio_path_1 = "https://qianwen-res.oss-cn-beijing.aliyuncs.com/Qwen3-TTS-Repo/clone_2.wav"
|
||||
ref_audio_single = ref_audio_path_1
|
||||
ref_text_single = (
|
||||
"Okay. Yeah. I resent you. I love you. I respect you. But you know what? You blew it! And thanks to you."
|
||||
)
|
||||
syn_text_single = "Good one. Okay, fine, I'm just gonna leave this sock monkey here. Goodbye."
|
||||
syn_lang_single = "Auto"
|
||||
x_vector_only_mode = mode_tag == "xvec_only"
|
||||
if use_batch_sample:
|
||||
syn_text_batch = [
|
||||
"Good one. Okay, fine, I'm just gonna leave this sock monkey here. Goodbye.",
|
||||
"其实我真的有发现,我是一个特别善于观察别人情绪的人。",
|
||||
]
|
||||
syn_lang_batch = ["Chinese", "English"]
|
||||
inputs = []
|
||||
for text, language in zip(syn_text_batch, syn_lang_batch):
|
||||
prompt = f"<|im_start|>assistant\n{text}<|im_end|>\n<|im_start|>assistant\n"
|
||||
inputs.append(
|
||||
{
|
||||
"prompt": prompt,
|
||||
"additional_information": {
|
||||
"task_type": [task_type],
|
||||
"ref_audio": [ref_audio_single],
|
||||
"ref_text": [ref_text_single],
|
||||
"text": [text],
|
||||
"language": [language],
|
||||
"x_vector_only_mode": [x_vector_only_mode],
|
||||
"max_new_tokens": [2048],
|
||||
},
|
||||
}
|
||||
)
|
||||
else:
|
||||
prompt = f"<|im_start|>assistant\n{syn_text_single}<|im_end|>\n<|im_start|>assistant\n"
|
||||
inputs = {
|
||||
"prompt": prompt,
|
||||
"additional_information": {
|
||||
"task_type": [task_type],
|
||||
"ref_audio": [ref_audio_single],
|
||||
"ref_text": [ref_text_single],
|
||||
"text": [syn_text_single],
|
||||
"language": [syn_lang_single],
|
||||
"x_vector_only_mode": [x_vector_only_mode],
|
||||
"max_new_tokens": [2048],
|
||||
},
|
||||
}
|
||||
return QueryResult(
|
||||
inputs=inputs,
|
||||
model_name="Qwen/Qwen3-TTS-12Hz-1.7B-Base",
|
||||
)
|
||||
|
||||
|
||||
def main(args):
|
||||
"""Run offline inference with Omni using prepared sample inputs.
|
||||
|
||||
Args:
|
||||
args: Parsed CLI args from parse_args().
|
||||
"""
|
||||
query_func = query_map[args.query_type]
|
||||
if args.query_type in {"CustomVoice", "VoiceDesign"}:
|
||||
query_result = query_func(use_batch_sample=args.use_batch_sample)
|
||||
elif args.query_type == "Base":
|
||||
query_result = query_func(
|
||||
use_batch_sample=args.use_batch_sample,
|
||||
mode_tag=args.mode_tag,
|
||||
)
|
||||
else:
|
||||
query_result = query_func()
|
||||
|
||||
model_name = query_result.model_name
|
||||
omni = Omni(
|
||||
model=model_name,
|
||||
stage_configs_path=args.stage_configs_path,
|
||||
log_stats=args.enable_stats,
|
||||
stage_init_timeout=args.stage_init_timeout,
|
||||
)
|
||||
|
||||
sampling_params = SamplingParams(
|
||||
temperature=0.9,
|
||||
top_p=1.0,
|
||||
top_k=50,
|
||||
max_tokens=2048,
|
||||
seed=42,
|
||||
detokenize=False,
|
||||
repetition_penalty=1.05,
|
||||
)
|
||||
|
||||
sampling_params_list = [
|
||||
sampling_params,
|
||||
]
|
||||
|
||||
output_dir = args.output_dir if getattr(args, "output_dir", None) else args.output_wav
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
omni_generator = omni.generate(query_result.inputs, sampling_params_list)
|
||||
for stage_outputs in omni_generator:
|
||||
for output in stage_outputs.request_output:
|
||||
request_id = output.request_id
|
||||
audio_tensor = output.multimodal_output["audio"]
|
||||
output_wav = os.path.join(output_dir, f"output_{request_id}.wav")
|
||||
audio_samplerate = output.multimodal_output["sr"].item()
|
||||
# Convert to numpy array and ensure correct format
|
||||
audio_numpy = audio_tensor.float().detach().cpu().numpy()
|
||||
|
||||
# Ensure audio is 1D (flatten if needed)
|
||||
if audio_numpy.ndim > 1:
|
||||
audio_numpy = audio_numpy.flatten()
|
||||
|
||||
# Save audio file with explicit WAV format
|
||||
sf.write(output_wav, audio_numpy, samplerate=audio_samplerate, format="WAV")
|
||||
print(f"Request ID: {request_id}, Saved audio to {output_wav}")
|
||||
|
||||
|
||||
def parse_args():
|
||||
"""Parse CLI arguments for offline TTS inference.
|
||||
|
||||
Returns:
|
||||
argparse.Namespace with CLI options.
|
||||
"""
|
||||
parser = FlexibleArgumentParser(description="Demo on using vLLM for offline inference with audio language models")
|
||||
parser.add_argument(
|
||||
"--query-type",
|
||||
"-q",
|
||||
type=str,
|
||||
default="CustomVoice",
|
||||
choices=query_map.keys(),
|
||||
help="Query type.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--enable-stats",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Enable writing detailed statistics (default: disabled)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--stage-init-timeout",
|
||||
type=int,
|
||||
default=300,
|
||||
help="Timeout for initializing a single stage in seconds (default: 300)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--batch-timeout",
|
||||
type=int,
|
||||
default=5,
|
||||
help="Timeout for batching in seconds (default: 5)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--init-timeout",
|
||||
type=int,
|
||||
default=300,
|
||||
help="Timeout for initializing stages in seconds (default: 300)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--shm-threshold-bytes",
|
||||
type=int,
|
||||
default=65536,
|
||||
help="Threshold for using shared memory in bytes (default: 65536)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output-wav",
|
||||
default="output_audio",
|
||||
help="[Deprecated] Output wav directory (use --output-dir).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--num-prompts",
|
||||
type=int,
|
||||
default=1,
|
||||
help="Number of prompts to generate.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--txt-prompts",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Path to a .txt file with one prompt per line (preferred).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--stage-configs-path",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Path to a stage configs file.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--audio-path",
|
||||
"-a",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Path to local audio file. If not provided, uses default audio asset.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--sampling-rate",
|
||||
type=int,
|
||||
default=16000,
|
||||
help="Sampling rate for audio loading (default: 16000).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--log-dir",
|
||||
type=str,
|
||||
default="logs",
|
||||
help="Log directory (default: logs).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--py-generator",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Use py_generator mode. The returned type of Omni.generate() is a Python Generator object.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--use-batch-sample",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Use batch input sample for CustomVoice/VoiceDesign/Base query.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--mode-tag",
|
||||
type=str,
|
||||
default="icl",
|
||||
choices=["icl", "xvec_only"],
|
||||
help="Mode tag for Base query x_vector_only_mode (default: icl).",
|
||||
)
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
query_map = {
|
||||
"CustomVoice": get_custom_voice_query,
|
||||
"VoiceDesign": get_voice_design_query,
|
||||
"Base": get_base_query,
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
args = parse_args()
|
||||
main(args)
|
||||
@@ -0,0 +1,200 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
"""
|
||||
Example script for text-to-audio generation using Stable Audio Open.
|
||||
|
||||
This script demonstrates how to generate audio from text prompts using
|
||||
the Stable Audio Open model with vLLM-Omni.
|
||||
|
||||
Usage:
|
||||
python text_to_audio.py --prompt "The sound of a dog barking"
|
||||
python text_to_audio.py --prompt "A piano playing a gentle melody" --audio_length 10.0
|
||||
python text_to_audio.py --prompt "Thunder and rain sounds" --negative_prompt "Low quality"
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
from vllm_omni.entrypoints.omni import Omni
|
||||
from vllm_omni.utils.platform_utils import detect_device_type
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Generate audio with Stable Audio Open.")
|
||||
parser.add_argument(
|
||||
"--model",
|
||||
default="stabilityai/stable-audio-open-1.0",
|
||||
help="Stable Audio model name or local path.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--prompt",
|
||||
default="The sound of a hammer hitting a wooden surface.",
|
||||
help="Text prompt for audio generation.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--negative_prompt",
|
||||
default="Low quality.",
|
||||
help="Negative prompt for classifier-free guidance.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--seed",
|
||||
type=int,
|
||||
default=42,
|
||||
help="Random seed for deterministic results.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--guidance_scale",
|
||||
type=float,
|
||||
default=7.0,
|
||||
help="Classifier-free guidance scale.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--audio_start",
|
||||
type=float,
|
||||
default=0.0,
|
||||
help="Audio start time in seconds.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--audio_length",
|
||||
type=float,
|
||||
default=10.0,
|
||||
help="Audio length in seconds (max ~47s for stable-audio-open-1.0).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--num_inference_steps",
|
||||
type=int,
|
||||
default=100,
|
||||
help="Number of denoising steps for the diffusion sampler.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--num_waveforms",
|
||||
type=int,
|
||||
default=1,
|
||||
help="Number of audio waveforms to generate for the given prompt.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output",
|
||||
type=str,
|
||||
default="stable_audio_output.wav",
|
||||
help="Path to save the generated audio (WAV format).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--sample_rate",
|
||||
type=int,
|
||||
default=44100,
|
||||
help="Sample rate for output audio (Stable Audio uses 44100 Hz).",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def save_audio(audio_data: np.ndarray, output_path: str, sample_rate: int = 44100):
|
||||
"""Save audio data to a WAV file."""
|
||||
try:
|
||||
import soundfile as sf
|
||||
|
||||
sf.write(output_path, audio_data, sample_rate)
|
||||
except ImportError:
|
||||
try:
|
||||
import scipy.io.wavfile as wav
|
||||
|
||||
# Ensure audio is in the correct format for scipy
|
||||
if audio_data.dtype == np.float32 or audio_data.dtype == np.float64:
|
||||
# Normalize to int16 range
|
||||
audio_data = np.clip(audio_data, -1.0, 1.0)
|
||||
audio_data = (audio_data * 32767).astype(np.int16)
|
||||
wav.write(output_path, sample_rate, audio_data)
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Either 'soundfile' or 'scipy' is required to save audio files. "
|
||||
"Install with: pip install soundfile or pip install scipy"
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
device = detect_device_type()
|
||||
generator = torch.Generator(device=device).manual_seed(args.seed)
|
||||
|
||||
print(f"\n{'=' * 60}")
|
||||
print("Stable Audio Open - Text-to-Audio Generation")
|
||||
print(f"{'=' * 60}")
|
||||
print(f" Model: {args.model}")
|
||||
print(f" Prompt: {args.prompt}")
|
||||
print(f" Negative prompt: {args.negative_prompt}")
|
||||
print(f" Audio length: {args.audio_length}s")
|
||||
print(f" Inference steps: {args.num_inference_steps}")
|
||||
print(f" Guidance scale: {args.guidance_scale}")
|
||||
print(f" Seed: {args.seed}")
|
||||
print(f"{'=' * 60}\n")
|
||||
|
||||
# Initialize Omni with Stable Audio model
|
||||
omni = Omni(model=args.model)
|
||||
|
||||
# Calculate audio end time
|
||||
audio_end_in_s = args.audio_start + args.audio_length
|
||||
|
||||
# Time profiling for generation
|
||||
generation_start = time.perf_counter()
|
||||
|
||||
# Generate audio
|
||||
audio = omni.generate(
|
||||
args.prompt,
|
||||
negative_prompt=args.negative_prompt,
|
||||
generator=generator,
|
||||
guidance_scale=args.guidance_scale,
|
||||
num_inference_steps=args.num_inference_steps,
|
||||
num_outputs_per_prompt=args.num_waveforms,
|
||||
extra={
|
||||
"audio_start_in_s": args.audio_start,
|
||||
"audio_end_in_s": audio_end_in_s,
|
||||
},
|
||||
)
|
||||
|
||||
generation_end = time.perf_counter()
|
||||
generation_time = generation_end - generation_start
|
||||
|
||||
print(f"Total generation time: {generation_time:.2f} seconds")
|
||||
|
||||
# Process and save audio
|
||||
output_path = Path(args.output)
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
suffix = output_path.suffix or ".wav"
|
||||
stem = output_path.stem or "stable_audio_output"
|
||||
|
||||
# Handle different output formats
|
||||
if isinstance(audio, torch.Tensor):
|
||||
audio = audio.cpu().float().numpy()
|
||||
|
||||
# Audio shape is typically [batch, channels, samples] or [channels, samples]
|
||||
if audio.ndim == 3:
|
||||
# [batch, channels, samples]
|
||||
if args.num_waveforms <= 1:
|
||||
audio_data = audio[0].T # [samples, channels]
|
||||
save_audio(audio_data, str(output_path), args.sample_rate)
|
||||
print(f"Saved generated audio to {output_path}")
|
||||
else:
|
||||
for idx in range(audio.shape[0]):
|
||||
audio_data = audio[idx].T # [samples, channels]
|
||||
save_path = output_path.parent / f"{stem}_{idx}{suffix}"
|
||||
save_audio(audio_data, str(save_path), args.sample_rate)
|
||||
print(f"Saved generated audio to {save_path}")
|
||||
elif audio.ndim == 2:
|
||||
# [channels, samples]
|
||||
audio_data = audio.T # [samples, channels]
|
||||
save_audio(audio_data, str(output_path), args.sample_rate)
|
||||
print(f"Saved generated audio to {output_path}")
|
||||
else:
|
||||
# [samples] - mono audio
|
||||
save_audio(audio, str(output_path), args.sample_rate)
|
||||
print(f"Saved generated audio to {output_path}")
|
||||
|
||||
print(f"\nGenerated {args.audio_length}s of audio at {args.sample_rate} Hz")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -2,6 +2,7 @@
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
@@ -23,7 +24,9 @@ def parse_args() -> argparse.Namespace:
|
||||
)
|
||||
parser.add_argument("--prompt", default="a cup of coffee on the table", help="Text prompt for image generation.")
|
||||
parser.add_argument(
|
||||
"--negative_prompt", default="", help="negative prompt for classifier-free conditional guidance."
|
||||
"--negative_prompt",
|
||||
default=None,
|
||||
help="negative prompt for classifier-free conditional guidance.",
|
||||
)
|
||||
parser.add_argument("--seed", type=int, default=142, help="Random seed for deterministic results.")
|
||||
parser.add_argument(
|
||||
@@ -81,6 +84,24 @@ def parse_args() -> argparse.Namespace:
|
||||
default=1,
|
||||
help="Number of GPUs used for ring sequence parallelism.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--cfg_parallel_size",
|
||||
type=int,
|
||||
default=1,
|
||||
choices=[1, 2],
|
||||
help="Number of GPUs used for classifier free guidance parallel size.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--enforce_eager",
|
||||
action="store_true",
|
||||
help="Disable torch.compile and force eager execution.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--tensor_parallel_size",
|
||||
type=int,
|
||||
default=1,
|
||||
help="Number of GPUs used for tensor parallelism (TP) inside the DiT.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
@@ -123,7 +144,16 @@ def main():
|
||||
}
|
||||
|
||||
# assert args.ring_degree == 1, "Ring attention is not supported yet"
|
||||
parallel_config = DiffusionParallelConfig(ulysses_degree=args.ulysses_degree, ring_degree=args.ring_degree)
|
||||
parallel_config = DiffusionParallelConfig(
|
||||
ulysses_degree=args.ulysses_degree,
|
||||
ring_degree=args.ring_degree,
|
||||
cfg_parallel_size=args.cfg_parallel_size,
|
||||
tensor_parallel_size=args.tensor_parallel_size,
|
||||
)
|
||||
|
||||
# Check if profiling is requested via environment variable
|
||||
profiler_enabled = bool(os.getenv("VLLM_TORCH_PROFILER_DIR"))
|
||||
|
||||
omni = Omni(
|
||||
model=args.model,
|
||||
vae_use_slicing=vae_use_slicing,
|
||||
@@ -131,15 +161,23 @@ def main():
|
||||
cache_backend=args.cache_backend,
|
||||
cache_config=cache_config,
|
||||
parallel_config=parallel_config,
|
||||
enforce_eager=args.enforce_eager,
|
||||
)
|
||||
|
||||
if profiler_enabled:
|
||||
print("[Profiler] Starting profiling...")
|
||||
omni.start_profile()
|
||||
|
||||
# Time profiling for generation
|
||||
print(f"\n{'=' * 60}")
|
||||
print("Generation Configuration:")
|
||||
print(f" Model: {args.model}")
|
||||
print(f" Inference steps: {args.num_inference_steps}")
|
||||
print(f" Cache backend: {args.cache_backend if args.cache_backend else 'None (no acceleration)'}")
|
||||
print(f" Parallel configuration: ulysses_degree={args.ulysses_degree}, ring_degree={args.ring_degree}")
|
||||
print(
|
||||
f" Parallel configuration: tensor_parallel_size={args.tensor_parallel_size}, "
|
||||
f"ulysses_degree={args.ulysses_degree}, ring_degree={args.ring_degree}, cfg_parallel_size={args.cfg_parallel_size}"
|
||||
)
|
||||
print(f" Image size: {args.width}x{args.height}")
|
||||
print(f"{'=' * 60}\n")
|
||||
|
||||
@@ -161,6 +199,23 @@ def main():
|
||||
# Print profiling results
|
||||
print(f"Total generation time: {generation_time:.4f} seconds ({generation_time * 1000:.2f} ms)")
|
||||
|
||||
if profiler_enabled:
|
||||
print("\n[Profiler] Stopping profiler and collecting results...")
|
||||
profile_results = omni.stop_profile()
|
||||
if profile_results and isinstance(profile_results, dict):
|
||||
traces = profile_results.get("traces", [])
|
||||
print("\n" + "=" * 60)
|
||||
print("PROFILING RESULTS:")
|
||||
for rank, trace in enumerate(traces):
|
||||
print(f"\nRank {rank}:")
|
||||
if trace:
|
||||
print(f" • Trace: {trace}")
|
||||
if not traces:
|
||||
print(" No traces collected.")
|
||||
print("=" * 60)
|
||||
else:
|
||||
print("[Profiler] No valid profiling data returned.")
|
||||
|
||||
# Extract images from OmniRequestOutput
|
||||
# omni.generate() returns list[OmniRequestOutput], extract images from the first output
|
||||
if not outputs or len(outputs) == 0:
|
||||
|
||||
@@ -10,7 +10,7 @@ python text_to_video.py \
|
||||
--negative_prompt "<optional quality filter>" \
|
||||
--height 480 \
|
||||
--width 640 \
|
||||
--num_frames 32 \
|
||||
--num_frames 33 \
|
||||
--guidance_scale 4.0 \
|
||||
--guidance_scale_high 3.0 \
|
||||
--num_inference_steps 40 \
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
import argparse
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
@@ -46,6 +47,9 @@ def main():
|
||||
vae_use_slicing = is_npu()
|
||||
vae_use_tiling = is_npu()
|
||||
|
||||
# Check if profiling is requested via environment variable
|
||||
profiler_enabled = bool(os.getenv("VLLM_TORCH_PROFILER_DIR"))
|
||||
|
||||
omni = Omni(
|
||||
model=args.model,
|
||||
vae_use_slicing=vae_use_slicing,
|
||||
@@ -54,6 +58,10 @@ def main():
|
||||
flow_shift=args.flow_shift,
|
||||
)
|
||||
|
||||
if profiler_enabled:
|
||||
print("[Profiler] Starting profiling...")
|
||||
omni.start_profile()
|
||||
|
||||
frames = omni.generate(
|
||||
args.prompt,
|
||||
negative_prompt=args.negative_prompt,
|
||||
@@ -126,6 +134,23 @@ def main():
|
||||
export_to_video(video_array, str(output_path), fps=args.fps)
|
||||
print(f"Saved generated video to {output_path}")
|
||||
|
||||
if profiler_enabled:
|
||||
print("\n[Profiler] Stopping profiler and collecting results...")
|
||||
profile_results = omni.stop_profile()
|
||||
if profile_results and isinstance(profile_results, dict):
|
||||
traces = profile_results.get("traces", [])
|
||||
print("\n" + "=" * 60)
|
||||
print("PROFILING RESULTS:")
|
||||
for rank, trace in enumerate(traces):
|
||||
print(f"\nRank {rank}:")
|
||||
if trace:
|
||||
print(f" • Trace: {trace}")
|
||||
if not traces:
|
||||
print(" No traces collected.")
|
||||
print("=" * 60)
|
||||
else:
|
||||
print("[Profiler] No valid profiling data returned.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Bagel OpenAI-compatible chat client for image generation and multimodal tasks.
|
||||
|
||||
Usage:
|
||||
python openai_chat_client.py --prompt "A cute cat" --output output.png
|
||||
python openai_chat_client.py --prompt "Describe this image" --image-url https://example.com/image.png
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
def generate_image(
|
||||
prompt: str,
|
||||
server_url: str = "http://localhost:8091",
|
||||
image_url: str | None = None,
|
||||
height: int | None = None,
|
||||
width: int | None = None,
|
||||
steps: int | None = None,
|
||||
seed: int | None = None,
|
||||
negative_prompt: str | None = None,
|
||||
modality: str = "text2img", # "text2img" (default), "img2img", "img2text", "text2text"
|
||||
) -> bytes | str | None:
|
||||
"""Generate an image or text using the chat completions API.
|
||||
|
||||
Args:
|
||||
prompt: Text description or prompt
|
||||
server_url: Server URL
|
||||
image_url: URL or path to input image (for img2img/img2text)
|
||||
height: Image height in pixels
|
||||
width: Image width in pixels
|
||||
steps: Number of inference steps
|
||||
seed: Random seed
|
||||
negative_prompt: Negative prompt
|
||||
modality: Task modality hint
|
||||
|
||||
Returns:
|
||||
Image bytes (for image outputs) or Text string (for text outputs) or None if failed
|
||||
"""
|
||||
|
||||
# Construct Message Content
|
||||
content = [{"type": "text", "text": f"<|im_start|>{prompt}<|im_end|>"}]
|
||||
|
||||
if image_url:
|
||||
# Check if local file
|
||||
if Path(image_url).exists():
|
||||
with open(image_url, "rb") as f:
|
||||
b64_data = base64.b64encode(f.read()).decode("utf-8")
|
||||
final_image_url = f"data:image/jpeg;base64,{b64_data}"
|
||||
else:
|
||||
final_image_url = image_url
|
||||
|
||||
content.append({"type": "image_url", "image_url": {"url": final_image_url}})
|
||||
|
||||
messages = [{"role": "user", "content": content}]
|
||||
|
||||
# Build request payload with all parameters at top level
|
||||
# Note: vLLM ignores "extra_body", so we put parameters directly in the payload
|
||||
payload = {"messages": messages}
|
||||
|
||||
# Set output modalities at top level
|
||||
if modality == "text2img" or modality == "img2img":
|
||||
payload["modalities"] = ["image"]
|
||||
elif modality == "img2text" or modality == "text2text":
|
||||
payload["modalities"] = ["text"]
|
||||
|
||||
# Add generation parameters directly to payload
|
||||
if height is not None:
|
||||
payload["height"] = height
|
||||
if width is not None:
|
||||
payload["width"] = width
|
||||
if steps is not None:
|
||||
payload["num_inference_steps"] = steps
|
||||
if seed is not None:
|
||||
payload["seed"] = seed
|
||||
if negative_prompt:
|
||||
payload["negative_prompt"] = negative_prompt
|
||||
|
||||
# Send request
|
||||
try:
|
||||
print(f"Sending request to {server_url} with modality {modality}...")
|
||||
response = requests.post(
|
||||
f"{server_url}/v1/chat/completions",
|
||||
headers={"Content-Type": "application/json"},
|
||||
json=payload,
|
||||
timeout=300,
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
# Extract content - check ALL choices since server may return multiple
|
||||
# (e.g., text in choices[0], image in choices[1])
|
||||
choices = data.get("choices", [])
|
||||
|
||||
# First pass: look for image output in any choice
|
||||
for choice in choices:
|
||||
choice_content = choice.get("message", {}).get("content")
|
||||
|
||||
# Handle Image Output
|
||||
if isinstance(choice_content, list) and len(choice_content) > 0:
|
||||
first_item = choice_content[0]
|
||||
if isinstance(first_item, dict) and "image_url" in first_item:
|
||||
img_url_str = first_item["image_url"].get("url", "")
|
||||
if img_url_str.startswith("data:image"):
|
||||
_, b64_data = img_url_str.split(",", 1)
|
||||
return base64.b64decode(b64_data)
|
||||
|
||||
# Second pass: look for text output if no image found
|
||||
for choice in choices:
|
||||
choice_content = choice.get("message", {}).get("content")
|
||||
if isinstance(choice_content, str) and choice_content:
|
||||
return choice_content
|
||||
|
||||
print(f"Unexpected response format: {choices}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Bagel multimodal chat client")
|
||||
parser.add_argument("--prompt", "-p", default="<|im_start|>A cute cat<|im_end|>", help="Text prompt")
|
||||
parser.add_argument("--output", "-o", default="bagel_output.png", help="Output file (for image results)")
|
||||
parser.add_argument("--server", "-s", default="http://localhost:8091", help="Server URL")
|
||||
|
||||
# Modality Control
|
||||
parser.add_argument("--image-url", "-i", type=str, help="Input image URL or local path")
|
||||
parser.add_argument(
|
||||
"--modality",
|
||||
"-m",
|
||||
default="text2img",
|
||||
choices=["text2img", "img2img", "img2text", "text2text"],
|
||||
help="Task modality",
|
||||
)
|
||||
|
||||
# Generation Params
|
||||
parser.add_argument("--height", type=int, default=512, help="Image height")
|
||||
parser.add_argument("--width", type=int, default=512, help="Image width")
|
||||
parser.add_argument("--steps", type=int, default=25, help="Inference steps")
|
||||
parser.add_argument("--seed", type=int, default=42, help="Random seed")
|
||||
parser.add_argument("--negative", help="Negative prompt")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
print(f"Mode: {args.modality}")
|
||||
if args.image_url:
|
||||
print(f"Input Image: {args.image_url}")
|
||||
|
||||
result = generate_image(
|
||||
prompt=args.prompt,
|
||||
server_url=args.server,
|
||||
image_url=args.image_url,
|
||||
height=args.height,
|
||||
width=args.width,
|
||||
steps=args.steps,
|
||||
seed=args.seed,
|
||||
negative_prompt=args.negative,
|
||||
modality=args.modality,
|
||||
)
|
||||
|
||||
if result:
|
||||
if isinstance(result, bytes):
|
||||
# It's an image
|
||||
output_path = Path(args.output)
|
||||
output_path.write_bytes(result)
|
||||
print(f"Image saved to: {output_path}")
|
||||
print(f"Size: {len(result) / 1024:.1f} KB")
|
||||
elif isinstance(result, str):
|
||||
# It's text
|
||||
print("Response:")
|
||||
print(result)
|
||||
else:
|
||||
print("Failed to generate response")
|
||||
exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+12
@@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
# Bagel online serving startup script
|
||||
|
||||
MODEL="${MODEL:-ByteDance-Seed/BAGEL-7B-MoT}"
|
||||
PORT="${PORT:-8091}"
|
||||
|
||||
echo "Starting Bagel server..."
|
||||
echo "Model: $MODEL"
|
||||
echo "Port: $PORT"
|
||||
|
||||
vllm serve "$MODEL" --omni \
|
||||
--port "$PORT"
|
||||
@@ -127,7 +127,7 @@ def main():
|
||||
parser.add_argument("--width", type=int, default=1024, help="Output image width")
|
||||
parser.add_argument("--steps", type=int, default=50, help="Inference steps")
|
||||
parser.add_argument("--guidance", type=float, default=7.5, help="Guidance 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")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -134,6 +134,7 @@ response = client.chat.completions.create(
|
||||
# Response contains two choices: one with text, one with audio
|
||||
print(response.choices[0].message.content) # Text response
|
||||
print(response.choices[1].message.audio) # Audio response
|
||||
```
|
||||
|
||||
## Streaming Output
|
||||
If you want to enable streaming output, please set the argument as below. The final output will be obtained just after generated by corresponding stage. Now we only support text streaming output. Other modalities can output normally.
|
||||
|
||||
@@ -142,6 +142,7 @@ response = client.chat.completions.create(
|
||||
# Response contains two choices: one with text, one with audio
|
||||
print(response.choices[0].message.content) # Text response
|
||||
print(response.choices[1].message.audio) # Audio response
|
||||
```
|
||||
|
||||
## Streaming Output
|
||||
If you want to enable streaming output, please set the argument as below. The final output will be obtained just after generated by corresponding stage. Now we only support text streaming output. Other modalities can output normally.
|
||||
|
||||
+27
@@ -305,6 +305,26 @@ def get_multi_audios_query(custom_prompt: str | None = None):
|
||||
}
|
||||
|
||||
|
||||
def get_use_audio_in_video_query(
|
||||
video_path: str | None = None,
|
||||
audio_path: str | None = None,
|
||||
custom_prompt: str | None = None,
|
||||
):
|
||||
question = custom_prompt or (
|
||||
"Describe the content of the video in details, then convert what the baby say into text."
|
||||
)
|
||||
video_url = get_video_url_from_path(video_path)
|
||||
audio_url = get_audio_url_from_path(audio_path)
|
||||
return {
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "video_url", "video_url": {"url": video_url}},
|
||||
{"type": "audio_url", "audio_url": {"url": audio_url}},
|
||||
{"type": "text", "text": question},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
query_map = {
|
||||
"text": get_text_query,
|
||||
"use_audio": get_audio_query,
|
||||
@@ -312,6 +332,7 @@ query_map = {
|
||||
"use_video": get_video_query,
|
||||
"use_mixed_modalities": get_mixed_modalities_query,
|
||||
"use_multi_audios": get_multi_audios_query,
|
||||
"use_audio_in_video": get_use_audio_in_video_query,
|
||||
}
|
||||
|
||||
|
||||
@@ -372,6 +393,12 @@ def run_multimodal_generation(args) -> None:
|
||||
prompt = query_func(audio_path=audio_path, custom_prompt=custom_prompt)
|
||||
elif args.query_type == "text":
|
||||
prompt = query_func(custom_prompt=custom_prompt)
|
||||
elif args.query_type == "use_audio_in_video":
|
||||
prompt = query_func(
|
||||
video_path=video_path,
|
||||
audio_path=audio_path,
|
||||
custom_prompt=custom_prompt,
|
||||
)
|
||||
else:
|
||||
prompt = query_func()
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ This example demonstrates how to deploy Qwen-Image model for online image genera
|
||||
```bash
|
||||
vllm serve Qwen/Qwen-Image --omni --port 8091
|
||||
```
|
||||
!!! note
|
||||
If you encounter Out-of-Memory (OOM) issues or have limited GPU memory, you can enable VAE slicing and tiling to reduce memory usage, --vae-use-slicing --vae-use-tiling
|
||||
|
||||
### Start with Parameters
|
||||
|
||||
@@ -40,7 +42,7 @@ curl -s http://localhost:8091/v1/chat/completions \
|
||||
"true_cfg_scale": 4.0,
|
||||
"seed": 42
|
||||
}
|
||||
}' | jq -r '.choices[0].message.content[0].image_url.url' | cut -d',' -f2 | base64 -d > output.png
|
||||
}' | jq -r '.choices[0].message.content[0].image_url.url' | cut -d',' -f2- | base64 -d > output.png
|
||||
```
|
||||
|
||||
### Method 2: Using Python Client
|
||||
@@ -114,6 +116,7 @@ Use `extra_body` to pass generation parameters:
|
||||
| `seed` | int | None | Random seed (reproducible) |
|
||||
| `negative_prompt` | str | None | Negative prompt |
|
||||
| `num_outputs_per_prompt` | int | 1 | Number of images to generate |
|
||||
| `--cfg-parallel-size`. | int | 1 | Number of GPUs for CFG parallelism |
|
||||
|
||||
## Response Format
|
||||
|
||||
@@ -143,7 +146,7 @@ Use `extra_body` to pass generation parameters:
|
||||
|
||||
```bash
|
||||
# Extract base64 from response and decode to image
|
||||
cat response.json | jq -r '.choices[0].message.content[0].image_url.url' | cut -d',' -f2 | base64 -d > output.png
|
||||
cat response.json | jq -r '.choices[0].message.content[0].image_url.url' | cut -d',' -f2- | base64 -d > output.png
|
||||
```
|
||||
|
||||
## File Description
|
||||
|
||||
@@ -100,7 +100,7 @@ def main():
|
||||
parser.add_argument("--width", type=int, default=1024, help="Image width")
|
||||
parser.add_argument("--steps", type=int, default=50, help="Inference steps")
|
||||
parser.add_argument("--cfg-scale", type=float, default=4.0, help="True CFG scale")
|
||||
parser.add_argument("--seed", type=int, default=42, help="Random seed")
|
||||
parser.add_argument("--seed", type=int, default=0, help="Random seed")
|
||||
parser.add_argument("--negative", help="Negative prompt")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -60,6 +60,10 @@ hooks:
|
||||
- docs/mkdocs/hooks/url_schemes.py
|
||||
- docs/mkdocs/hooks/generate_examples.py
|
||||
|
||||
# Exclude include files from navigation warnings
|
||||
exclude_docs: |
|
||||
**/*.inc.md
|
||||
|
||||
# Plugins
|
||||
plugins:
|
||||
- meta
|
||||
@@ -86,6 +90,7 @@ plugins:
|
||||
api_root_uri: "api"
|
||||
nav_item_prefix: "" # No prefix in navigation tree (clean names)
|
||||
show_full_namespace: false # Show only module name, not full path
|
||||
on_implicit_namespace_package: skip # Skip directories without __init__.py (e.g., assets)
|
||||
exclude:
|
||||
- "re:vllm_omni\\._.*" # Internal modules
|
||||
- "vllm_omni.diffusion.models.qwen_image" # avoid importing vllm in mkdocs building
|
||||
|
||||
+38
-6
@@ -36,9 +36,13 @@ dependencies = [
|
||||
"accelerate==1.12.0",
|
||||
"gradio==5.50",
|
||||
"soundfile>=0.13.1",
|
||||
"cache-dit==1.1.8",
|
||||
"cache-dit==1.2.0",
|
||||
"tqdm>=4.66.0",
|
||||
# "vllm==0.12.0", # TODO: fix the entrypoints overwrite problem
|
||||
"torchsde>=0.2.6", # Required for Stable Audio scheduler
|
||||
"fa3-fwd", # flash attention 3, maintained by @ZJY0516
|
||||
"openai-whisper>=20250625",
|
||||
"imageio[ffmpeg]>=2.37.2",
|
||||
# "vllm==0.14.0", # TODO: fix the entrypoints overwrite problem
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
@@ -49,6 +53,11 @@ dev = [
|
||||
"pytest-cov>=4.0.0",
|
||||
"mypy==1.11.1",
|
||||
"pre-commit==4.0.1",
|
||||
"openai-whisper>=20250625",
|
||||
"psutil>=7.2.0",
|
||||
"soundfile>=0.13.1",
|
||||
"imageio[ffmpeg]>=0.6.0",
|
||||
"opencv-python>=4.12.0.88"
|
||||
]
|
||||
|
||||
docs = [
|
||||
@@ -150,11 +159,34 @@ addopts = [
|
||||
"--cov-report=xml",
|
||||
]
|
||||
markers = [
|
||||
"unit: Unit tests",
|
||||
"integration: Integration tests",
|
||||
# ci/cd required
|
||||
"core_model: Core model tests (run in each PR)",
|
||||
# function module markers
|
||||
"diffusion: Diffusion model tests",
|
||||
"omni: Omni model tests",
|
||||
"cache: Cache backend tests",
|
||||
"parallel: Parallelism/distributed tests",
|
||||
# platform markers
|
||||
"cpu: Tests that run on CPU",
|
||||
"gpu: Tests that run on GPU (auto-added)",
|
||||
"cuda: Tests that run on CUDA (auto-added)",
|
||||
"rocm: Tests that run on AMD/ROCm (auto-added)",
|
||||
"npu: Tests that run on NPU/Ascend (auto-added)",
|
||||
# specified computation resources marks (auto-added)
|
||||
"H100: Tests that require H100 GPU",
|
||||
"L4: Tests that require L4 GPU",
|
||||
"MI325: Tests that require MI325 GPU (AMD/ROCm)",
|
||||
"A2: Tests that require A2 NPU",
|
||||
"A3: Tests that require A3 NPU",
|
||||
"distributed_cuda: Tests that require multi cards on CUDA platform",
|
||||
"distributed_rocm: Tests that require multi cards on ROCm platform",
|
||||
"distributed_npu: Tests that require multi cards on NPU platform",
|
||||
"skipif_cuda: Skip if the num of CUDA cards is less than the required",
|
||||
"skipif_rocm: Skip if the num of ROCm cards is less than the required",
|
||||
"skipif_npu: Skip if the num of NPU cards is less than the required",
|
||||
# more detailed markers
|
||||
"slow: Slow tests (may skip in quick CI)",
|
||||
"benchmark: Benchmark tests",
|
||||
"slow: Slow tests",
|
||||
"core_model: enable this model test in each PR instead of only nightly",
|
||||
]
|
||||
|
||||
[tool.typos.default]
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
[pytest]
|
||||
markers =
|
||||
gpu_mem_high: needs high VRAM
|
||||
@@ -1,12 +1,55 @@
|
||||
import base64
|
||||
import datetime
|
||||
import io
|
||||
import math
|
||||
import os
|
||||
import random
|
||||
|
||||
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
|
||||
# Set CPU device for CI environments without GPU
|
||||
if "VLLM_TARGET_DEVICE" not in os.environ:
|
||||
os.environ["VLLM_TARGET_DEVICE"] = "cpu"
|
||||
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import psutil
|
||||
import pytest
|
||||
import torch
|
||||
import yaml
|
||||
from vllm.logger import init_logger
|
||||
from vllm.utils.network_utils import get_open_port
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def default_vllm_config():
|
||||
"""Set a default VllmConfig for all tests.
|
||||
|
||||
This fixture is auto-used for all tests to ensure that any test
|
||||
that directly instantiates vLLM CustomOps (e.g., RMSNorm, LayerNorm)
|
||||
or model components has the required VllmConfig context.
|
||||
|
||||
This fixture is required for vLLM 0.14.0+ where CustomOp initialization
|
||||
requires a VllmConfig context set via set_current_vllm_config().
|
||||
"""
|
||||
from vllm.config import DeviceConfig, VllmConfig, set_current_vllm_config
|
||||
|
||||
# Use CPU device if no GPU is available (e.g., in CI environments)
|
||||
has_gpu = torch.cuda.is_available() and torch.cuda.device_count() > 0
|
||||
device = "cuda" if has_gpu else "cpu"
|
||||
device_config = DeviceConfig(device=device)
|
||||
|
||||
with set_current_vllm_config(VllmConfig(device_config=device_config)):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def clean_gpu_memory_between_tests():
|
||||
if os.getenv("VLLM_TEST_CLEAN_GPU_MEMORY", "0") != "1":
|
||||
@@ -34,3 +77,663 @@ def clean_gpu_memory_between_tests():
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.empty_cache()
|
||||
gc.collect()
|
||||
|
||||
|
||||
def dummy_messages_from_mix_data(
|
||||
system_prompt: dict[str, Any] = None,
|
||||
video_data_url: Any = None,
|
||||
audio_data_url: Any = None,
|
||||
image_data_url: Any = None,
|
||||
content_text: str = None,
|
||||
):
|
||||
"""Create messages with video、image、audio data URL for OpenAI API."""
|
||||
|
||||
if content_text is not None:
|
||||
content = [{"type": "text", "text": content_text}]
|
||||
else:
|
||||
content = []
|
||||
|
||||
media_items = []
|
||||
if isinstance(video_data_url, list):
|
||||
for video_url in video_data_url:
|
||||
media_items.append((video_url, "video"))
|
||||
else:
|
||||
media_items.append((video_data_url, "video"))
|
||||
|
||||
if isinstance(image_data_url, list):
|
||||
for url in image_data_url:
|
||||
media_items.append((url, "image"))
|
||||
else:
|
||||
media_items.append((image_data_url, "image"))
|
||||
|
||||
if isinstance(audio_data_url, list):
|
||||
for url in audio_data_url:
|
||||
media_items.append((url, "audio"))
|
||||
else:
|
||||
media_items.append((audio_data_url, "audio"))
|
||||
|
||||
content.extend(
|
||||
{"type": f"{media_type}_url", f"{media_type}_url": {"url": url}}
|
||||
for url, media_type in media_items
|
||||
if url is not None
|
||||
)
|
||||
messages = [{"role": "user", "content": content}]
|
||||
if system_prompt is not None:
|
||||
messages = [system_prompt] + messages
|
||||
return messages
|
||||
|
||||
|
||||
def generate_synthetic_audio(
|
||||
duration: int, # seconds
|
||||
num_channels: int, # 1:Mono,2:Stereo 5:5.1 surround sound
|
||||
sample_rate: int = 48000, # Default use 48000Hz.
|
||||
save_to_file: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
""" "Generate synthetic audio with rain."""
|
||||
import soundfile as sf
|
||||
|
||||
# Initialize audio data array
|
||||
num_samples = int(sample_rate * duration)
|
||||
audio_data = np.zeros((num_samples, num_channels), dtype=np.float32)
|
||||
|
||||
# Configure parameters based on rain intensity
|
||||
drop_density = 10 # Number of raindrops per second
|
||||
drop_volume = 0.15 # Volume of individual raindrops
|
||||
background_volume = 0.02 # Volume of background rain noise
|
||||
|
||||
# Pink noise sounds more natural than white noise for rain
|
||||
white_noise = np.random.randn(num_samples)
|
||||
pink_noise = np.convolve(white_noise, np.ones(8) / 8, mode="same")
|
||||
pink_noise = pink_noise / np.max(np.abs(pink_noise)) if np.max(np.abs(pink_noise)) > 0 else pink_noise
|
||||
bg_noise = pink_noise * background_volume
|
||||
|
||||
# Add background noise to all channels
|
||||
for ch in range(num_channels):
|
||||
audio_data[:, ch] += bg_noise
|
||||
|
||||
# Total number of raindrops = density × duration × channels for stereo effect
|
||||
total_drops = int(drop_density * duration * num_channels)
|
||||
|
||||
for _ in range(total_drops):
|
||||
# Random timing for raindrop
|
||||
drop_time = random.uniform(0, duration)
|
||||
|
||||
# Random duration of raindrop sound (0.01-0.05 seconds)
|
||||
drop_duration = random.uniform(0.01, 0.05)
|
||||
|
||||
# Random frequency gives variation in raindrop pitch
|
||||
drop_freq = random.uniform(500, 5000) # Hz
|
||||
|
||||
# Random channel selection for stereo positioning
|
||||
channel = random.randint(0, num_channels - 1)
|
||||
|
||||
# Calculate sample positions for this raindrop
|
||||
start_sample = int(drop_time * sample_rate)
|
||||
drop_samples = int(drop_duration * sample_rate)
|
||||
end_sample = min(start_sample + drop_samples, num_samples)
|
||||
|
||||
if start_sample < end_sample:
|
||||
# Generate the raindrop sound
|
||||
num_drop_samples = end_sample - start_sample
|
||||
t = np.arange(num_drop_samples) / sample_rate
|
||||
|
||||
# Basic sine wave for raindrop sound
|
||||
drop_sound = drop_volume * np.sin(2 * math.pi * drop_freq * t)
|
||||
|
||||
# Apply envelope for natural attack and decay
|
||||
envelope = np.ones(num_drop_samples)
|
||||
attack_samples = int(num_drop_samples * 0.1) # 10% of samples for attack
|
||||
decay_samples = num_drop_samples - attack_samples
|
||||
|
||||
if attack_samples > 0:
|
||||
# Linear attack: volume increases from 0 to 1
|
||||
envelope[:attack_samples] = np.linspace(0, 1, attack_samples)
|
||||
|
||||
if decay_samples > 0:
|
||||
# Exponential decay for natural sound fade
|
||||
decay = np.exp(-8 * t[attack_samples:] / drop_duration)
|
||||
envelope[attack_samples:] = decay
|
||||
|
||||
# Apply envelope to raindrop sound
|
||||
drop_sound *= envelope
|
||||
|
||||
# Add raindrop sound to selected channel
|
||||
audio_data[start_sample:end_sample, channel] += drop_sound
|
||||
|
||||
# Step 3: Add simple reverb effect for realism
|
||||
# Reverb simulates sound reflections in environment
|
||||
if duration > 2:
|
||||
# Single delay reverb (100ms delay)
|
||||
delay_samples = int(0.1 * sample_rate)
|
||||
if delay_samples < num_samples:
|
||||
for ch in range(num_channels):
|
||||
delayed = np.zeros(num_samples)
|
||||
delayed[delay_samples:] = audio_data[:-delay_samples, ch] * 0.3
|
||||
audio_data[:, ch] += delayed
|
||||
|
||||
# Step 4: Normalize audio to prevent clipping
|
||||
# Find maximum amplitude and scale to 80% of maximum volume
|
||||
max_amp = np.max(np.abs(audio_data))
|
||||
if max_amp > 0:
|
||||
audio_data = audio_data / max_amp * 0.8
|
||||
|
||||
# Handle file saving
|
||||
audio_bytes = None
|
||||
|
||||
if save_to_file:
|
||||
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
output_path = f"audio_{num_channels}ch_{timestamp}.wav"
|
||||
|
||||
try:
|
||||
sf.write(output_path, audio_data, sample_rate, format="WAV", subtype="PCM_16")
|
||||
print(f"Audio saved: {output_path}")
|
||||
|
||||
with open(output_path, "rb") as f:
|
||||
audio_bytes = f.read()
|
||||
except Exception as e:
|
||||
print(f"Save failed: {e}")
|
||||
save_to_file = False
|
||||
|
||||
# If not saving or save failed, create in memory
|
||||
if not save_to_file or audio_bytes is None:
|
||||
buffer = io.BytesIO()
|
||||
sf.write(buffer, audio_data, sample_rate, format="WAV", subtype="PCM_16")
|
||||
buffer.seek(0)
|
||||
audio_bytes = buffer.read()
|
||||
|
||||
# Return result
|
||||
base64_audio = base64.b64encode(audio_bytes).decode("utf-8")
|
||||
result = {
|
||||
"base64": base64_audio,
|
||||
}
|
||||
if save_to_file and output_path:
|
||||
result["file_path"] = output_path
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def generate_synthetic_video(width: int, height: int, num_frames: int, save_to_file: bool = False) -> str:
|
||||
"""Generate synthetic video with bouncing balls and return base64 string."""
|
||||
|
||||
import cv2
|
||||
import imageio
|
||||
|
||||
# Create random balls
|
||||
num_balls = random.randint(3, 8)
|
||||
balls = []
|
||||
|
||||
for _ in range(num_balls):
|
||||
radius = min(width, height) // 8
|
||||
if radius < 1:
|
||||
raise ValueError(f"Video dimensions ({width}x{height}) are too small for synthetic video generation")
|
||||
x = random.randint(radius, width - radius)
|
||||
y = random.randint(radius, height - radius)
|
||||
|
||||
speed = random.uniform(3.0, 8.0)
|
||||
angle = random.uniform(0, 2 * math.pi)
|
||||
vx = speed * math.cos(angle)
|
||||
vy = speed * math.sin(angle)
|
||||
|
||||
# OpenCV uses BGR format, but imageio expects RGB
|
||||
# We'll create in BGR first, then convert to RGB later
|
||||
color_bgr = (random.randint(50, 255), random.randint(50, 255), random.randint(50, 255))
|
||||
|
||||
balls.append({"x": x, "y": y, "vx": vx, "vy": vy, "radius": radius, "color_bgr": color_bgr})
|
||||
|
||||
# Generate video frames
|
||||
video_frames = []
|
||||
|
||||
for frame_idx in range(num_frames):
|
||||
# Create black background (BGR format)
|
||||
frame_bgr = np.zeros((height, width, 3), dtype=np.uint8)
|
||||
|
||||
for ball in balls:
|
||||
# Update position
|
||||
ball["x"] += ball["vx"]
|
||||
ball["y"] += ball["vy"]
|
||||
|
||||
# Boundary collision detection
|
||||
if ball["x"] - ball["radius"] <= 0 or ball["x"] + ball["radius"] >= width:
|
||||
ball["vx"] = -ball["vx"]
|
||||
ball["x"] = max(ball["radius"], min(width - ball["radius"], ball["x"]))
|
||||
|
||||
if ball["y"] - ball["radius"] <= 0 or ball["y"] + ball["radius"] >= height:
|
||||
ball["vy"] = -ball["vy"]
|
||||
ball["y"] = max(ball["radius"], min(height - ball["radius"], ball["y"]))
|
||||
|
||||
# Use cv2 to draw circle
|
||||
x, y = int(ball["x"]), int(ball["y"])
|
||||
radius = ball["radius"]
|
||||
|
||||
# Draw solid circle (main circle)
|
||||
cv2.circle(frame_bgr, (x, y), radius, ball["color_bgr"], -1)
|
||||
|
||||
# Add simple 3D effect: draw a brighter center
|
||||
if radius > 3: # Only add highlight when radius is large enough
|
||||
highlight_radius = max(1, radius // 2)
|
||||
highlight_x = max(highlight_radius, min(x - radius // 4, width - highlight_radius))
|
||||
highlight_y = max(highlight_radius, min(y - radius // 4, height - highlight_radius))
|
||||
|
||||
# Create highlight color (brighter)
|
||||
highlight_color = tuple(min(c + 40, 255) for c in ball["color_bgr"])
|
||||
cv2.circle(frame_bgr, (highlight_x, highlight_y), highlight_radius, highlight_color, -1)
|
||||
|
||||
# Convert BGR to RGB for imageio
|
||||
frame_rgb = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB)
|
||||
video_frames.append(frame_rgb)
|
||||
|
||||
video_bytes = None
|
||||
saved_file_path = None
|
||||
|
||||
buffer = io.BytesIO()
|
||||
writer_kwargs = {
|
||||
"format": "mp4",
|
||||
"fps": 30,
|
||||
"codec": "libx264",
|
||||
"quality": 7,
|
||||
"pixelformat": "yuv420p",
|
||||
"macro_block_size": 16,
|
||||
"ffmpeg_params": [
|
||||
"-preset",
|
||||
"medium",
|
||||
"-crf",
|
||||
"23",
|
||||
"-movflags",
|
||||
"+faststart",
|
||||
"-pix_fmt",
|
||||
"yuv420p",
|
||||
"-vf",
|
||||
f"scale={width}:{height}",
|
||||
],
|
||||
}
|
||||
|
||||
if save_to_file:
|
||||
import datetime
|
||||
|
||||
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
output_path = f"video_{width}x{height}_{timestamp}.mp4"
|
||||
try:
|
||||
with imageio.get_writer(output_path, **writer_kwargs) as writer:
|
||||
for frame in video_frames:
|
||||
writer.append_data(frame)
|
||||
|
||||
saved_file_path = output_path
|
||||
print(f"Video saved to: {saved_file_path}")
|
||||
with open(output_path, "rb") as f:
|
||||
video_bytes = f.read()
|
||||
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to save video to file {output_path}: {e}")
|
||||
save_to_file = False
|
||||
|
||||
if not save_to_file or video_bytes is None:
|
||||
with imageio.get_writer(buffer, **writer_kwargs) as writer:
|
||||
for frame in video_frames:
|
||||
writer.append_data(frame)
|
||||
|
||||
buffer.seek(0)
|
||||
video_bytes = buffer.read()
|
||||
|
||||
base64_video = base64.b64encode(video_bytes).decode("utf-8")
|
||||
|
||||
result = {
|
||||
"base64": base64_video,
|
||||
}
|
||||
if save_to_file and saved_file_path:
|
||||
result["file_path"] = saved_file_path
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def generate_synthetic_image(width: int, height: int, save_to_file: bool = False) -> Any:
|
||||
"""Generate synthetic image with randomly colored squares and return base64 string."""
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
# Create white background
|
||||
image = Image.new("RGB", (width, height), (255, 255, 255))
|
||||
draw = ImageDraw.Draw(image)
|
||||
|
||||
# Generate random number of squares
|
||||
num_squares = random.randint(3, 8)
|
||||
|
||||
for _ in range(num_squares):
|
||||
# Random square size
|
||||
square_size = random.randint(min(width, height) // 8, min(width, height) // 4)
|
||||
|
||||
# Random position
|
||||
x = random.randint(0, width - square_size - 1)
|
||||
y = random.randint(0, height - square_size - 1)
|
||||
|
||||
# Random color
|
||||
color = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
|
||||
|
||||
# Random border width
|
||||
border_width = random.randint(1, 5)
|
||||
|
||||
# Draw square
|
||||
draw.rectangle([x, y, x + square_size, y + square_size], fill=color, outline=(0, 0, 0), width=border_width)
|
||||
|
||||
# Handle file saving
|
||||
image_bytes = None
|
||||
saved_file_path = None
|
||||
|
||||
if save_to_file:
|
||||
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
output_path = f"image_{width}x{height}_{timestamp}.jpg"
|
||||
|
||||
try:
|
||||
# Save image to file
|
||||
image.save(output_path, format="JPEG", quality=85, optimize=True)
|
||||
saved_file_path = output_path
|
||||
print(f"Image saved to: {saved_file_path}")
|
||||
|
||||
# Read file for base64 encoding
|
||||
with open(output_path, "rb") as f:
|
||||
image_bytes = f.read()
|
||||
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to save image to file {output_path}: {e}")
|
||||
save_to_file = False
|
||||
|
||||
# If not saving or save failed, create in memory
|
||||
if not save_to_file or image_bytes is None:
|
||||
buffer = io.BytesIO()
|
||||
image.save(buffer, format="JPEG", quality=85, optimize=True)
|
||||
buffer.seek(0)
|
||||
image_bytes = buffer.read()
|
||||
|
||||
# Generate base64
|
||||
base64_image = base64.b64encode(image_bytes).decode("utf-8")
|
||||
|
||||
# Return result
|
||||
result = {
|
||||
"base64": base64_image,
|
||||
}
|
||||
if save_to_file and saved_file_path:
|
||||
result["file_path"] = saved_file_path
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def preprocess_text(text):
|
||||
import re
|
||||
|
||||
word_to_num = {
|
||||
"zero": "0",
|
||||
"one": "1",
|
||||
"two": "2",
|
||||
"three": "3",
|
||||
"four": "4",
|
||||
"five": "5",
|
||||
"six": "6",
|
||||
"seven": "7",
|
||||
"eight": "8",
|
||||
"nine": "9",
|
||||
"ten": "10",
|
||||
}
|
||||
|
||||
for word, num in word_to_num.items():
|
||||
pattern = r"\b" + re.escape(word) + r"\b"
|
||||
text = re.sub(pattern, num, text, flags=re.IGNORECASE)
|
||||
|
||||
text = re.sub(r"[^\w\s]", "", text)
|
||||
text = re.sub(r"\s+", " ", text)
|
||||
return text.lower().strip()
|
||||
|
||||
|
||||
def cosine_similarity_text(text1, text2, n: int = 3):
|
||||
from collections import Counter
|
||||
|
||||
if not text1 or not text2:
|
||||
return 0.0
|
||||
|
||||
text1 = preprocess_text(text1)
|
||||
text2 = preprocess_text(text2)
|
||||
|
||||
ngrams1 = [text1[i : i + n] for i in range(len(text1) - n + 1)]
|
||||
ngrams2 = [text2[i : i + n] for i in range(len(text2) - n + 1)]
|
||||
|
||||
counter1 = Counter(ngrams1)
|
||||
counter2 = Counter(ngrams2)
|
||||
|
||||
all_ngrams = set(counter1.keys()) | set(counter2.keys())
|
||||
vec1 = [counter1.get(ng, 0) for ng in all_ngrams]
|
||||
vec2 = [counter2.get(ng, 0) for ng in all_ngrams]
|
||||
|
||||
dot_product = sum(a * b for a, b in zip(vec1, vec2))
|
||||
norm1 = sum(a * a for a in vec1) ** 0.5
|
||||
norm2 = sum(b * b for b in vec2) ** 0.5
|
||||
|
||||
if norm1 == 0 or norm2 == 0:
|
||||
return 0.0
|
||||
return dot_product / (norm1 * norm2)
|
||||
|
||||
|
||||
def convert_audio_to_text(audio_data):
|
||||
"""
|
||||
Convert base64 encoded audio data to text using speech recognition.
|
||||
"""
|
||||
import whisper
|
||||
|
||||
audio_data = base64.b64decode(audio_data)
|
||||
output_path = f"./test_{int(time.time())}"
|
||||
with open(output_path, "wb") as audio_file:
|
||||
audio_file.write(audio_data)
|
||||
|
||||
print(f"audio data is saved: {output_path}")
|
||||
|
||||
model = whisper.load_model("base")
|
||||
text = model.transcribe(
|
||||
output_path,
|
||||
temperature=0.0,
|
||||
word_timestamps=True,
|
||||
condition_on_previous_text=False,
|
||||
initial_prompt="Please transcribe with proper word spacing.",
|
||||
)["text"]
|
||||
if text:
|
||||
return text
|
||||
else:
|
||||
return ""
|
||||
|
||||
|
||||
def modify_stage_config(
|
||||
yaml_path: str,
|
||||
stage_updates: dict[int, dict[str, Any]],
|
||||
) -> str:
|
||||
"""
|
||||
Batch modify configurations for multiple stages in a YAML file.
|
||||
|
||||
Args:
|
||||
yaml_path: Path to the YAML configuration file.
|
||||
stage_updates: Dictionary where keys are stage IDs and values are dictionaries of
|
||||
modifications for that stage. Each modification dictionary uses
|
||||
dot-separated paths as keys and new configuration values as values.
|
||||
Example: {
|
||||
0: {'engine_args.max_model_len': 5800},
|
||||
1: {'runtime.max_batch_size': 2}
|
||||
}
|
||||
|
||||
Returns:
|
||||
str: Path to the newly created modified YAML file with timestamp suffix.
|
||||
|
||||
Example:
|
||||
>>> output_file = modify_stage_config(
|
||||
... 'config.yaml',
|
||||
... {
|
||||
... 0: {'engine_args.max_model_len': 5800},
|
||||
... 1: {'runtime.max_batch_size': 2}
|
||||
... }
|
||||
... )
|
||||
>>> print(f"Modified configuration saved to: {output_file}")
|
||||
Modified configuration saved to: config_1698765432.yaml
|
||||
"""
|
||||
path = Path(yaml_path)
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(f"yaml does not exist: {path}")
|
||||
try:
|
||||
with open(yaml_path, encoding="utf-8") as f:
|
||||
config = yaml.safe_load(f) or {}
|
||||
except Exception as e:
|
||||
raise ValueError(f"Cannot parse YAML file: {e}")
|
||||
|
||||
stage_args = config.get("stage_args", [])
|
||||
if not stage_args:
|
||||
raise ValueError("the stage_args does not exist")
|
||||
|
||||
for stage_id, config_dict in stage_updates.items():
|
||||
target_stage = None
|
||||
for stage in stage_args:
|
||||
if stage.get("stage_id") == stage_id:
|
||||
target_stage = stage
|
||||
break
|
||||
|
||||
if target_stage is None:
|
||||
available_ids = [s.get("stage_id") for s in stage_args if "stage_id" in s]
|
||||
raise KeyError(f"Stage ID {stage_id} is not exist, available IDs: {available_ids}")
|
||||
|
||||
for key_path, value in config_dict.items():
|
||||
current = target_stage
|
||||
keys = key_path.split(".")
|
||||
for i in range(len(keys) - 1):
|
||||
key = keys[i]
|
||||
if key not in current:
|
||||
raise KeyError(f"the {'.'.join(keys[: i + 1])} does not exist")
|
||||
|
||||
elif not isinstance(current[key], dict) and i < len(keys) - 2:
|
||||
raise ValueError(f"{'.'.join(keys[: i + 1])}' cannot continue deeper because it's not a dict")
|
||||
current = current[key]
|
||||
current[keys[-1]] = value
|
||||
|
||||
output_path = f"{yaml_path.split('.')[0]}_{int(time.time())}.yaml"
|
||||
with open(output_path, "w", encoding="utf-8") as f:
|
||||
yaml.dump(config, f, default_flow_style=False, sort_keys=False, allow_unicode=True, indent=2)
|
||||
|
||||
return output_path
|
||||
|
||||
|
||||
class OmniServer:
|
||||
"""Omniserver for vLLM-Omni tests."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model: str,
|
||||
serve_args: list[str],
|
||||
*,
|
||||
env_dict: dict[str, str] | None = None,
|
||||
) -> None:
|
||||
self.model = model
|
||||
self.serve_args = serve_args
|
||||
self.env_dict = env_dict
|
||||
self.proc: subprocess.Popen | None = None
|
||||
self.host = "127.0.0.1"
|
||||
self.port = get_open_port()
|
||||
|
||||
def _start_server(self) -> None:
|
||||
"""Start the vLLM-Omni server subprocess."""
|
||||
env = os.environ.copy()
|
||||
env["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
|
||||
if self.env_dict is not None:
|
||||
env.update(self.env_dict)
|
||||
|
||||
cmd = [
|
||||
sys.executable,
|
||||
"-m",
|
||||
"vllm_omni.entrypoints.cli.main",
|
||||
"serve",
|
||||
self.model,
|
||||
"--omni",
|
||||
"--host",
|
||||
self.host,
|
||||
"--port",
|
||||
str(self.port),
|
||||
] + self.serve_args
|
||||
|
||||
print(f"Launching OmniServer with: {' '.join(cmd)}")
|
||||
self.proc = subprocess.Popen(
|
||||
cmd,
|
||||
env=env,
|
||||
cwd=os.path.dirname(os.path.dirname(os.path.abspath(__file__))), # Set working directory to vllm-omni root
|
||||
)
|
||||
|
||||
# Wait for server to be ready
|
||||
max_wait = 600 # 10 minutes
|
||||
start_time = time.time()
|
||||
while time.time() - start_time < max_wait:
|
||||
try:
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
||||
sock.settimeout(1)
|
||||
result = sock.connect_ex((self.host, self.port))
|
||||
if result == 0:
|
||||
print(f"Server ready on {self.host}:{self.port}")
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(2)
|
||||
|
||||
raise RuntimeError(f"Server failed to start within {max_wait} seconds")
|
||||
|
||||
def _kill_process_tree(self, pid):
|
||||
"""kill process and its children"""
|
||||
try:
|
||||
parent = psutil.Process(pid)
|
||||
children = parent.children(recursive=True)
|
||||
for child in children:
|
||||
try:
|
||||
child.terminate()
|
||||
except psutil.NoSuchProcess:
|
||||
pass
|
||||
|
||||
gone, still_alive = psutil.wait_procs(children, timeout=10)
|
||||
|
||||
for child in still_alive:
|
||||
try:
|
||||
child.kill()
|
||||
except psutil.NoSuchProcess:
|
||||
pass
|
||||
|
||||
try:
|
||||
parent.terminate()
|
||||
parent.wait(timeout=10)
|
||||
except (psutil.NoSuchProcess, psutil.TimeoutExpired):
|
||||
try:
|
||||
parent.kill()
|
||||
except psutil.NoSuchProcess:
|
||||
pass
|
||||
|
||||
except psutil.NoSuchProcess:
|
||||
pass
|
||||
|
||||
def __enter__(self):
|
||||
self._start_server()
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
if self.proc:
|
||||
try:
|
||||
parent = psutil.Process(self.proc.pid)
|
||||
children = parent.children(recursive=True)
|
||||
for child in children:
|
||||
try:
|
||||
child.terminate()
|
||||
except psutil.NoSuchProcess:
|
||||
pass
|
||||
|
||||
gone, still_alive = psutil.wait_procs(children, timeout=10)
|
||||
|
||||
for child in still_alive:
|
||||
try:
|
||||
child.kill()
|
||||
except psutil.NoSuchProcess:
|
||||
pass
|
||||
|
||||
try:
|
||||
parent.terminate()
|
||||
parent.wait(timeout=10)
|
||||
except (psutil.NoSuchProcess, psutil.TimeoutExpired):
|
||||
try:
|
||||
parent.kill()
|
||||
except psutil.NoSuchProcess:
|
||||
pass
|
||||
|
||||
except psutil.NoSuchProcess:
|
||||
pass
|
||||
+7
-2
@@ -12,13 +12,13 @@ from vllm_omni.diffusion.attention.layer import Attention
|
||||
from vllm_omni.diffusion.data import (
|
||||
DiffusionParallelConfig,
|
||||
OmniDiffusionConfig,
|
||||
set_current_omni_diffusion_config,
|
||||
)
|
||||
from vllm_omni.diffusion.distributed.parallel_state import (
|
||||
destroy_distributed_env,
|
||||
init_distributed_environment,
|
||||
initialize_model_parallel,
|
||||
)
|
||||
from vllm_omni.diffusion.forward_context import set_forward_context
|
||||
from vllm_omni.utils.platform_utils import detect_device_type
|
||||
|
||||
device_type = detect_device_type()
|
||||
@@ -170,6 +170,11 @@ def test_sequence_parallel(
|
||||
"""Test Ulysses attention by comparing with and without SP enabled."""
|
||||
sequence_parallel_size = ulysses_degree * ring_degree
|
||||
|
||||
# Skip if not enough GPUs available
|
||||
available_gpus = torch_device.device_count()
|
||||
if available_gpus < sequence_parallel_size:
|
||||
pytest.skip(f"Test requires {sequence_parallel_size} GPUs but only {available_gpus} available")
|
||||
|
||||
# Create temporary files to share results between processes
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix=".pkl") as f:
|
||||
baseline_output_file = f.name
|
||||
@@ -385,7 +390,7 @@ def ulysses_attention_on_test_model(
|
||||
)
|
||||
|
||||
# Set the config so Attention can access it
|
||||
with set_current_omni_diffusion_config(od_config):
|
||||
with set_forward_context(omni_diffusion_config=od_config):
|
||||
# Create model
|
||||
hidden_size = num_heads * head_size
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
"""
|
||||
Test script for FlashAttention backend with padding handling.
|
||||
|
||||
This script tests two main scenarios:
|
||||
1. Case 1: Comparing padded vs unpadded inputs for batch_size=1
|
||||
2. Case 2: Comparing FlashAttention and SDPA backends for batch_size=2 with padding
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from vllm_omni.diffusion.attention.backends.abstract import AttentionMetadata
|
||||
from vllm_omni.diffusion.attention.backends.flash_attn import FlashAttentionImpl
|
||||
from vllm_omni.diffusion.attention.backends.sdpa import SDPAImpl
|
||||
|
||||
|
||||
def create_attention_mask(batch_size: int, seq_len: int, valid_len: int, device: torch.device) -> torch.Tensor:
|
||||
"""
|
||||
Create attention mask where first valid_len tokens are valid (1) and rest are padding (0).
|
||||
|
||||
Args:
|
||||
batch_size: Batch size
|
||||
seq_len: Total sequence length (including padding)
|
||||
valid_len: Number of valid (non-padded) tokens
|
||||
|
||||
Returns:
|
||||
Attention mask of shape (batch_size, seq_len)
|
||||
"""
|
||||
mask = torch.zeros(batch_size, seq_len, dtype=torch.bool, device=device)
|
||||
mask[:, :valid_len] = True
|
||||
return mask
|
||||
|
||||
|
||||
def pad_tensor(tensor: torch.Tensor, target_seq_len: int, pad_value: float = 0.0) -> torch.Tensor:
|
||||
"""
|
||||
Pad tensor along sequence dimension (dim=1).
|
||||
|
||||
Args:
|
||||
tensor: Input tensor of shape (batch_size, seq_len, num_heads, head_dim)
|
||||
target_seq_len: Target sequence length after padding
|
||||
pad_value: Value to use for padding
|
||||
|
||||
Returns:
|
||||
Padded tensor of shape (batch_size, target_seq_len, num_heads, head_dim)
|
||||
"""
|
||||
batch_size, seq_len, num_heads, head_dim = tensor.shape
|
||||
if target_seq_len <= seq_len:
|
||||
return tensor
|
||||
|
||||
padding = torch.full(
|
||||
(batch_size, target_seq_len - seq_len, num_heads, head_dim), pad_value, dtype=tensor.dtype, device=tensor.device
|
||||
)
|
||||
return torch.cat([tensor, padding], dim=1)
|
||||
|
||||
|
||||
@pytest.mark.skipif(not torch.cuda.is_available(), reason="FlashAttention requires CUDA")
|
||||
def test_padding_equivalence():
|
||||
"""
|
||||
Case 1: Test that padded and unpadded inputs produce similar outputs.
|
||||
|
||||
- Input A: batch_size=1, hidden_states (1, 48), encoder_hidden_states (1, 16)
|
||||
Concatenated length: 64, NO attention_mask
|
||||
- Input B: Same data but padded: hidden_states (1, 58), encoder_hidden_states (1, 26)
|
||||
Concatenated length: 84, WITH attention_mask
|
||||
|
||||
Expected: Output A and Output B should be very close.
|
||||
"""
|
||||
device = torch.device("cuda")
|
||||
dtype = torch.bfloat16
|
||||
|
||||
# Configuration
|
||||
batch_size = 1
|
||||
hidden_seq_len = 48
|
||||
encoder_seq_len = 16
|
||||
pad_length = 10
|
||||
num_heads = 8
|
||||
head_dim = 64
|
||||
|
||||
# Initialize FlashAttention
|
||||
fa_impl = FlashAttentionImpl(
|
||||
num_heads=num_heads, head_size=head_dim, softmax_scale=1.0 / (head_dim**0.5), causal=False
|
||||
)
|
||||
|
||||
# Create base tensors with random values (same for both A and B)
|
||||
torch.manual_seed(42)
|
||||
hidden_states_base = torch.randn(batch_size, hidden_seq_len, num_heads, head_dim, device=device, dtype=dtype)
|
||||
encoder_hidden_states_base = torch.randn(
|
||||
batch_size, encoder_seq_len, num_heads, head_dim, device=device, dtype=dtype
|
||||
)
|
||||
|
||||
# ========== Input A: Unpadded, no attention mask ==========
|
||||
query_a = torch.cat([hidden_states_base, encoder_hidden_states_base], dim=1)
|
||||
key_a = query_a.clone()
|
||||
value_a = query_a.clone()
|
||||
|
||||
attn_metadata_a = AttentionMetadata(attn_mask=None)
|
||||
|
||||
output_a = fa_impl.forward(query=query_a, key=key_a, value=value_a, attn_metadata=attn_metadata_a)
|
||||
|
||||
# ========== Input B: Padded with attention mask ==========
|
||||
hidden_states_padded = pad_tensor(hidden_states_base, hidden_seq_len + pad_length)
|
||||
encoder_hidden_states_padded = pad_tensor(encoder_hidden_states_base, encoder_seq_len + pad_length)
|
||||
|
||||
query_b = torch.cat([hidden_states_padded, encoder_hidden_states_padded], dim=1)
|
||||
key_b = query_b.clone()
|
||||
value_b = query_b.clone()
|
||||
|
||||
# Create attention mask
|
||||
attn_mask_b = torch.cat(
|
||||
[
|
||||
create_attention_mask(batch_size, hidden_seq_len + pad_length, hidden_seq_len, device),
|
||||
create_attention_mask(batch_size, encoder_seq_len + pad_length, encoder_seq_len, device),
|
||||
],
|
||||
dim=1,
|
||||
)
|
||||
|
||||
attn_metadata_b = AttentionMetadata(attn_mask=attn_mask_b)
|
||||
|
||||
output_b = fa_impl.forward(query=query_b, key=key_b, value=value_b, attn_metadata=attn_metadata_b)
|
||||
|
||||
# Extract non-padded portion from output_b
|
||||
output_b_unpadded = torch.cat(
|
||||
[
|
||||
output_b[:, :hidden_seq_len, :, :],
|
||||
output_b[:, hidden_seq_len + pad_length : hidden_seq_len + pad_length + encoder_seq_len, :, :],
|
||||
],
|
||||
dim=1,
|
||||
)
|
||||
|
||||
# Compare outputs
|
||||
max_diff = torch.max(torch.abs(output_a - output_b_unpadded)).item()
|
||||
mean_diff = torch.mean(torch.abs(output_a - output_b_unpadded)).item()
|
||||
|
||||
print("\n=== Case 1: Padding Equivalence Test ===")
|
||||
print(f"Output A shape: {output_a.shape}")
|
||||
print(f"Output B shape: {output_b.shape}")
|
||||
print(f"Output B unpadded shape: {output_b_unpadded.shape}")
|
||||
print(f"Max absolute difference: {max_diff:.6f}")
|
||||
print(f"Mean absolute difference: {mean_diff:.6f}")
|
||||
|
||||
# Assert that outputs are close
|
||||
# Using higher tolerance for bfloat16
|
||||
assert max_diff < 0.1, f"Max difference {max_diff} exceeds threshold 0.1"
|
||||
assert mean_diff < 0.01, f"Mean difference {mean_diff} exceeds threshold 0.01"
|
||||
|
||||
print("✓ Case 1 PASSED: Padded and unpadded outputs are very close!")
|
||||
|
||||
|
||||
@pytest.mark.skipif(not torch.cuda.is_available(), reason="FlashAttention requires CUDA")
|
||||
def test_fa_vs_sdpa():
|
||||
"""
|
||||
Case 2: Compare FlashAttention and SDPA backends with padding.
|
||||
|
||||
- batch_size=2
|
||||
- hidden_states: (2, 48) padded to (2, 58)
|
||||
- encoder_hidden_states: (2, 16) padded to (2, 26)
|
||||
- Concatenated length: 84
|
||||
- Compare FA and SDPA outputs
|
||||
|
||||
Expected: FA and SDPA outputs should be very close.
|
||||
"""
|
||||
device = torch.device("cuda")
|
||||
dtype = torch.bfloat16
|
||||
|
||||
# Configuration
|
||||
batch_size = 2
|
||||
hidden_seq_len = 48
|
||||
encoder_seq_len = 16
|
||||
pad_length = 10
|
||||
num_heads = 8
|
||||
head_dim = 64
|
||||
|
||||
# Initialize both backends
|
||||
fa_impl = FlashAttentionImpl(
|
||||
num_heads=num_heads, head_size=head_dim, softmax_scale=1.0 / (head_dim**0.5), causal=False
|
||||
)
|
||||
|
||||
sdpa_impl = SDPAImpl(num_heads=num_heads, head_size=head_dim, softmax_scale=1.0 / (head_dim**0.5), causal=False)
|
||||
|
||||
# Create base tensors
|
||||
torch.manual_seed(123)
|
||||
hidden_states_base = torch.randn(batch_size, hidden_seq_len, num_heads, head_dim, device=device, dtype=dtype)
|
||||
encoder_hidden_states_base = torch.randn(
|
||||
batch_size, encoder_seq_len, num_heads, head_dim, device=device, dtype=dtype
|
||||
)
|
||||
|
||||
# Pad tensors
|
||||
hidden_states_padded = pad_tensor(hidden_states_base, hidden_seq_len + pad_length)
|
||||
encoder_hidden_states_padded = pad_tensor(encoder_hidden_states_base, encoder_seq_len + pad_length)
|
||||
|
||||
# Concatenate
|
||||
query = torch.cat([hidden_states_padded, encoder_hidden_states_padded], dim=1)
|
||||
key = query.clone()
|
||||
value = query.clone()
|
||||
|
||||
# Create attention mask
|
||||
attn_mask = torch.cat(
|
||||
[
|
||||
create_attention_mask(batch_size, hidden_seq_len + pad_length, hidden_seq_len, device),
|
||||
create_attention_mask(batch_size, encoder_seq_len + pad_length, encoder_seq_len, device),
|
||||
],
|
||||
dim=1,
|
||||
)
|
||||
|
||||
attn_metadata = AttentionMetadata(attn_mask=attn_mask)
|
||||
|
||||
# Run FlashAttention
|
||||
output_fa = fa_impl.forward(query=query.clone(), key=key.clone(), value=value.clone(), attn_metadata=attn_metadata)
|
||||
|
||||
# Run SDPA
|
||||
# SDPA expects 4D attention mask: (batch_size, 1, seq_len, seq_len) or (batch_size, seq_len)
|
||||
# For causal=False, we need to convert 2D mask to 4D
|
||||
if attn_mask is not None:
|
||||
# Expand mask for SDPA: (batch_size, seq_len) -> (batch_size, 1, 1, seq_len)
|
||||
attn_mask_4d = attn_mask.unsqueeze(1).unsqueeze(2)
|
||||
# Convert bool to float: True -> 0.0, False -> -inf
|
||||
attn_mask_float = torch.zeros_like(attn_mask_4d, dtype=dtype)
|
||||
attn_mask_float.masked_fill_(~attn_mask_4d, float("-inf"))
|
||||
attn_metadata_sdpa = AttentionMetadata(attn_mask=attn_mask_float)
|
||||
else:
|
||||
attn_metadata_sdpa = AttentionMetadata(attn_mask=None)
|
||||
|
||||
output_sdpa = sdpa_impl.forward(
|
||||
query=query.clone(), key=key.clone(), value=value.clone(), attn_metadata=attn_metadata_sdpa
|
||||
)
|
||||
|
||||
# Compare outputs (only compare valid regions)
|
||||
output_fa_valid = torch.cat(
|
||||
[
|
||||
output_fa[:, :hidden_seq_len, :, :],
|
||||
output_fa[:, hidden_seq_len + pad_length : hidden_seq_len + pad_length + encoder_seq_len, :, :],
|
||||
],
|
||||
dim=1,
|
||||
)
|
||||
output_sdpa_valid = torch.cat(
|
||||
[
|
||||
output_sdpa[:, :hidden_seq_len, :, :],
|
||||
output_sdpa[:, hidden_seq_len + pad_length : hidden_seq_len + pad_length + encoder_seq_len, :, :],
|
||||
],
|
||||
dim=1,
|
||||
)
|
||||
|
||||
max_diff = torch.max(torch.abs(output_fa_valid - output_sdpa_valid)).item()
|
||||
mean_diff = torch.mean(torch.abs(output_fa_valid - output_sdpa_valid)).item()
|
||||
|
||||
print("\n=== Case 2: FA vs SDPA Comparison ===")
|
||||
print(f"Batch size: {batch_size}")
|
||||
print(f"FA output shape: {output_fa.shape}")
|
||||
print(f"SDPA output shape: {output_sdpa.shape}")
|
||||
print(f"Max absolute difference (valid region): {max_diff:.6f}")
|
||||
print(f"Mean absolute difference (valid region): {mean_diff:.6f}")
|
||||
|
||||
# Assert that outputs are close
|
||||
# Using higher tolerance for bfloat16 and different implementations
|
||||
assert max_diff < 0.01, f"Max difference {max_diff} exceeds threshold 0.01"
|
||||
assert mean_diff < 0.001, f"Mean difference {mean_diff} exceeds threshold 0.001"
|
||||
|
||||
print("✓ Case 2 PASSED: FA and SDPA outputs are very close!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Running FlashAttention Padding Tests...")
|
||||
print("=" * 60)
|
||||
|
||||
# Try to run CUDA tests
|
||||
if torch.cuda.is_available():
|
||||
try:
|
||||
print("\n[Running Case 1: Padding Equivalence for FA]")
|
||||
test_padding_equivalence()
|
||||
except Exception as e:
|
||||
print(f"✗ Case 1 failed: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
|
||||
try:
|
||||
print("\n[Running Case 2: FA vs SDPA]")
|
||||
test_fa_vs_sdpa()
|
||||
except Exception as e:
|
||||
print(f"✗ Case 2 failed: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
else:
|
||||
raise RuntimeError("CUDA is not available")
|
||||
print("\n" + "=" * 60)
|
||||
print("Test suite completed!")
|
||||
@@ -48,6 +48,11 @@ def test_4d_identity(
|
||||
use_sync: bool,
|
||||
):
|
||||
"""Test that two consecutive all-to-all operations return the original input."""
|
||||
# Skip if not enough GPUs available
|
||||
available_gpus = torch_device.device_count()
|
||||
if available_gpus < world_size:
|
||||
pytest.skip(f"Test requires {world_size} GPUs but only {available_gpus} available")
|
||||
|
||||
# Ensure num_heads is divisible by world_size
|
||||
if num_heads % world_size != 0:
|
||||
pytest.skip(f"num_heads ({num_heads}) not divisible by world_size ({world_size})")
|
||||
@@ -177,6 +182,11 @@ def test_5d_identity(
|
||||
use_sync: bool,
|
||||
):
|
||||
"""Test that two consecutive all-to-all operations return the original input."""
|
||||
# Skip if not enough GPUs available
|
||||
available_gpus = torch_device.device_count()
|
||||
if available_gpus < world_size:
|
||||
pytest.skip(f"Test requires {world_size} GPUs but only {available_gpus} available")
|
||||
|
||||
# Ensure num_heads is divisible by world_size
|
||||
if num_heads % world_size != 0:
|
||||
pytest.skip(f"num_heads ({num_heads}) not divisible by world_size ({world_size})")
|
||||
@@ -305,6 +315,11 @@ def test_ring_p2p(
|
||||
head_size: int,
|
||||
):
|
||||
"""Test Ring P2P communication (send_recv)."""
|
||||
# Skip if not enough GPUs available
|
||||
available_gpus = torch_device.device_count()
|
||||
if available_gpus < world_size:
|
||||
pytest.skip(f"Test requires {world_size} GPUs but only {available_gpus} available")
|
||||
|
||||
torch.multiprocessing.spawn(
|
||||
_test_ring_p2p_worker,
|
||||
args=(world_size, dtype, batch_size, num_heads, head_size),
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,44 @@
|
||||
import pytest
|
||||
|
||||
from vllm_omni.diffusion.models.z_image.z_image_transformer import validate_zimage_tp_constraints
|
||||
|
||||
|
||||
def test_validate_zimage_tp_constraints_tp2_ok():
|
||||
ffn_hidden_dim, final_out_dims, supported_tp = validate_zimage_tp_constraints(
|
||||
dim=3840,
|
||||
n_heads=30,
|
||||
n_kv_heads=30,
|
||||
in_channels=16,
|
||||
all_patch_size=(2,),
|
||||
all_f_patch_size=(1,),
|
||||
tensor_parallel_size=2,
|
||||
)
|
||||
assert ffn_hidden_dim == 10240
|
||||
assert final_out_dims == [64]
|
||||
assert supported_tp == [1, 2]
|
||||
|
||||
|
||||
def test_validate_zimage_tp_constraints_tp4_fails_on_heads():
|
||||
with pytest.raises(ValueError, match=r"n_heads % tensor_parallel_size"):
|
||||
validate_zimage_tp_constraints(
|
||||
dim=3840,
|
||||
n_heads=30,
|
||||
n_kv_heads=30,
|
||||
in_channels=16,
|
||||
all_patch_size=(2,),
|
||||
all_f_patch_size=(1,),
|
||||
tensor_parallel_size=4,
|
||||
)
|
||||
|
||||
|
||||
def test_validate_zimage_tp_constraints_tp3_fails_on_ffn_hidden_dim():
|
||||
with pytest.raises(ValueError, match=r"ffn_hidden_dim % tensor_parallel_size"):
|
||||
validate_zimage_tp_constraints(
|
||||
dim=3840,
|
||||
n_heads=30,
|
||||
n_kv_heads=30,
|
||||
in_channels=16,
|
||||
all_patch_size=(2,),
|
||||
all_f_patch_size=(1,),
|
||||
tensor_parallel_size=3,
|
||||
)
|
||||
@@ -0,0 +1,262 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
"""
|
||||
Unit tests for GPUDiffusionWorker class.
|
||||
|
||||
This module tests the GPUDiffusionWorker implementation:
|
||||
- load_weights: Loading model weights
|
||||
- sleep: Putting worker into sleep mode (levels 1 and 2)
|
||||
- wake_up: Waking worker from sleep mode
|
||||
"""
|
||||
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from vllm_omni.diffusion.worker.gpu_diffusion_worker import GPUDiffusionWorker
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_od_config():
|
||||
"""Create a mock OmniDiffusionConfig."""
|
||||
config = Mock()
|
||||
config.num_gpus = 1
|
||||
config.master_port = 12345
|
||||
config.enable_sleep_mode = False
|
||||
config.cache_backend = None
|
||||
config.cache_config = None
|
||||
config.model = "test-model"
|
||||
return config
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_gpu_worker(mock_od_config):
|
||||
"""Create a GPUDiffusionWorker with mocked initialization."""
|
||||
with patch.object(GPUDiffusionWorker, "init_device"):
|
||||
worker = GPUDiffusionWorker(local_rank=0, rank=0, od_config=mock_od_config)
|
||||
# Mock the model_runner with pipeline
|
||||
worker.model_runner = Mock()
|
||||
worker.model_runner.pipeline = Mock()
|
||||
worker._sleep_saved_buffers = {}
|
||||
return worker
|
||||
|
||||
|
||||
class TestGPUDiffusionWorkerLoadWeights:
|
||||
"""Test GPUDiffusionWorker.load_weights method."""
|
||||
|
||||
def test_load_weights_calls_pipeline(self, mock_gpu_worker):
|
||||
"""Test that load_weights delegates to model_runner.load_weights."""
|
||||
# Setup mock weights
|
||||
mock_weights = [
|
||||
("layer1.weight", torch.randn(10, 10)),
|
||||
("layer2.weight", torch.randn(20, 20)),
|
||||
]
|
||||
expected_loaded = {"layer1.weight", "layer2.weight"}
|
||||
|
||||
# Configure model_runner mock
|
||||
mock_gpu_worker.model_runner.load_weights = Mock(return_value=expected_loaded)
|
||||
|
||||
# Call load_weights
|
||||
result = mock_gpu_worker.load_weights(mock_weights)
|
||||
|
||||
# Verify model_runner.load_weights was called with the weights
|
||||
mock_gpu_worker.model_runner.load_weights.assert_called_once_with(mock_weights)
|
||||
assert result == expected_loaded
|
||||
|
||||
def test_load_weights_empty_iterable(self, mock_gpu_worker):
|
||||
"""Test load_weights with empty weights iterable."""
|
||||
mock_gpu_worker.model_runner.load_weights = Mock(return_value=set())
|
||||
|
||||
result = mock_gpu_worker.load_weights([])
|
||||
|
||||
mock_gpu_worker.model_runner.load_weights.assert_called_once_with([])
|
||||
assert result == set()
|
||||
|
||||
|
||||
class TestGPUDiffusionWorkerSleep:
|
||||
"""Test GPUDiffusionWorker.sleep method."""
|
||||
|
||||
@patch("vllm_omni.diffusion.worker.gpu_diffusion_worker.torch.cuda.mem_get_info")
|
||||
@patch("vllm.device_allocator.cumem.CuMemAllocator")
|
||||
def test_sleep_level_1(self, mock_allocator_class, mock_mem_info, mock_gpu_worker):
|
||||
"""Test sleep mode level 1 (offload weights only)."""
|
||||
# Setup memory info mocks
|
||||
# Before sleep: 1GB free, 8GB total
|
||||
# After sleep: 3GB free, 8GB total (freed 2GB)
|
||||
mock_mem_info.side_effect = [
|
||||
(1 * 1024**3, 8 * 1024**3), # Before sleep
|
||||
(3 * 1024**3, 8 * 1024**3), # After sleep
|
||||
]
|
||||
|
||||
# Setup allocator mock
|
||||
mock_allocator = Mock()
|
||||
mock_allocator_class.get_instance = Mock(return_value=mock_allocator)
|
||||
mock_allocator.sleep = Mock()
|
||||
|
||||
# Call sleep with level 1
|
||||
result = mock_gpu_worker.sleep(level=1)
|
||||
|
||||
# Verify sleep was called with correct tags
|
||||
mock_allocator.sleep.assert_called_once_with(offload_tags=("weights",))
|
||||
assert result is True
|
||||
# Verify buffers were NOT saved (level 1 doesn't save buffers)
|
||||
assert len(mock_gpu_worker._sleep_saved_buffers) == 0
|
||||
|
||||
@patch("vllm_omni.diffusion.worker.gpu_diffusion_worker.torch.cuda.mem_get_info")
|
||||
@patch("vllm.device_allocator.cumem.CuMemAllocator")
|
||||
def test_sleep_level_2(self, mock_allocator_class, mock_mem_info, mock_gpu_worker):
|
||||
"""Test sleep mode level 2 (offload all, save buffers)."""
|
||||
# Setup memory info mocks
|
||||
mock_mem_info.side_effect = [
|
||||
(1 * 1024**3, 8 * 1024**3), # Before sleep
|
||||
(5 * 1024**3, 8 * 1024**3), # After sleep (freed 4GB)
|
||||
]
|
||||
|
||||
# Setup allocator mock
|
||||
mock_allocator = Mock()
|
||||
mock_allocator_class.get_instance = Mock(return_value=mock_allocator)
|
||||
mock_allocator.sleep = Mock()
|
||||
|
||||
# Mock pipeline buffers
|
||||
mock_buffer1 = torch.randn(10, 10)
|
||||
mock_buffer2 = torch.randn(20, 20)
|
||||
mock_gpu_worker.model_runner.pipeline.named_buffers = Mock(
|
||||
return_value=[
|
||||
("buffer1", mock_buffer1),
|
||||
("buffer2", mock_buffer2),
|
||||
]
|
||||
)
|
||||
|
||||
# Call sleep with level 2
|
||||
result = mock_gpu_worker.sleep(level=2)
|
||||
|
||||
# Verify sleep was called with empty tags (offload all)
|
||||
mock_allocator.sleep.assert_called_once_with(offload_tags=tuple())
|
||||
assert result is True
|
||||
|
||||
# Verify buffers were saved
|
||||
assert len(mock_gpu_worker._sleep_saved_buffers) == 2
|
||||
assert "buffer1" in mock_gpu_worker._sleep_saved_buffers
|
||||
assert "buffer2" in mock_gpu_worker._sleep_saved_buffers
|
||||
|
||||
@patch("vllm_omni.diffusion.worker.gpu_diffusion_worker.torch.cuda.mem_get_info")
|
||||
@patch("vllm.device_allocator.cumem.CuMemAllocator")
|
||||
def test_sleep_memory_freed_validation(self, mock_allocator_class, mock_mem_info, mock_gpu_worker):
|
||||
"""Test that sleep validates memory was actually freed."""
|
||||
# Simulate memory increase (should trigger assertion error)
|
||||
mock_mem_info.side_effect = [
|
||||
(3 * 1024**3, 8 * 1024**3), # Before sleep: 3GB free
|
||||
(1 * 1024**3, 8 * 1024**3), # After sleep: 1GB free (negative freed!)
|
||||
]
|
||||
|
||||
mock_allocator = Mock()
|
||||
mock_allocator_class.get_instance = Mock(return_value=mock_allocator)
|
||||
mock_allocator.sleep = Mock()
|
||||
|
||||
# This should raise an assertion error
|
||||
with pytest.raises(AssertionError, match="Memory usage increased after sleeping"):
|
||||
mock_gpu_worker.sleep(level=1)
|
||||
|
||||
|
||||
class TestGPUDiffusionWorkerWakeUp:
|
||||
"""Test GPUDiffusionWorker.wake_up method."""
|
||||
|
||||
@patch("vllm.device_allocator.cumem.CuMemAllocator")
|
||||
def test_wake_up_without_buffers(self, mock_allocator_class, mock_gpu_worker):
|
||||
"""Test wake_up without saved buffers (level 1 sleep)."""
|
||||
# Setup allocator mock
|
||||
mock_allocator = Mock()
|
||||
mock_allocator_class.get_instance = Mock(return_value=mock_allocator)
|
||||
mock_allocator.wake_up = Mock()
|
||||
|
||||
# Ensure no saved buffers
|
||||
mock_gpu_worker._sleep_saved_buffers = {}
|
||||
|
||||
# Call wake_up
|
||||
result = mock_gpu_worker.wake_up(tags=["weights"])
|
||||
|
||||
# Verify allocator.wake_up was called
|
||||
mock_allocator.wake_up.assert_called_once_with(["weights"])
|
||||
assert result is True
|
||||
|
||||
@patch("vllm.device_allocator.cumem.CuMemAllocator")
|
||||
def test_wake_up_with_buffers(self, mock_allocator_class, mock_gpu_worker):
|
||||
"""Test wake_up with saved buffers (level 2 sleep)."""
|
||||
# Setup allocator mock
|
||||
mock_allocator = Mock()
|
||||
mock_allocator_class.get_instance = Mock(return_value=mock_allocator)
|
||||
mock_allocator.wake_up = Mock()
|
||||
|
||||
# Create saved buffers
|
||||
saved_buffer1 = torch.randn(10, 10)
|
||||
saved_buffer2 = torch.randn(20, 20)
|
||||
mock_gpu_worker._sleep_saved_buffers = {
|
||||
"buffer1": saved_buffer1,
|
||||
"buffer2": saved_buffer2,
|
||||
}
|
||||
|
||||
# Mock pipeline buffers (these will be restored)
|
||||
mock_buffer1 = Mock()
|
||||
mock_buffer1.data = Mock()
|
||||
mock_buffer2 = Mock()
|
||||
mock_buffer2.data = Mock()
|
||||
|
||||
mock_gpu_worker.model_runner.pipeline.named_buffers = Mock(
|
||||
return_value=[
|
||||
("buffer1", mock_buffer1),
|
||||
("buffer2", mock_buffer2),
|
||||
]
|
||||
)
|
||||
|
||||
# Call wake_up
|
||||
result = mock_gpu_worker.wake_up(tags=None)
|
||||
|
||||
# Verify allocator.wake_up was called
|
||||
mock_allocator.wake_up.assert_called_once_with(None)
|
||||
|
||||
# Verify buffers were restored
|
||||
mock_buffer1.data.copy_.assert_called_once()
|
||||
mock_buffer2.data.copy_.assert_called_once()
|
||||
|
||||
# Verify saved buffers were cleared
|
||||
assert len(mock_gpu_worker._sleep_saved_buffers) == 0
|
||||
assert result is True
|
||||
|
||||
@patch("vllm.device_allocator.cumem.CuMemAllocator")
|
||||
def test_wake_up_partial_buffer_restore(self, mock_allocator_class, mock_gpu_worker):
|
||||
"""Test wake_up only restores buffers that were saved."""
|
||||
# Setup allocator mock
|
||||
mock_allocator = Mock()
|
||||
mock_allocator_class.get_instance = Mock(return_value=mock_allocator)
|
||||
mock_allocator.wake_up = Mock()
|
||||
|
||||
# Only save buffer1, not buffer2
|
||||
saved_buffer1 = torch.randn(10, 10)
|
||||
mock_gpu_worker._sleep_saved_buffers = {
|
||||
"buffer1": saved_buffer1,
|
||||
}
|
||||
|
||||
# Mock pipeline has both buffers
|
||||
mock_buffer1 = Mock()
|
||||
mock_buffer1.data = Mock()
|
||||
mock_buffer2 = Mock()
|
||||
mock_buffer2.data = Mock()
|
||||
|
||||
mock_gpu_worker.model_runner.pipeline.named_buffers = Mock(
|
||||
return_value=[
|
||||
("buffer1", mock_buffer1),
|
||||
("buffer2", mock_buffer2),
|
||||
]
|
||||
)
|
||||
|
||||
# Call wake_up
|
||||
result = mock_gpu_worker.wake_up()
|
||||
|
||||
# Verify only buffer1 was restored
|
||||
mock_buffer1.data.copy_.assert_called_once()
|
||||
# buffer2 should NOT be restored since it wasn't saved
|
||||
mock_buffer2.data.copy_.assert_not_called()
|
||||
|
||||
assert result is True
|
||||
@@ -0,0 +1,189 @@
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import torch
|
||||
|
||||
from vllm_omni.diffusion.models.bagel.pipeline_bagel import BagelPipeline
|
||||
from vllm_omni.diffusion.request import OmniDiffusionRequest
|
||||
from vllm_omni.worker.gpu_ar_model_runner import GPUARModelRunner
|
||||
|
||||
|
||||
class MockGPUARModelRunner(GPUARModelRunner):
|
||||
"""Subclass to bypass heavy initialization."""
|
||||
|
||||
def __init__(self, kv_caches, input_batch):
|
||||
self.kv_caches = kv_caches
|
||||
self.input_batch = input_batch
|
||||
self.device = "cpu"
|
||||
self.cache_config = MagicMock()
|
||||
self.cache_config.block_size = 16
|
||||
self.cache_config.cache_dtype = "auto"
|
||||
self.logger = MagicMock()
|
||||
|
||||
|
||||
class MockBagelPipeline(BagelPipeline):
|
||||
"""Subclass to bypass heavy initialization."""
|
||||
|
||||
def __init__(self):
|
||||
self.device = "cpu"
|
||||
self.od_config = MagicMock()
|
||||
self.bagel = MagicMock()
|
||||
self.tokenizer = MagicMock()
|
||||
self.language_model = MagicMock()
|
||||
self.new_token_ids = {}
|
||||
|
||||
|
||||
class TestKVFlow(unittest.TestCase):
|
||||
def setUp(self):
|
||||
# Common constants
|
||||
self.num_layers = 2
|
||||
self.num_heads = 4
|
||||
self.head_dim = 16
|
||||
self.block_size = 8
|
||||
self.x = 8 # PagedAttention factor
|
||||
self.seq_len = 20
|
||||
self.req_id = "req_test_1"
|
||||
|
||||
def test_sender_extraction_logic(self):
|
||||
"""Test extraction logic in GPUARModelRunner."""
|
||||
if GPUARModelRunner is object:
|
||||
self.skipTest("vLLM not installed")
|
||||
|
||||
# 1. Setup KV Cache (List of tuples (K, V))
|
||||
# Shape: [num_blocks, block_size, num_heads, head_dim] matching 4D expectation
|
||||
num_blocks = 10
|
||||
kv_caches = []
|
||||
for _ in range(self.num_layers):
|
||||
k_cache = torch.randn(num_blocks, self.block_size, self.num_heads, self.head_dim)
|
||||
v_cache = torch.randn(num_blocks, self.block_size, self.num_heads, self.head_dim)
|
||||
# Stack K and V to create [2, num_blocks, block_size, n_heads, head_dim]
|
||||
layer_cache = torch.stack([k_cache, v_cache], dim=0)
|
||||
kv_caches.append(layer_cache)
|
||||
|
||||
# 2. Setup Input Batch Mock
|
||||
block_ids = [1, 3, 5]
|
||||
mock_input_batch = MagicMock()
|
||||
mock_input_batch.req_id_to_index = {self.req_id: 0}
|
||||
mock_input_batch.block_table.get_row.return_value = block_ids
|
||||
|
||||
# 3. Instantiate Runner
|
||||
runner = MockGPUARModelRunner(kv_caches, mock_input_batch)
|
||||
|
||||
# 4. Run Extraction
|
||||
# calling _extract_kv_cache directly: (req_id, block_ids, seq_len)
|
||||
result = runner._extract_kv_cache(self.req_id, block_ids, self.seq_len)
|
||||
|
||||
# 5. Verify Result
|
||||
self.assertIsNotNone(result)
|
||||
self.assertEqual(result.request_id, self.req_id)
|
||||
|
||||
# Check keys "key_cache" and "value_cache" exist (length 2)
|
||||
self.assertEqual(len(result.layer_blocks["key_cache"]), 2)
|
||||
self.assertEqual(len(result.layer_blocks["value_cache"]), 2)
|
||||
|
||||
# Check Tensor Shape: [seq_len, num_heads, head_dim]
|
||||
# result.layer_blocks["key_cache"] is a list of tensors
|
||||
expected_shape = (self.seq_len, self.num_heads, self.head_dim)
|
||||
self.assertEqual(result.layer_blocks["key_cache"][0].shape, expected_shape)
|
||||
|
||||
return result # Return for use in next test
|
||||
|
||||
def test_receiver_injection_logic(self):
|
||||
"""Test injection logic in BagelPipeline."""
|
||||
if BagelPipeline is object:
|
||||
self.skipTest("vLLM not installed")
|
||||
|
||||
# 1. Get Data from Sender Test (simulate transfer)
|
||||
# Re-create data manually to be independent
|
||||
key_cache = []
|
||||
value_cache = []
|
||||
expected_shape = (self.seq_len, self.num_heads, self.head_dim)
|
||||
for i in range(self.num_layers):
|
||||
key_cache.append(torch.randn(expected_shape))
|
||||
value_cache.append(torch.randn(expected_shape))
|
||||
|
||||
layer_blocks = {"key_cache": key_cache, "value_cache": value_cache}
|
||||
|
||||
transfer_data = MagicMock() # Mock KVCacheTransferData
|
||||
transfer_data.layer_blocks = layer_blocks
|
||||
transfer_data.metadata = {"kv_lens": [self.seq_len], "ropes": [0]}
|
||||
|
||||
# 2. Setup Request with Injected Data
|
||||
req = OmniDiffusionRequest(prompt="test")
|
||||
from types import SimpleNamespace
|
||||
|
||||
req.past_key_values = SimpleNamespace(**layer_blocks)
|
||||
req.kv_metadata = transfer_data.metadata
|
||||
|
||||
# 3. Setup Pipeline
|
||||
pipeline = MockBagelPipeline()
|
||||
|
||||
# Mock Bagel's NaiveCache
|
||||
class RealNaiveCache: # Minimal impl
|
||||
def __init__(self, n):
|
||||
self.key_cache = {i: None for i in range(n)}
|
||||
self.value_cache = {i: None for i in range(n)}
|
||||
|
||||
pipeline.bagel.config.llm_config.num_hidden_layers = self.num_layers
|
||||
|
||||
captured_context = {}
|
||||
|
||||
def mock_prepare_prompts(curr_kvlens, curr_rope, **kwargs):
|
||||
# Capture the state passed to prepare_prompts
|
||||
captured_context["kv_lens"] = curr_kvlens
|
||||
captured_context["ropes"] = curr_rope
|
||||
return {}, [0], [0]
|
||||
|
||||
pipeline.bagel.prepare_prompts = MagicMock(side_effect=mock_prepare_prompts)
|
||||
with patch("vllm_omni.diffusion.models.bagel.pipeline_bagel.NaiveCache") as MockNaiveCacheCls:
|
||||
# Setup the instance returned by the constructor
|
||||
mock_cache_instance = RealNaiveCache(self.num_layers)
|
||||
MockNaiveCacheCls.return_value = mock_cache_instance
|
||||
|
||||
# Verification of Injection Logic (Simulation)
|
||||
current_cache = RealNaiveCache(self.num_layers)
|
||||
|
||||
# --- Logic from Source Code ---
|
||||
injected_kv = req.past_key_values
|
||||
if isinstance(current_cache, RealNaiveCache) and hasattr(injected_kv, "key_cache"):
|
||||
# Assuming injected_kv is SimpleNamespace or object with list attrs
|
||||
for layer_idx in range(len(injected_kv.key_cache)):
|
||||
if injected_kv.key_cache[layer_idx] is not None:
|
||||
k_tensor = injected_kv.key_cache[layer_idx]
|
||||
v_tensor = injected_kv.value_cache[layer_idx]
|
||||
|
||||
if k_tensor.device != pipeline.device:
|
||||
k_tensor = k_tensor.to(pipeline.device)
|
||||
if v_tensor.device != pipeline.device:
|
||||
v_tensor = v_tensor.to(pipeline.device)
|
||||
|
||||
current_cache.key_cache[layer_idx] = k_tensor
|
||||
current_cache.value_cache[layer_idx] = v_tensor
|
||||
|
||||
self.assertTrue(torch.allclose(current_cache.key_cache[0], layer_blocks["key_cache"][0]))
|
||||
self.assertTrue(torch.allclose(current_cache.value_cache[1], layer_blocks["value_cache"][1]))
|
||||
|
||||
def test_integration(self):
|
||||
"""Simulate the flow from Sender -> Connector (Dict) -> Receiver."""
|
||||
if GPUARModelRunner is object or BagelPipeline is object:
|
||||
self.skipTest("vLLM not installed")
|
||||
|
||||
# 1. Sender (Extraction)
|
||||
runner_test_result = self.test_sender_extraction_logic() # Get KVCacheTransferData
|
||||
|
||||
# 2. Connector (Serialize/Deserialize Simulation)
|
||||
# KVCacheTransferData has to_dict
|
||||
data_dict = runner_test_result.to_dict()
|
||||
|
||||
# 3. Receiver (Request Setup)
|
||||
req = OmniDiffusionRequest(prompt="integration_test")
|
||||
req.past_key_values = data_dict["layer_blocks"]
|
||||
req.kv_metadata = data_dict["metadata"]
|
||||
|
||||
# 4. Receiver (Injection Simulation)
|
||||
# Use the logic verification again
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -78,6 +78,7 @@ stage_args:
|
||||
trust_remote_code: true
|
||||
enable_prefix_caching: false
|
||||
engine_output_type: audio
|
||||
max_num_batched_tokens: 4069
|
||||
engine_input_source: [1]
|
||||
final_output: true
|
||||
final_output_type: audio
|
||||
|
||||
@@ -84,6 +84,7 @@ stage_args:
|
||||
max_num_batched_tokens: 1000000
|
||||
hf_config_name: thinker_config
|
||||
load_format: dummy
|
||||
async_scheduling: false
|
||||
engine_input_source: [1]
|
||||
custom_process_input_func: vllm_omni.model_executor.stage_input_processors.qwen3_omni.talker2code2wav
|
||||
final_output: true
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
# Stage config for running Qwen3-Omni-MoE with 3-stage architecture
|
||||
# Stage 0: Thinker (multimodal understanding + text generation)
|
||||
# Stage 1: Talker (text embeddings → 16-layer RVQ codec codes)
|
||||
# Stage 2: Code2Wav (8-layer RVQ codes → audio waveform)
|
||||
|
||||
# The following config has been verified on 2x H100-80G GPUs.
|
||||
stage_args:
|
||||
- stage_id: 0
|
||||
runtime:
|
||||
devices: "0,1"
|
||||
max_batch_size: 1
|
||||
engine_args:
|
||||
model_stage: thinker
|
||||
model_arch: Qwen3OmniMoeForConditionalGeneration
|
||||
worker_cls: vllm_omni.worker.gpu_ar_worker.GPUARWorker
|
||||
scheduler_cls: vllm_omni.core.sched.omni_ar_scheduler.OmniARScheduler
|
||||
gpu_memory_utilization: 0.6
|
||||
enforce_eager: false
|
||||
trust_remote_code: true
|
||||
engine_output_type: latent # Output hidden states for talker
|
||||
distributed_executor_backend: "mp"
|
||||
enable_prefix_caching: false
|
||||
hf_config_name: thinker_config
|
||||
tensor_parallel_size: 2
|
||||
load_format: dummy
|
||||
final_output: true
|
||||
final_output_type: text
|
||||
is_comprehension: true
|
||||
default_sampling_params:
|
||||
temperature: 0.4
|
||||
top_p: 0.9
|
||||
top_k: 1
|
||||
max_tokens: 100
|
||||
seed: 42
|
||||
detokenize: True
|
||||
repetition_penalty: 1.05
|
||||
|
||||
- stage_id: 1
|
||||
runtime:
|
||||
devices: "1"
|
||||
max_batch_size: 1
|
||||
engine_args:
|
||||
model_stage: talker
|
||||
model_arch: Qwen3OmniMoeForConditionalGeneration
|
||||
worker_cls: vllm_omni.worker.gpu_ar_worker.GPUARWorker
|
||||
scheduler_cls: vllm_omni.core.sched.omni_ar_scheduler.OmniARScheduler
|
||||
gpu_memory_utilization: 0.3
|
||||
enforce_eager: true
|
||||
trust_remote_code: true
|
||||
engine_output_type: latent # Output codec codes for code2wav
|
||||
# tensor_parallel_size: 2
|
||||
enable_prefix_caching: false
|
||||
distributed_executor_backend: "mp"
|
||||
hf_config_name: talker_config
|
||||
load_format: dummy
|
||||
engine_input_source: [0]
|
||||
custom_process_input_func: vllm_omni.model_executor.stage_input_processors.qwen3_omni.thinker2talker
|
||||
# final_output: true
|
||||
# final_output_type: text
|
||||
default_sampling_params:
|
||||
temperature: 0.9
|
||||
top_k: 50
|
||||
max_tokens: 100
|
||||
seed: 42
|
||||
detokenize: False
|
||||
repetition_penalty: 1.05
|
||||
stop_token_ids: [2150]
|
||||
|
||||
- stage_id: 2
|
||||
runtime:
|
||||
devices: "0"
|
||||
max_batch_size: 1
|
||||
engine_args:
|
||||
model_stage: code2wav
|
||||
model_arch: Qwen3OmniMoeForConditionalGeneration
|
||||
worker_cls: vllm_omni.worker.gpu_generation_worker.GPUGenerationWorker
|
||||
scheduler_cls: vllm_omni.core.sched.omni_generation_scheduler.OmniGenerationScheduler
|
||||
enforce_eager: true
|
||||
trust_remote_code: true
|
||||
enable_prefix_caching: false
|
||||
engine_output_type: audio # Final output: audio waveform
|
||||
gpu_memory_utilization: 0.1
|
||||
distributed_executor_backend: "mp"
|
||||
max_num_batched_tokens: 1000000
|
||||
hf_config_name: thinker_config
|
||||
load_format: dummy
|
||||
async_scheduling: false
|
||||
engine_input_source: [1]
|
||||
custom_process_input_func: vllm_omni.model_executor.stage_input_processors.qwen3_omni.talker2code2wav
|
||||
final_output: true
|
||||
final_output_type: audio
|
||||
default_sampling_params:
|
||||
temperature: 0.0
|
||||
top_p: 1.0
|
||||
top_k: -1
|
||||
max_tokens: 200
|
||||
seed: 42
|
||||
detokenize: True
|
||||
repetition_penalty: 1.1
|
||||
@@ -0,0 +1,52 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from tests.utils import GPUMemoryMonitor
|
||||
from vllm_omni.utils.platform_utils import is_npu, is_rocm
|
||||
|
||||
# ruff: noqa: E402
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(REPO_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(REPO_ROOT))
|
||||
|
||||
from vllm_omni import Omni
|
||||
|
||||
models = ["riverclouds/qwen_image_random"]
|
||||
|
||||
|
||||
@pytest.mark.skipif(is_npu() or is_rocm(), reason="Hardware not supported")
|
||||
@pytest.mark.parametrize("model_name", models)
|
||||
def test_cpu_offload_diffusion_model(model_name: str):
|
||||
def inference(offload: bool = True):
|
||||
torch.cuda.empty_cache()
|
||||
device_index = torch.cuda.current_device()
|
||||
monitor = GPUMemoryMonitor(device_index=device_index, interval=0.02)
|
||||
monitor.start()
|
||||
m = Omni(model=model_name, enable_cpu_offload=offload)
|
||||
torch.cuda.reset_peak_memory_stats(device=device_index)
|
||||
height = 256
|
||||
width = 256
|
||||
|
||||
m.generate(
|
||||
"a photo of a cat sitting on a laptop keyboard",
|
||||
height=height,
|
||||
width=width,
|
||||
num_inference_steps=9,
|
||||
guidance_scale=0.0,
|
||||
generator=torch.Generator("cuda").manual_seed(42),
|
||||
)
|
||||
peak = monitor.peak_used_mb
|
||||
monitor.stop()
|
||||
|
||||
return peak
|
||||
|
||||
offload_peak_memory = inference(offload=True)
|
||||
no_offload_peak_memory = inference(offload=False)
|
||||
print(f"Offload peak memory: {offload_peak_memory} MB")
|
||||
print(f"No offload peak memory: {no_offload_peak_memory} MB")
|
||||
assert offload_peak_memory + 2500 < no_offload_peak_memory, (
|
||||
f"Offload peak memory {offload_peak_memory} MB should be less than no offload peak memory {no_offload_peak_memory} MB"
|
||||
)
|
||||
@@ -233,7 +233,8 @@ def test_real_transformer_init_and_forward():
|
||||
}
|
||||
)
|
||||
|
||||
od_config = OmniDiffusionConfig(model="dummy-ovis", tf_model_config=tf_config, dtype=torch.float32, num_gpus=1)
|
||||
od_config = OmniDiffusionConfig(model="dummy-ovis", tf_model_config=tf_config, dtype=torch.bfloat16, num_gpus=1)
|
||||
torch.set_default_dtype(torch.bfloat16)
|
||||
|
||||
# Mock distributed state for QKVParallelLinear initialization
|
||||
# We patch get_tp_group because get_tensor_model_parallel_rank calls it and asserts _TP is not None
|
||||
|
||||
@@ -35,7 +35,7 @@ test_params = [(model, stage_config) for model in models]
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.parametrize("test_config", test_params)
|
||||
@create_new_process_for_each_test()
|
||||
@create_new_process_for_each_test("spawn")
|
||||
def test_mixed_modalities_to_audio(omni_runner: type[OmniRunner], test_config: tuple[str, str]) -> None:
|
||||
"""Test processing audio, image, and video together, generating audio output."""
|
||||
model, stage_config_path = test_config
|
||||
@@ -95,7 +95,7 @@ def test_mixed_modalities_to_audio(omni_runner: type[OmniRunner], test_config: t
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.parametrize("test_config", test_params)
|
||||
@create_new_process_for_each_test()
|
||||
@create_new_process_for_each_test("spawn")
|
||||
def test_mixed_modalities_to_text_only(omni_runner: type[OmniRunner], test_config: tuple[str, str]) -> None:
|
||||
"""Test processing audio, image, and video together, generating audio output."""
|
||||
model, stage_config_path = test_config
|
||||
|
||||
@@ -5,19 +5,25 @@ E2E offline tests for Omni model with video input and audio output.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from vllm.assets.video import VideoAsset
|
||||
|
||||
from .conftest import OmniRunner
|
||||
from vllm_omni.utils import is_rocm
|
||||
|
||||
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
|
||||
from .conftest import OmniRunner
|
||||
|
||||
models = ["Qwen/Qwen3-Omni-30B-A3B-Instruct"]
|
||||
|
||||
# CI stage config for 2xH100-80G GPUs
|
||||
stage_configs = [str(Path(__file__).parent / "stage_configs" / "qwen3_omni_ci.yaml")]
|
||||
# CI stage config for 2xH100-80G GPUs or AMD GPU MI325
|
||||
if is_rocm():
|
||||
# ROCm stage config optimized for MI325 GPU
|
||||
stage_configs = [str(Path(__file__).parent / "stage_configs" / "rocm" / "qwen3_omni_ci.yaml")]
|
||||
else:
|
||||
stage_configs = [str(Path(__file__).parent / "stage_configs" / "qwen3_omni_ci.yaml")]
|
||||
|
||||
# Create parameter combinations for model and stage config
|
||||
test_params = [(model, stage_config) for model in models for stage_config in stage_configs]
|
||||
|
||||
@@ -9,6 +9,7 @@ correctly with diffusion models. It uses minimal settings to keep test time
|
||||
short for CI.
|
||||
"""
|
||||
|
||||
import gc
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
@@ -21,7 +22,7 @@ import torch.distributed as dist
|
||||
from PIL import Image
|
||||
|
||||
# ruff: noqa: E402
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
REPO_ROOT = Path(__file__).resolve().parents[3]
|
||||
if str(REPO_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(REPO_ROOT))
|
||||
|
||||
@@ -30,9 +31,6 @@ from vllm_omni.diffusion.data import DiffusionParallelConfig
|
||||
from vllm_omni.diffusion.distributed.parallel_state import device_count
|
||||
from vllm_omni.diffusion.envs import get_device_name
|
||||
|
||||
os.environ["VLLM_TEST_CLEAN_GPU_MEMORY"] = "1"
|
||||
|
||||
# Use random weights model for testing
|
||||
models = ["riverclouds/qwen_image_random"]
|
||||
|
||||
PROMPT = "a photo of a cat sitting on a laptop keyboard"
|
||||
@@ -53,44 +51,145 @@ def _diff_metrics(a: Image.Image, b: Image.Image) -> tuple[float, float]:
|
||||
return abs_diff.mean().item(), abs_diff.max().item()
|
||||
|
||||
|
||||
def _get_images(output):
|
||||
"""Extract images from output, handling both dict and SimpleNamespace types.
|
||||
def _cleanup_distributed():
|
||||
"""Clean up distributed environment and GPU resources."""
|
||||
if dist.is_initialized():
|
||||
dist.destroy_process_group()
|
||||
|
||||
The output structure varies depending on serialization path:
|
||||
- Direct memory: SimpleNamespace with .images attribute
|
||||
- SHM serialization: dict with "images" key (dataclass converted via asdict)
|
||||
- Wrapped output: SimpleNamespace(output=...) which needs unwrapping
|
||||
"""
|
||||
# Check if output has direct images attribute (diffusion mode)
|
||||
if hasattr(output, "images") and output.images:
|
||||
return output.images
|
||||
for key in ["MASTER_ADDR", "MASTER_PORT", "RANK", "WORLD_SIZE", "LOCAL_RANK"]:
|
||||
os.environ.pop(key, None)
|
||||
|
||||
# Check request_output for pipeline mode
|
||||
if output.request_output is None:
|
||||
return None
|
||||
gc.collect()
|
||||
|
||||
if isinstance(output.request_output, list) and len(output.request_output) == 0:
|
||||
return None
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.empty_cache()
|
||||
torch.cuda.synchronize()
|
||||
|
||||
item = output.request_output[0]
|
||||
time.sleep(5)
|
||||
|
||||
# Handle wrapped SimpleNamespace (e.g. from omni_stage.py)
|
||||
# Some items are wrapped as SimpleNamespace(request_id=..., output=...)
|
||||
while hasattr(item, "output") and not hasattr(item, "images"):
|
||||
item = item.output
|
||||
if item is None:
|
||||
return None
|
||||
|
||||
# Handle both dict (from SHM serialization) and object (direct) types
|
||||
if isinstance(item, dict):
|
||||
return item.get("images")
|
||||
return getattr(item, "images", None)
|
||||
def _run_baseline(model_name: str, dtype: torch.dtype, attn_backend: str, height: int, width: int, seed: int):
|
||||
"""Run baseline inference (no SP)."""
|
||||
baseline_parallel_config = DiffusionParallelConfig(ulysses_degree=1, ring_degree=1)
|
||||
baseline = Omni(
|
||||
model=model_name,
|
||||
parallel_config=baseline_parallel_config,
|
||||
dtype=dtype,
|
||||
attention_backend=attn_backend,
|
||||
)
|
||||
|
||||
try:
|
||||
outputs = baseline.generate(
|
||||
PROMPT,
|
||||
height=height,
|
||||
width=width,
|
||||
num_inference_steps=4,
|
||||
guidance_scale=0.0,
|
||||
generator=torch.Generator(get_device_name()).manual_seed(seed),
|
||||
num_outputs_per_prompt=1,
|
||||
)
|
||||
return outputs[0].request_output[0].images
|
||||
finally:
|
||||
baseline.close()
|
||||
_cleanup_distributed()
|
||||
|
||||
|
||||
def _run_sp(
|
||||
model_name: str,
|
||||
dtype: torch.dtype,
|
||||
attn_backend: str,
|
||||
height: int,
|
||||
width: int,
|
||||
seed: int,
|
||||
ulysses_degree: int,
|
||||
ring_degree: int,
|
||||
):
|
||||
"""Run SP inference."""
|
||||
sp_parallel_config = DiffusionParallelConfig(ulysses_degree=ulysses_degree, ring_degree=ring_degree)
|
||||
sp = Omni(
|
||||
model=model_name,
|
||||
parallel_config=sp_parallel_config,
|
||||
dtype=dtype,
|
||||
attention_backend=attn_backend,
|
||||
)
|
||||
|
||||
try:
|
||||
outputs = sp.generate(
|
||||
PROMPT,
|
||||
height=height,
|
||||
width=width,
|
||||
num_inference_steps=4,
|
||||
guidance_scale=0.0,
|
||||
generator=torch.Generator(get_device_name()).manual_seed(seed),
|
||||
num_outputs_per_prompt=1,
|
||||
)
|
||||
return outputs[0].request_output[0].images
|
||||
finally:
|
||||
sp.close()
|
||||
_cleanup_distributed()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model_name", models)
|
||||
@pytest.mark.parametrize("dtype", [torch.bfloat16])
|
||||
@pytest.mark.parametrize("attn_backend", ["sdpa"])
|
||||
def test_baseline_only(model_name: str, dtype: torch.dtype, attn_backend: str):
|
||||
"""Test baseline inference only (no SP)."""
|
||||
height = 256
|
||||
width = 256
|
||||
seed = 42
|
||||
|
||||
images = _run_baseline(model_name, dtype, attn_backend, height, width, seed)
|
||||
|
||||
assert images is not None
|
||||
assert len(images) == 1
|
||||
assert images[0].width == width
|
||||
assert images[0].height == height
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model_name", models)
|
||||
@pytest.mark.parametrize("dtype", [torch.bfloat16])
|
||||
@pytest.mark.parametrize("attn_backend", ["sdpa"])
|
||||
def test_sp_ulysses2_only(model_name: str, dtype: torch.dtype, attn_backend: str):
|
||||
"""Test SP inference only (ulysses=2)."""
|
||||
if device_count() < 2:
|
||||
pytest.skip(f"Test requires 2 GPUs but only {device_count()} available")
|
||||
|
||||
height = 256
|
||||
width = 256
|
||||
seed = 42
|
||||
|
||||
images = _run_sp(model_name, dtype, attn_backend, height, width, seed, ulysses_degree=2, ring_degree=1)
|
||||
|
||||
assert images is not None
|
||||
assert len(images) == 1
|
||||
assert images[0].width == width
|
||||
assert images[0].height == height
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model_name", models)
|
||||
@pytest.mark.parametrize("dtype", [torch.bfloat16])
|
||||
@pytest.mark.parametrize("attn_backend", ["sdpa"])
|
||||
def test_sp_ring2_only(model_name: str, dtype: torch.dtype, attn_backend: str):
|
||||
"""Test SP inference only (ring=2)."""
|
||||
if device_count() < 2:
|
||||
pytest.skip(f"Test requires 2 GPUs but only {device_count()} available")
|
||||
|
||||
height = 256
|
||||
width = 256
|
||||
seed = 42
|
||||
|
||||
images = _run_sp(model_name, dtype, attn_backend, height, width, seed, ulysses_degree=1, ring_degree=2)
|
||||
|
||||
assert images is not None
|
||||
assert len(images) == 1
|
||||
assert images[0].width == width
|
||||
assert images[0].height == height
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model_name", models)
|
||||
@pytest.mark.parametrize("ulysses_degree", [1, 2])
|
||||
@pytest.mark.parametrize("ring_degree", [1, 2])
|
||||
@pytest.mark.parametrize("dtype", [torch.bfloat16]) # Only test bfloat16 to reduce CI time
|
||||
@pytest.mark.parametrize("dtype", [torch.bfloat16])
|
||||
@pytest.mark.parametrize("attn_backend", ["sdpa"])
|
||||
def test_sequence_parallel(
|
||||
model_name: str,
|
||||
@@ -102,103 +201,60 @@ def test_sequence_parallel(
|
||||
"""Compare baseline (ulysses_degree=1) vs SP (ulysses_degree>1) outputs."""
|
||||
if ulysses_degree <= 1 and ring_degree <= 1:
|
||||
pytest.skip(
|
||||
"This test compares ulysses_degree * ring_degree = 1 vs ulysses_degree * ring_degree > 1; provide ulysses_degree or ring_degree>1."
|
||||
"This test compares ulysses_degree * ring_degree = 1 vs ulysses_degree * ring_degree > 1; "
|
||||
"provide ulysses_degree or ring_degree>1."
|
||||
)
|
||||
|
||||
# Skip if not enough GPUs available for SP run
|
||||
sp_size = ulysses_degree * ring_degree
|
||||
if device_count() < sp_size:
|
||||
pytest.skip(f"Test requires {sp_size} GPUs but only {device_count()} available")
|
||||
|
||||
height = 256
|
||||
width = 256
|
||||
seed = 42
|
||||
|
||||
baseline_images = _run_baseline(model_name, dtype, attn_backend, height, width, seed)
|
||||
assert baseline_images is not None and len(baseline_images) == 1
|
||||
|
||||
sp_images = _run_sp(model_name, dtype, attn_backend, height, width, seed, ulysses_degree, ring_degree)
|
||||
assert sp_images is not None and len(sp_images) == 1
|
||||
|
||||
mean_abs_diff, max_abs_diff = _diff_metrics(baseline_images[0], sp_images[0])
|
||||
|
||||
mean_threshold = 2e-2
|
||||
max_threshold = 2e-1
|
||||
|
||||
assert mean_abs_diff <= mean_threshold and max_abs_diff <= max_threshold, (
|
||||
f"Image diff exceeded threshold: mean={mean_abs_diff:.6e}, max={max_abs_diff:.6e}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model_name", models)
|
||||
@pytest.mark.parametrize("dtype", [torch.bfloat16])
|
||||
@pytest.mark.parametrize("attn_backend", ["sdpa"])
|
||||
def test_sequence_parallel_ulysses4(model_name: str, dtype: torch.dtype, attn_backend: str):
|
||||
"""Test SP with ulysses_degree=4."""
|
||||
ulysses_degree = 4
|
||||
ring_degree = 1
|
||||
|
||||
if device_count() < ulysses_degree * ring_degree:
|
||||
pytest.skip(f"Test requires {ulysses_degree * ring_degree} GPUs but only {device_count()} available")
|
||||
|
||||
# Use minimal settings for fast testing
|
||||
height = 256
|
||||
width = 256
|
||||
num_inference_steps = 4 # Minimal steps for fast test
|
||||
height = 272
|
||||
width = 272
|
||||
seed = 42
|
||||
|
||||
# Step 1: Baseline (no Ulysses sequence parallel)
|
||||
baseline_parallel_config = DiffusionParallelConfig(ulysses_degree=1, ring_degree=1)
|
||||
baseline = Omni(
|
||||
model=model_name,
|
||||
parallel_config=baseline_parallel_config,
|
||||
dtype=dtype,
|
||||
attention_backend=attn_backend,
|
||||
)
|
||||
try:
|
||||
outputs = baseline.generate(
|
||||
PROMPT,
|
||||
height=height,
|
||||
width=width,
|
||||
num_inference_steps=num_inference_steps,
|
||||
guidance_scale=0.0,
|
||||
generator=torch.Generator(get_device_name()).manual_seed(seed),
|
||||
num_outputs_per_prompt=1,
|
||||
)
|
||||
baseline_images = outputs[0].request_output[0].images
|
||||
finally:
|
||||
baseline.close()
|
||||
if dist.is_initialized():
|
||||
dist.destroy_process_group()
|
||||
for key in ["MASTER_ADDR", "MASTER_PORT", "RANK", "WORLD_SIZE", "LOCAL_RANK"]:
|
||||
os.environ.pop(key, None)
|
||||
time.sleep(2) # Wait for resources to release
|
||||
baseline_images = _run_baseline(model_name, dtype, attn_backend, height, width, seed)
|
||||
assert baseline_images is not None and len(baseline_images) == 1
|
||||
|
||||
assert baseline_images is not None
|
||||
assert len(baseline_images) == 1
|
||||
assert baseline_images[0].width == width
|
||||
assert baseline_images[0].height == height
|
||||
sp_images = _run_sp(model_name, dtype, attn_backend, height, width, seed, ulysses_degree, ring_degree)
|
||||
assert sp_images is not None and len(sp_images) == 1
|
||||
|
||||
# Step 2: SP (Ulysses-SP + Ring-SP)
|
||||
sp_parallel_config = DiffusionParallelConfig(ulysses_degree=ulysses_degree, ring_degree=ring_degree)
|
||||
sp = Omni(
|
||||
model=model_name,
|
||||
parallel_config=sp_parallel_config,
|
||||
dtype=dtype,
|
||||
attention_backend=attn_backend,
|
||||
)
|
||||
try:
|
||||
outputs = sp.generate(
|
||||
PROMPT,
|
||||
height=height,
|
||||
width=width,
|
||||
num_inference_steps=num_inference_steps,
|
||||
guidance_scale=0.0,
|
||||
generator=torch.Generator(get_device_name()).manual_seed(seed),
|
||||
num_outputs_per_prompt=1,
|
||||
)
|
||||
sp_images = outputs[0].request_output[0].images
|
||||
finally:
|
||||
sp.close()
|
||||
if dist.is_initialized():
|
||||
dist.destroy_process_group()
|
||||
for key in ["MASTER_ADDR", "MASTER_PORT", "RANK", "WORLD_SIZE", "LOCAL_RANK"]:
|
||||
os.environ.pop(key, None)
|
||||
time.sleep(2)
|
||||
|
||||
assert sp_images is not None
|
||||
assert len(sp_images) == 1
|
||||
assert sp_images[0].width == width
|
||||
assert sp_images[0].height == height
|
||||
|
||||
# Step 3: Compare outputs
|
||||
mean_abs_diff, max_abs_diff = _diff_metrics(baseline_images[0], sp_images[0])
|
||||
|
||||
# FP16/BF16 may differ slightly due to different computation order under parallelism.
|
||||
if dtype in (torch.float16, torch.bfloat16):
|
||||
mean_threshold = 2e-2
|
||||
max_threshold = 2e-1
|
||||
else:
|
||||
mean_threshold = 1e-2
|
||||
max_threshold = 1e-1
|
||||
|
||||
print(
|
||||
"Image diff stats (baseline ulysses_degree*ring_degree=1 vs SP): "
|
||||
f"mean_abs_diff={mean_abs_diff:.6e}, max_abs_diff={max_abs_diff:.6e}; "
|
||||
f"thresholds: mean<={mean_threshold:.6e}, max<={max_threshold:.6e}; "
|
||||
f"ulysses_degree={ulysses_degree}, ring_degree={ring_degree}, dtype={dtype}"
|
||||
)
|
||||
mean_threshold = 2e-2
|
||||
max_threshold = 2e-1
|
||||
|
||||
assert mean_abs_diff <= mean_threshold and max_abs_diff <= max_threshold, (
|
||||
f"Image diff exceeded threshold: mean_abs_diff={mean_abs_diff:.6e}, max_abs_diff={max_abs_diff:.6e} "
|
||||
f"(thresholds: mean<={mean_threshold:.6e}, max<={max_threshold:.6e}); "
|
||||
f"ulysses_degree={ulysses_degree}, ring_degree={ring_degree}, dtype={dtype}"
|
||||
f"Image diff exceeded threshold: mean={mean_abs_diff:.6e}, max={max_abs_diff:.6e}"
|
||||
)
|
||||
@@ -0,0 +1,63 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from vllm_omni.outputs import OmniRequestOutput
|
||||
|
||||
# ruff: noqa: E402
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(REPO_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(REPO_ROOT))
|
||||
|
||||
from vllm_omni import Omni
|
||||
|
||||
# Use random weights model for CI testing (small, no authentication required)
|
||||
models = ["linyueqian/stable_audio_random"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model_name", models)
|
||||
def test_stable_audio_model(model_name: str):
|
||||
m = Omni(model=model_name)
|
||||
|
||||
# Use minimal settings for testing
|
||||
# Generate a short 2-second audio clip with minimal inference steps
|
||||
audio_start_in_s = 0.0
|
||||
audio_end_in_s = 2.0 # Short duration for fast testing
|
||||
sample_rate = 44100 # Stable Audio uses 44100 Hz
|
||||
|
||||
outputs = m.generate(
|
||||
"The sound of a dog barking",
|
||||
negative_prompt="Low quality.",
|
||||
num_inference_steps=4, # Minimal steps for speed
|
||||
guidance_scale=7.0,
|
||||
generator=torch.Generator("cuda").manual_seed(42),
|
||||
num_outputs_per_prompt=1,
|
||||
extra={
|
||||
"audio_start_in_s": audio_start_in_s,
|
||||
"audio_end_in_s": audio_end_in_s,
|
||||
},
|
||||
)
|
||||
|
||||
# Extract audio from OmniRequestOutput
|
||||
assert outputs is not None
|
||||
first_output = outputs[0]
|
||||
assert first_output.final_output_type == "image" # Generic output type
|
||||
assert hasattr(first_output, "request_output") and first_output.request_output
|
||||
|
||||
req_out = first_output.request_output[0]
|
||||
assert isinstance(req_out, OmniRequestOutput)
|
||||
assert hasattr(req_out, "images") and len(req_out.images) >= 1
|
||||
|
||||
# For stable audio, the "images" field contains audio numpy arrays
|
||||
audio = req_out.images[0]
|
||||
assert isinstance(audio, np.ndarray)
|
||||
# audio shape: (batch, channels, samples)
|
||||
# For stable-audio-open-1.0: sample_rate=44100, so 2 seconds = 88200 samples
|
||||
assert audio.ndim == 3
|
||||
assert audio.shape[0] == 1 # batch size
|
||||
assert audio.shape[1] == 2 # stereo channels
|
||||
expected_samples = int((audio_end_in_s - audio_start_in_s) * sample_rate)
|
||||
assert audio.shape[2] == expected_samples # 88200 samples for 2 seconds
|
||||
@@ -11,8 +11,10 @@ if str(REPO_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(REPO_ROOT))
|
||||
|
||||
from vllm_omni import Omni
|
||||
from vllm_omni.outputs import OmniRequestOutput
|
||||
|
||||
os.environ["VLLM_TEST_CLEAN_GPU_MEMORY"] = "1"
|
||||
# os.environ["VLLM_TEST_CLEAN_GPU_MEMORY"] = "1"
|
||||
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
|
||||
|
||||
models = ["Wan-AI/Wan2.2-T2V-A14B-Diffusers"]
|
||||
|
||||
@@ -45,10 +47,10 @@ def test_video_diffusion_model(model_name: str):
|
||||
raise ValueError("No request_output found in OmniRequestOutput")
|
||||
|
||||
req_out = first_output.request_output[0]
|
||||
if not isinstance(req_out, dict) or "images" not in req_out:
|
||||
if not isinstance(req_out, OmniRequestOutput) or not hasattr(req_out, "images"):
|
||||
raise ValueError("Invalid request_output structure or missing 'images' key")
|
||||
|
||||
frames = req_out["images"][0]
|
||||
frames = req_out.images[0]
|
||||
|
||||
assert frames is not None
|
||||
assert hasattr(frames, "shape")
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
import torch
|
||||
from PIL import Image
|
||||
from vllm.distributed.parallel_state import cleanup_dist_env_and_memory
|
||||
|
||||
# ruff: noqa: E402
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(REPO_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(REPO_ROOT))
|
||||
|
||||
from tests.utils import GPUMemoryMonitor
|
||||
from vllm_omni import Omni
|
||||
from vllm_omni.diffusion.data import DiffusionParallelConfig
|
||||
from vllm_omni.outputs import OmniRequestOutput
|
||||
from vllm_omni.utils.platform_utils import is_npu, is_rocm
|
||||
|
||||
# os.environ["VLLM_TEST_CLEAN_GPU_MEMORY"] = "1"
|
||||
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
|
||||
|
||||
PROMPT = "a photo of a cat sitting on a laptop keyboard"
|
||||
|
||||
|
||||
def _get_zimage_model() -> str:
|
||||
# Allow overriding the model for local/offline environments.
|
||||
# Can be either a HuggingFace repo id or a local path.
|
||||
return os.environ.get("VLLM_TEST_ZIMAGE_MODEL", "Tongyi-MAI/Z-Image-Turbo")
|
||||
|
||||
|
||||
def _pil_to_float_rgb_tensor(img: Image.Image) -> torch.Tensor:
|
||||
"""Convert PIL image to float32 RGB tensor in [0, 1] with shape [H, W, 3]."""
|
||||
arr = np.asarray(img.convert("RGB"), dtype=np.float32) / 255.0
|
||||
return torch.from_numpy(arr)
|
||||
|
||||
|
||||
def _diff_metrics(a: Image.Image, b: Image.Image) -> tuple[float, float]:
|
||||
"""Return (mean_abs_diff, max_abs_diff) over RGB pixels in [0, 1]."""
|
||||
ta = _pil_to_float_rgb_tensor(a)
|
||||
tb = _pil_to_float_rgb_tensor(b)
|
||||
assert ta.shape == tb.shape, f"Image shapes differ: {ta.shape} vs {tb.shape}"
|
||||
abs_diff = torch.abs(ta - tb)
|
||||
return abs_diff.mean().item(), abs_diff.max().item()
|
||||
|
||||
|
||||
def _extract_single_image(outputs) -> Image.Image:
|
||||
first_output = outputs[0]
|
||||
assert first_output.final_output_type == "image"
|
||||
if not hasattr(first_output, "request_output") or not first_output.request_output:
|
||||
raise ValueError("No request_output found in OmniRequestOutput")
|
||||
|
||||
req_out = first_output.request_output[0]
|
||||
if not isinstance(req_out, OmniRequestOutput) or not hasattr(req_out, "images"):
|
||||
raise ValueError("Invalid request_output structure or missing 'images' key")
|
||||
|
||||
images = req_out.images
|
||||
if images is None or len(images) != 1:
|
||||
raise ValueError(f"Expected 1 image, got {0 if images is None else len(images)}")
|
||||
return images[0]
|
||||
|
||||
|
||||
def _run_zimage_generate(
|
||||
*, tp_size: int, height: int, width: int, num_inference_steps: int, seed: int
|
||||
) -> tuple[Image.Image, float, float]:
|
||||
torch.cuda.empty_cache()
|
||||
device_index = torch.cuda.current_device()
|
||||
monitor = GPUMemoryMonitor(device_index=device_index, interval=0.02)
|
||||
monitor.start()
|
||||
|
||||
m = Omni(
|
||||
model=_get_zimage_model(),
|
||||
parallel_config=DiffusionParallelConfig(tensor_parallel_size=tp_size),
|
||||
)
|
||||
try:
|
||||
# NOTE: Omni closes itself when a generate() call is exhausted.
|
||||
# To avoid measuring teardown time (process shutdown, memory cleanup),
|
||||
# we measure the latency to produce *subsequent* outputs within a single
|
||||
# generator run.
|
||||
#
|
||||
# This also serves as a warmup: the first output may include extra
|
||||
# compilation/caching overhead, while later outputs are closer to
|
||||
# steady-state inference.
|
||||
num_requests = 4 # 1 warmup + 3 timed
|
||||
gen = m.generate(
|
||||
[PROMPT] * num_requests,
|
||||
height=height,
|
||||
width=width,
|
||||
num_inference_steps=num_inference_steps,
|
||||
guidance_scale=0.0,
|
||||
seed=seed,
|
||||
num_outputs_per_prompt=1,
|
||||
py_generator=True,
|
||||
)
|
||||
|
||||
warmup_output = next(gen)
|
||||
t_prev = time.perf_counter()
|
||||
per_request_times_s: list[float] = []
|
||||
last_output = warmup_output
|
||||
for _ in range(num_requests - 1):
|
||||
last_output = next(gen)
|
||||
t_now = time.perf_counter()
|
||||
per_request_times_s.append(t_now - t_prev)
|
||||
t_prev = t_now
|
||||
|
||||
# Ensure the generator is fully consumed so it can clean up.
|
||||
for _ in gen:
|
||||
pass
|
||||
|
||||
median_time_s = float(np.median(per_request_times_s))
|
||||
|
||||
peak_memory_mb = monitor.peak_used_mb
|
||||
|
||||
return _extract_single_image([last_output]), median_time_s, peak_memory_mb
|
||||
finally:
|
||||
monitor.stop()
|
||||
cleanup_dist_env_and_memory()
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_zimage_tensor_parallel_tp2(tmp_path: Path):
|
||||
if is_npu() or is_rocm():
|
||||
pytest.skip("Z-Image TP e2e test is only supported on CUDA for now.")
|
||||
if not torch.cuda.is_available() or torch.cuda.device_count() < 2:
|
||||
pytest.skip("Z-Image TP=2 requires >= 2 CUDA devices.")
|
||||
|
||||
height = 512
|
||||
width = 512
|
||||
num_inference_steps = 2
|
||||
seed = 42
|
||||
|
||||
tp1_img, tp1_time_s, tp1_peak_mem = _run_zimage_generate(
|
||||
tp_size=1,
|
||||
height=height,
|
||||
width=width,
|
||||
num_inference_steps=num_inference_steps,
|
||||
seed=seed,
|
||||
)
|
||||
tp2_img, tp2_time_s, tp2_peak_mem = _run_zimage_generate(
|
||||
tp_size=2,
|
||||
height=height,
|
||||
width=width,
|
||||
num_inference_steps=num_inference_steps,
|
||||
seed=seed,
|
||||
)
|
||||
|
||||
tp1_path = tmp_path / "zimage_tp1.png"
|
||||
tp2_path = tmp_path / "zimage_tp2.png"
|
||||
tp1_img.save(tp1_path)
|
||||
tp2_img.save(tp2_path)
|
||||
|
||||
assert tp1_img.width == width and tp1_img.height == height
|
||||
assert tp2_img.width == width and tp2_img.height == height
|
||||
|
||||
mean_abs_diff, max_abs_diff = _diff_metrics(tp1_img, tp2_img)
|
||||
mean_threshold = 3e-2
|
||||
max_threshold = 5e-1
|
||||
print(
|
||||
"Z-Image TP image diff stats (TP=1 vs TP=2): "
|
||||
f"mean_abs_diff={mean_abs_diff:.6e}, max_abs_diff={max_abs_diff:.6e}; "
|
||||
f"thresholds: mean<={mean_threshold:.6e}, max<={max_threshold:.6e}; "
|
||||
f"tp1_img={tp1_path}, tp2_img={tp2_path}"
|
||||
)
|
||||
assert mean_abs_diff <= mean_threshold and max_abs_diff <= max_threshold, (
|
||||
f"Image diff exceeded threshold: mean_abs_diff={mean_abs_diff:.6e}, max_abs_diff={max_abs_diff:.6e} "
|
||||
f"(thresholds: mean<={mean_threshold:.6e}, max<={max_threshold:.6e})"
|
||||
)
|
||||
|
||||
print(f"Z-Image TP perf (lower is better): tp1_time_s={tp1_time_s:.6f}, tp2_time_s={tp2_time_s:.6f}")
|
||||
assert tp2_time_s < tp1_time_s, f"Expected TP=2 to be faster than TP=1 (tp1={tp1_time_s}, tp2={tp2_time_s})"
|
||||
|
||||
print(f"Z-Image TP peak memory (MB): tp1_peak_mem={tp1_peak_mem:.2f}, tp2_peak_mem={tp2_peak_mem:.2f}")
|
||||
assert tp2_peak_mem < tp1_peak_mem, (
|
||||
f"Expected TP=2 to use less peak memory than TP=1 (tp1={tp1_peak_mem}, tp2={tp2_peak_mem})"
|
||||
)
|
||||
@@ -45,7 +45,7 @@ stage_args:
|
||||
worker_cls: vllm_omni.worker.gpu_ar_worker.GPUARWorker
|
||||
scheduler_cls: vllm_omni.core.sched.omni_ar_scheduler.OmniARScheduler
|
||||
gpu_memory_utilization: 0.3
|
||||
enforce_eager: true
|
||||
enforce_eager: false
|
||||
trust_remote_code: true
|
||||
engine_output_type: latent # Output codec codes for code2wav
|
||||
# tensor_parallel_size: 2
|
||||
@@ -77,6 +77,7 @@ stage_args:
|
||||
scheduler_cls: vllm_omni.core.sched.omni_generation_scheduler.OmniGenerationScheduler
|
||||
enforce_eager: true
|
||||
trust_remote_code: true
|
||||
async_scheduling: false
|
||||
enable_prefix_caching: false
|
||||
engine_output_type: audio # Final output: audio waveform
|
||||
gpu_memory_utilization: 0.1
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
# Stage config for running Qwen3-Omni-MoE with 3-stage architecture
|
||||
# Stage 0: Thinker (multimodal understanding + text generation)
|
||||
# Stage 1: Talker (text embeddings → 16-layer RVQ codec codes)
|
||||
# Stage 2: Code2Wav (8-layer RVQ codes → audio waveform)
|
||||
|
||||
# The following config has been verified on 2x H100-80G GPUs.
|
||||
stage_args:
|
||||
- stage_id: 0
|
||||
runtime:
|
||||
devices: "0,1"
|
||||
max_batch_size: 5
|
||||
engine_args:
|
||||
model_stage: thinker
|
||||
model_arch: Qwen3OmniMoeForConditionalGeneration
|
||||
worker_cls: vllm_omni.worker.gpu_ar_worker.GPUARWorker
|
||||
scheduler_cls: vllm_omni.core.sched.omni_ar_scheduler.OmniARScheduler
|
||||
gpu_memory_utilization: 0.6
|
||||
enforce_eager: false
|
||||
trust_remote_code: true
|
||||
engine_output_type: latent # Output hidden states for talker
|
||||
distributed_executor_backend: "mp"
|
||||
enable_prefix_caching: false
|
||||
hf_config_name: thinker_config
|
||||
tensor_parallel_size: 2
|
||||
load_format: dummy
|
||||
final_output: true
|
||||
final_output_type: text
|
||||
is_comprehension: true
|
||||
default_sampling_params:
|
||||
temperature: 0.4
|
||||
top_p: 0.9
|
||||
top_k: 1
|
||||
max_tokens: 100
|
||||
seed: 42
|
||||
detokenize: True
|
||||
repetition_penalty: 1.05
|
||||
|
||||
- stage_id: 1
|
||||
runtime:
|
||||
devices: "1"
|
||||
max_batch_size: 5
|
||||
engine_args:
|
||||
model_stage: talker
|
||||
model_arch: Qwen3OmniMoeForConditionalGeneration
|
||||
worker_cls: vllm_omni.worker.gpu_ar_worker.GPUARWorker
|
||||
scheduler_cls: vllm_omni.core.sched.omni_ar_scheduler.OmniARScheduler
|
||||
gpu_memory_utilization: 0.3
|
||||
enforce_eager: true
|
||||
trust_remote_code: true
|
||||
engine_output_type: latent # Output codec codes for code2wav
|
||||
# tensor_parallel_size: 2
|
||||
enable_prefix_caching: false
|
||||
distributed_executor_backend: "mp"
|
||||
hf_config_name: talker_config
|
||||
load_format: dummy
|
||||
engine_input_source: [0]
|
||||
custom_process_input_func: vllm_omni.model_executor.stage_input_processors.qwen3_omni.thinker2talker
|
||||
# final_output: true
|
||||
# final_output_type: text
|
||||
default_sampling_params:
|
||||
temperature: 0.9
|
||||
top_k: 50
|
||||
max_tokens: 100
|
||||
seed: 42
|
||||
detokenize: False
|
||||
repetition_penalty: 1.05
|
||||
stop_token_ids: [2150]
|
||||
|
||||
- stage_id: 2
|
||||
runtime:
|
||||
devices: "0"
|
||||
max_batch_size: 1
|
||||
engine_args:
|
||||
model_stage: code2wav
|
||||
model_arch: Qwen3OmniMoeForConditionalGeneration
|
||||
worker_cls: vllm_omni.worker.gpu_generation_worker.GPUGenerationWorker
|
||||
scheduler_cls: vllm_omni.core.sched.omni_generation_scheduler.OmniGenerationScheduler
|
||||
enforce_eager: true
|
||||
trust_remote_code: true
|
||||
enable_prefix_caching: false
|
||||
engine_output_type: audio # Final output: audio waveform
|
||||
gpu_memory_utilization: 0.1
|
||||
distributed_executor_backend: "mp"
|
||||
max_num_batched_tokens: 1000000
|
||||
hf_config_name: thinker_config
|
||||
load_format: dummy
|
||||
async_scheduling: false
|
||||
engine_input_source: [1]
|
||||
custom_process_input_func: vllm_omni.model_executor.stage_input_processors.qwen3_omni.talker2code2wav
|
||||
final_output: true
|
||||
final_output_type: audio
|
||||
default_sampling_params:
|
||||
temperature: 0.0
|
||||
top_p: 1.0
|
||||
top_k: -1
|
||||
max_tokens: 200
|
||||
seed: 42
|
||||
detokenize: True
|
||||
repetition_penalty: 1.1
|
||||
@@ -1,11 +1,15 @@
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
from contextlib import ExitStack
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from vllm.inputs import PromptType
|
||||
|
||||
from vllm_omni.entrypoints.async_omni import AsyncOmni
|
||||
from vllm_omni.entrypoints.async_omni import AsyncOmni, ClientRequestState
|
||||
|
||||
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
|
||||
|
||||
SEED = 42
|
||||
|
||||
@@ -55,13 +59,19 @@ async def generate(
|
||||
@pytest.mark.asyncio
|
||||
async def test_abort():
|
||||
with ExitStack() as after:
|
||||
engine = AsyncOmni(model=model, stage_configs_path=stage_config)
|
||||
# Avoid SHM IPC in tests to prevent /dev/shm exhaustion and SIGBUS.
|
||||
engine = AsyncOmni(
|
||||
model=model,
|
||||
stage_configs_path=stage_config,
|
||||
shm_threshold_bytes=sys.maxsize,
|
||||
)
|
||||
after.callback(engine.shutdown)
|
||||
|
||||
NUM_REQUESTS = 5
|
||||
NUM_EXPECTED_TOKENS = 100
|
||||
NUM_EXPECTED_TOKENS_LONG = 1000
|
||||
REQUEST_IDS_TO_ABORT = [1, 2, 3]
|
||||
# Keep token counts modest to reduce flakiness on slow test hardware.
|
||||
NUM_REQUESTS = 3
|
||||
NUM_EXPECTED_TOKENS = 64
|
||||
NUM_EXPECTED_TOKENS_LONG = 256
|
||||
REQUEST_IDS_TO_ABORT = [1]
|
||||
|
||||
prompt = "Hello my name is Robert and "
|
||||
|
||||
@@ -74,8 +84,10 @@ async def test_abort():
|
||||
tasks.append(asyncio.create_task(generate(engine, request_id, prompt, max_tokens)))
|
||||
|
||||
# API server cancels requests when they disconnect.
|
||||
# Explicitly abort in the engine to avoid orphaned requests hanging.
|
||||
for idx in REQUEST_IDS_TO_ABORT:
|
||||
tasks[idx].cancel()
|
||||
await engine.abort(request_ids[idx])
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
# Confirm the other requests are okay.
|
||||
@@ -83,10 +95,10 @@ async def test_abort():
|
||||
# Confirm that it was actually canceled.
|
||||
if idx in REQUEST_IDS_TO_ABORT:
|
||||
with pytest.raises((asyncio.CancelledError, GeneratorExit)):
|
||||
await task
|
||||
await asyncio.wait_for(task, timeout=60)
|
||||
else:
|
||||
# Otherwise, make sure the request was not impacted.
|
||||
num_generated_tokens, request_id = await task
|
||||
num_generated_tokens, request_id = await asyncio.wait_for(task, timeout=180)
|
||||
expected_tokens = NUM_EXPECTED_TOKENS
|
||||
assert num_generated_tokens == expected_tokens, (
|
||||
f"{request_id} generated {num_generated_tokens} but expected {expected_tokens}"
|
||||
@@ -98,3 +110,51 @@ async def test_abort():
|
||||
num_generated_tokens, request_id = await task
|
||||
assert num_generated_tokens == NUM_EXPECTED_TOKENS
|
||||
await asyncio.sleep(5)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_and_log_summary(monkeypatch):
|
||||
from vllm_omni.entrypoints.utils import get_final_stage_id_for_e2e
|
||||
|
||||
RealCRS = ClientRequestState
|
||||
capture_metrics = {}
|
||||
|
||||
class MockCRS(RealCRS):
|
||||
def __init__(self, request_id: str):
|
||||
super().__init__(request_id)
|
||||
capture_metrics[request_id] = self
|
||||
|
||||
monkeypatch.setattr("vllm_omni.entrypoints.async_omni.ClientRequestState", MockCRS)
|
||||
monkeypatch.setattr("vllm_omni.entrypoints.client_request_state.ClientRequestState", MockCRS)
|
||||
|
||||
with ExitStack() as after:
|
||||
# Avoid SHM IPC in tests to prevent /dev/shm exhaustion and SIGBUS.
|
||||
engine = AsyncOmni(
|
||||
model=model,
|
||||
stage_configs_path=stage_config,
|
||||
shm_threshold_bytes=sys.maxsize,
|
||||
)
|
||||
after.callback(engine.shutdown)
|
||||
prompt = "Hello my name is Robert and "
|
||||
NUM_EXPECTED_TOKENS = 64
|
||||
NUM_REQUESTS = 3
|
||||
request_ids = [f"request-{i}" for i in range(NUM_REQUESTS)]
|
||||
|
||||
# Create concurrent requests.
|
||||
tasks: list[asyncio.Task] = []
|
||||
for idx, request_id in enumerate(request_ids):
|
||||
tasks.append(asyncio.create_task(generate(engine, request_id, prompt, NUM_EXPECTED_TOKENS)))
|
||||
|
||||
# Confirm the requests are okay.
|
||||
for idx, task in enumerate(tasks):
|
||||
await task
|
||||
output_modalities = ["text"]
|
||||
final_stage_id_for_e2e = get_final_stage_id_for_e2e(
|
||||
output_modalities, engine.output_modalities, engine.stage_list
|
||||
)
|
||||
summary = capture_metrics[request_ids[idx]].metrics.build_and_log_summary(final_stage_id_for_e2e)
|
||||
|
||||
# Check that total tokens matches sum of stage tokens.
|
||||
assert summary["e2e_total_tokens"] == sum(stage["tokens"] for stage in summary["stages"])
|
||||
# Check that total time matches sum of stage times.
|
||||
assert summary["e2e_total_time_ms"] >= sum(stage["total_time_ms"] for stage in summary["stages"])
|
||||
@@ -17,7 +17,7 @@ import openai
|
||||
import pytest
|
||||
from PIL import Image
|
||||
from vllm.assets.image import ImageAsset
|
||||
from vllm.utils import get_open_port
|
||||
from vllm.utils.network_utils import get_open_port
|
||||
|
||||
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
|
||||
|
||||
|
||||
Loaded 100 of 256 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user