Compare commits

...
Author SHA1 Message Date
hsliu 67b50d99fc docs: address recipe review feedback
Signed-off-by: hsliu <liuhongsheng4@huawei.com>
2026-04-18 09:47:41 +08:00
Hongsheng LiuandCopilot 13d074e5db Apply suggestions from code review
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Hongsheng Liu <liuhongsheng4@huawei.com>
Signed-off-by: hsliu <liuhongsheng4@huawei.com>
2026-04-18 09:47:41 +08:00
hsliuustc0106 189824dfb9 draft vllm-omni recipes
Signed-off-by: hsliuustc0106 <liuhongsheng4@huawei.com>
Signed-off-by: hsliu <liuhongsheng4@huawei.com>
2026-04-18 09:47:41 +08:00
Samit 956f53b278 [Refactor] Use trajectory_* fields for Qwen-Image structured RL outputs (#2513)
Signed-off-by: samithuang <285365963@qq.com>
2026-04-09 03:06:40 -07:00
Ziming Huang d2aa9cf08a Revert "[Fix] Fix slow hasattr in CUDAGraphWrapper.__getattr__ (#1982)" (#2639)
Signed-off-by: ZeldaHuang <hzm414167@alibaba-inc.com>
2026-04-09 17:19:58 +08:00
2d980133fe [Bugfix] Fix precedence between caller runtime args and default stage configs (#2076)
Signed-off-by: xiaohajiayou <923390377@qq.com>
Co-authored-by: 汪志鹏 <wangzhipeng628@gmail.com>
Co-authored-by: SYLAR <125541396+lishunyang12@users.noreply.github.com>
2026-04-09 16:44:31 +08:00
e2b0ee4b87 [Model] Add Dynin-omni model in vllm-omni (#1759)
Signed-off-by: kdg6245@snu.ac.kr <kdg6245@snu.ac.kr>
Signed-off-by: Yejoon Lee (IPAI) <leeyejoon@snu.ac.kr>
Signed-off-by: aidas (arpa-kt) <aidaslab@gmail.com>
Signed-off-by: Dogeun Kim <82812668+DOGEUNNKIM@users.noreply.github.com>
Co-authored-by: Yejoon Lee (IPAI) <leeyejoon@snu.ac.kr>
Co-authored-by: aidas (arpa-kt) <aidaslab@gmail.com>
2026-04-09 16:37:34 +08:00
Samit a7bf4050de [Docs] Add multi-thread weight loading documentation (#2445)
Signed-off-by: samithuang <285365963@qq.com>
2026-04-09 15:54:23 +08:00
LiBai 9225039d17 [Bug] Lazy-import entrypoints to fix subprocess pynvml crash (#2187)
Signed-off-by: Meng Jianwen <mengjianwen@liblib.ai>
2026-04-09 15:35:22 +08:00
Ting FU 0e8e630c5b [Feat] /v1/images/generations api supports request cancel (#2621)
Signed-off-by: Semmer <semmer@live.cn>
2026-04-09 07:08:08 +00:00
Zhengyuan Su (苏政渊)andClaude Opus 4.6 92c788e5fd [Misc] Clean up method name in BAGEL. (#2501)
Signed-off-by: Zhengyuan Su <su.zhengyuan@u.nus.edu>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 14:55:15 +08:00
汪志鹏 9d87229a30 [MagiHuman] Fix audio sample rate and fps propagation for online serving (#2554)
Signed-off-by: princepride <wangzhipeng628@gmail.com>
2026-04-09 06:52:12 +00:00
pjh4993 ed7a448cd3 [Bugfix] Guard app.state access during server shutdown (#2587)
Signed-off-by: pjh4993 <pjh4993@naver.com>
2026-04-09 14:41:23 +08:00
wangyu 0edc356fc8 [CI]Refactor nightly test configuration in Buildkite, Add group for Omni and Diffusion models (#2582)
Signed-off-by: wangyu <410167048@qq.com>
2026-04-09 14:35:35 +08:00
Nick CaoandClaude 3bd8a5239e [Bugfix] Enforce --max-generated-image-size on /v1/images/generations (#2599)
Signed-off-by: Nick Cao <ncao@redhat.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-09 03:54:12 +00:00
zhumingjue138 e6f88f7c5a [CI] Reorganize the L1 L2 use cases and add markers (#2449)
Signed-off-by: zhumingjue <zhumingjue@huawei.com>
2026-04-09 10:31:21 +08:00
Zhengyuan Su (苏政渊)andClaude Opus 4.6 c3f1042061 [Feature] End-to-end LoRA support for BAGEL (#2494)
Signed-off-by: Zhengyuan Su <su.zhengyuan@u.nus.edu>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 01:33:55 +00:00
Nick CaoandClaude 149b9f179a [Perf][Qwen3-TTS][Voxtral-TTS] Share CUDA graph memory pool across decoder capture sizes (#2386)
Signed-off-by: Nick Cao <ncao@redhat.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-08 20:40:17 -04:00
vveerrggandvveerrgg 2c6c07c4f6 [TTS] Add missing _generate_pcm_chunks for OmniOpenAIServingSpeech streaming (#2569)
Signed-off-by: Yueqian Lin <pandaleefree@gmail.com>
Signed-off-by: Yueqian Lin <70319226+linyueqian@users.noreply.github.com>
Co-authored-by: vveerrgg <vveerrgg@users.noreply.github.com>
2026-04-08 22:20:33 +00:00
Peiqi Yin fcda835f4d [Unit Test] Add unit tests for orchestrator (#2096)
Signed-off-by: yinpe <11810305@mail.sustech.edu.cn>
2026-04-08 20:58:14 +08:00
Dnoob 7e7efdd1ab [Bugfix] Fix benchmark Total input tokens for multimodal requests (#2540) (#2549)
Signed-off-by: Dnoob <dxpouo@gmail.com>
2026-04-08 11:00:53 +00:00
Jinheng aefa2ee45c [Fix] Align diffusion proc test mock with current output fields (#2584)
Signed-off-by: Jinheng Li <ahengljh@gmail.com>
2026-04-08 17:45:10 +08:00
R0CKSTAR fb3c6bd9b1 [Feat] Add MUSA flash attention support via mate package (#2451)
Signed-off-by: Xiaodong Ye <yeahdongcn@gmail.com>
2026-04-08 16:01:30 +08:00
c3c736d488 [Feature] Port Bagel RDMA flow to latest main (#2000)
Signed-off-by: Jinheng Li <ahengljh@gmail.com>
Signed-off-by: natureofnature <wzliu@connect.hku.hk>
Co-authored-by: ahengljh <ahengljh@users.noreply.github.com>
Co-authored-by: natureofnature <wzliu@connect.hku.hk>
2026-04-08 14:55:35 +08:00
Lancer 8609bc8ed9 [Feat] image2image for Z-Image (#1580)
Signed-off-by: Lancer <maruixiang6688@gmail.com>
Signed-off-by: Lancer <402430575@qq.com>
2026-04-08 14:41:24 +08:00
Yangshen DengandClaude Opus 4.6 1cd5210440 [Diffusion] Refactor LTX2 to use unified CFG parallel framework (#2160)
Signed-off-by: Yangshen Deng <yangshen.d@outlook.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 11:44:04 +08:00
Y. FisherandCanlin Guo ec082add35 [Perf] Wan2.2 I2V optimization: convert datatype from FP32 to BF16 in vae (#2391)
Signed-off-by: KexiongYu <yukexiong1@huawei.com>
Signed-off-by: Canlin Guo <961750412@qq.com>
Co-authored-by: Canlin Guo <961750412@qq.com>
2026-04-08 03:26:39 +00:00
cb6a8739c1 [Feature] Add trajectory recording to BAGEL denoising loop (#2483)
Signed-off-by: Zhengyuan Su <su.zhengyuan@u.nus.edu>
Signed-off-by: Zhengyuan Su (苏政渊) <su.zhengyuan@u.nus.edu>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Samit <285365963@qq.com>
2026-04-08 10:43:01 +08:00
skfandCanlin Guo 6433847249 [skipCI][Docs] Add expert_parallel.md (#2471)
Signed-off-by: skf1999 <13234016272@163.com>
Co-authored-by: Canlin Guo <canlinguosdu@gmail.com>
2026-04-08 02:35:14 +00:00
Yuanheng Zhao 8a55d3d01f [Feat] Enable Layerwise CPU offloading for SD3.5, Ovis-Image, Nextstep_1, LongCat-Image (#2339)
Signed-off-by: Yuanheng Zhao <jonathan.zhaoyh@gmail.com>
Signed-off-by: yuanheng <jonathan.zhaoyh@gmail.com>
2026-04-08 09:55:09 +08:00
Kevin H. Luu b246617fe5 [release] Fix lint issue (#2567)
Signed-off-by: khluu <khluu000@gmail.com>
2026-04-08 01:47:42 +00:00
Kevin H. Luu bc5e945544 [release] Fix release script (#2566)
Signed-off-by: khluu <khluu000@gmail.com>
2026-04-07 14:50:11 -07:00
129 changed files with 12440 additions and 1407 deletions

No files matched your search

@@ -19,7 +19,7 @@ has_new_python=$($PYTHON -c "print(1 if __import__('sys').version_info >= (3,12)
if [[ "$has_new_python" -eq 0 ]]; then
# use new python from docker
docker pull python:3-slim
PYTHON="docker run --rm -v $(pwd):/app -w /app python:3-slim python3"
PYTHON="docker run --rm --user $(id -u):$(id -g) -v $(pwd):/app -w /app python:3-slim python3"
fi
echo "Using python interpreter: $PYTHON"
@@ -36,7 +36,7 @@ mkdir -p "$INDICES_OUTPUT_DIR"
# HACK: we do not need regex module here, but it is required by pre-commit hook
# To avoid any external dependency, we simply replace it back to the stdlib re module
sed -i 's/import regex as re/import re/g' .buildkite/scripts/generate-nightly-index.py
sed -i.bak 's/import regex as re/import re/g' .buildkite/scripts/generate-nightly-index.py && rm -f .buildkite/scripts/generate-nightly-index.py.bak
# Generate indices -- the version is just the commit hash (not omni/{commit})
# because relative paths are computed between the index and wheel directories,
@@ -73,15 +73,16 @@ echo "Pure version (without variant): $pure_version"
# re-generate and copy to /omni/{version}/ only if it does not have "dev" in the version
if [[ "$version" != *"dev"* ]]; then
echo "Re-generating indices for /omni/$pure_version/"
s3_version="v$pure_version"
echo "Re-generating indices for /omni/$s3_version/"
rm -rf "${INDICES_OUTPUT_DIR:?}"
mkdir -p "$INDICES_OUTPUT_DIR"
# wheel-dir is overridden to be the commit directory, so that the indices point to the correct wheel path
$PYTHON .buildkite/scripts/generate-nightly-index.py \
--version "$pure_version" \
--version "$s3_version" \
--wheel-dir "$BUILDKITE_COMMIT" \
--current-objects "$obj_json" \
--output-dir "$INDICES_OUTPUT_DIR" \
--comment "version $pure_version"
aws s3 cp --recursive "$INDICES_OUTPUT_DIR/" "s3://$BUCKET/omni/$pure_version/"
aws s3 cp --recursive "$INDICES_OUTPUT_DIR/" "s3://$BUCKET/omni/$s3_version/"
fi
+1 -2
View File
@@ -4,6 +4,7 @@
import argparse
import json
import re
import sys
from dataclasses import asdict, dataclass
from datetime import datetime
@@ -11,8 +12,6 @@ from pathlib import Path
from typing import Any
from urllib.parse import quote
import regex as re
def normalize_package_name(name: str) -> str:
"""Normalize package name per PEP 503."""
+367
View File
@@ -0,0 +1,367 @@
# Nightly diffusion GPU tests — appended to the main nightly build via
# buildkite-agent pipeline upload .buildkite/test-nightly-diffusion.yml
# from test-nightly.yml (step key: nightly-diffusion-model-test). Top-level groups are
# foldable in the Buildkite UI (Other / Wan / Qwen-Image).
steps:
- group: ":card_index_dividers: Other Model Test"
key: nightly-other-model-test-group
steps:
- label: ":full_moon: Diffusion · Other · Function Test with H100"
timeout_in_minutes: 120
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test"
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- pytest -s -v tests/e2e/online_serving/test_*_expansion.py -k "not test_wan22_expansion and not test_wan_2_1_vace_expansion and not test_qwen_image" -m "advanced_model and diffusion and H100" --run-level "advanced_model"
agents:
queue: "mithril-h100-pool"
plugins:
- kubernetes:
podSpec:
containers:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
resources:
limits:
nvidia.com/gpu: 2
volumeMounts:
- name: devshm
mountPath: /dev/shm
- name: hf-cache
mountPath: /root/.cache/huggingface
env:
- name: HF_HOME
value: /root/.cache/huggingface
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token-secret
key: token
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
- name: devshm
emptyDir:
medium: Memory
- name: hf-cache
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreate
- label: ":full_moon: Diffusion · Other · Function Test with L4"
timeout_in_minutes: 60
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test"
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- pytest -s -v tests/e2e/online_serving/test_*_expansion.py -m "advanced_model and diffusion and L4" --run-level "advanced_model"
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"
- "HF_TOKEN"
volumes:
- "/fsx/hf_cache:/fsx/hf_cache"
- label: ":full_moon: Diffusion · Other · Doc Test"
timeout_in_minutes: 60
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test"
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- export VLLM_TEST_CLEAN_GPU_MEMORY="1"
- pytest -s -v tests/examples/online_serving/test_text_to_image.py tests/examples/offline_inference/test_text_to_image.py -m "advanced_model and example and H100" --run-level "advanced_model"
agents:
queue: "mithril-h100-pool"
plugins:
- kubernetes:
podSpec:
containers:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
resources:
limits:
nvidia.com/gpu: 2
volumeMounts:
- name: devshm
mountPath: /dev/shm
- name: hf-cache
mountPath: /root/.cache/huggingface
env:
- name: HF_HOME
value: /root/.cache/huggingface
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token-secret
key: token
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
- name: devshm
emptyDir:
medium: Memory
- name: hf-cache
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreate
- group: ":card_index_dividers: Wan Series Model Test"
key: nightly-wan-model-test-group
steps:
- label: ":full_moon: Diffusion · Wan · Function Test"
timeout_in_minutes: 90
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test"
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- pytest -s -v tests/e2e/online_serving/test_wan22_expansion.py tests/e2e/online_serving/test_wan_2_1_vace_expansion.py -m "advanced_model" --run-level "advanced_model"
agents:
queue: "mithril-h100-pool"
plugins:
- kubernetes:
podSpec:
containers:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
resources:
limits:
nvidia.com/gpu: 2
volumeMounts:
- name: devshm
mountPath: /dev/shm
- name: hf-cache
mountPath: /root/.cache/huggingface
env:
- name: HF_HOME
value: /root/.cache/huggingface
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token-secret
key: token
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
- name: devshm
emptyDir:
medium: Memory
- name: hf-cache
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreate
- label: ":full_moon: Diffusion · Wan · Accuracy Test"
key: nightly-wan22-i2v-accuracy
timeout_in_minutes: 180
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test"
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- pytest -s -v tests/e2e/accuracy/wan22_i2v/test_wan22_i2v_video_similarity.py --run-level advanced_model
agents:
queue: "mithril-h100-pool"
plugins:
- kubernetes:
podSpec:
containers:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
resources:
limits:
nvidia.com/gpu: 2
volumeMounts:
- name: devshm
mountPath: /dev/shm
- name: hf-cache
mountPath: /root/.cache/huggingface
env:
- name: HF_HOME
value: /root/.cache/huggingface
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token-secret
key: token
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
- name: devshm
emptyDir:
medium: Memory
- name: hf-cache
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreate
- group: ":card_index_dividers: Qwen-Image Series Model Test"
key: nightly-qwen-image-edit-group
steps:
- label: ":full_moon: Diffusion · Qwen-Image · Function Test with H100"
timeout_in_minutes: 120
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test"
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- pytest -s -v tests/e2e/online_serving/test_qwen_image*_expansion.py -m "advanced_model and diffusion and H100" --run-level "advanced_model"
agents:
queue: "mithril-h100-pool"
plugins:
- kubernetes:
podSpec:
containers:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
resources:
limits:
nvidia.com/gpu: 2
volumeMounts:
- name: devshm
mountPath: /dev/shm
- name: hf-cache
mountPath: /root/.cache/huggingface
env:
- name: HF_HOME
value: /root/.cache/huggingface
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token-secret
key: token
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
- name: devshm
emptyDir:
medium: Memory
- name: hf-cache
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreate
- label: ":full_moon: Diffusion · Qwen-Image · GEBench Accuracy Test"
key: nightly-gebench-accuracy
timeout_in_minutes: 60
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test"
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- pytest -s -v tests/e2e/accuracy/test_gebench_h100_smoke.py --run-level advanced_model --gebench-model Qwen/Qwen-Image-2512 --accuracy-judge-model QuantTrio/Qwen3-VL-30B-A3B-Instruct-AWQ --accuracy-gpu 0 --gebench-port 8093 --accuracy-workers 1
- buildkite-agent artifact upload "tests/e2e/accuracy/artifacts/gebench_qwen-image-2512/summary*.json"
agents:
queue: "mithril-h100-pool"
plugins:
- kubernetes:
podSpec:
containers:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
resources:
limits:
nvidia.com/gpu: 1
volumeMounts:
- name: devshm
mountPath: /dev/shm
- name: hf-cache
mountPath: /root/.cache/huggingface
env:
- name: HF_HOME
value: /root/.cache/huggingface
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token-secret
key: token
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
- name: devshm
emptyDir:
medium: Memory
- name: hf-cache
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreate
- label: ":full_moon: Diffusion · Qwen-Image · GEdit-Bench Accuracy Test"
key: nightly-gedit-bench-accuracy
timeout_in_minutes: 60
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test"
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- pytest -s -v tests/e2e/accuracy/test_gedit_bench_h100_smoke.py --run-level advanced_model --gedit-model Qwen/Qwen-Image-Edit --accuracy-judge-model QuantTrio/Qwen3-VL-30B-A3B-Instruct-AWQ --accuracy-gpu 0 --gedit-port 8093 --gedit-samples-per-group 20 --accuracy-workers 1
- buildkite-agent artifact upload "tests/e2e/accuracy/artifacts/gedit_scores_qwen-image-edit/qwen-image-edit_all_all_vie_score_*.csv"
- buildkite-agent artifact upload "tests/e2e/accuracy/artifacts/gedit_scores_qwen-image-edit/qwen-image-edit_all_all_summary_*.json"
agents:
queue: "mithril-h100-pool"
plugins:
- kubernetes:
podSpec:
containers:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
resources:
limits:
nvidia.com/gpu: 1
volumeMounts:
- name: devshm
mountPath: /dev/shm
- name: hf-cache
mountPath: /root/.cache/huggingface
env:
- name: HF_HOME
value: /root/.cache/huggingface
- name: VLLM_HTTP_TIMEOUT_KEEP_ALIVE
value: "120"
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token-secret
key: token
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
- name: devshm
emptyDir:
medium: Memory
- name: hf-cache
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreate
- label: ":full_moon: Diffusion · Qwen-Image · Perf Test"
key: nightly-qwen-image-performance
timeout_in_minutes: 180
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test"
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- export DIFFUSION_BENCHMARK_DIR=tests/dfx/perf/results
- export CACHE_DIT_VERSION=1.3.0
- pytest -s -v tests/dfx/perf/scripts/run_diffusion_benchmark.py --config-file tests/dfx/perf/tests/test_qwen_image_vllm_omni.json
- buildkite-agent artifact upload "tests/dfx/perf/results/benchmark_results_*.json"
- buildkite-agent artifact upload "tests/dfx/perf/results/logs/*.log"
agents:
queue: "mithril-h100-pool"
plugins:
- kubernetes:
podSpec:
containers:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
resources:
limits:
nvidia.com/gpu: 4
volumeMounts:
- name: devshm
mountPath: /dev/shm
- name: hf-cache
mountPath: /root/.cache/huggingface
env:
- name: HF_HOME
value: /root/.cache/huggingface
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token-secret
key: token
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
- name: devshm
emptyDir:
medium: Memory
- name: hf-cache
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreate
+183 -388
View File
@@ -1,228 +1,199 @@
steps:
- label: ":full_moon: Omni Model Test with H100"
timeout_in_minutes: 90
depends_on: upload-nightly-pipeline
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test"
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- |
pytest -s -v \
tests/examples/ \
tests/e2e/online_serving/test_*_expansion.py \
-m "advanced_model and H100 and omni" --run-level "advanced_model"
agents:
queue: "mithril-h100-pool"
plugins:
- kubernetes:
podSpec:
containers:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
resources:
limits:
nvidia.com/gpu: 2
volumeMounts:
# Group: collapses under one heading in the Buildkite UI; child steps still run in parallel.
- group: ":card_index_dividers: Omni Model Test"
key: nightly-omni-test-group
steps:
- label: ":full_moon: Omni · Function Test with H100"
timeout_in_minutes: 90
depends_on: upload-nightly-pipeline
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test"
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- pytest -s -v tests/e2e/online_serving/test_*_expansion.py -m "advanced_model and H100 and omni" --run-level "advanced_model"
agents:
queue: "mithril-h100-pool"
plugins:
- kubernetes:
podSpec:
containers:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
resources:
limits:
nvidia.com/gpu: 2
volumeMounts:
- name: devshm
mountPath: /dev/shm
- name: hf-cache
mountPath: /root/.cache/huggingface
env:
- name: HF_HOME
value: /root/.cache/huggingface
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token-secret
key: token
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
- name: devshm
mountPath: /dev/shm
emptyDir:
medium: Memory
- name: hf-cache
mountPath: /root/.cache/huggingface
env:
- name: HF_HOME
value: /root/.cache/huggingface
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token-secret
key: token
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
- name: devshm
emptyDir:
medium: Memory
- name: hf-cache
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreate
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreate
- label: ":full_moon: Omni Model Test with L4"
timeout_in_minutes: 90
- label: ":full_moon: Omni · Function Test with L4"
timeout_in_minutes: 90
depends_on: upload-nightly-pipeline
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test"
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- export VLLM_ALLOW_LONG_MAX_MODEL_LEN="1"
- pytest -s -v tests/e2e/online_serving/test_*_expansion.py -m "advanced_model and L4 and omni" --run-level "advanced_model"
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"
- "HF_TOKEN"
volumes:
- "/fsx/hf_cache:/fsx/hf_cache"
- label: ":full_moon: Omni · Doc Test with L4"
timeout_in_minutes: 90
depends_on: upload-nightly-pipeline
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test"
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- export VLLM_ALLOW_LONG_MAX_MODEL_LEN="1"
- pytest -s -v tests/examples/ -m "advanced_model and omni and L4" --run-level "advanced_model"
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"
- "HF_TOKEN"
volumes:
- "/fsx/hf_cache:/fsx/hf_cache"
- label: ":full_moon: Omni · Doc Test with H100"
timeout_in_minutes: 90
depends_on: upload-nightly-pipeline
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test"
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- pytest -s -v tests/examples/ -m "advanced_model and omni and H100" --run-level "advanced_model"
agents:
queue: "mithril-h100-pool"
plugins:
- kubernetes:
podSpec:
containers:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
resources:
limits:
nvidia.com/gpu: 2
volumeMounts:
- name: devshm
mountPath: /dev/shm
- name: hf-cache
mountPath: /root/.cache/huggingface
env:
- name: HF_HOME
value: /root/.cache/huggingface
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token-secret
key: token
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
- name: devshm
emptyDir:
medium: Memory
- name: hf-cache
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreate
- label: ":full_moon: Omni · Perf Test"
key: nightly-omni-performance
timeout_in_minutes: 180
depends_on: upload-nightly-pipeline
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test"
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- export BENCHMARK_DIR=tests/dfx/perf/results
- export VLLM_ALLOW_LONG_MAX_MODEL_LEN="1"
- pytest -s -v tests/dfx/perf/scripts/run_benchmark.py
- buildkite-agent artifact upload "tests/dfx/perf/results/*.json"
- buildkite-agent artifact upload "tests/dfx/perf/results/*.html"
agents:
queue: "mithril-h100-pool"
plugins:
- kubernetes:
podSpec:
containers:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
resources:
limits:
nvidia.com/gpu: 2
volumeMounts:
- name: devshm
mountPath: /dev/shm
- name: hf-cache
mountPath: /root/.cache/huggingface
env:
- name: HF_HOME
value: /root/.cache/huggingface
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token-secret
key: token
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
- name: devshm
emptyDir:
medium: Memory
- name: hf-cache
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreate
# Dynamically appends steps from test-nightly-diffusion.yml into this build (same mechanism as
# pipeline.yml → test-ready.yml / test-merge.yml / test-nightly.yml). Foldable groups stay in the
# uploaded YAML (Other / Wan / Qwen-Image).
- label: ":card_index_dividers: Diffusion Model Test"
key: nightly-diffusion-model-test
depends_on: upload-nightly-pipeline
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test"
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- export VLLM_ALLOW_LONG_MAX_MODEL_LEN="1"
- pytest -s -v tests/examples/ tests/e2e/online_serving/test_*_expansion.py -m "advanced_model and L4 and omni" --run-level "advanced_model"
- buildkite-agent pipeline upload .buildkite/test-nightly-diffusion.yml
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"
- "HF_TOKEN"
volumes:
- "/fsx/hf_cache:/fsx/hf_cache"
queue: "cpu_queue_premerge"
- label: ":full_moon: Diffusion Model Test with H100"
- label: ":bar_chart: Testcase Statistics"
key: nightly-testcase-statistics
timeout_in_minutes: 120
depends_on: upload-nightly-pipeline
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test"
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- pytest -s -v tests/e2e/online_serving/test_*_expansion.py -k "not test_wan22_expansion and not test_wan_2_1_vace_expansion" -m "advanced_model and diffusion and H100" --run-level "advanced_model"
agents:
queue: "mithril-h100-pool"
plugins:
- kubernetes:
podSpec:
containers:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
resources:
limits:
nvidia.com/gpu: 2
volumeMounts:
- name: devshm
mountPath: /dev/shm
- name: hf-cache
mountPath: /root/.cache/huggingface
env:
- name: HF_HOME
value: /root/.cache/huggingface
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token-secret
key: token
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
- name: devshm
emptyDir:
medium: Memory
- name: hf-cache
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreate
- label: ":full_moon: Diffusion Model (Wan) Test with H100"
timeout_in_minutes: 90
depends_on: upload-nightly-pipeline
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test"
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- pytest -s -v tests/e2e/online_serving/test_wan22_expansion.py tests/e2e/online_serving/test_wan_2_1_vace_expansion.py -m "advanced_model" --run-level "advanced_model"
agents:
queue: "mithril-h100-pool"
plugins:
- kubernetes:
podSpec:
containers:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
resources:
limits:
nvidia.com/gpu: 2
volumeMounts:
- name: devshm
mountPath: /dev/shm
- name: hf-cache
mountPath: /root/.cache/huggingface
env:
- name: HF_HOME
value: /root/.cache/huggingface
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token-secret
key: token
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
- name: devshm
emptyDir:
medium: Memory
- name: hf-cache
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreate
- label: ":full_moon: Diffusion Model Test"
timeout_in_minutes: 60
depends_on: upload-nightly-pipeline
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test"
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- pytest -s -v tests/e2e/online_serving/test_*_expansion.py -m "advanced_model and diffusion and L4" --run-level "advanced_model"
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"
- "HF_TOKEN"
volumes:
- "/fsx/hf_cache:/fsx/hf_cache"
- label: ":full_moon: Doc Example Code Test with H100"
timeout_in_minutes: 60
depends_on: upload-nightly-pipeline
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test"
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- export VLLM_TEST_CLEAN_GPU_MEMORY="1"
- pytest -s -v tests/examples/online_serving/test_text_to_image.py tests/examples/offline_inference/test_text_to_image.py -m "advanced_model and example and H100" --run-level "advanced_model"
agents:
queue: "mithril-h100-pool"
plugins:
- kubernetes:
podSpec:
containers:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
resources:
limits:
nvidia.com/gpu: 2
volumeMounts:
- name: devshm
mountPath: /dev/shm
- name: hf-cache
mountPath: /root/.cache/huggingface
env:
- name: HF_HOME
value: /root/.cache/huggingface
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token-secret
key: token
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
- name: devshm
emptyDir:
medium: Memory
- name: hf-cache
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreate
- label: ":full_moon: Omni Model Perf Test & Testcase Statistics with H100"
key: nightly-omni-performance
timeout_in_minutes: 180
depends_on: upload-nightly-pipeline
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test"
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- export BENCHMARK_DIR=tests/dfx/perf/results
- export VLLM_ALLOW_LONG_MAX_MODEL_LEN="1"
- pytest -s -v tests/dfx/perf/scripts/run_benchmark.py
- buildkite-agent artifact upload "tests/dfx/perf/results/*.json"
- python tools/nightly/buildkite_testcase_statistics.py -o tests/dfx/perf/results/buildkite_testcase_statistics.html
- buildkite-agent artifact upload "tests/dfx/perf/results/*.html"
agents:
@@ -259,189 +230,13 @@ steps:
path: /mnt/hf-cache
type: DirectoryOrCreate
- label: ":full_moon: GEBench Accuracy Test with H100"
key: nightly-gebench-accuracy
timeout_in_minutes: 60
depends_on: upload-nightly-pipeline
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test"
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- pytest -s -v tests/e2e/accuracy/test_gebench_h100_smoke.py --run-level advanced_model --gebench-model Qwen/Qwen-Image-2512 --accuracy-judge-model QuantTrio/Qwen3-VL-30B-A3B-Instruct-AWQ --accuracy-gpu 0 --gebench-port 8093 --accuracy-workers 1
- buildkite-agent artifact upload "tests/e2e/accuracy/artifacts/gebench_qwen-image-2512/summary*.json"
agents:
queue: "mithril-h100-pool"
plugins:
- kubernetes:
podSpec:
containers:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
resources:
limits:
nvidia.com/gpu: 1
volumeMounts:
- name: devshm
mountPath: /dev/shm
- name: hf-cache
mountPath: /root/.cache/huggingface
env:
- name: HF_HOME
value: /root/.cache/huggingface
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token-secret
key: token
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
- name: devshm
emptyDir:
medium: Memory
- name: hf-cache
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreate
- label: ":full_moon: GEdit-Bench Accuracy Test with H100"
key: nightly-gedit-bench-accuracy
timeout_in_minutes: 60
depends_on: upload-nightly-pipeline
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test"
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- pytest -s -v tests/e2e/accuracy/test_gedit_bench_h100_smoke.py --run-level advanced_model --gedit-model Qwen/Qwen-Image-Edit --accuracy-judge-model QuantTrio/Qwen3-VL-30B-A3B-Instruct-AWQ --accuracy-gpu 0 --gedit-port 8093 --gedit-samples-per-group 20 --accuracy-workers 1
- buildkite-agent artifact upload "tests/e2e/accuracy/artifacts/gedit_scores_qwen-image-edit/qwen-image-edit_all_all_vie_score_*.csv"
- buildkite-agent artifact upload "tests/e2e/accuracy/artifacts/gedit_scores_qwen-image-edit/qwen-image-edit_all_all_summary_*.json"
agents:
queue: "mithril-h100-pool"
plugins:
- kubernetes:
podSpec:
containers:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
resources:
limits:
nvidia.com/gpu: 1
volumeMounts:
- name: devshm
mountPath: /dev/shm
- name: hf-cache
mountPath: /root/.cache/huggingface
env:
- name: HF_HOME
value: /root/.cache/huggingface
- name: VLLM_HTTP_TIMEOUT_KEEP_ALIVE
value: "120"
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token-secret
key: token
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
- name: devshm
emptyDir:
medium: Memory
- name: hf-cache
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreate
- label: ":full_moon: Wan22 I2V Accuracy Test with H100"
key: nightly-wan22-i2v-accuracy
timeout_in_minutes: 180
depends_on: upload-nightly-pipeline
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test"
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- pytest -s -v tests/e2e/accuracy/wan22_i2v/test_wan22_i2v_video_similarity.py --run-level advanced_model
agents:
queue: "mithril-h100-pool"
plugins:
- kubernetes:
podSpec:
containers:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
resources:
limits:
nvidia.com/gpu: 2
volumeMounts:
- name: devshm
mountPath: /dev/shm
- name: hf-cache
mountPath: /root/.cache/huggingface
env:
- name: HF_HOME
value: /root/.cache/huggingface
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token-secret
key: token
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
- name: devshm
emptyDir:
medium: Memory
- name: hf-cache
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreate
- label: ":full_moon: Diffusion Perf Test with H100"
key: nightly-qwen-image-performance
timeout_in_minutes: 180
depends_on: upload-nightly-pipeline
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test"
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- export DIFFUSION_BENCHMARK_DIR=tests/dfx/perf/results
- export CACHE_DIT_VERSION=1.3.0
- pytest -s -v tests/dfx/perf/scripts/run_diffusion_benchmark.py --config-file tests/dfx/perf/tests/test_qwen_image_vllm_omni.json
- buildkite-agent artifact upload "tests/dfx/perf/results/benchmark_results_*.json"
- buildkite-agent artifact upload "tests/dfx/perf/results/logs/*.log"
agents:
queue: "mithril-h100-pool"
plugins:
- kubernetes:
podSpec:
containers:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
resources:
limits:
nvidia.com/gpu: 4
volumeMounts:
- name: devshm
mountPath: /dev/shm
- name: hf-cache
mountPath: /root/.cache/huggingface
env:
- name: HF_HOME
value: /root/.cache/huggingface
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token-secret
key: token
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
- name: devshm
emptyDir:
medium: Memory
- name: hf-cache
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreate
# No need to run this step for PRs with label nightly-test
- label: ":email: Nightly Collection & Email"
key: nightly-perf-distribution
depends_on:
- nightly-omni-performance
- nightly-qwen-image-performance
- nightly-testcase-statistics
if: build.env("NIGHTLY") == "1"
commands:
- pip install openpyxl
+18 -21
View File
@@ -16,11 +16,10 @@ steps:
volumes:
- "/fsx/hf_cache:/fsx/hf_cache"
- label: "Voxtral TTS CUDA Unit Test"
timeout_in_minutes: 10
- label: "CUDA Unit Test with single card"
depends_on: upload-ready-pipeline
commands:
- "timeout 10m pytest -s -v tests/model_executor/models/voxtral_tts/test_cuda_graph_acoustic_transformer.py"
- timeout 10m pytest -v -s -m 'core_model and cuda and L4 and not distributed_cuda' --ignore=tests/e2e --ignore=tests/engine/test_async_omni_engine_abort.py --cov=vllm_omni --cov-branch --cov-report=term-missing --cov-report=html --cov-report=xml
agents:
queue: "gpu_1_queue"
plugins:
@@ -33,6 +32,22 @@ steps:
volumes:
- "/fsx/hf_cache:/fsx/hf_cache"
- label: "CUDA Unit Test with multi cards"
depends_on: upload-ready-pipeline
commands:
- timeout 10m pytest -v -s -m 'core_model and cuda and L4 and distributed_cuda' --ignore=tests/e2e --cov=vllm_omni --cov-branch --cov-report=term-missing --cov-report=html --cov-report=xml
agents:
queue: "gpu_4_queue"
plugins:
- docker#v5.2.0:
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
always-pull: true
propagate-environment: true
environment:
- "HF_HOME=/fsx/hf_cache"
volumes:
- "/fsx/hf_cache:/fsx/hf_cache"
- label: "Diffusion Model Test"
depends_on: upload-ready-pipeline
commands:
@@ -152,24 +167,6 @@ steps:
volumes:
- "/fsx/hf_cache:/fsx/hf_cache"
- label: "Diffusion GPU Worker Test"
depends_on: upload-ready-pipeline
commands:
- timeout 20m pytest -s -v tests/diffusion/test_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"
- "HF_TOKEN"
volumes:
- "/fsx/hf_cache:/fsx/hf_cache"
- label: "Engine Test"
depends_on: upload-ready-pipeline
+1
View File
@@ -98,6 +98,7 @@ nav:
- design/feature/ray_based_execution.md
- design/feature/omni_connectors/
- design/feature/cfg_parallel.md
- design/feature/expert_parallel.md
- design/feature/sequence_parallel.md
- design/feature/tensor_parallel.md
- design/feature/vae_parallel.md
+221
View File
@@ -0,0 +1,221 @@
# Expert Parallel
This section describes how to add Expert Parallel (EP) to a diffusion transformer that uses Mixture-of-Experts (MoE) layers.
We use **HunyuanImage3.0** as the reference implementation.
---
## Table of Contents
- [Overview](#overview)
- [Step-by-Step Implementation](#step-by-step-implementation)
- [Testing](#testing)
- [Reference Implementations](#reference-implementations)
- [Summary](#summary)
---
## Overview
### What is Expert Parallel?
**Expert Parallel** is a parallelism strategy in Mixture-of-Experts (MoE) models that distributes different expert networks across distinct computational devices. Each device holds and computes only a subset of experts (local experts), with tokens dispatched to and gathered from remote devices via collective communication operations (e.g., All-to-All, All-Gather).
| Backend | Description |
|---------|-------------|
| `allgather_reducescatter` | Default backend based on allgather/reducescatter primitives, suitable for general EP+DP deployments.|
## Configuration
Enable EP by setting the `--enable-expert-parallel` flag. The EP size is automatically calculated as:
```text
EP_SIZE = TP_SIZE × SP_SIZE × CFG_SIZE × DP_SIZE
```
Where:
- `TP_SIZE`: Tensor parallel size
- `SP_SIZE`: Sequence parallel size
- `CFG_SIZE`: Classifier-free guidance parallel size
- `DP_SIZE`: Data parallel size
- `EP_SIZE`: Expert parallel size (computed automatically)
Note:
- Expert parallelism is only applicable to Mixture-of-Experts (MoE) models.
- The EP group is created **per pipeline stage**, meaning it includes all ranks that participate in model parallelism except pipeline parallelism.
- The underlying communication pattern for expert parallelism is **All-to-All** among the ranks in the EP group.
For example, consider a configuration with `TP=2`, `SP=1`, `CFG=2`, and `DP=4` (total 2×1×2×4 = 16 GPUs).
- Expert layers are handled by an EP group of size 16.
- Attention layers use tensor parallelism of size 2 within each of the 8 DP groups (because `DP×CFG×SP = 4×2×1 = 8` groups, each containing the 2 TP ranks). Inside each such group, the attention weights are sharded across the 2 GPUs.
## Step-by-Step Implementation
### Step 1: Configure Expert Parallelism Settings
Calculate local experts per rank:
```
ep_size = 8 # Expert Parallel size (typically equals TP size)
num_experts = 64
num_local_experts = num_experts // ep_size # 8 experts per card
# Check divisibility
assert num_experts % ep_size == 0, "Experts must be divisible by EP size"
```
### Step 2: Use Sparse MoE Block to enable EP routing.
Example:
```
from vllm.model_executor.layers.linear import ReplicatedLinear
class HunYuanSparseMoeBlock(nn.Module):
def __init__(
self,
config: PretrainedConfig,
layer_id: int = -1,
prefix: str = "",
):
super().__init__()
self.tp_size = get_tensor_model_parallel_world_size()
self.n_routed_experts = config.num_experts # 64
# Calculate local experts per rank (key for EP)
if self.tp_size > self.n_routed_experts:
raise ValueError(f"TP size {self.tp_size} > experts {self.n_routed_experts}")
# Routing gate (replicated on all ranks, computes scores for all tokens to all experts)
self.gate = ReplicatedLinear(
config.hidden_size,
config.num_experts,
bias=False,
quant_config=None,
prefix=f"{prefix}.gate",
)
# EP expert layer (factory loads platform-specific implementation)
self.experts = HunyuanFusedMoE(...)
```
**Key Points:**
- gate is **ReplicatedLinear** (replicated on all ranks)
- experts is created via **HunyuanFusedMoE factory**, which automatically handles EP dispatch
### Step 3: Initialize EP Runtime
Initialize the EP communication context before model loading.
```
from vllm.utils.import_utils import resolve_obj_by_qualname
# Call during __init__ or model loading
op_name = "hunyuan_fused_moe"
# Prepare EP runtime: establish communication groups, assign local expert indices, init _expert_map
current_omni_platform.prepare_diffusion_op_runtime(op_name)
# Factory automatically resolves platform implementation (GPU: FusedMoE / NPU: AscendFusedMoE)
impl = resolve_obj_by_qualname(
current_omni_platform.get_diffusion_model_impl_qualname(op_name)
)
```
### Step 4: Expert Weight Mapping & Loading
Each rank loads only the expert weights assigned to its local allocation.
```
# Get expert parameter mapping (different per rank)
expert_mapping = HunyuanFusedMoE.make_expert_params_mapping(
model=self,
ckpt_gate_proj_name="gate_proj",
ckpt_down_proj_name="down_proj",
ckpt_up_proj_name="up_proj",
num_experts=64,
num_redundant_experts=0,
)
# Returns: [(param_name, weight_name, expert_id, shard_id), ...]
# Note: Each rank only contains mappings for its local expert_ids
# Filter non-local experts during loading
for name, loaded_weight in weights:
if "mlp.experts" in name:
# Parse expert_id from weight name (implementation needed)
expert_id = parse_expert_id_from_name(name)
local_expert_start = (ep_rank) * num_local_experts
local_expert_end = (ep_rank + 1) * num_local_experts
if not (local_expert_start <= expert_id < local_expert_end):
continue # Skip non-local expert weights
```
### Step 5: Forward Pass with EP
Example (MoE Forward):
```
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
orig_shape = hidden_states.shape
hidden_states = hidden_states.view(-1, hidden_states.shape[-1])
# 1. Global routing computation (all tokens, all expert scores)
# hidden_states: [num_tokens, hidden_dim] (full tensor)
router_logits, _ = self.gate(hidden_states) # [num_tokens, num_experts]
# 2. EP dispatch and compute (HunyuanFusedMoE handles all_to_all internally)
# - Dispatch: Send tokens to target ranks based on router_logits
# - Local Compute: Each rank processes only its num_local_experts
# - Combine: Results returned to original token positions
final_hidden_states = self.experts(
hidden_states=hidden_states,
router_logits=router_logits,
)
# 3. Add shared expert output (not EP, computed on all ranks)
if self.shared_mlp is not None:
shared_out = self.shared_mlp(hidden_states)
final_hidden_states = final_hidden_states + shared_out
# 4. Tensor Parallel All-Reduce (synchronize across TP group)
if self.tp_size > 1:
final_hidden_states = self.experts.maybe_all_reduce_tensor_model_parallel(
final_hidden_states
)
return final_hidden_states.view(orig_shape)
```
## Testing
After adding Expert Parallel support, test via command line:
```bash
cd examples/offline_inference/text_to_image
python text_to_image.py \
--model Your-org/your-model \
--prompt "a cup of coffee on the table" \
--output "ep_enabled.png" \
--num-inference-steps 50 \
--guidance-scale 5.0 \
--tensor-parallel-size 8 \
--seed 1234 \
--enable-expert-parallel
```
vLLM‑Omni currently focuses on core diffusion model inference acceleration, so the Expert Parallel implementation includes only the basic multi‑GPU expert sharding functionality (enabled via --enable-expert-parallel). Advanced features such as communication backend selection (--all2all-backend), load balancing (--enable-eplb and its configuration), and multi‑node deployment belong to the extended capabilities of the main vLLM project and have not yet been integrated into Omni.
## Reference Implementations
Complete examples in the codebase:
| Model | Path | Pattern | Notes |
|-------|------|---------|-------|
| **HunyuanImage3.0** | `vllm_omni/diffusion/models/hunyuan_image_3/hunyuan_image_3_transformer.py` | Standard EP | Full implementation with validation |
| **EP Tests** | `vllm-omni/tests/e2e/offline_inference/test_expert_parallel.py` | E2E testing | EP correctness and performance |
| **Constraint Tests** | `vllm-omni/tests/diffusion/models/hunyuan_image_3/test_hunyuan_fused_moe.py` | Unit testing | Validation logic |
---
## Summary
Adding Expert Parallel support to diffusion model:
1. **Identify MoE layers** - Locate the router and expert networks in each transformer block.
2. **Validate EP constraints** – Ensure num_experts is divisible by expert_parallel_size.
3. **Test** - Run with enable-expert-parallel, check memory reduction, speedup, and output quality against single‑GPU baseline.
@@ -33,8 +33,8 @@ runtime:
zmq_port: 50051 # ZMQ base port (see "Port Offset Scheme" below)
protocol: "rdma" # "rdma" or "tcp"
device_name: "" # RDMA device (e.g., "mlx5_0"), empty for auto-detect
memory_pool_size: 2147483648 # 2GB memory pool
memory_pool_device: "cpu" # "cpu" for pinned memory, "cuda" for GPUDirect RDMA
memory_pool_size: 4294967296 # 4 GB (CPU); use 2147483648 (2 GB) for GPU
memory_pool_device: "cpu" # "cpu" for pinned memory (recommended), "cuda" for GPUDirect RDMA
```
Wire stages to the connector:
@@ -64,8 +64,8 @@ stage_args:
| Parameter | Default | Description |
|---|---|---|
| `memory_pool_size` | 1 GB | Total size of the RDMA-registered memory pool in bytes. |
| `memory_pool_device` | `"cpu"` | `"cpu"`: pinned host memory (recommended). `"cuda"`: GPU VRAM for GPUDirect RDMA (requires NIC-GPU direct PCIe connectivity). |
| `memory_pool_size` | 4 GB (CPU) / 2 GB (GPU) | Total size of the RDMA-registered memory pool in bytes. Recommended 4 GB for CPU pinned memory; 2 GB for GPU VRAM to conserve device memory. |
| `memory_pool_device` | `"cpu"` | `"cpu"`: pinned host memory (recommended, works on all topologies). `"cuda"`: GPU VRAM for GPUDirect RDMA (requires NIC-GPU direct PCIe connectivity, PIX topology). |
### Networking
@@ -107,10 +107,10 @@ receiver_connect = remote_side_channel_port + tp_rank
## Memory Pool Modes
| Mode | Config | Data Flow | Best For |
|---|---|---|---|
| CPU Pinned | `memory_pool_device: "cpu"` | GPU → CPU pool → RDMA → CPU pool → GPU | Most hardware topologies (recommended) |
| GPUDirect | `memory_pool_device: "cuda"` | GPU → GPU pool → RDMA (NIC reads GPU BAR1) → GPU pool | NIC-GPU direct PCIe (PIX topology) |
| Mode | Config | Recommended Pool Size | Data Flow | Best For |
|---|---|---|---|---|
| CPU Pinned | `memory_pool_device: "cpu"` | 4 GB | GPU → CPU pool → RDMA → CPU pool → GPU | Most hardware topologies (recommended) |
| GPUDirect | `memory_pool_device: "cuda"` | 2 GB | GPU → GPU pool → RDMA (NIC reads GPU BAR1) → GPU pool | NIC-GPU direct PCIe (PIX topology) |
> **Note**: GPUDirect RDMA requires the NIC and GPU to share a direct PCIe
> switch (PIX topology). On systems where they are connected via PXB or NODE,
+1
View File
@@ -60,5 +60,6 @@ th {
| `HunyuanVideo15Pipeline` | HunyuanVideo-1.5-T2V | `hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-480p_t2v`, `hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-720p_t2v` | ✅︎ | ✅︎ | | |
| `HunyuanVideo15ImageToVideoPipeline` | HunyuanVideo-1.5-I2V | `hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-480p_i2v`, `hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-720p_i2v` | ✅︎ | ✅︎ | | |
| `VoxtralTTSForConditionalGeneration` | Voxtral TTS | `mistralai/Voxtral-4B-TTS-2603` | ✅︎ | ✅︎ | | |
|`DyninOmniForConditionalGeneration` | Dynin-Omni | `snu-aidas/Dynin-Omni` | ✅︎ | | | |
✅︎ indicates the model is supported on that backend. Empty cells mean not listed as supported on that backend.
@@ -139,11 +139,15 @@ Factory function `get_offload_backend()` selects the appropriate backend based o
## Supported Models
| Architecture | Example Models | DiT Class | Model-Level Offload | Layerwise Offload | Blocks Attr (Layerwise specific) |
|--------------|----------------|-----------|---------------------|-------------------|-------------|
| Wan22Pipeline | `Wan-AI/Wan2.2-T2V-A14B-Diffusers` | `WanTransformer3DModel` | ✓ | ✓ | `"blocks"` |
| Wan22I2VPipeline | `Wan-AI/Wan2.2-I2V-A14B-Diffusers` | `WanTransformer3DModel` | ✓ | ✓ | `"blocks"` |
| Architecture | Example Models | DiT Class | Model-Level Offload | Layerwise Offload | Blocks Attrs (Layerwise specific) |
|--------------|----------------|-----------|---------------------|-------------------|-----------------------------------|
| LongCatImagePipeline | `meituan-longcat/LongCat-Image` | `LongCatImageTransformer2DModel` | - | ✓ | `"transformer_blocks"`, `"single_transformer_blocks"` |
| NextStep11Pipeline | `stepfun-ai/NextStep-1.1` | `NextStepModel` | - | ✓ | `"layers"` |
| OvisImagePipeline | `AIDC-AI/Ovis-Image-7B` | `OvisImageTransformer2DModel` | - | ✓ | `"transformer"` |
| QwenImagePipeline | `Qwen/Qwen-Image` | `QwenImageTransformer2DModel` | ✓ | ✓ | `"transformer_blocks"` |
| StableDiffusion3Pipeline | `stabilityai/stable-diffusion-3.5-medium` | `SD3Transformer2DModel` | - | ✓ | `"transformer_blocks"` |
| Wan22I2VPipeline | `Wan-AI/Wan2.2-I2V-A14B-Diffusers` | `WanTransformer3DModel` | ✓ | ✓ | `"blocks"` |
| Wan22Pipeline | `Wan-AI/Wan2.2-T2V-A14B-Diffusers` | `WanTransformer3DModel` | ✓ | ✓ | `"blocks"` |
**Notes:**
- Model-Level Offloading is expected to be supported by all common diffusion models (DiT and encoders) naturally
+69 -6
View File
@@ -12,7 +12,7 @@
vLLM-Omni supports various advanced features for diffusion models:
- Acceleration: **cache methods**, **parallelism methods**
- Acceleration: **cache methods**, **parallelism methods**, **startup optimizations**
- Memory optimization: **cpu offloading**, **quantization**
- Extensions: **LoRA inference**
- Execution modes: **step execution**
@@ -44,6 +44,12 @@ Parallelism methods distribute computation across GPUs without quality loss (mat
| **[HSDP](diffusion/parallelism/hsdp.md)** | Weight sharding via FSDP2, redistributed on-demand at runtime | Very large models (14B+) on limited VRAM, combinable with SP |
| **[Expert Parallelism](diffusion/parallelism/expert_parallel.md)** | Shards MoE expert MLP blocks across devices | MoE diffusion models (e.g., HunyuanImage3.0) |
#### Startup Optimization
| Method | Description | Best For |
|--------|-------------|----------|
| **[Multi-Thread Weight Loading](#multi-thread-weight-loading)** | Loads safetensors shards in parallel using a thread pool | All diffusion models; reduces startup from minutes to seconds |
**Note:** Some acceleration methods can be combined together for optimized performance. See [Feature Compatibility Table](#feature-compatibility) and [Feature Compatibility Tutorial](feature_compatibility.md) for detailed configuration examples.
### Memory Optimization
@@ -107,19 +113,19 @@ The following tables show which models support each feature:
| **FLUX.2-dev** | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| **GLM-Image** | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| **HunyuanImage3** | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | ❌ |
| **LongCat-Image** | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| **LongCat-Image-Edit** | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| **LongCat-Image** | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ |
| **LongCat-Image-Edit** | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ |
| **MagiHuman** | ❌ | ❌ | ❌ | ❓ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ |
| **MammothModa2(T2I)** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| **Nextstep_1(T2I)** | ❓ | ❓ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| **Nextstep_1(T2I)** | ❓ | ❓ | ❌ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ |
| **OmniGen2** | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| **Ovis-Image** | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| **Ovis-Image** | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
| **Qwen-Image** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ (decode) | ✅ | ✅ |
| **Qwen-Image-2512** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ (decode) | ✅ | ✅ |
| **Qwen-Image-Edit** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ (decode) | ❌ | ❌ |
| **Qwen-Image-Edit-2509** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ (decode) | ✅ | ❌ | ❌ |
| **Qwen-Image-Layered** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ (decode) | ❌ | ❌ |
| **Stable-Diffusion3.5** | ❌ | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ | ✅ (decode) | ❌ | ❌ |
| **Stable-Diffusion3.5** | ❌ | ✅ | ❌ | ✅ | ✅ | ❌ | ✅ | ✅ (decode) | ❌ | ❌ |
| **Z-Image** | ✅ | ✅ | ✅ | ❓ | ✅ (TP=2 only) | ✅ | ❌ | ✅ (decode) | ✅ | ❌ |
> Notes:
@@ -179,6 +185,59 @@ The following tables show which models support each feature:
6. Step Execution is not compatible with cache backends (TeaCache, Cache-DiT) or LoRA.
## Multi-Thread Weight Loading
Large diffusion models can take several minutes to load weights at startup (e.g., ~3 min for Qwen-Image, ~5 min for Wan2.2 I2V 14B). Multi-thread weight loading speeds up this process by loading safetensors shards in parallel using a thread pool instead of sequentially.
This optimization is **enabled by default** with 4 threads. No configuration is needed for the default behavior.
### Configuration
| Parameter | CLI Flag | Default | Description |
|-----------|----------|---------|-------------|
| `enable_multithread_weight_load` | `--disable-multithread-weight-load` | `True` (enabled) | Pass the flag to disable multi-thread loading |
| `num_weight_load_threads` | `--num-weight-load-threads` | `4` | Number of threads for parallel weight loading |
!!! tip
The default of 4 threads balances speed and disk I/O contention. On fast NVMe storage you may benefit from more threads (e.g., 8). On HDD or network storage, the default of 4 avoids saturating I/O bandwidth.
### Online Serving
```bash
# Default (multi-thread enabled, 4 threads)
vllm serve Qwen/Qwen-Image --omni --port 8091
# Custom thread count
vllm serve Wan-AI/Wan2.2-I2V-A14B-Diffusers --omni --num-weight-load-threads 8
# Disable multi-thread loading
vllm serve Qwen/Qwen-Image --omni --disable-multithread-weight-load
```
### Offline Inference
```python
from vllm_omni import Omni
# Default (multi-thread enabled, 4 threads)
omni = Omni(model="Qwen/Qwen-Image")
# Custom thread count
omni = Omni(
model="Wan-AI/Wan2.2-I2V-A14B-Diffusers",
num_weight_load_threads=8,
)
```
### Benchmarks
Measured on NVIDIA H800:
| Model | Before | After | Speedup |
|-------|--------|-------|---------|
| **Qwen/Qwen-Image** (53.7 GiB) | 168s | 27s | **6.2x** |
| **Wan-AI/Wan2.2-I2V-A14B-Diffusers** (64.5 GiB) | 283s | 56s | **5.1x** |
## Learn More
**Cache Acceleration:**
@@ -204,6 +263,10 @@ The following tables show which models support each feature:
- **[Step Execution Guide](diffusion/step_execution.md)** - Per-step denoise execution with mid-request abort support
**Startup Optimization:**
- **[Multi-Thread Weight Loading](#multi-thread-weight-loading)** - Speed up model startup by loading safetensors shards in parallel
**Advanced Topics:**
- **[Feature Compatibility](feature_compatibility.md)** - How to combine multiple features for maximum performance
@@ -0,0 +1,110 @@
# Dynin-Omni Offline End2End Example
This folder contains a unified offline inference entrypoint:
- `end2end.py`
## 1. Environment Setup
Run from repository root:
```bash
cd <REPO_ROOT>
```
If needed, install this repo in editable mode:
```bash
pip install -e .
```
## 2. Extra Dependencies (EMOVA)
Install the following packages for EMOVA-related components:
```bash
pip install \
"phonemizer==3.3.0" \
"Unidecode==1.4.0" \
"hydra-core==1.3.2" \
"pytorch-lightning==1.1.0" \
"wget==3.2" \
"wrapt==2.1.1" \
"onnx==1.20.1" \
"frozendict==2.4.7" \
"inflect==7.5.0" \
"braceexpand==0.1.7" \
"webdataset==1.0.2" \
"torch-stft==0.1.4" \
"editdistance==0.8.1"
```
## 3. Hardware and VRAM Requirements
This example uses a 3-stage pipeline on one GPU by default
([`dynin_omni.yaml`](../../../vllm_omni/model_executor/stage_configs/dynin_omni.yaml)):
- Stage-0 (`token2text`): `gpu_memory_utilization: 0.5`
- Stage-1 (`token2image`): `gpu_memory_utilization: 0.1`
- Stage-2 (`token2audio`): `gpu_memory_utilization: 0.1`
### Requested GPU Memory Budget from `gpu_memory_utilization`
| Stage | Utilization | A100 80GB | H200 141GB |
| :-- | :-- | :-- | :-- |
| Stage-0 (token2text) | 0.5 | ~40.0 GB | ~70.5 GB |
| Stage-1 (token2image) | 0.1 | ~8.0 GB | ~14.1 GB |
| Stage-2 (token2audio) | 0.1 | ~8.0 GB | ~14.1 GB |
| Total requested budget | 0.7 | ~56.0 GB | ~98.7 GB |
### Observed Runtime Signal (from your log)
- Stage-0 reported: `Model loading took 15.12 GiB memory` (weights footprint signal).
- Stages 1/2 can still add runtime memory depending on task path and backend allocations.
- Keep extra headroom for CUDA/PyTorch overhead and temporary allocations.
### GPU Compatibility
- Confirmed target GPUs for this setup: **NVIDIA H200**, **NVIDIA A100**.
- CI/e2e coverage in this repo also includes CUDA **L4** markers for Dynin tests.
## 4. End2End Run Examples
```bash
# t2t
python <REPO_ROOT>/examples/offline_inference/dynin_omni/end2end.py \
--task t2t --model snu-aidas/Dynin-Omni --text <INSTRUCTION_TEXT>
# i2t
python <REPO_ROOT>/examples/offline_inference/dynin_omni/end2end.py \
--task i2t --model snu-aidas/Dynin-Omni --image <IMAGE_PATH> --text "Please describe this image in detail."
# s2t
python <REPO_ROOT>/examples/offline_inference/dynin_omni/end2end.py \
--task s2t --model snu-aidas/Dynin-Omni --audio <AUDIO_PATH> --text "Transcribe the given audio."
# t2i
python <REPO_ROOT>/examples/offline_inference/dynin_omni/end2end.py \
--task t2i --model snu-aidas/Dynin-Omni --text <INSTRUCTION_TEXT>
# v2t
python <REPO_ROOT>/examples/offline_inference/dynin_omni/end2end.py \
--task v2t --model snu-aidas/Dynin-Omni --video <VIDEO_PATH> --text "Describe this video in detail."
# i2i
python <REPO_ROOT>/examples/offline_inference/dynin_omni/end2end.py \
--task i2i --model snu-aidas/Dynin-Omni --image <IMAGE_PATH> --text <INSTRUCTION_TEXT>
# t2s
python <REPO_ROOT>/examples/offline_inference/dynin_omni/end2end.py \
--task t2s --model snu-aidas/Dynin-Omni --text <INSTRUCTION_TEXT>
```
## 5. Notes
- Outputs are saved under task-specific directories in `/tmp` by default.
- You can override output path with `--output-dir`.
- If you want to force local config resolution, pass `--dynin-config-path <PATH_TO_DYNIN_OMNI_YAML>`.
- If you see the warning
`max_num_batched_tokens (32768) exceeds max_num_seqs * max_model_len (4096)`,
reduce `max_num_batched_tokens` in stage config (for example, `4096` in CI config).
File diff suppressed because it is too large. Load diff
@@ -51,5 +51,6 @@ Key arguments:
- `--vae-use-tiling`: enable VAE tiling for memory optimization.
- `--cfg-parallel-size`: set it to 2 to enable CFG Parallel. See more examples in [`user_guide`](../../../docs/user_guide/diffusion/parallelism_acceleration.md#cfg-parallel).
- `--enable-cpu-offload`: enable CPU offloading for diffusion models.
- `--strength`: **Z-Image only** - controls the denoising start timestep for I2I (default: 0.6). Range: [0.0, 1.0]. Lower values preserve more of the original image; higher values allow more creative changes.
> ℹ️ If you encounter OOM errors, try using `--vae-use-slicing` and `--vae-use-tiling` to reduce memory usage.
@@ -146,7 +146,7 @@ def parse_args() -> argparse.Namespace:
"--audio-sample-rate",
type=int,
default=24000,
help="Sample rate for audio output when saved (default: 24000 for LTX2).",
help="Sample rate for audio output when saved (default: 24000).",
)
parser.add_argument(
"--cache-backend",
@@ -471,15 +471,9 @@ def main():
video_array = _ensure_frame_list(video_array)
use_ltx2_export = is_ltx2
encode_video = None
if use_ltx2_export:
try:
from diffusers.pipelines.ltx2.export_utils import encode_video
except ImportError:
encode_video = None
if audio is not None:
from vllm_omni.diffusion.utils.media_utils import mux_video_audio_bytes
if use_ltx2_export and encode_video is not None:
if isinstance(video_array, list):
frames_np = np.stack(video_array, axis=0)
elif isinstance(video_array, np.ndarray):
@@ -490,25 +484,24 @@ def main():
if frames_np.ndim == 4 and frames_np.shape[-1] == 4:
frames_np = frames_np[..., :3]
audio_out = None
if audio is not None:
if isinstance(audio, list):
audio = audio[0] if audio else None
if isinstance(audio, np.ndarray):
audio = torch.from_numpy(audio)
if isinstance(audio, torch.Tensor):
audio_out = audio
if audio_out.dim() > 1:
audio_out = audio_out[0]
audio_out = audio_out.float().cpu()
frames_u8 = (np.clip(frames_np, 0.0, 1.0) * 255).round().clip(0, 255).astype("uint8")
encode_video(
frames_np,
fps=fps,
audio=audio_out,
audio_sample_rate=args.audio_sample_rate if audio_out is not None else None,
output_path=str(output_path),
audio_np = audio
if isinstance(audio_np, list):
audio_np = audio_np[0] if audio_np else None
if isinstance(audio_np, torch.Tensor):
audio_np = audio_np.detach().cpu().float().numpy()
if isinstance(audio_np, np.ndarray):
audio_np = np.squeeze(audio_np).astype(np.float32)
video_bytes = mux_video_audio_bytes(
frames_u8,
audio_np,
fps=float(fps),
audio_sample_rate=args.audio_sample_rate,
)
with open(str(output_path), "wb") as f:
f.write(video_bytes)
else:
export_to_video(video_array, str(output_path), fps=fps)
print(f"Saved generated video to {output_path}")
@@ -94,16 +94,21 @@ def main():
print(f"Video frames: shape={video_frames.shape}, dtype={video_frames.dtype}")
audio_waveform = None
if hasattr(first, "multimodal_output") and first.multimodal_output:
audio_waveform = first.multimodal_output.get("audio")
mm = first.multimodal_output or {}
if mm:
audio_waveform = mm.get("audio")
if audio_waveform is not None:
print(f"Audio waveform: shape={audio_waveform.shape}, dtype={audio_waveform.dtype}")
output_fps = float(mm.get("fps", 25))
output_sr = int(mm.get("audio_sample_rate", 24000))
print(f"Using fps={output_fps}, audio_sample_rate={output_sr} from model output")
video_bytes = mux_video_audio_bytes(
video_frames,
audio_waveform,
fps=25.0,
audio_sample_rate=44100,
fps=output_fps,
audio_sample_rate=output_sr,
)
with open(args.output, "wb") as f:
f.write(video_bytes)
@@ -376,7 +376,7 @@ def main():
f"vae_patch_parallel_size={args.vae_patch_parallel_size}, "
f"enable_expert_parallel={args.enable_expert_parallel}."
)
print(f" CPU offload: {args.enable_cpu_offload}")
print(f" CPU offload: {args.enable_cpu_offload}; CPU Layerwise Offload: {args.enable_layerwise_offload}")
print(f" Image size: {args.width}x{args.height}")
if args.lora_path:
print(f" LoRA: scale={args.lora_scale}")
@@ -160,7 +160,7 @@ def parse_args() -> argparse.Namespace:
"--audio-sample-rate",
type=int,
default=24000,
help="Sample rate for audio output when saved (default: 24000 for LTX2).",
help="Sample rate for audio output when saved (default: 24000).",
)
parser.add_argument(
"--vae-patch-parallel-size",
@@ -430,17 +430,8 @@ def main():
video_array = _ensure_frame_list(video_array)
use_ltx2_export = False
if args.model and "ltx" in str(args.model).lower():
use_ltx2_export = True
if audio is not None:
use_ltx2_export = True
if use_ltx2_export:
try:
from diffusers.pipelines.ltx2.export_utils import encode_video
except ImportError:
raise ImportError("diffusers is required for LTX2 encode_video.")
from vllm_omni.diffusion.utils.media_utils import mux_video_audio_bytes
if isinstance(video_array, list):
frames_np = np.stack(video_array, axis=0)
@@ -449,28 +440,24 @@ def main():
else:
frames_np = np.asarray(video_array)
frames_u8 = (frames_np * 255).round().clip(0, 255).astype("uint8")
video_tensor = torch.from_numpy(frames_u8)
frames_u8 = (np.clip(frames_np, 0.0, 1.0) * 255).round().clip(0, 255).astype("uint8")
audio_out = None
if audio is not None:
if isinstance(audio, list):
audio = audio[0] if audio else None
if isinstance(audio, np.ndarray):
audio = torch.from_numpy(audio)
if isinstance(audio, torch.Tensor):
audio_out = audio
if audio_out.dim() > 1:
audio_out = audio_out[0]
audio_out = audio_out.float().cpu()
audio_np = audio
if isinstance(audio_np, list):
audio_np = audio_np[0] if audio_np else None
if isinstance(audio_np, torch.Tensor):
audio_np = audio_np.detach().cpu().float().numpy()
if isinstance(audio_np, np.ndarray):
audio_np = np.squeeze(audio_np).astype(np.float32)
encode_video(
video_tensor,
fps=args.fps,
audio=audio_out,
audio_sample_rate=args.audio_sample_rate if audio_out is not None else None,
output_path=str(output_path),
video_bytes = mux_video_audio_bytes(
frames_u8,
audio_np,
fps=float(args.fps),
audio_sample_rate=args.audio_sample_rate,
)
with open(str(output_path), "wb") as f:
f.write(video_bytes)
else:
export_to_video(video_array, str(output_path), fps=args.fps)
print(f"Saved generated video to {output_path}")
@@ -0,0 +1,97 @@
# Dynin-Omni Online Serving Example
## Installation
Please refer to [README.md](../../../README.md).
## Launch the Server
First, find the `transformers_modules` path:
```bash
python - <<'PY'
from transformers.utils.hub import HF_MODULES_CACHE
print(HF_MODULES_CACHE)
PY
```
Then export it for both `PYTHONPATH` and `HF_MODULES_CACHE`:
```bash
export PYTHONPATH=<transformers_modules_path>:$PYTHONPATH
export HF_MODULES_CACHE=<transformers_modules_path>
```
Run from repository root:
```bash
vllm-omni serve snu-aidas/Dynin-Omni \
--omni \
--port 8091 \
--stage-configs-path "$(pwd)/vllm_omni/model_executor/stage_configs/dynin_omni.yaml"
```
If `vllm-omni` is not in PATH, run:
```bash
PYTHONPATH="$(pwd)" python -m vllm_omni.entrypoints.cli.main serve snu-aidas/Dynin-Omni \
--omni \
--port 8091 \
--stage-configs-path "$(pwd)/vllm_omni/model_executor/stage_configs/dynin_omni.yaml"
```
Wait until the server logs show both `All stages initialized successfully` and
`Application startup complete.` before sending requests.
## Send Requests via Python Client
Move to the example directory:
```bash
cd examples/online_serving/dynin_omni
```
### Text -> Image
```bash
python openai_chat_completion_client_for_multimodal_generation.py \
--query-type t2i \
--prompt "A realistic indoor living room with natural daylight."
```
### Image -> Image
```bash
python openai_chat_completion_client_for_multimodal_generation.py \
--query-type i2i \
--image-path ../../offline_inference/dynin_omni/data/image/sofa_under_water.jpg \
--prompt "Transform this surreal underwater setting into a realistic indoor living room while preserving the sofa layout."
```
### Text -> Speech
```bash
python openai_chat_completion_client_for_multimodal_generation.py \
--query-type t2s \
--prompt "Hello. This is Dynin-omni."
```
## CLI Arguments
- `--query-type` (`t2i|t2s|i2i`)
- `--model` (default: `snu-aidas/Dynin-Omni`)
- `--host` / `--port` (OpenAI-compatible vLLM endpoint)
- `--prompt` (custom text)
- `--image-path` (required for `i2i`)
- `--modalities` (optional output modalities override)
- `--output-dir` (default: `/tmp/dynin_online_outputs`)
## Notes
- This client currently supports only `t2i`, `t2s`, and `i2i`.
- `t2t` is intentionally not exposed in this online example.
- This example intentionally uses the OpenAI-compatible chat completion endpoint.
- Task routing for non-text outputs relies on Dynin task trigger tokens (`<|t2i|>`, `<|i2i|>`, `<|t2s|>`) injected by the client.
- Outputs are saved under `/tmp/dynin_online_outputs` by default.
- Dynin stage-0 warmup can take a while on first startup; do not send requests before startup completes.
- Dynin itself can execute text-returning tasks such as `t2t`, `s2t`, `i2t`, and `v2t`, but this online serving example currently runs stage-0 in `generation` mode. In that path, the generation worker does not surface the final text as `output.text`, so OpenAI chat responses for those text-output tasks may complete internally but still return empty text.
@@ -0,0 +1,342 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations
import argparse
import base64
import json
import mimetypes
import os
import time
from pathlib import Path
from typing import Any
DEFAULT_MODEL = "snu-aidas/Dynin-Omni"
DEFAULT_OUTPUT_DIR = "/tmp/dynin_online_outputs"
QUERY_CHOICES = ("t2i", "t2s", "i2i")
DEFAULT_PROMPT_BY_QUERY = {
"t2i": "A high quality detailed living room interior photo.",
"t2s": "Please read this sentence naturally: Hello from Dynin-Omni online serving.",
"i2i": "Transform this image into a realistic indoor living room while preserving layout.",
}
DEFAULT_MODALITIES_BY_QUERY = {
"t2i": ["image"],
"t2s": ["audio"],
"i2i": ["image"],
}
OFFLINE_PARITY_STAGE_COUNT = 3
OFFLINE_PARITY_STAGE_SAMPLING = {
"max_tokens": 1,
"temperature": 0.0,
"top_p": 1.0,
"detokenize": False,
}
def _infer_mime_type(path: Path) -> str:
mime_type, _ = mimetypes.guess_type(str(path))
return mime_type or "application/octet-stream"
def _encode_file_as_data_url(path: Path) -> str:
mime_type = _infer_mime_type(path)
raw = path.read_bytes()
encoded = base64.b64encode(raw).decode("utf-8")
return f"data:{mime_type};base64,{encoded}"
def _to_image_url(path_or_url: str) -> str:
value = str(path_or_url)
if value.startswith(("http://", "https://", "data:image/")):
return value
path = Path(value).expanduser().resolve()
if not path.exists():
raise FileNotFoundError(f"Image file not found: {path}")
return _encode_file_as_data_url(path)
def _build_user_content(query_type: str, prompt: str, image_path: str | None) -> list[dict[str, Any]]:
if query_type == "t2i":
return [{"type": "text", "text": f"<|t2i|> {prompt}"}]
if query_type == "t2s":
return [{"type": "text", "text": f"<|t2s|> {prompt}"}]
if query_type == "i2i":
if not image_path:
raise ValueError("--image-path is required for query type i2i")
return [
{"type": "text", "text": f"<|i2i|> {prompt}"},
{"type": "image_url", "image_url": {"url": _to_image_url(image_path)}},
]
raise ValueError(f"Unsupported query_type: {query_type}")
def _collect_text_from_content(content: Any) -> list[str]:
texts: list[str] = []
if isinstance(content, str):
stripped = content.strip()
if stripped:
texts.append(stripped)
return texts
if isinstance(content, dict):
for key in ("text", "content", "value", "output_text"):
text_value = content.get(key)
if isinstance(text_value, str) and text_value.strip():
texts.append(text_value.strip())
return texts
if isinstance(content, list):
for item in content:
texts.extend(_collect_text_from_content(item))
return texts
content_text = getattr(content, "text", None)
if isinstance(content_text, str) and content_text.strip():
texts.append(content_text.strip())
content_value = getattr(content, "content", None)
if isinstance(content_value, str) and content_value.strip():
texts.append(content_value.strip())
output_text = getattr(content, "output_text", None)
if isinstance(output_text, str) and output_text.strip():
texts.append(output_text.strip())
return texts
def _extract_text_outputs(chat_completion: Any) -> list[str]:
texts: list[str] = []
for choice in getattr(chat_completion, "choices", []) or []:
message = getattr(choice, "message", None)
if message is None:
continue
content = getattr(message, "content", None)
texts.extend(_collect_text_from_content(content))
reasoning_content = getattr(message, "reasoning_content", None)
if isinstance(reasoning_content, str) and reasoning_content.strip():
texts.append(reasoning_content.strip())
choice_text = getattr(choice, "text", None)
if isinstance(choice_text, str) and choice_text.strip():
texts.append(choice_text.strip())
top_level_output_text = getattr(chat_completion, "output_text", None)
if isinstance(top_level_output_text, str) and top_level_output_text.strip():
texts.append(top_level_output_text.strip())
return texts
def _extract_image_data_urls(chat_completion: Any) -> list[str]:
urls: list[str] = []
for choice in getattr(chat_completion, "choices", []) or []:
message = getattr(choice, "message", None)
if message is None:
continue
content = getattr(message, "content", None)
if not isinstance(content, list):
continue
for item in content:
if not isinstance(item, dict):
continue
if item.get("type") != "image_url":
continue
image_url = (item.get("image_url") or {}).get("url")
if isinstance(image_url, str) and image_url.startswith("data:image"):
urls.append(image_url)
return urls
def _extract_audio_payloads(chat_completion: Any) -> list[bytes]:
payloads: list[bytes] = []
for choice in getattr(chat_completion, "choices", []) or []:
message = getattr(choice, "message", None)
if message is None:
continue
message_audio = getattr(message, "audio", None)
if message_audio is None:
continue
data_b64 = getattr(message_audio, "data", None)
if isinstance(data_b64, str) and data_b64:
try:
payloads.append(base64.b64decode(data_b64))
except Exception:
continue
return payloads
def _decode_data_url(data_url: str) -> tuple[bytes, str]:
header, data = data_url.split(",", 1)
mime_type = "image/png"
if ";" in header and ":" in header:
mime_type = header.split(":", 1)[1].split(";", 1)[0]
return base64.b64decode(data), mime_type
def _image_extension_from_mime(mime_type: str) -> str:
if mime_type == "image/jpeg":
return ".jpg"
if mime_type == "image/webp":
return ".webp"
if mime_type == "image/gif":
return ".gif"
return ".png"
def _save_outputs(
*,
query_type: str,
chat_completion: Any,
output_dir: Path,
) -> None:
output_dir.mkdir(parents=True, exist_ok=True)
stamp = time.strftime("%Y%m%d_%H%M%S")
text_outputs = _extract_text_outputs(chat_completion)
image_data_urls = _extract_image_data_urls(chat_completion)
audio_payloads = _extract_audio_payloads(chat_completion)
if text_outputs:
text_path = output_dir / f"{query_type}_{stamp}.txt"
text_path.write_text("\n\n".join(text_outputs) + "\n", encoding="utf-8")
print(f"[dynin-online] text saved: {text_path}")
print(text_outputs[0])
for idx, image_url in enumerate(image_data_urls):
image_bytes, mime_type = _decode_data_url(image_url)
ext = _image_extension_from_mime(mime_type)
image_path = output_dir / f"{query_type}_{stamp}_{idx}{ext}"
image_path.write_bytes(image_bytes)
print(f"[dynin-online] image saved: {image_path}")
for idx, audio_bytes in enumerate(audio_payloads):
audio_path = output_dir / f"{query_type}_{stamp}_{idx}.wav"
audio_path.write_bytes(audio_bytes)
print(f"[dynin-online] audio saved: {audio_path}")
if not text_outputs and not image_data_urls and not audio_payloads:
print("[dynin-online] no output extracted from response")
raw_path = output_dir / f"{query_type}_{stamp}_raw_response.json"
try:
if hasattr(chat_completion, "model_dump_json"):
serialized = chat_completion.model_dump_json(indent=2)
else:
if hasattr(chat_completion, "model_dump"):
raw_payload: Any = chat_completion.model_dump(mode="json")
else:
raw_payload = chat_completion
try:
serialized = json.dumps(raw_payload, ensure_ascii=False, indent=2)
except Exception:
serialized = json.dumps({"repr": repr(raw_payload)}, ensure_ascii=False, indent=2)
raw_path.write_text(serialized + "\n", encoding="utf-8")
print(f"[dynin-online] raw response saved: {raw_path}")
except Exception:
pass
def _build_offline_parity_sampling_params_list() -> list[dict[str, Any]]:
return [dict(OFFLINE_PARITY_STAGE_SAMPLING) for _ in range(OFFLINE_PARITY_STAGE_COUNT)]
def run_request(args: argparse.Namespace) -> None:
from openai import OpenAI
client = OpenAI(
api_key="EMPTY",
base_url=f"http://{args.host}:{args.port}/v1",
)
prompt = args.prompt.strip() if args.prompt else DEFAULT_PROMPT_BY_QUERY[args.query_type]
user_content = _build_user_content(
query_type=args.query_type,
prompt=prompt,
image_path=args.image_path,
)
if args.modalities:
modalities = [item.strip() for item in args.modalities.split(",") if item.strip()]
else:
modalities = DEFAULT_MODALITIES_BY_QUERY[args.query_type]
extra_body = {
"sampling_params_list": _build_offline_parity_sampling_params_list(),
}
chat_completion = client.chat.completions.create(
model=args.model,
messages=[{"role": "user", "content": user_content}],
modalities=modalities,
extra_body=extra_body,
)
_save_outputs(
query_type=args.query_type,
chat_completion=chat_completion,
output_dir=Path(args.output_dir).expanduser(),
)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Dynin-Omni online chat completion client")
parser.add_argument(
"--query-type",
"-q",
type=str,
default="t2i",
choices=QUERY_CHOICES,
help="Dynin query type",
)
parser.add_argument(
"--model",
"-m",
type=str,
default=DEFAULT_MODEL,
help="Model name/path",
)
parser.add_argument(
"--host",
type=str,
default="localhost",
help="Host/IP of the vLLM Omni API server",
)
parser.add_argument(
"--port",
type=int,
default=8091,
help="Port of the vLLM Omni API server",
)
parser.add_argument(
"--prompt",
"-p",
type=str,
default="",
help="Custom prompt text",
)
parser.add_argument(
"--image-path",
"-i",
type=str,
default=None,
help="Image path/URL for i2i",
)
parser.add_argument(
"--modalities",
type=str,
default="",
help="Comma-separated output modalities override (e.g., text,image,audio)",
)
parser.add_argument(
"--output-dir",
"-o",
type=str,
default=DEFAULT_OUTPUT_DIR,
help="Directory to save outputs",
)
return parser.parse_args()
def main() -> None:
args = parse_args()
os.environ.setdefault("HF_HUB_DISABLE_XET", "1")
run_request(args)
if __name__ == "__main__":
main()
@@ -314,6 +314,7 @@ count, use `size` and `n` rather than `height`, `width`, or
| `seed` | int | None | Random seed (reproducible) |
| `negative_prompt` | str | None | Negative prompt |
| `num_outputs_per_prompt` | int | 1 | Number of images to generate |
| `strength` | float | 0.6 | **Z-Image only** - Denoising start timestep for I2I. Range: [0.0, 1.0]. Lower preserves more of original image. |
| `layers` | int | 4 | Number of layers (Qwen-Image-Layered) |
| `resolution` | int | 640 | Resolution, 640 or 1024 (Qwen-Image-Layered) |
+90
View File
@@ -0,0 +1,90 @@
# Qwen3-Omni for multimodal chat on 1x A100 80GB
## Summary
- Vendor: Qwen
- Model: `Qwen/Qwen3-Omni-30B-A3B-Instruct`
- Task: Multimodal chat with text, image, audio, or video input
- Mode: Online serving with the OpenAI-compatible API
- Maintainer: Community
## When to use this recipe
Use this recipe when you want a known-good starting point for serving
`Qwen/Qwen3-Omni-30B-A3B-Instruct` with vLLM-Omni on a single 80 GB A100 and
validate the deployment with the existing multimodal client examples in this
repository.
## References
- Upstream or canonical docs:
[`docs/user_guide/examples/online_serving/qwen3_omni.md`](../../docs/user_guide/examples/online_serving/qwen3_omni.md)
- Related example under `examples/`:
[`examples/online_serving/qwen3_omni/README.md`](../../examples/online_serving/qwen3_omni/README.md)
- Related issue or discussion:
[RFC: add recipes folder](https://github.com/vllm-project/vllm-omni/issues/2645)
## Hardware Support
This recipe currently documents one tested-style reference configuration for
CUDA GPU serving. Add more sections for other hardware as community validation
lands.
## GPU
### 1x A100 80GB
#### Environment
- OS: Linux
- Python: 3.10+
- Driver / runtime: NVIDIA CUDA environment with an A100 80 GB GPU
- vLLM version: Match the repository requirements for your checkout
- vLLM-Omni version or commit: Use the commit you are deploying from
#### Command
Start the server from the repository root:
```bash
vllm serve Qwen/Qwen3-Omni-30B-A3B-Instruct --omni --port 8091
```
To enable async chunking, use the bundled stage config:
```bash
vllm serve Qwen/Qwen3-Omni-30B-A3B-Instruct \
--omni \
--port 8091 \
--stage-configs-path vllm_omni/model_executor/stage_configs/qwen3_omni_moe_async_chunk.yaml
```
#### Verification
Run one of the existing example clients after the server is ready:
```bash
python examples/online_serving/openai_chat_completion_client_for_multimodal_generation.py \
--model Qwen/Qwen3-Omni-30B-A3B-Instruct \
--query-type use_image \
--port 8091 \
--host localhost
```
For a quick API smoke test, request text-only output:
```bash
curl http://localhost:8091/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "Qwen/Qwen3-Omni-30B-A3B-Instruct",
"messages": [{"role": "user", "content": "Describe vLLM in brief."}],
"modalities": ["text"]
}'
```
#### Notes
- Memory usage: Size depends on runtime options and output modalities; leave headroom for multimodal workloads.
- Key flags: `--omni` is required; `--stage-configs-path` is optional for custom or async-chunk stage configs.
- Known limitations: This starter recipe is intentionally narrow and focuses on the single-GPU online-serving path already documented in the repo examples.
+35
View File
@@ -0,0 +1,35 @@
# Community Recipes
This directory contains community-maintained recipes for answering a
practical user question:
> How do I run model X on hardware Y for task Z?
Add recipes for this repository under this in-repo `recipes/` directory. To
keep naming and layout consistent, organize recipes by model vendor in a way
that is aligned with
[`vllm-project/recipes`](https://github.com/vllm-project/recipes), but treat
that external repository as a reference for structure rather than the place to
add files for this repo. Use one Markdown file per model family by default.
Example layout:
```text
recipes/
Qwen/
Qwen3-Omni.md
Qwen3-TTS.md
Tencent-Hunyuan/
HunyuanVideo.md
```
## Available Recipes
- [`Qwen/Qwen3-Omni.md`](./Qwen/Qwen3-Omni.md): online serving recipe for
multimodal chat on `1x A100 80GB`
Within a single recipe file, include different hardware support sections such
as `GPU`, `ROCm`, and `NPU`, and add concrete tested configurations like
`1x A100 80GB` or `2x L40S` inside those sections when applicable.
See [TEMPLATE.md](./TEMPLATE.md) for the recommended format.
+82
View File
@@ -0,0 +1,82 @@
# Recipe Title
> Example: Qwen3-Omni for speech chat on 1x A100 80GB
## Summary
- Vendor:
- Model:
- Task:
- Mode:
- Maintainer:
## When to use this recipe
Briefly describe the concrete scenario this recipe covers.
## References
- Upstream or canonical docs:
- Related example under `examples/`:
- Related issue or discussion:
## Hardware Support
Add one section per platform, such as `GPU`, `ROCm`, or `NPU`. Under each
platform section, document one or more tested hardware configurations.
## GPU
### 1x A100 80GB
#### Environment
- OS:
- Python:
- Driver / runtime:
- vLLM version:
- vLLM-Omni version or commit:
#### Command
```bash
# Add the exact command(s) here
```
#### Verification
```bash
# Add a quick validation command or expected output here
```
#### Notes
- Memory usage:
- Key flags:
- Known limitations:
### 2x L40S
Repeat the same structure for other hardware setups as needed.
## ROCm
### Example hardware configuration
Repeat the same nested structure for ROCm setups as needed:
- `#### Environment`
- `#### Command`
- `#### Verification`
- `#### Notes`
## NPU
### Example hardware configuration
Repeat the same nested structure for NPU setups as needed:
- `#### Environment`
- `#### Command`
- `#### Verification`
- `#### Notes`
+67
View File
@@ -0,0 +1,67 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""
Unit tests for metrics.py
"""
import pytest
from vllm.benchmarks.serve import TaskType
from vllm_omni.benchmarks.metrics.metrics import calculate_metrics
from vllm_omni.benchmarks.patch.patch import MixRequestFuncOutput
pytestmark = [pytest.mark.core_model, pytest.mark.benchmark, pytest.mark.cpu]
def _make_output(prompt_len: int, output_tokens: int = 10) -> MixRequestFuncOutput:
"""Build a minimal successful MixRequestFuncOutput for metrics aggregation."""
output = MixRequestFuncOutput()
output.success = True
output.prompt_len = prompt_len
output.output_tokens = output_tokens
output.generated_text = "x" * output_tokens
output.ttft = 0.1
output.text_latency = 1.0
output.latency = 1.0
output.start_time = 0.0
output.itl = [0.1] * max(output_tokens - 1, 0)
output.audio_ttfp = 0.0
output.audio_rtf = 0.0
output.audio_duration = 0.0
output.audio_frames = 0
output.input_audio_duration = 0.0
output.error = ""
return output
# ============================================================================
# total_input Tests
# ============================================================================
def test_total_input_aggregated_from_output_prompt_len():
"""Test that total_input sums outputs[i].prompt_len, not input_requests[i].prompt_len."""
outputs = [_make_output(4992), _make_output(3000)]
metrics, _ = calculate_metrics(
input_requests=[],
outputs=outputs,
dur_s=10.0,
tokenizer=None,
selected_percentiles=[99.0],
goodput_config_dict={},
task_type=TaskType.GENERATION,
selected_percentile_metrics=[],
max_concurrency=None,
request_rate=float("inf"),
benchmark_duration=10.0,
)
assert metrics.total_input == 7992, (
"total_input should aggregate from outputs[i].prompt_len to reflect the true multimodal input token count"
)
if __name__ == "__main__":
pytest.main([__file__, "-v", "-s"])
+54
View File
@@ -574,5 +574,59 @@ class TestTextLatencyAttribute:
)
# ============================================================================
# prompt_len Tests
# ============================================================================
@pytest.mark.asyncio
async def test_prompt_len_assigned_from_usage(mocker: MockerFixture):
# Arrange: request claims prompt_len=100, but server reports 4992 (multimodal).
request_input = RequestFuncInput(
model="test-model",
model_name="test-model",
prompt="test prompt",
api_url="http://test.com/v1/chat/completions",
prompt_len=100,
output_len=20,
)
chunks = [
create_sse_chunk(
{
"choices": [{"delta": {"content": "Hello"}}],
"modality": "text",
}
),
create_sse_chunk(
{
"choices": [{"delta": {"content": " world"}}],
"modality": "text",
}
),
# Final usage chunk emitted because stream_options.include_usage=True.
create_sse_chunk(
{
"choices": [],
"usage": {"prompt_tokens": 4992, "completion_tokens": 2, "total_tokens": 4994},
}
),
b"data: [DONE]\n\n",
]
mock_response = MockResponse(200, chunks)
mock_session = mocker.AsyncMock()
mock_session.post = mocker.MagicMock(return_value=mock_response)
# Act
output = await async_request_openai_chat_omni_completions(request_input, mock_session)
# Assert
assert output.success is True
assert output.prompt_len == 4992, (
"prompt_len should be overridden by usage.prompt_tokens to reflect the true multimodal input token count"
)
if __name__ == "__main__":
pytest.main([__file__, "-v", "-s"])
+23 -5
View File
@@ -16,6 +16,7 @@ if "VLLM_TARGET_DEVICE" not in os.environ:
os.environ["VLLM_TARGET_DEVICE"] = "cpu"
import concurrent.futures
import contextlib
import gc
import multiprocessing
import socket
@@ -52,6 +53,7 @@ from vllm_omni.platforms import current_omni_platform
logger = init_logger(__name__)
PromptAudioInput = list[tuple[Any, int]] | tuple[Any, int] | None
PromptImageInput = list[Any] | Any | None
PromptVideoInput = list[Any] | Any | None
@@ -337,10 +339,10 @@ def log_test_name_before_test(request):
def _run_pre_test_cleanup(enable_force=False):
if os.getenv("VLLM_TEST_CLEAN_GPU_MEMORY", "0") != "1" and not enable_force:
print("GPU cleanup disabled")
print("\nPre-test GPU cleanup skipped(Default off is typical when one worker/instance runs many tests.)\n")
return
print("Pre-test GPU status:")
print("\nPre-test GPU status:")
num_gpus = torch.cuda.device_count()
if num_gpus > 0:
@@ -1087,6 +1089,22 @@ def _merge_base64_audio_to_segment(base64_list: list[str]):
return merged
@contextlib.contextmanager
def _serialize_whisper_small_model_download():
"""Serialize Whisper ``small`` cache writes across processes (Linux; ``fcntl``)."""
import fcntl
lock_path = Path.home() / ".cache" / "whisper" / ".small_model_download.lock"
lock_path.parent.mkdir(parents=True, exist_ok=True)
f = open(lock_path, "a+b")
try:
fcntl.flock(f.fileno(), fcntl.LOCK_EX)
yield
finally:
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
f.close()
def _whisper_transcribe_in_current_process(output_path: str) -> str:
import whisper
@@ -1107,7 +1125,8 @@ def _whisper_transcribe_in_current_process(output_path: str) -> str:
else:
use_accelerator = False
device = "cpu"
model = whisper.load_model("small", device=device)
with _serialize_whisper_small_model_download():
model = whisper.load_model("small", device=device)
try:
text = model.transcribe(
output_path,
@@ -1126,8 +1145,7 @@ def _whisper_transcribe_in_current_process(output_path: str) -> str:
def convert_audio_file_to_text(output_path: str) -> str:
"""Convert an audio file to text in an isolated subprocess."""
# Import locally to avoid impacting test module import time.
"""Convert an audio file to text in an isolated subprocess (spawn)."""
ctx = multiprocessing.get_context("spawn")
with concurrent.futures.ProcessPoolExecutor(max_workers=1, mp_context=ctx) as executor:
future = executor.submit(_whisper_transcribe_in_current_process, output_path)
@@ -9,6 +9,10 @@ those requests are permanently orphaned.
import unittest
from collections import deque
import pytest
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
class FakeAdapter:
"""Minimal mock of OmniChunkTransferAdapter tracking restore calls."""
@@ -0,0 +1,217 @@
[
{
"test_name": "test_ltx2_baseline_eager",
"description": "Single-device baseline with enforce-eager (no torch.compile)",
"server_type": "vllm-omni",
"server_params": {
"model": "Lightricks/LTX-2",
"serve_args": {
"enforce-eager": true,
"enable-diffusion-pipeline-profiler": true
}
},
"benchmark_params": [
{
"name": "256x256_145f_steps6",
"dataset": "random",
"task": "t2v",
"backend": "v1/videos",
"width": 256,
"height": 256,
"num-frames": 145,
"fps": 24,
"num-inference-steps": 6,
"num-prompts": 3,
"max-concurrency": 1,
"enable-negative-prompt": true
},
{
"name": "480x768_41f_steps20",
"dataset": "random",
"task": "t2v",
"backend": "v1/videos",
"width": 768,
"height": 480,
"num-frames": 41,
"fps": 24,
"num-inference-steps": 20,
"num-prompts": 3,
"max-concurrency": 1,
"enable-negative-prompt": true
}
]
},
{
"test_name": "test_ltx2_torch_compile",
"description": "Single-device with torch.compile (default, no enforce-eager)",
"server_type": "vllm-omni",
"server_params": {
"model": "Lightricks/LTX-2",
"serve_args": {
"enable-diffusion-pipeline-profiler": true
}
},
"benchmark_params": [
{
"name": "256x256_145f_steps6",
"dataset": "random",
"task": "t2v",
"backend": "v1/videos",
"width": 256,
"height": 256,
"num-frames": 145,
"fps": 24,
"num-inference-steps": 6,
"num-prompts": 3,
"max-concurrency": 1,
"enable-negative-prompt": true
},
{
"name": "480x768_41f_steps20",
"dataset": "random",
"task": "t2v",
"backend": "v1/videos",
"width": 768,
"height": 480,
"num-frames": 41,
"fps": 24,
"num-inference-steps": 20,
"num-prompts": 3,
"max-concurrency": 1,
"enable-negative-prompt": true
}
]
},
{
"test_name": "test_ltx2_cfg2_eager",
"description": "CFG-parallel=2 with enforce-eager",
"server_type": "vllm-omni",
"server_params": {
"model": "Lightricks/LTX-2",
"serve_args": {
"cfg-parallel-size": 2,
"enforce-eager": true,
"enable-diffusion-pipeline-profiler": true
}
},
"benchmark_params": [
{
"name": "256x256_145f_steps6",
"dataset": "random",
"task": "t2v",
"backend": "v1/videos",
"width": 256,
"height": 256,
"num-frames": 145,
"fps": 24,
"num-inference-steps": 6,
"num-prompts": 3,
"max-concurrency": 1,
"enable-negative-prompt": true
},
{
"name": "480x768_41f_steps20",
"dataset": "random",
"task": "t2v",
"backend": "v1/videos",
"width": 768,
"height": 480,
"num-frames": 41,
"fps": 24,
"num-inference-steps": 20,
"num-prompts": 3,
"max-concurrency": 1,
"enable-negative-prompt": true
}
]
},
{
"test_name": "test_ltx2_cfg2_compile",
"description": "CFG-parallel=2 with torch.compile",
"server_type": "vllm-omni",
"server_params": {
"model": "Lightricks/LTX-2",
"serve_args": {
"cfg-parallel-size": 2,
"enable-diffusion-pipeline-profiler": true
}
},
"benchmark_params": [
{
"name": "256x256_145f_steps6",
"dataset": "random",
"task": "t2v",
"backend": "v1/videos",
"width": 256,
"height": 256,
"num-frames": 145,
"fps": 24,
"num-inference-steps": 6,
"num-prompts": 3,
"max-concurrency": 1,
"enable-negative-prompt": true
},
{
"name": "480x768_41f_steps20",
"dataset": "random",
"task": "t2v",
"backend": "v1/videos",
"width": 768,
"height": 480,
"num-frames": 41,
"fps": 24,
"num-inference-steps": 20,
"num-prompts": 3,
"max-concurrency": 1,
"enable-negative-prompt": true
}
]
},
{
"test_name": "test_ltx2_cache_dit_eager",
"description": "CacheDiT with enforce-eager",
"server_type": "vllm-omni",
"server_params": {
"model": "Lightricks/LTX-2",
"serve_args": {
"cache-backend": "cache_dit",
"enforce-eager": true,
"enable-diffusion-pipeline-profiler": true
}
},
"benchmark_params": [
{
"name": "256x256_145f_steps6",
"dataset": "random",
"task": "t2v",
"backend": "v1/videos",
"width": 256,
"height": 256,
"num-frames": 145,
"fps": 24,
"num-inference-steps": 6,
"num-prompts": 3,
"max-concurrency": 1,
"enable-negative-prompt": true
},
{
"name": "480x768_41f_steps20",
"dataset": "random",
"task": "t2v",
"backend": "v1/videos",
"width": 768,
"height": 480,
"num-frames": 41,
"fps": 24,
"num-inference-steps": 20,
"num-prompts": 3,
"max-concurrency": 1,
"enable-negative-prompt": true
}
]
}
]
+7 -1
View File
@@ -21,12 +21,13 @@ from unittest.mock import MagicMock, Mock, patch
import pytest
import torch
from tests.utils import hardware_test
from vllm_omni.diffusion.cache.teacache.extractors import extract_flux2_klein_context
from vllm_omni.diffusion.models.flux2_klein.flux2_klein_transformer import (
Flux2Transformer2DModel,
)
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
pytestmark = [pytest.mark.core_model]
@pytest.fixture(scope="function", autouse=True)
@@ -113,6 +114,7 @@ class TestFlux2KleinExtractor(BaseExtractorTest):
def get_sample_inputs(self, sample_inputs):
return sample_inputs
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_modulated_input_shape(self, flux2_klein_module, sample_inputs):
"""Test that modulated_input has correct shape matching the model's inner_dim.
@@ -126,16 +128,19 @@ class TestFlux2KleinExtractor(BaseExtractorTest):
inner_dim = flux2_klein_module.inner_dim
assert context.modulated_input.shape == (batch_size, img_seq_len, inner_dim)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_run_transformer_blocks_callable(self, flux2_klein_module, sample_inputs):
"""Test that run_transformer_blocks is callable."""
context = extract_flux2_klein_context(flux2_klein_module, **sample_inputs)
assert callable(context.run_transformer_blocks)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_postprocess_callable(self, flux2_klein_module, sample_inputs):
"""Test that postprocess is callable."""
context = extract_flux2_klein_context(flux2_klein_module, **sample_inputs)
assert callable(context.postprocess)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_extra_states_contains_full_transformer(self, flux2_klein_module, sample_inputs):
"""Test that extra_states contains run_flux2_full_transformer_with_single."""
context = extract_flux2_klein_context(flux2_klein_module, **sample_inputs)
@@ -154,6 +159,7 @@ class TestFlux2KleinExtractor(BaseExtractorTest):
assert context is not None
assert context.temb is not None
@pytest.mark.cpu
def test_invalid_module_raises_error(self):
"""Test that invalid module without transformer_blocks raises ValueError."""
invalid_module = Mock()
@@ -0,0 +1,43 @@
import pytest
import torch
from vllm_omni.diffusion.distributed.autoencoders import autoencoder_kl_wan as wan_vae_module
from vllm_omni.diffusion.distributed.autoencoders.autoencoder_kl_wan import OmniAutoencoderKLWan
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
class _DummyOmniAutoencoderKLWan(OmniAutoencoderKLWan):
def __init__(self, *, dtype: torch.dtype):
torch.nn.Module.__init__(self)
self.register_parameter("dummy_weight", torch.nn.Parameter(torch.ones(1, dtype=dtype)))
def test_wan_vae_execution_context_handles_fp32():
model = _DummyOmniAutoencoderKLWan(dtype=torch.float32)
with model._execution_context():
output = model.dummy_weight + 1
assert output.dtype == torch.float32
def test_wan_vae_execution_context_handles_bf16():
model = _DummyOmniAutoencoderKLWan(dtype=torch.bfloat16)
with model._execution_context():
output = model.dummy_weight + 1
assert output.dtype == torch.bfloat16
def test_wan_vae_execution_context_uses_platform_autocast(mocker):
sentinel = object()
platform = mocker.Mock()
platform.create_autocast_context.return_value = sentinel
mocker.patch.object(wan_vae_module, "current_omni_platform", platform)
model = _DummyOmniAutoencoderKLWan(dtype=torch.bfloat16)
assert model._execution_context() is sentinel
platform.create_autocast_context.assert_called_once_with(
device_type=model.dummy_weight.device.type,
dtype=torch.bfloat16,
enabled=True,
)
@@ -11,6 +11,8 @@ from vllm_omni.diffusion.distributed.autoencoders.distributed_vae_executor impor
TileTask,
)
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
class E2EOperator:
"""tiles with (2, 3) -- (H,W)"""
@@ -17,6 +17,7 @@ import pytest
import torch
import torch.distributed as dist
from tests.utils import hardware_test
from vllm_omni.diffusion.attention.parallel.ulysses import (
_all_gather_int,
_ulysses_all_to_all_any_o,
@@ -69,6 +70,8 @@ PERF_CASES: list[_PerfCase] = [
@pytest.mark.parametrize("case", PERF_CASES)
@pytest.mark.core_model
@hardware_test(res={"cuda": "L4"}, num_cards=4)
def test_ulysses_advanced_uaa_comm_overhead(case: _PerfCase) -> None:
available_gpus = current_omni_platform.get_device_count()
if available_gpus < case.world_size:
@@ -0,0 +1,236 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Unit tests for BAGEL trajectory recording in the denoising loop."""
import types
from dataclasses import dataclass
from unittest.mock import MagicMock, patch
import pytest
import torch
from vllm_omni.diffusion.models.bagel.bagel_transformer import (
Bagel,
NaiveCache,
)
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
NUM_TOKENS = 8
HIDDEN_DIM = 16
NUM_TIMESTEPS = 5
# generate_image uses timesteps[:-1], so actual steps = NUM_TIMESTEPS - 1
EXPECTED_STEPS = NUM_TIMESTEPS - 1
def _make_mock_bagel():
"""Create a mock Bagel with forward returning constant velocity."""
mock = MagicMock(spec=Bagel)
mock._sp_size = 1
# forward returns a small constant velocity so x_t changes each step
def fake_forward(self, x_t, **kwargs):
return torch.ones_like(x_t) * 0.1
mock.forward = types.MethodType(fake_forward, mock)
# _merge_naive_caches is called in the batched CFG path
mock._merge_naive_caches = types.MethodType(lambda self, caches: NaiveCache(1), mock)
# Bind the real generate_image to our mock
mock.generate_image = types.MethodType(Bagel.generate_image, mock)
return mock
def _make_generate_args(num_tokens=NUM_TOKENS, hidden_dim=HIDDEN_DIM, cfg=False):
"""Tensor arguments for generate_image.
Args:
cfg: If True, enable batched CFG path (cfg_text_scale > 1.0).
"""
seq_len = num_tokens + 2 # packed_seqlens includes 2 extra tokens
base = dict(
packed_text_ids=torch.zeros(2, dtype=torch.long),
packed_text_indexes=torch.tensor([0, 1], dtype=torch.long),
packed_init_noises=torch.randn(num_tokens, hidden_dim),
packed_vae_position_ids=torch.arange(num_tokens, dtype=torch.long),
packed_vae_token_indexes=torch.arange(2, seq_len, dtype=torch.long),
packed_seqlens=torch.tensor([seq_len], dtype=torch.int),
packed_position_ids=torch.arange(seq_len, dtype=torch.long),
packed_indexes=torch.arange(seq_len, dtype=torch.long),
past_key_values=NaiveCache(1),
key_values_lens=torch.tensor([0], dtype=torch.int),
packed_key_value_indexes=torch.zeros(0, dtype=torch.long),
num_timesteps=NUM_TIMESTEPS,
timestep_shift=1.0,
cfg_text_scale=1.0,
cfg_img_scale=1.0,
)
if cfg:
base |= dict(
cfg_text_scale=4.0,
cfg_text_packed_query_indexes=torch.arange(seq_len, dtype=torch.long),
cfg_text_packed_position_ids=torch.arange(seq_len, dtype=torch.long),
cfg_text_past_key_values=NaiveCache(1),
cfg_text_key_values_lens=torch.tensor([0], dtype=torch.int),
cfg_text_packed_key_value_indexes=torch.zeros(0, dtype=torch.long),
)
return base
@pytest.fixture(params=[False, True], ids=["no_cfg", "batched_cfg"])
def bagel_and_args(request):
"""Mock Bagel instance and generate_image arguments.
Parametrized over CFG mode so every test runs on both the no-CFG
and batched-CFG code paths.
"""
cfg = request.param
with patch(
"vllm_omni.diffusion.models.bagel.bagel_transformer.get_classifier_free_guidance_world_size",
return_value=1,
):
yield _make_mock_bagel(), _make_generate_args(cfg=cfg)
class TestTrajectoryRecording:
"""Tests for trajectory latent/timestep recording in generate_image."""
def test_trajectory_disabled_returns_none(self, bagel_and_args):
bagel, args = bagel_and_args
unpacked, trajectory_latents, trajectory_timesteps, trajectory_log_probs = bagel.generate_image(
**args, return_trajectory_latents=False
)
assert isinstance(unpacked, (list, tuple))
assert len(unpacked) == 1 # one sequence
assert trajectory_latents is None
assert trajectory_timesteps is None
assert trajectory_log_probs is None
def test_trajectory_enabled_returns_correct_count(self, bagel_and_args):
bagel, args = bagel_and_args
_, trajectory_latents, trajectory_timesteps, trajectory_log_probs = bagel.generate_image(
**args, return_trajectory_latents=True
)
assert trajectory_latents is not None
assert trajectory_timesteps is not None
assert len(trajectory_latents) == EXPECTED_STEPS
assert len(trajectory_timesteps) == EXPECTED_STEPS
# log_probs is None without a scheduler (default ODE path)
assert trajectory_log_probs is None
def test_trajectory_latents_shape_matches_input(self, bagel_and_args):
bagel, args = bagel_and_args
expected_shape = args["packed_init_noises"].shape
_, trajectory_latents, *_ = bagel.generate_image(**args, return_trajectory_latents=True)
for i, lat in enumerate(trajectory_latents):
assert lat.shape == expected_shape, f"Step {i}: expected {expected_shape}, got {lat.shape}"
def test_trajectory_latents_are_distinct(self, bagel_and_args):
bagel, args = bagel_and_args
_, trajectory_latents, *_ = bagel.generate_image(**args, return_trajectory_latents=True)
for i in range(1, len(trajectory_latents)):
assert not torch.equal(trajectory_latents[i], trajectory_latents[i - 1]), (
f"Steps {i - 1} and {i} should differ"
)
def test_trajectory_timesteps_are_decreasing(self, bagel_and_args):
bagel, args = bagel_and_args
_, _, trajectory_timesteps, _ = bagel.generate_image(**args, return_trajectory_latents=True)
for i in range(1, len(trajectory_timesteps)):
assert trajectory_timesteps[i] < trajectory_timesteps[i - 1], (
f"Timestep {i} ({trajectory_timesteps[i]:.4f}) should be less than "
f"timestep {i - 1} ({trajectory_timesteps[i - 1]:.4f})"
)
def test_trajectory_final_latent_matches_output(self, bagel_and_args):
bagel, args = bagel_and_args
unpacked, trajectory_latents, *_ = bagel.generate_image(**args, return_trajectory_latents=True)
# Reconstruct the full final latent from unpacked pieces
final_latent = torch.cat(unpacked, dim=0)
assert torch.allclose(trajectory_latents[-1], final_latent, atol=1e-6), (
"Last trajectory latent should match the final output"
)
# ---------------------------------------------------------------------------
# Mock scheduler for log-prob tests
# ---------------------------------------------------------------------------
@dataclass
class _MockStepOutput:
prev_sample: torch.Tensor
log_prob: torch.Tensor
class _MockScheduler:
"""Minimal scheduler: Euler step + constant log-prob per step."""
def step(self, model_output, sigma, sample, dt, **kwargs):
prev_sample = sample - model_output * dt
log_prob = torch.tensor(-1.0)
return _MockStepOutput(prev_sample=prev_sample, log_prob=log_prob)
class TestTrajectoryLogProbs:
"""Tests for log-prob recording when a scheduler is provided."""
@pytest.fixture()
def bagel_scheduler_args(self):
with patch(
"vllm_omni.diffusion.models.bagel.bagel_transformer.get_classifier_free_guidance_world_size",
return_value=1,
):
yield _make_mock_bagel(), _make_generate_args(), _MockScheduler()
def test_log_probs_recorded_with_scheduler(self, bagel_scheduler_args):
bagel, args, scheduler = bagel_scheduler_args
_, _, _, trajectory_log_probs = bagel.generate_image(
**args, return_trajectory_latents=True, scheduler=scheduler
)
assert trajectory_log_probs is not None
assert len(trajectory_log_probs) == EXPECTED_STEPS
def test_log_probs_are_finite(self, bagel_scheduler_args):
bagel, args, scheduler = bagel_scheduler_args
_, _, _, trajectory_log_probs = bagel.generate_image(
**args, return_trajectory_latents=True, scheduler=scheduler
)
for i, lp in enumerate(trajectory_log_probs):
assert torch.isfinite(lp).all(), f"Step {i}: log_prob is not finite"
def test_log_probs_none_without_scheduler(self, bagel_scheduler_args):
bagel, args, _ = bagel_scheduler_args
_, _, _, trajectory_log_probs = bagel.generate_image(**args, return_trajectory_latents=True, scheduler=None)
assert trajectory_log_probs is None
def test_scheduler_updates_latents(self, bagel_scheduler_args):
"""Verify the scheduler's prev_sample is used (not the raw Euler step)."""
bagel, args, scheduler = bagel_scheduler_args
_, traj_with_sched, *_ = bagel.generate_image(**args, return_trajectory_latents=True, scheduler=scheduler)
_, traj_without, *_ = bagel.generate_image(**args, return_trajectory_latents=True, scheduler=None)
# Mock scheduler does the same Euler step, so latents should match
for i in range(len(traj_with_sched)):
assert torch.allclose(traj_with_sched[i], traj_without[i], atol=1e-5), (
f"Step {i}: scheduler and ODE paths should produce same latents"
)
@@ -3,6 +3,7 @@ from unittest.mock import MagicMock, patch
import pytest
import torch
from tests.utils import hardware_test
from vllm_omni.diffusion.models.flux2.flux2_transformer import (
Flux2PosEmbed,
Flux2Transformer2DModel,
@@ -24,6 +25,8 @@ def setup_tp_group():
class TestFlux2TransformerWeightLoading:
"""Test Flux2Transformer weight loading functionality"""
@pytest.mark.core_model
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_weight_loading_tp2(self, setup_tp_group):
"""Verify weights load correctly with TP=2"""
# Prepare test data
@@ -78,6 +81,8 @@ class TestFlux2TransformerWeightLoading:
class TestFlux2RopePositionEmbedding:
"""Test Flux2 RoPE position embedding functionality"""
@pytest.mark.core_model
@pytest.mark.cpu
def test_rope_position_embedding(self):
"""Verify RoPE produces correct embeddings for 4D coordinates"""
# Prepare test data - use model default configuration
@@ -132,6 +137,8 @@ class TestFlux2RopePositionEmbedding:
class TestFlux2PackedModuleMapping:
"""Test Flux2 packed module mapping functionality"""
@pytest.mark.core_model
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_packed_module_mapping(self, setup_tp_group):
"""Verify to_qkv packing matches HF checkpoint"""
model = Flux2Transformer2DModel(
@@ -208,6 +215,8 @@ class TestFlux2PackedModuleMapping:
f"add_kv_proj weight dimension should be {expected_add_kv_shape}, got {attn_block.add_kv_proj.weight.shape}"
)
@pytest.mark.core_model
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_packed_mapping_edge_cases(self, setup_tp_group):
"""Test edge cases for packed mapping"""
model = Flux2Transformer2DModel(
@@ -0,0 +1,58 @@
from types import SimpleNamespace
import pytest
import torch
from vllm_omni.diffusion.models.ltx2.pipeline_ltx2 import LTX2Pipeline
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
def _make_pipeline(sequence_parallel_size: int = 1) -> LTX2Pipeline:
pipeline = object.__new__(LTX2Pipeline)
torch.nn.Module.__init__(pipeline)
pipeline.audio_vae_temporal_compression_ratio = 4
pipeline.audio_vae_mel_compression_ratio = 4
pipeline.od_config = SimpleNamespace(parallel_config=SimpleNamespace(sequence_parallel_size=sequence_parallel_size))
# Mock audio_vae with identity normalization (mean=0, std=1) so
# _normalize_audio_latents is a no-op and test values are preserved.
pipeline.audio_vae = SimpleNamespace(
latents_mean=torch.tensor(0.0),
latents_std=torch.tensor(1.0),
)
return pipeline
def test_prepare_audio_latents_pads_packed_sequence_dim_for_provided_latents():
pipeline = _make_pipeline(sequence_parallel_size=4)
latents = torch.arange(40, dtype=torch.float32).view(1, 10, 4)
padded, original_num_frames, padded_num_frames = pipeline.prepare_audio_latents(
batch_size=1,
num_channels_latents=2,
num_mel_bins=8,
audio_latent_length=10,
dtype=torch.float32,
device=torch.device("cpu"),
latents=latents,
)
assert original_num_frames == 10
assert padded_num_frames == 12
assert padded.shape == (1, 12, 4)
torch.testing.assert_close(padded[:, :10], latents)
torch.testing.assert_close(padded[:, 10:], torch.zeros(1, 2, 4))
def test_unpad_audio_latents_restores_original_frames_before_unpack():
pipeline = _make_pipeline()
original = torch.arange(40, dtype=torch.float32).view(1, 10, 4)
padded = torch.cat([original, torch.full((1, 2, 4), 999.0)], dim=1)
unpadded = pipeline._unpad_audio_latents(padded, 10)
unpacked = pipeline._unpack_audio_latents(unpadded, latent_length=10, num_mel_bins=2)
expected = pipeline._unpack_audio_latents(original, latent_length=10, num_mel_bins=2)
assert unpacked.shape == (1, 2, 10, 2)
assert not (unpacked == 999.0).any()
torch.testing.assert_close(unpacked, expected)
@@ -5,7 +5,7 @@
import pytest
from torch import nn
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion]
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cpu]
def test_build_quant_config_fp8():
@@ -8,9 +8,10 @@ import pytest
import torch
import vllm_omni.diffusion.worker.diffusion_model_runner as model_runner_module
from tests.utils import hardware_test
from vllm_omni.diffusion.worker.diffusion_model_runner import DiffusionModelRunner
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cpu]
pytestmark = [pytest.mark.diffusion]
@contextmanager
@@ -64,6 +65,8 @@ def _make_runner(cache_backend, cache_backend_name: str, enable_cache_dit_summar
return runner
@pytest.mark.core_model
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_execute_model_skips_cache_summary_without_active_cache_backend(monkeypatch):
"""Guard cache diagnostics with runtime backend state to avoid stale-config crashes."""
runner = _make_runner(cache_backend=None, cache_backend_name="cache_dit")
@@ -84,6 +87,8 @@ def test_execute_model_skips_cache_summary_without_active_cache_backend(monkeypa
assert cache_summary_calls == []
@pytest.mark.core_model
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_execute_model_emits_cache_summary_with_active_cache_dit_backend(monkeypatch):
class _EnabledCacheBackend:
def is_enabled(self):
@@ -107,6 +112,8 @@ def test_execute_model_emits_cache_summary_with_active_cache_dit_backend(monkeyp
assert cache_summary_calls == [(runner.pipeline, True)]
@pytest.mark.core_model
@pytest.mark.cpu
def test_load_model_clears_cache_backend_for_unsupported_pipeline(monkeypatch):
class _DummyLoader:
def __init__(self, load_config, od_config=None):
@@ -0,0 +1,75 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import asyncio
from concurrent.futures import ThreadPoolExecutor
from dataclasses import asdict
from types import SimpleNamespace
import pytest
from vllm_omni.diffusion.stage_diffusion_proc import StageDiffusionProc
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cpu]
def test_process_batch_request_preserves_parent_request_id_and_kv_sender_info():
async def run_test():
captured = {}
def step(request):
captured["request"] = request
return [
SimpleNamespace(
images=["img-1"],
_multimodal_output={},
_custom_output={},
metrics={},
stage_durations={},
peak_memory_mb=0.0,
latents=None,
trajectory_latents=None,
trajectory_timesteps=None,
trajectory_log_probs=None,
trajectory_decoded=None,
final_output_type="image",
),
SimpleNamespace(
images=["img-2"],
_multimodal_output={},
_custom_output={},
metrics={},
stage_durations={},
peak_memory_mb=0.0,
latents=None,
trajectory_latents=None,
trajectory_timesteps=None,
trajectory_log_probs=None,
trajectory_decoded=None,
final_output_type="image",
),
]
proc = object.__new__(StageDiffusionProc)
proc._engine = SimpleNamespace(step=step)
proc._executor = ThreadPoolExecutor(max_workers=1)
try:
result = await proc._process_batch_request(
request_id="req-parent",
prompts=["hello", "world"],
sampling_params_dict=asdict(OmniDiffusionSamplingParams()),
kv_sender_info={0: {"host": "10.0.0.2", "zmq_port": 50151}},
)
finally:
proc._executor.shutdown(wait=True)
request = captured["request"]
assert request.request_id == "req-parent"
assert request.request_ids == ["req-parent-0", "req-parent-1"]
assert request.kv_sender_info == {0: {"host": "10.0.0.2", "zmq_port": 50151}}
assert result.request_id == "req-parent"
assert result.images == ["img-1", "img-2"]
asyncio.run(run_test())
@@ -9,7 +9,7 @@ from vllm_omni.distributed.omni_connectors.factory import OmniConnectorFactory
from vllm_omni.distributed.omni_connectors.utils.config import ConnectorSpec
from vllm_omni.distributed.omni_connectors.utils.serialization import OmniSerializer
# pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
def test_basic_serialization():
@@ -120,3 +120,61 @@ def test_get_invalid_metadata(shm_connector):
result = shm_connector.get("stage_0", "stage_1", "req_3", {"unknown": "format"})
assert result is None
def test_mooncake_connector_defaults_missing_host_to_detected_ip(monkeypatch: pytest.MonkeyPatch):
import vllm_omni.distributed.omni_connectors.connectors.mooncake_transfer_engine_connector as mooncake_module
class _FakePool:
is_cuda = False
def pin_memory(self):
return self
def data_ptr(self):
return 1234
class _FakeTransferEngine:
def initialize(self, host, mode, protocol, device_name):
self.host = host
self.mode = mode
self.protocol = protocol
self.device_name = device_name
return 0
def get_rpc_port(self):
return 23456
def register_memory(self, base_ptr, pool_size):
del base_ptr, pool_size
return 0
def unregister_memory(self, base_ptr):
del base_ptr
return 0
monkeypatch.setattr(mooncake_module, "TransferEngine", _FakeTransferEngine)
monkeypatch.setattr(mooncake_module.torch, "empty", lambda *args, **kwargs: _FakePool())
monkeypatch.setattr(
mooncake_module.MooncakeTransferEngineConnector,
"_get_local_ip",
lambda self: "10.20.30.40",
)
monkeypatch.setattr(
mooncake_module.MooncakeTransferEngineConnector,
"_zmq_listener_loop",
lambda self: self._listener_ready.set(),
)
connector = mooncake_module.MooncakeTransferEngineConnector(
{
"zmq_port": 50051,
"memory_pool_size": 4096,
}
)
try:
assert connector.host == "10.20.30.40"
assert connector.engine.host == "10.20.30.40"
assert connector.get_connection_info()["host"] == "10.20.30.40"
finally:
connector.close()
@@ -1,8 +1,14 @@
import json
import struct
import numpy as np
import pytest
import torch
import vllm_omni.distributed.omni_connectors.kv_transfer_manager as kv_transfer_manager_module
from vllm_omni.diffusion.request import OmniDiffusionRequest
from vllm_omni.distributed.omni_connectors.kv_transfer_manager import (
KVCacheTransferData,
OmniKVCacheConfig,
OmniKVTransferManager,
)
@@ -60,6 +66,35 @@ def common_constants():
}
def _decode_stored_payload(data):
if isinstance(data, torch.Tensor) and data.dtype == torch.uint8 and data.dim() == 1:
return KVCacheTransferData.from_bytes(data.cpu().numpy().tobytes())
if isinstance(data, (bytes, bytearray, memoryview)):
return KVCacheTransferData.from_bytes(data)
return data
def _make_serialized_payload() -> tuple[bytes, torch.Tensor]:
key_tensor = torch.arange(12, dtype=torch.float32).reshape(3, 4)
payload = KVCacheTransferData(
request_id="req-payload",
layer_blocks={"key_cache": [key_tensor], "value_cache": [None]},
block_ids=[1],
metadata={"seq_len": 3},
).to_bytes()
return payload, key_tensor
def _rewrite_serialized_header(payload: bytes, mutate_header) -> bytes:
header_len = struct.unpack(">I", payload[:4])[0]
header = json.loads(payload[4 : 4 + header_len])
mutate_header(header)
new_header = json.dumps(header, separators=(",", ":")).encode("utf-8")
return struct.pack(">I", len(new_header)) + new_header + payload[4 + header_len :]
def test_manager_extraction(kv_config, mock_connector, common_constants):
"""Test extraction and sending logic in OmniKVTransferManager."""
num_layers = common_constants["num_layers"]
@@ -95,7 +130,7 @@ def test_manager_extraction(kv_config, mock_connector, common_constants):
expected_key = f"stage1->stage2:{full_request_id}"
assert expected_key in mock_connector.store
data = mock_connector.store[expected_key]
data = _decode_stored_payload(mock_connector.store[expected_key])
assert data["request_id"] == req_id
assert "layer_blocks" in data
assert len(data["layer_blocks"]["key_cache"]) == num_layers
@@ -106,6 +141,116 @@ def test_manager_extraction(kv_config, mock_connector, common_constants):
assert data["layer_blocks"]["key_cache"][0].shape == expected_shape
def test_from_bytes_rejects_out_of_bounds_header_len():
payload, _ = _make_serialized_payload()
bad_payload = struct.pack(">I", len(payload)) + payload[4:]
with pytest.raises(ValueError, match="header_len"):
KVCacheTransferData.from_bytes(bad_payload)
with pytest.raises(ValueError, match="header_len"):
KVCacheTransferData.from_bytes_gpu(torch.tensor(list(bad_payload), dtype=torch.uint8))
def test_from_bytes_rejects_out_of_bounds_tensor_span():
payload, _ = _make_serialized_payload()
bad_payload = _rewrite_serialized_header(payload, lambda header: header["td"][0].update({"o": 4096}))
with pytest.raises(ValueError, match="tensor span"):
KVCacheTransferData.from_bytes(bad_payload)
with pytest.raises(ValueError, match="tensor span"):
KVCacheTransferData.from_bytes_gpu(torch.tensor(list(bad_payload), dtype=torch.uint8))
def test_from_bytes_rejects_unsupported_dtype():
payload, _ = _make_serialized_payload()
bad_payload = _rewrite_serialized_header(payload, lambda header: header["td"][0].update({"d": "cuda"}))
with pytest.raises(ValueError, match="Unsupported dtype"):
KVCacheTransferData.from_bytes(bad_payload)
with pytest.raises(ValueError, match="Unsupported dtype"):
KVCacheTransferData.from_bytes_gpu(torch.tensor(list(bad_payload), dtype=torch.uint8))
def test_from_bytes_uses_explicit_layer_index_descriptor():
payload, key_tensor = _make_serialized_payload()
payload_with_explicit_index = _rewrite_serialized_header(
payload,
lambda header: header["td"][0].update({"n": "key_cache_extra_suffix", "i": 0}),
)
data = KVCacheTransferData.from_bytes(payload_with_explicit_index)
assert torch.equal(data["layer_blocks"]["key_cache"][0], key_tensor)
def test_update_sender_info_uses_configured_source_stage():
config = OmniKVCacheConfig(
connector_config={"type": "mock"},
stage_id=2,
engine_input_source=[1],
need_recv_cache=True,
)
manager = OmniKVTransferManager(config)
manager.update_sender_info(
{
0: {"host": "10.0.0.1", "zmq_port": 50151},
1: {"host": "10.0.0.2", "zmq_port": 50152},
}
)
assert manager.config.connector_config["sender_host"] == "10.0.0.2"
assert manager.config.connector_config["sender_zmq_port"] == 50152
def test_clone_received_payload_tensors_breaks_buffer_alias():
payload, key_tensor = _make_serialized_payload()
raw = np.frombuffer(bytearray(payload), dtype=np.uint8)
data = KVCacheTransferData.from_bytes(memoryview(raw))
OmniKVTransferManager._clone_received_payload_tensors(data)
raw[:] = 0
assert torch.equal(data["layer_blocks"]["key_cache"][0], key_tensor)
def test_receive_kv_cache_uses_exponential_backoff(monkeypatch):
config = OmniKVCacheConfig(
connector_config={"type": "mock"},
from_stage="sender",
stage_id="receiver",
need_recv_cache=True,
recv_timeout=0.3,
)
manager = OmniKVTransferManager(config)
class _NeverReadyConnector:
def get(self, **kwargs):
del kwargs
return None
manager._connector = _NeverReadyConnector()
now = {"value": 0.0}
sleep_intervals = []
monkeypatch.setattr(kv_transfer_manager_module.time, "time", lambda: now["value"])
def _fake_sleep(interval: float) -> None:
sleep_intervals.append(interval)
now["value"] += interval
monkeypatch.setattr(kv_transfer_manager_module.time, "sleep", _fake_sleep)
data, size = manager.receive_kv_cache_for_request("req-backoff")
assert (data, size) == (None, 0)
assert sleep_intervals == pytest.approx([0.01, 0.02, 0.04, 0.08, 0.16])
def test_manager_extraction_tuple_layout(kv_config, mock_connector, common_constants):
"""Test extraction with tuple layout."""
num_layers = common_constants["num_layers"]
@@ -135,7 +280,7 @@ def test_manager_extraction_tuple_layout(kv_config, mock_connector, common_const
expected_key = f"stage1->stage2:{full_request_id}"
assert expected_key in mock_connector.store
data = mock_connector.store[expected_key]
data = _decode_stored_payload(mock_connector.store[expected_key])
expected_shape = (seq_len, num_heads, head_dim)
for idx in range(len(kv_caches)):
assert data["layer_blocks"]["key_cache"][idx].shape == expected_shape
@@ -165,7 +310,7 @@ def test_manager_extraction_mismatched_kv_block_counts(kv_config, mock_connector
expected_key = f"stage1->stage2:{full_request_id}"
assert expected_key in mock_connector.store
data = mock_connector.store[expected_key]
data = _decode_stored_payload(mock_connector.store[expected_key])
expected_shape = (2 * block_size, num_heads, head_dim)
assert data["layer_blocks"]["key_cache"][0].shape == expected_shape
assert data["layer_blocks"]["value_cache"][0].shape == expected_shape
@@ -254,6 +399,82 @@ def test_manager_reception(kv_config, mock_connector, common_constants):
assert req.kv_metadata["seq_len"] == seq_len
def test_manager_reception_prefers_parent_request_id_for_batched_request(kv_config, mock_connector, common_constants):
"""Batched diffusion requests must fetch KV using the parent/global request ID."""
num_layers = common_constants["num_layers"]
num_heads = common_constants["num_heads"]
head_dim = common_constants["head_dim"]
seq_len = common_constants["seq_len"]
parent_req_id = common_constants["req_id"]
expected_shape = (seq_len, num_heads, head_dim)
key_cache = [torch.randn(expected_shape) for _ in range(num_layers)]
value_cache = [torch.randn(expected_shape) for _ in range(num_layers)]
data_to_receive = {
"request_id": parent_req_id,
"layer_blocks": {"key_cache": key_cache, "value_cache": value_cache},
"metadata": {"seq_len": seq_len},
"block_ids": [],
}
manager = OmniKVTransferManager(kv_config)
manager._connector = mock_connector
full_request_id = f"omni_stage1_to_stage2_kv_cache_{parent_req_id}"
store_key = f"stage1->stage2:{full_request_id}"
mock_connector.store[store_key] = data_to_receive
req = OmniDiffusionRequest(
prompts=["prompt-a", "prompt-b"],
sampling_params=OmniDiffusionSamplingParams(),
request_ids=[f"{parent_req_id}-0", f"{parent_req_id}-1"],
request_id=parent_req_id,
)
success = manager.receive_kv_cache(req, target_device=torch.device("cpu"))
assert success
assert req.kv_metadata["seq_len"] == seq_len
assert torch.allclose(req.past_key_values.key_cache[0], key_cache[0])
def test_receive_multi_kv_cache_uses_parent_request_id_for_cfg_collection(kv_config):
manager = OmniKVTransferManager(kv_config)
seen = {}
def collect_cfg(request_id, cfg_request_ids, kv_transfer_manager, target_device):
seen["request_id"] = request_id
seen["cfg_request_ids"] = cfg_request_ids
seen["kv_transfer_manager"] = kv_transfer_manager
seen["target_device"] = target_device
return {"cfg_text_kv_metadata": {"ok": True}}
req = OmniDiffusionRequest(
prompts=["prompt-a", "prompt-b"],
sampling_params=OmniDiffusionSamplingParams(),
request_ids=["req-parent-0", "req-parent-1"],
request_id="req-parent",
)
req.sampling_params.cfg_kv_request_ids = {"cfg_text": "req-parent__cfg_text"}
manager.receive_kv_cache = lambda request, target_device=None: request is req
success = manager.receive_multi_kv_cache(
req,
cfg_kv_collect_func=collect_cfg,
target_device=torch.device("cpu"),
)
assert success
assert seen["request_id"] == "req-parent"
assert seen["cfg_request_ids"] == {"cfg_text": "req-parent__cfg_text"}
assert seen["kv_transfer_manager"] is manager
assert seen["target_device"] == torch.device("cpu")
assert req.sampling_params.cfg_text_kv_metadata == {"ok": True}
def test_integration_flow(common_constants):
"""Simulate extraction -> connector -> reception."""
num_layers = common_constants["num_layers"]
@@ -3,12 +3,16 @@
from time import time
import pytest
from vllm_omni.distributed.omni_coordinator import (
InstanceInfo,
RandomBalancer,
StageStatus,
)
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
def test_load_balancer_select_returns_valid_index():
"""Verify RandomBalancer.select() returns a valid index for instances."""
@@ -12,6 +12,8 @@ from vllm_omni.distributed.omni_coordinator import (
OmniCoordClientForHub,
)
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
def _bind_pub() -> tuple[zmq.Context, zmq.Socket, str]:
ctx = zmq.Context.instance()
@@ -17,6 +17,8 @@ from vllm_omni.distributed.omni_coordinator import (
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
def _bind_router() -> tuple[zmq.Context, zmq.Socket, str]:
ctx = zmq.Context.instance()
@@ -4,6 +4,7 @@
import json
import time
import pytest
import zmq
from vllm.v1.utils import get_engine_client_zmq_addr
@@ -13,6 +14,8 @@ from vllm_omni.distributed.omni_coordinator import (
StageStatus,
)
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
def _recv_instance_list(sub: zmq.Socket, timeout_ms: int = 2000) -> dict | None:
"""Receive InstanceList JSON from SUB socket. Returns None on timeout."""
@@ -6,7 +6,8 @@
This pipeline follows the structure of the user's reference implementation:
- supports pre-tokenized prompt IDs via OmniCustomPrompt-style dict input
- uses an SDE scheduler that can return step logprobs
- returns rich custom_output fields for testing
- returns structured trajectory_* fields (latents, timesteps, log_probs)
consistent with the BAGEL trajectory recording design
"""
from __future__ import annotations
@@ -393,10 +394,10 @@ class QwenImagePipelineWithLogProbForTest(QwenImagePipeline):
return DiffusionOutput(
output=_maybe_to_cpu(image),
trajectory_latents=_maybe_to_cpu(all_latents),
trajectory_log_probs=_maybe_to_cpu(all_log_probs),
trajectory_timesteps=_maybe_to_cpu(all_timesteps),
custom_output={
"all_latents": _maybe_to_cpu(all_latents),
"all_log_probs": _maybe_to_cpu(all_log_probs),
"all_timesteps": _maybe_to_cpu(all_timesteps),
"prompt_embeds": _maybe_to_cpu(prompt_embeds),
"prompt_embeds_mask": _maybe_to_cpu(prompt_embeds_mask),
"negative_prompt_embeds": _maybe_to_cpu(negative_prompt_embeds),
@@ -1,7 +1,12 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""E2E tests for AsyncOmni Qwen-Image generation flow (no Ray, no HTTP server)."""
"""E2E tests for AsyncOmni Qwen-Image generation with trajectory_* fields.
Validates that the custom Qwen-Image pipeline returns structured trajectory
outputs (latents, timesteps, log_probs) via OmniRequestOutput's trajectory_*
fields instead of the legacy custom_output dict.
"""
from __future__ import annotations
@@ -191,10 +196,17 @@ async def test_async_omni_generate_with_logprobs():
_assert_valid_image_output(output)
all_log_probs = output.custom_output.get("all_log_probs")
assert all_log_probs is not None, "all_log_probs should be present when logprobs=True"
assert hasattr(all_log_probs, "shape")
assert all_log_probs.numel() > 0
assert output.trajectory_latents is not None, "trajectory_latents should be present"
assert hasattr(output.trajectory_latents, "shape")
assert output.trajectory_latents.numel() > 0
assert output.trajectory_timesteps is not None, "trajectory_timesteps should be present"
assert hasattr(output.trajectory_timesteps, "shape")
assert output.trajectory_timesteps.numel() > 0
assert output.trajectory_log_probs is not None, "trajectory_log_probs should be present when logprobs=True"
assert hasattr(output.trajectory_log_probs, "shape")
assert output.trajectory_log_probs.numel() > 0
@pytest.mark.core_model
@@ -0,0 +1,198 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""
End-to-end test for BAGEL LoRA support (Stage 1 / DiT).
Validates that LoRA adapters are correctly loaded, applied with controllable
scale, and cleanly deactivated. Uses a synthetic rank-1 adapter targeting the
first decoder layer's QKV projection.
Assertions:
(a) LoRA at scale=1.0 visibly changes the output (diff > 0.5)
(b) scale=2.0 produces a larger delta than scale=1.0 (linearity)
(c) The delta is bounded (diff < 80, not corrupted)
(d) Deactivating LoRA exactly restores the baseline (diff == 0)
"""
import json
import os
from vllm_omni.inputs.data import OmniSamplingParams
from vllm_omni.outputs import OmniRequestOutput
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
os.environ["VLLM_TEST_CLEAN_GPU_MEMORY"] = "1"
from pathlib import Path
import numpy as np
import pytest
import torch
from PIL import Image
from safetensors.torch import save_file
from tests.conftest import modify_stage_config
from tests.utils import hardware_test
from vllm_omni.entrypoints.omni import Omni
from vllm_omni.lora.request import LoRARequest
from vllm_omni.lora.utils import stable_lora_int_id
MODEL = "ByteDance-Seed/BAGEL-7B-MoT"
BAGEL_STAGE_CONFIG = str(Path(__file__).parent / "stage_configs" / "bagel_sharedmemory_ci.yaml")
DEFAULT_PROMPT = "<|im_start|>A cute cat<|im_end|>"
# ---------------------------------------------------------------------------
# Helpers (reused from test_bagel_text2img.py patterns)
# ---------------------------------------------------------------------------
def _resolve_stage_config(config_path: str, run_level: str) -> str:
if run_level == "advanced_model":
return modify_stage_config(
config_path,
deletes={
"stage_args": {
0: ["engine_args.load_format"],
1: ["engine_args.load_format"],
}
},
)
return config_path
def _configure_sampling_params(omni: Omni, num_inference_steps: int = 10) -> list[OmniSamplingParams]:
params_list = omni.default_sampling_params_list
if len(params_list) > 1:
params_list[1].num_inference_steps = num_inference_steps
params_list[1].extra_args = {
"cfg_text_scale": 4.0,
"cfg_img_scale": 1.5,
}
return params_list
def _extract_generated_image(omni_outputs: list[OmniRequestOutput]) -> Image.Image | None:
for req_output in omni_outputs:
if req_output.images:
return req_output.images[0]
return None
def _generate_bagel_image(omni: Omni) -> Image.Image:
params_list = _configure_sampling_params(omni)
params_list[1].lora_request = None
outputs = list(
omni.generate(
prompts=[{"prompt": DEFAULT_PROMPT, "modalities": ["image"]}],
sampling_params_list=params_list,
)
)
img = _extract_generated_image(outputs)
assert img is not None, "No image generated"
return img
def _generate_bagel_image_with_lora(
omni: Omni,
lora_request: LoRARequest,
lora_scale: float = 1.0,
) -> Image.Image:
params_list = _configure_sampling_params(omni)
params_list[1].lora_request = lora_request
params_list[1].lora_scale = lora_scale
outputs = list(
omni.generate(
prompts=[{"prompt": DEFAULT_PROMPT, "modalities": ["image"]}],
sampling_params_list=params_list,
)
)
img = _extract_generated_image(outputs)
assert img is not None, "No image generated with LoRA"
return img
# BAGEL uses GQA: hidden_size=3584, 28 Q heads, 4 KV heads, head_dim=128
# QKV packed dim = 28*128 + 4*128 + 4*128 = 3584 + 512 + 512 = 4608
_LORA_DIM = 3584
_LORA_QKV_DIM = 4608
_LORA_MODULE = "bagel.language_model.model.layers.0.self_attn.qkv_proj"
_LORA_RANK = 4
def _make_file_lora_request(adapter_dir: Path) -> LoRARequest:
"""Write synthetic adapter to disk and return a file-backed LoRARequest."""
adapter_dir.mkdir(parents=True, exist_ok=True)
gen = torch.Generator().manual_seed(42)
lora_a = torch.randn((_LORA_RANK, _LORA_DIM), dtype=torch.float32, generator=gen) * 0.1
lora_b = torch.randn((_LORA_QKV_DIM, _LORA_RANK), dtype=torch.float32, generator=gen) * 0.5
save_file(
{
f"base_model.model.{_LORA_MODULE}.lora_A.weight": lora_a,
f"base_model.model.{_LORA_MODULE}.lora_B.weight": lora_b,
},
str(adapter_dir / "adapter_model.safetensors"),
)
(adapter_dir / "adapter_config.json").write_text(
json.dumps({"r": _LORA_RANK, "lora_alpha": _LORA_RANK, "target_modules": [_LORA_MODULE]}),
encoding="utf-8",
)
lora_dir = str(adapter_dir)
return LoRARequest(lora_name="test_file", lora_int_id=stable_lora_int_id(lora_dir), lora_path=lora_dir)
# ---------------------------------------------------------------------------
# Test
# ---------------------------------------------------------------------------
@pytest.mark.core_model
@pytest.mark.advanced_model
@pytest.mark.diffusion
@hardware_test(res={"cuda": "H100", "rocm": "MI325"})
def test_bagel_lora_scale_and_deactivation(run_level, tmp_path):
"""Validate LoRA effect, bounded perturbation, and clean deactivation."""
config_path = _resolve_stage_config(BAGEL_STAGE_CONFIG, run_level)
omni = Omni(model=MODEL, stage_configs_path=config_path, stage_init_timeout=300)
try:
lora_request = _make_file_lora_request(tmp_path / "bagel_lora")
# 1) Baseline (no LoRA)
baseline = _generate_bagel_image(omni)
# 2) LoRA with scale=1.0
img_1x = _generate_bagel_image_with_lora(omni, lora_request, lora_scale=1.0)
# 3) LoRA with scale=2.0
img_2x = _generate_bagel_image_with_lora(omni, lora_request, lora_scale=2.0)
# 4) No LoRA again (deactivation)
restored = _generate_bagel_image(omni)
baseline_arr = np.array(baseline, dtype=np.int16)
img_1x_arr = np.array(img_1x, dtype=np.int16)
img_2x_arr = np.array(img_2x, dtype=np.int16)
restored_arr = np.array(restored, dtype=np.int16)
diff_1x = np.abs(baseline_arr - img_1x_arr).mean()
diff_2x = np.abs(baseline_arr - img_2x_arr).mean()
diff_restored = np.abs(baseline_arr - restored_arr).mean()
# (a) Adapter has visible effect at both scales
assert diff_1x > 0.5, f"LoRA scale=1.0 had no visible effect: diff={diff_1x}"
assert diff_2x > 0.5, f"LoRA scale=2.0 had no visible effect: diff={diff_2x}"
# (b) Different scales produce different outputs
assert not np.isclose(diff_1x, diff_2x, atol=1.0), (
f"LoRA scale has no effect: diff_1x={diff_1x:.2f}, diff_2x={diff_2x:.2f}"
)
# (c) Output is not corrupted
assert diff_1x < 80, f"LoRA output looks corrupted: diff_1x={diff_1x}"
assert diff_2x < 80, f"LoRA output looks corrupted: diff_2x={diff_2x}"
# (d) Deactivation fully restores base model
assert diff_restored == 0.0, f"Base model not restored after LoRA deactivation: diff={diff_restored}"
finally:
omni.close()
@@ -0,0 +1,419 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""
E2E offline smoke tests for Dynin-Omni.
- model: "snu-aidas/Dynin-Omni"
- stage config: tests/e2e/stage_configs/dynin_omni_ci.yaml
"""
from __future__ import annotations
import os
from pathlib import Path
from typing import Any
import numpy as np
import pytest
import torch
from transformers import AutoTokenizer
from tests.conftest import OmniRunner
from tests.utils import hardware_test
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
os.environ["VLLM_TEST_CLEAN_GPU_MEMORY"] = "0"
_REPO_ROOT = Path(__file__).resolve().parents[3]
_DEFAULT_DYNIN_CONFIG_PATH: Path | None = None
_DEFAULT_STAGE_CONFIG_PATH = _REPO_ROOT / "tests" / "e2e" / "stage_configs" / "dynin_omni_ci.yaml"
models = ["snu-aidas/Dynin-Omni"]
stage_configs = [str(_DEFAULT_STAGE_CONFIG_PATH)]
test_params = [(model, stage_config) for model in models for stage_config in stage_configs]
DYNIN_CONFIG_PATH = str(_DEFAULT_DYNIN_CONFIG_PATH) if _DEFAULT_DYNIN_CONFIG_PATH is not None else None
pytestmark = [
pytest.mark.core_model,
pytest.mark.omni,
]
# prompting util
def _build_mmu_prompt(tokenizer: Any, question: str, dynin_config_path: str | None) -> dict[str, Any]:
encoded = tokenizer(question, return_tensors="pt", add_special_tokens=True)
token_ids = [int(v) for v in encoded["input_ids"][0].tolist()]
attention_mask = [int(v) for v in encoded["attention_mask"][0].tolist()]
additional_information: dict[str, Any] = {
"task": ["mmu"],
"detok_id": [0],
"prompt_length": [len(token_ids)],
"attention_mask": [attention_mask],
"max_new_tokens": [64],
"steps": [64],
"block_length": [16],
"temperature": [0.0],
}
if dynin_config_path:
additional_information["dynin_config_path"] = [str(dynin_config_path)]
return {
"prompt_token_ids": token_ids,
"additional_information": additional_information,
"modalities": ["text"],
}
def _build_mmu_multimodal_prompt(
tokenizer: Any,
question: str,
dynin_config_path: str | None,
*,
image: Any | None = None,
audio: tuple[np.ndarray, int] | None = None,
) -> dict[str, Any]:
if image is None and audio is None:
raise ValueError("At least one multimodal input (image or audio) must be provided.")
prefix_chunks: list[str] = []
mm_data: dict[str, Any] = {}
if image is not None:
prefix_chunks.append("<|soi|><|image|><|eoi|>")
mm_data["image"] = image
if audio is not None:
prefix_chunks.append("<|soa|><|audio|><|eoa|>")
mm_data["audio"] = audio
prefixed_question = " ".join(prefix_chunks + [question]).strip()
prompt = _build_mmu_prompt(
tokenizer=tokenizer,
question=prefixed_question,
dynin_config_path=dynin_config_path,
)
prompt["multi_modal_data"] = mm_data
prompt["modalities"] = ["text"]
return prompt
def _generate_synthetic_image(width: int = 224, height: int = 224) -> np.ndarray:
x = np.linspace(0, 255, width, dtype=np.uint8)
y = np.linspace(0, 255, height, dtype=np.uint8)[:, None]
red = np.tile(x, (height, 1))
green = np.tile(y, (1, width))
blue = ((red.astype(np.uint16) + green.astype(np.uint16)) // 2).astype(np.uint8)
return np.stack([red, green, blue], axis=-1)
def _generate_synthetic_audio(duration_s: int = 5, sample_rate: int = 48_000) -> tuple[np.ndarray, int]:
t = np.linspace(0, duration_s, int(sample_rate * duration_s), endpoint=False, dtype=np.float32)
waveform = 0.1 * np.sin(2.0 * np.pi * 440.0 * t)
return waveform.astype(np.float32), sample_rate
# prompting util
def _build_t2s_decode_prompt(dynin_config_path: str | None) -> dict[str, Any]:
# Bypass stage-0 generation and directly validate token->audio decode path.
generated_audio_token_ids = [int(v) for v in ([10, 11, 12, 13, 14] * 32)]
additional_information: dict[str, Any] = {
"task": ["t2s"],
"detok_id": [1],
"generated_token_ids": [generated_audio_token_ids],
"audio_codebook_size": [4096],
}
if dynin_config_path:
additional_information["dynin_config_path"] = [str(dynin_config_path)]
return {
"prompt_token_ids": [0],
"additional_information": additional_information,
"modalities": ["audio"],
}
# prompting util
def _build_t2i_decode_prompt(dynin_config_path: str | None) -> dict[str, Any]:
# Bypass stage-0 generation and directly validate token->image decode path.
# MAGVIT decode path expects a square token grid; 1024 tokens -> 32x32.
generated_image_token_ids = [int(v) for v in ([10, 11, 12, 13, 14, 15, 16, 17] * 128)]
additional_information: dict[str, Any] = {
"task": ["t2i"],
"detok_id": [2],
"generated_token_ids": [generated_image_token_ids],
"codebook_size": [8192],
}
if dynin_config_path:
additional_information["dynin_config_path"] = [str(dynin_config_path)]
return {
"prompt_token_ids": [0],
"additional_information": additional_information,
"modalities": ["image"],
}
def _configure_dynin_config_env() -> None:
if DYNIN_CONFIG_PATH:
os.environ["DYNIN_CONFIG_PATH"] = str(DYNIN_CONFIG_PATH)
else:
os.environ.pop("DYNIN_CONFIG_PATH", None)
def _is_finished_request_output(request_output: Any) -> bool:
if request_output is None:
return False
req_list = request_output if isinstance(request_output, list) else [request_output]
for req in req_list:
if req is not None and bool(getattr(req, "finished", False)):
return True
return False
def _find_stage_output(outputs: list[Any], output_type: str) -> Any | None:
matched = [
stage_output for stage_output in outputs if getattr(stage_output, "final_output_type", None) == output_type
]
if not matched:
return None
# Prefer the latest finished chunk to avoid picking an intermediate stream output.
for stage_output in reversed(matched):
if _is_finished_request_output(getattr(stage_output, "request_output", None)):
return stage_output
return matched[-1]
def _to_token_list(value: Any) -> list[int]:
if value is None:
return []
if hasattr(value, "detach"):
value = value.detach()
if hasattr(value, "cpu"):
value = value.cpu()
if hasattr(value, "flatten"):
value = value.flatten().tolist()
if isinstance(value, tuple):
value = list(value)
if not isinstance(value, list):
return []
out: list[int] = []
for token in value:
if isinstance(token, bool):
continue
try:
out.append(int(token))
except Exception:
continue
return out
def _extract_text(stage_output: Any, tokenizer: Any | None = None) -> str:
request_output = getattr(stage_output, "request_output", None)
if request_output is None:
return ""
req_list = request_output if isinstance(request_output, list) else [request_output]
for req in req_list:
completions = getattr(req, "outputs", None) or []
if not completions:
continue
completion = completions[0]
mm_out = (
getattr(completion, "multimodal_output", None)
or getattr(req, "multimodal_output", None)
or getattr(stage_output, "multimodal_output", None)
or {}
)
text = mm_out.get("text")
if isinstance(text, list) and text:
text = text[-1]
if isinstance(text, str) and text.strip():
return text.strip()
if tokenizer is not None:
for key in ("text_tokens", "token_ids"):
token_ids = _to_token_list(mm_out.get(key))
if not token_ids:
continue
decoded = tokenizer.decode(token_ids, skip_special_tokens=True)
if isinstance(decoded, str) and decoded.strip():
return decoded.strip()
fallback = getattr(completion, "text", None)
if isinstance(fallback, str) and fallback.strip():
return fallback.strip()
return ""
def _extract_audio(stage_output: Any) -> Any | None:
request_output = getattr(stage_output, "request_output", None)
if request_output is None:
return None
req_list = request_output if isinstance(request_output, list) else [request_output]
for req in req_list:
completions = getattr(req, "outputs", None) or []
if not completions:
continue
completion = completions[0]
mm_out = getattr(completion, "multimodal_output", None) or {}
if "audio" in mm_out:
return mm_out["audio"]
return None
def _extract_image(stage_output: Any) -> Any | None:
request_output = getattr(stage_output, "request_output", None)
if request_output is None:
return None
req_list = request_output if isinstance(request_output, list) else [request_output]
for req in req_list:
completions = getattr(req, "outputs", None) or []
if not completions:
continue
completion = completions[0]
mm_out = getattr(completion, "multimodal_output", None) or {}
if "image" in mm_out:
return mm_out["image"]
return None
def _numel(value: Any) -> int:
if value is None:
return 0
if isinstance(value, torch.Tensor):
return int(value.numel())
shape = getattr(value, "shape", None)
if shape is not None:
try:
total = 1
for dim in shape:
total *= int(dim)
return int(total)
except Exception:
pass
if isinstance(value, (list, tuple)):
return len(value)
return 0
@hardware_test(res={"cuda": "L4", "rocm": "MI325"})
@pytest.mark.parametrize("test_config", test_params)
def test_dynin_t2i_decode_to_image(test_config: tuple[str, str]) -> None:
model, stage_config_path = test_config
_configure_dynin_config_env()
prompt = _build_t2i_decode_prompt(dynin_config_path=DYNIN_CONFIG_PATH)
with OmniRunner(
model,
seed=42,
stage_configs_path=stage_config_path,
stage_init_timeout=600,
init_timeout=600,
) as runner:
outputs = runner.generate([prompt])
image_output = _find_stage_output(outputs, "image")
assert image_output is not None
image_value = _extract_image(image_output)
assert image_value is not None
assert _numel(image_value) > 0
@hardware_test(res={"cuda": "L4", "rocm": "MI325"})
@pytest.mark.parametrize("test_config", test_params)
def test_dynin_mmu_to_text(test_config: tuple[str, str]) -> None:
model, stage_config_path = test_config
_configure_dynin_config_env()
tokenizer = AutoTokenizer.from_pretrained(model, trust_remote_code=True)
prompt = _build_mmu_prompt(
tokenizer=tokenizer,
question="What is 2 + 2? Answer in one short sentence.",
dynin_config_path=DYNIN_CONFIG_PATH,
)
with OmniRunner(
model,
seed=42,
stage_configs_path=stage_config_path,
stage_init_timeout=600,
init_timeout=600,
) as runner:
outputs = runner.generate([prompt])
text_output = _find_stage_output(outputs, "text")
assert text_output is not None
text_content = _extract_text(text_output, tokenizer=tokenizer)
assert text_content
@hardware_test(res={"cuda": "L4", "rocm": "MI325"})
@pytest.mark.parametrize("test_config", test_params)
def test_dynin_image_to_text(test_config: tuple[str, str]) -> None:
model, stage_config_path = test_config
_configure_dynin_config_env()
tokenizer = AutoTokenizer.from_pretrained(model, trust_remote_code=True)
prompt = _build_mmu_multimodal_prompt(
tokenizer=tokenizer,
question="Describe the image briefly in one sentence.",
dynin_config_path=DYNIN_CONFIG_PATH,
image=_generate_synthetic_image(),
)
with OmniRunner(
model,
seed=42,
stage_configs_path=stage_config_path,
stage_init_timeout=600,
init_timeout=600,
) as runner:
outputs = runner.generate([prompt])
text_output = _find_stage_output(outputs, "text")
assert text_output is not None
text_content = _extract_text(text_output, tokenizer=tokenizer)
assert text_content
@hardware_test(res={"cuda": "L4", "rocm": "MI325"})
@pytest.mark.parametrize("test_config", test_params)
def test_dynin_speech_to_text(test_config: tuple[str, str]) -> None:
model, stage_config_path = test_config
_configure_dynin_config_env()
tokenizer = AutoTokenizer.from_pretrained(model, trust_remote_code=True)
prompt = _build_mmu_multimodal_prompt(
tokenizer=tokenizer,
question="Transcribe the audio briefly in one sentence.",
dynin_config_path=DYNIN_CONFIG_PATH,
audio=_generate_synthetic_audio(),
)
with OmniRunner(
model,
seed=42,
stage_configs_path=stage_config_path,
stage_init_timeout=600,
init_timeout=600,
) as runner:
outputs = runner.generate([prompt])
text_output = _find_stage_output(outputs, "text")
assert text_output is not None
text_content = _extract_text(text_output, tokenizer=tokenizer)
assert text_content
@hardware_test(res={"cuda": "L4", "rocm": "MI325"})
@pytest.mark.parametrize("test_config", test_params)
def test_dynin_t2s_decode_to_audio(test_config: tuple[str, str]) -> None:
model, stage_config_path = test_config
_configure_dynin_config_env()
prompt = _build_t2s_decode_prompt(dynin_config_path=DYNIN_CONFIG_PATH)
with OmniRunner(
model,
seed=42,
stage_configs_path=stage_config_path,
stage_init_timeout=600,
init_timeout=600,
) as runner:
outputs = runner.generate([prompt])
audio_output = _find_stage_output(outputs, "audio")
assert audio_output is not None
audio_value = _extract_audio(audio_output)
assert audio_value is not None
assert _numel(audio_value) > 0
@@ -0,0 +1,243 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import hashlib
import os
import subprocess
import sys
from pathlib import Path
import numpy as np
import pytest
from PIL import Image
from tests.utils import hardware_test
REPO_ROOT = Path(__file__).resolve().parents[3]
T2V_EXAMPLE = REPO_ROOT / "examples" / "offline_inference" / "text_to_video" / "text_to_video.py"
I2V_EXAMPLE = REPO_ROOT / "examples" / "offline_inference" / "image_to_video" / "image_to_video.py"
T2V_PROMPT = (
"At sunrise, a glowing paper lantern boat drifts through a narrow canal between mossy stone walls, "
"soft fog above the water, the camera slowly gliding forward as golden reflections shimmer across "
"the ripples, cinematic, realistic, highly detailed."
)
T2V_NEGATIVE_PROMPT = "worst quality, blurry, jittery motion, distorted, oversaturated, artifacts"
I2V_PROMPT = "A cinematic dolly shot of a boat drifting on calm water at sunset"
I2V_NEGATIVE_PROMPT = "worst quality, blurry, jittery motion"
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
def _get_ltx2_model() -> str:
return os.environ.get("VLLM_TEST_LTX2_MODEL", "Lightricks/LTX-2")
def _md5(path: Path) -> str:
digest = hashlib.md5(usedforsecurity=False)
with path.open("rb") as f:
for chunk in iter(lambda: f.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def _make_deterministic_test_image(path: Path) -> None:
"""Create a deterministic 256x256 test image for I2V tests."""
rng = np.random.RandomState(42)
img = Image.fromarray(rng.randint(0, 255, (256, 256, 3), dtype=np.uint8))
img.save(path)
def _run_and_check(cmd: list[str], env: dict, output_path: Path, expected_md5: str) -> None:
result = subprocess.run(cmd, cwd=REPO_ROOT, env=env, capture_output=True, text=True, check=False)
assert result.returncode == 0, (
f"Command failed (exit {result.returncode}).\nstdout:\n{result.stdout}\nstderr:\n{result.stderr}"
)
generated_md5 = _md5(output_path)
assert generated_md5 == expected_md5, (
f"Unexpected output md5: {generated_md5} != {expected_md5}.\nstdout:\n{result.stdout}\nstderr:\n{result.stderr}"
)
# ── T2V tests ──
@pytest.mark.advanced_model
@pytest.mark.diffusion
@pytest.mark.parallel
@pytest.mark.slow
@hardware_test(res={"cuda": "L4"}, num_cards=2)
def test_ltx2_t2v_cfg_parallel(tmp_path: Path):
"""T2V with CFG=4.0, cfg-parallel-size=2."""
output = tmp_path / "t2v_cfg4.mp4"
env = os.environ.copy()
env.setdefault("CUDA_VISIBLE_DEVICES", "0,1")
cmd = [
sys.executable,
str(T2V_EXAMPLE),
"--model",
_get_ltx2_model(),
"--prompt",
T2V_PROMPT,
"--negative-prompt",
T2V_NEGATIVE_PROMPT,
"--height",
"256",
"--width",
"256",
"--num-frames",
"145",
"--num-inference-steps",
"6",
"--guidance-scale",
"4.0",
"--frame-rate",
"24",
"--fps",
"24",
"--seed",
"42",
"--cfg-parallel-size",
"2",
"--enforce-eager",
"--output",
str(output),
]
_run_and_check(cmd, env, output, expected_md5="08e606b9c522fee4b6f30cee8b77db40")
@pytest.mark.advanced_model
@pytest.mark.diffusion
@pytest.mark.slow
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_ltx2_t2v_no_cfg(tmp_path: Path):
"""T2V with CFG=1.0 (no classifier-free guidance)."""
output = tmp_path / "t2v_nocfg.mp4"
env = os.environ.copy()
env.setdefault("CUDA_VISIBLE_DEVICES", "0")
cmd = [
sys.executable,
str(T2V_EXAMPLE),
"--model",
_get_ltx2_model(),
"--prompt",
T2V_PROMPT,
"--height",
"256",
"--width",
"256",
"--num-frames",
"145",
"--num-inference-steps",
"6",
"--guidance-scale",
"1.0",
"--frame-rate",
"24",
"--fps",
"24",
"--seed",
"42",
"--enforce-eager",
"--output",
str(output),
]
_run_and_check(cmd, env, output, expected_md5="a83994b94b6e67c54a524e0383c45ce8")
# ── I2V tests ──
@pytest.mark.advanced_model
@pytest.mark.diffusion
@pytest.mark.parallel
@pytest.mark.slow
@hardware_test(res={"cuda": "L4"}, num_cards=2)
def test_ltx2_i2v_cfg_parallel(tmp_path: Path):
"""I2V with CFG=4.0, cfg-parallel-size=2."""
test_image = tmp_path / "test_input.png"
_make_deterministic_test_image(test_image)
output = tmp_path / "i2v_cfg4.mp4"
env = os.environ.copy()
env.setdefault("CUDA_VISIBLE_DEVICES", "0,1")
cmd = [
sys.executable,
str(I2V_EXAMPLE),
"--model",
_get_ltx2_model(),
"--model-class-name",
"LTX2ImageToVideoPipeline",
"--image",
str(test_image),
"--prompt",
I2V_PROMPT,
"--negative-prompt",
I2V_NEGATIVE_PROMPT,
"--height",
"256",
"--width",
"256",
"--num-frames",
"73",
"--num-inference-steps",
"6",
"--guidance-scale",
"4.0",
"--frame-rate",
"24",
"--fps",
"24",
"--seed",
"42",
"--cfg-parallel-size",
"2",
"--enforce-eager",
"--output",
str(output),
]
_run_and_check(cmd, env, output, expected_md5="aed7e56084b36373244d8f839b16d115")
@pytest.mark.advanced_model
@pytest.mark.diffusion
@pytest.mark.slow
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_ltx2_i2v_no_cfg(tmp_path: Path):
"""I2V with CFG=1.0 (no classifier-free guidance)."""
test_image = tmp_path / "test_input.png"
_make_deterministic_test_image(test_image)
output = tmp_path / "i2v_nocfg.mp4"
env = os.environ.copy()
env.setdefault("CUDA_VISIBLE_DEVICES", "0")
cmd = [
sys.executable,
str(I2V_EXAMPLE),
"--model",
_get_ltx2_model(),
"--model-class-name",
"LTX2ImageToVideoPipeline",
"--image",
str(test_image),
"--prompt",
I2V_PROMPT,
"--height",
"256",
"--width",
"256",
"--num-frames",
"73",
"--num-inference-steps",
"6",
"--guidance-scale",
"1.0",
"--frame-rate",
"24",
"--fps",
"24",
"--seed",
"42",
"--enforce-eager",
"--output",
str(output),
]
_run_and_check(cmd, env, output, expected_md5="81b21ede12753e9e14a357a6c548b666")
+20 -5
View File
@@ -110,16 +110,31 @@ def test_magi_human_e2e(run_level):
assert isinstance(video_frames, np.ndarray), f"Expected numpy array, got {type(video_frames)}"
assert video_frames.ndim == 4, f"Expected 4D array (T,H,W,3), got shape {video_frames.shape}"
audio_waveform = None
if hasattr(first, "multimodal_output") and first.multimodal_output:
audio_waveform = first.multimodal_output.get("audio")
mm = first.multimodal_output
assert mm, "multimodal_output is empty or missing"
audio_waveform = mm.get("audio")
assert audio_waveform is not None, "No audio waveform in multimodal_output"
audio_sample_rate = mm.get("audio_sample_rate")
assert audio_sample_rate is not None, (
"audio_sample_rate not found in multimodal_output; model post-process must propagate it"
)
assert isinstance(audio_sample_rate, (int, float)), (
f"audio_sample_rate should be numeric, got {type(audio_sample_rate)}"
)
assert int(audio_sample_rate) > 0, f"audio_sample_rate must be positive, got {audio_sample_rate}"
fps = mm.get("fps")
assert fps is not None, "fps not found in multimodal_output; model post-process must propagate it"
assert isinstance(fps, (int, float)), f"fps should be numeric, got {type(fps)}"
assert int(fps) > 0, f"fps must be positive, got {fps}"
video_bytes = mux_video_audio_bytes(
video_frames,
audio_waveform,
fps=25.0,
audio_sample_rate=44100,
fps=float(fps),
audio_sample_rate=int(audio_sample_rate),
)
assert isinstance(video_bytes, bytes), f"Expected MP4 bytes, got {type(video_bytes)}"
assert len(video_bytes) > 1000, f"MP4 too small ({len(video_bytes)} bytes)"
@@ -0,0 +1,160 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""
Example online tests for Dynin-Omni model.
"""
import base64
import gc
import os
from io import BytesIO
from pathlib import Path
import numpy as np
import pytest
import soundfile as sf
from vllm.assets.image import ImageAsset
from tests import conftest as tests_conftest
from tests.conftest import OmniServerParams
from tests.utils import hardware_test
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
os.environ["VLLM_TEST_CLEAN_GPU_MEMORY"] = "0"
MODEL = "snu-aidas/Dynin-Omni"
STAGE_CONFIG = str(Path(__file__).parent.parent / "stage_configs" / "dynin_omni_ci.yaml")
_WHISPER_SAMPLE_RATE_HZ = 16_000
T2I_PROMPT = "A high quality detailed living room interior photo."
T2S_PROMPT = "Please read this sentence naturally: Hello from Dynin-Omni online serving."
I2I_PROMPT = "Transform this outdoor nature boardwalk scene into a painting style with vivid colors."
TEST_PARAMS = [OmniServerParams(model=MODEL, stage_config_path=STAGE_CONFIG)]
_STAGE_COUNT = 3
_I2I_STAGE_SAMPLING = {"max_tokens": 1, "temperature": 0.0, "top_p": 1.0, "detokenize": False}
def _prepare_audio_waveform_for_whisper(audio_data: np.ndarray, samplerate: int) -> np.ndarray:
"""Normalize decoded audio into a mono 16 kHz float32 waveform for Whisper."""
if samplerate <= 0:
raise ValueError(f"Invalid audio sample rate: {samplerate}")
waveform = np.asarray(audio_data, dtype=np.float32)
if waveform.ndim == 0:
raise ValueError("Audio waveform must have at least one dimension")
if waveform.ndim > 1:
waveform = np.mean(waveform, axis=1)
if waveform.size == 0:
raise ValueError("Empty audio waveform")
if samplerate != _WHISPER_SAMPLE_RATE_HZ:
target_num_samples = max(int(round(waveform.shape[0] * _WHISPER_SAMPLE_RATE_HZ / samplerate)), 1)
source_positions = np.arange(waveform.shape[0], dtype=np.float64)
target_positions = np.linspace(
0.0,
max(waveform.shape[0] - 1, 0),
num=target_num_samples,
dtype=np.float64,
)
waveform = np.interp(target_positions, source_positions, waveform).astype(np.float32)
return np.ascontiguousarray(np.clip(waveform, -1.0, 1.0), dtype=np.float32)
def _convert_audio_bytes_to_text_without_ffmpeg(raw_bytes: bytes) -> str:
"""Dynin t2s keeps Whisper transcription local to this test module and avoids ffmpeg."""
import whisper
data, samplerate = sf.read(BytesIO(raw_bytes), dtype="float32", always_2d=True)
audio_waveform = _prepare_audio_waveform_for_whisper(data, samplerate)
model = whisper.load_model("small", device="cpu")
try:
transcript = model.transcribe(
audio_waveform,
temperature=0.0,
word_timestamps=True,
condition_on_previous_text=False,
)["text"]
finally:
del model
gc.collect()
return transcript or ""
@pytest.fixture
def dynin_t2s_openai_client(openai_client, monkeypatch):
monkeypatch.setattr(
tests_conftest,
"convert_audio_bytes_to_text",
_convert_audio_bytes_to_text_without_ffmpeg,
)
return openai_client
def _build_t2i_messages(prompt: str) -> list[dict]:
return [{"role": "user", "content": [{"type": "text", "text": f"<|t2i|> {prompt}"}]}]
def _build_t2s_messages(prompt: str) -> list[dict]:
return [{"role": "user", "content": [{"type": "text", "text": f"<|t2s|> {prompt}"}]}]
def _build_i2i_messages(prompt: str) -> list[dict]:
input_image = ImageAsset("2560px-Gfp-wisconsin-madison-the-nature-boardwalk").pil_image.convert("RGB")
buffer = BytesIO()
input_image.save(buffer, format="JPEG")
image_b64 = base64.b64encode(buffer.getvalue()).decode("utf-8")
return [
{
"role": "user",
"content": [
{"type": "text", "text": f"<|i2i|> {prompt}"},
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{image_b64}"}},
],
}
]
@pytest.mark.advanced_model
@pytest.mark.omni
@hardware_test(res={"cuda": "L4", "rocm": "MI325"})
@pytest.mark.parametrize("omni_server", TEST_PARAMS, indirect=True)
def test_send_i2i_request_001(omni_server, openai_client) -> None:
request_config = {
"model": omni_server.model,
"messages": _build_i2i_messages(I2I_PROMPT),
"modalities": ["image"],
"extra_body": {
"sampling_params_list": [dict(_I2I_STAGE_SAMPLING) for _ in range(_STAGE_COUNT)],
},
}
openai_client.send_diffusion_request(request_config)
@pytest.mark.advanced_model
@pytest.mark.omni
@hardware_test(res={"cuda": "L4", "rocm": "MI325"})
@pytest.mark.parametrize("omni_server", TEST_PARAMS, indirect=True)
def test_send_t2i_request_001(omni_server, openai_client) -> None:
request_config = {
"model": omni_server.model,
"messages": _build_t2i_messages(T2I_PROMPT),
"modalities": ["image"],
}
openai_client.send_diffusion_request(request_config)
@pytest.mark.core_model
@pytest.mark.omni
@hardware_test(res={"cuda": "L4", "rocm": "MI325"})
@pytest.mark.parametrize("omni_server", TEST_PARAMS, indirect=True)
def test_send_t2s_request_001(omni_server, dynin_t2s_openai_client) -> None:
request_config = {
"model": omni_server.model,
"messages": _build_t2s_messages(T2S_PROMPT),
"modalities": ["audio"],
}
dynin_t2s_openai_client.send_omni_request(request_config)
@@ -0,0 +1,84 @@
# stage config for running dynin_omni with a 3-stage architecture.
# this config is intended for e2e smoke tests.
stage_args:
- stage_id: 0
stage_type: llm
runtime:
process: true
devices: "0"
max_batch_size: 1
engine_args:
model_stage: token2text
model_arch: DyninOmniForConditionalGeneration
worker_type: generation
scheduler_cls: vllm_omni.core.sched.omni_generation_scheduler.OmniGenerationScheduler
engine_output_type: latent
trust_remote_code: true
gpu_memory_utilization: 0.5
enforce_eager: true
enable_prefix_caching: false
async_scheduling: false
max_num_batched_tokens: 4096
is_comprehension: true
final_output: true
final_output_type: text
- stage_id: 1
stage_type: llm
runtime:
process: true
devices: "0"
max_batch_size: 1
engine_args:
model_stage: token2image
model_arch: DyninOmniForConditionalGeneration
worker_type: generation
scheduler_cls: vllm_omni.core.sched.omni_generation_scheduler.OmniGenerationScheduler
engine_output_type: latent
trust_remote_code: true
gpu_memory_utilization: 0.2
enforce_eager: true
enable_prefix_caching: false
async_scheduling: false
max_num_batched_tokens: 4096
engine_input_source: [0]
custom_process_input_func: vllm_omni.model_executor.stage_input_processors.dynin_omni.token2text_to_token2image
final_output: true
final_output_type: image
- stage_id: 2
stage_type: llm
runtime:
process: true
devices: "0"
max_batch_size: 1
engine_args:
model_stage: token2audio
model_arch: DyninOmniForConditionalGeneration
worker_type: generation
scheduler_cls: vllm_omni.core.sched.omni_generation_scheduler.OmniGenerationScheduler
engine_output_type: latent
trust_remote_code: true
gpu_memory_utilization: 0.2
enforce_eager: true
enable_prefix_caching: false
async_scheduling: false
max_num_batched_tokens: 4096
engine_input_source: [1]
custom_process_input_func: vllm_omni.model_executor.stage_input_processors.dynin_omni.token2image_to_token2audio
final_output: true
final_output_type: audio
runtime:
enabled: true
defaults:
window_size: -1
max_inflight: 1
edges:
- from: 0
to: 1
window_size: -1
- from: 1
to: 2
window_size: -1
+44
View File
@@ -0,0 +1,44 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Unit tests for cross-stage LoRA routing in the orchestrator."""
from __future__ import annotations
import pytest
from vllm.lora.request import LoRARequest
from vllm.sampling_params import SamplingParams
from vllm_omni.engine.orchestrator import build_engine_core_request_from_tokens
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
class TestBuildEngineCoreRequestLoRA:
"""Verify build_engine_core_request_from_tokens passes LoRA from params."""
def test_lora_extracted_from_diffusion_params(self):
lr = LoRARequest(lora_name="test", lora_int_id=1, lora_path="/tmp/fake")
params = OmniDiffusionSamplingParams(lora_request=lr)
# OmniDiffusionSamplingParams is not a SamplingParams, so
# build_engine_core_request_from_tokens takes the pooling path.
# We only care that lora_request is extracted via getattr.
request = build_engine_core_request_from_tokens(
request_id="req-1",
prompt={"prompt_token_ids": [1, 2, 3]},
params=params,
model_config=None,
)
assert request.lora_request is lr
def test_no_lora_on_sampling_params(self):
params = SamplingParams(max_tokens=10)
request = build_engine_core_request_from_tokens(
request_id="req-2",
prompt={"prompt_token_ids": [1, 2, 3]},
params=params,
model_config=None,
)
assert request.lora_request is None
+510
View File
@@ -0,0 +1,510 @@
from __future__ import annotations
import asyncio
import concurrent.futures
import queue
import threading
import time
from dataclasses import dataclass
from types import SimpleNamespace
from typing import Any
import janus
import pytest
from vllm.outputs import CompletionOutput, RequestOutput
from vllm.sampling_params import SamplingParams
from vllm_omni.engine.orchestrator import Orchestrator
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
from vllm_omni.outputs import OmniRequestOutput
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
@dataclass
class OrchestratorFixture:
orchestrator: Orchestrator
request_sync_q: Any
output_sync_q: Any
queues: tuple[janus.Queue, ...]
thread: threading.Thread
result_future: concurrent.futures.Future[None]
class FakeStageClient:
def __init__(
self,
*,
stage_type: str = "llm",
final_output: bool = False,
final_output_type: str = "text",
next_inputs: list[dict] | None = None,
) -> None:
self.stage_type = stage_type
self.final_output = final_output
self.final_output_type = final_output_type
self.next_inputs = list(next_inputs or [])
self.custom_process_input_func = None
self.add_request_calls: list[tuple] = []
self.abort_calls: list[list[str]] = []
self.shutdown_calls = 0
self._engine_core_outputs = queue.Queue()
self._diffusion_outputs = queue.Queue()
# Orchestrator-facing interface.
async def add_request_async(self, *args, **_kwargs) -> None:
self.add_request_calls.append(args)
async def get_output_async(self):
try:
return self._engine_core_outputs.get_nowait()
except queue.Empty:
return SimpleNamespace(outputs=[])
def get_diffusion_output_nowait(self):
try:
return self._diffusion_outputs.get_nowait()
except queue.Empty:
return None
def set_engine_outputs(self, outputs) -> None:
return None
def process_engine_inputs(self, stage_list, prompt=None):
return list(self.next_inputs)
async def abort_requests_async(self, request_ids: list[str]) -> None:
self.abort_calls.append(list(request_ids))
def shutdown(self) -> None:
self.shutdown_calls += 1
# Test helpers for seeding fake stage outputs.
def push_engine_core_outputs(self, outputs) -> None:
self._engine_core_outputs.put_nowait(outputs)
def push_diffusion_output(self, output) -> None:
self._diffusion_outputs.put_nowait(output)
class FakeOutputProcessor:
def __init__(self, *, request_outputs: list[object] | None = None) -> None:
self.request_outputs = list(request_outputs or [])
def add_request(self, *_args, **_kwargs) -> None:
return None
def process_outputs(self, *_args, **_kwargs):
return SimpleNamespace(
request_outputs=list(self.request_outputs),
reqs_to_abort=[],
)
def update_scheduler_stats(self, _scheduler_stats) -> None:
return None
def _sampling_params(max_tokens: int = 4) -> SamplingParams:
return SamplingParams(max_tokens=max_tokens)
def _engine_core_outputs(tag: str, timestamp: float) -> SimpleNamespace:
return SimpleNamespace(outputs=[tag], timestamp=timestamp, scheduler_stats=None)
def _build_request_output(
request_id: str,
*,
token_ids: list[int] | None = None,
prompt_token_ids: list[int] | None = None,
finished: bool = True,
text: str = "test",
) -> RequestOutput:
completion = CompletionOutput(
index=0,
text=text,
token_ids=list(token_ids or [1, 2]),
cumulative_logprob=0.0,
logprobs=None,
finish_reason="stop" if finished else None,
stop_reason=None,
)
return RequestOutput(
request_id=request_id,
prompt="prompt",
prompt_token_ids=list(prompt_token_ids or [10, 11]),
prompt_logprobs=None,
outputs=[completion],
finished=finished,
metrics=None,
lora_request=None,
)
def _build_harness(
stage_clients: list[object],
*,
output_processors: list[object] | None = None,
stage_vllm_configs: list[object] | None = None,
async_chunk: bool = False,
) -> OrchestratorFixture:
if output_processors is None:
output_processors = [FakeOutputProcessor() for _ in stage_clients]
if stage_vllm_configs is None:
stage_vllm_configs = [SimpleNamespace(model_config=SimpleNamespace(max_model_len=64)) for _ in stage_clients]
ready_future: concurrent.futures.Future[tuple[Orchestrator, janus.Queue, janus.Queue, janus.Queue]] = (
concurrent.futures.Future()
)
result_future: concurrent.futures.Future[None] = concurrent.futures.Future()
def _runner() -> None:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
async def _run() -> None:
request_queue = janus.Queue()
output_queue = janus.Queue()
rpc_queue = janus.Queue()
orchestrator = Orchestrator(
request_async_queue=request_queue.async_q,
output_async_queue=output_queue.async_q,
rpc_async_queue=rpc_queue.async_q,
stage_clients=stage_clients,
output_processors=output_processors,
stage_vllm_configs=stage_vllm_configs,
async_chunk=async_chunk,
)
ready_future.set_result((orchestrator, request_queue, output_queue, rpc_queue))
await orchestrator.run()
try:
loop.run_until_complete(_run())
result_future.set_result(None)
except Exception as exc:
result_future.set_exception(exc)
finally:
try:
pending = [task for task in asyncio.all_tasks(loop) if not task.done()]
for task in pending:
task.cancel()
if pending:
loop.run_until_complete(asyncio.gather(*pending, return_exceptions=True))
loop.run_until_complete(loop.shutdown_asyncgens())
finally:
asyncio.set_event_loop(None)
loop.close()
thread = threading.Thread(target=_runner, daemon=True, name="test-orchestrator")
thread.start()
orchestrator, request_queue, output_queue, rpc_queue = ready_future.result(timeout=5)
return OrchestratorFixture(
orchestrator=orchestrator,
request_sync_q=request_queue.sync_q,
output_sync_q=output_queue.sync_q,
queues=(request_queue, output_queue, rpc_queue),
thread=thread,
result_future=result_future,
)
async def _shutdown_orchestrator(orchestrator_fixture: OrchestratorFixture) -> None:
orchestrator_fixture.request_sync_q.put_nowait({"type": "shutdown"})
await asyncio.to_thread(orchestrator_fixture.thread.join, 5)
if orchestrator_fixture.thread.is_alive():
raise AssertionError("Timed out waiting for orchestrator thread shutdown")
orchestrator_fixture.result_future.result(timeout=0)
async def _wait_for(predicate, *, timeout: float = 2.0) -> None:
deadline = time.monotonic() + timeout
while not predicate():
if time.monotonic() >= deadline:
raise AssertionError("Timed out waiting for predicate")
await asyncio.sleep(0.01)
async def _get_output_message(orchestrator_fixture: OrchestratorFixture, *, timeout: float = 2.0) -> dict:
deadline = time.monotonic() + timeout
while True:
if time.monotonic() >= deadline:
raise AssertionError("Timed out waiting for orchestrator output")
try:
msg = orchestrator_fixture.output_sync_q.get_nowait()
except queue.Empty:
await asyncio.sleep(0.01)
continue
if msg.get("type") == "output":
return msg
async def _enqueue_add_request(
orchestrator_fixture: OrchestratorFixture,
*,
request_id: str,
prompt,
original_prompt,
sampling_params_list,
final_stage_id: int,
) -> None:
orchestrator_fixture.request_sync_q.put_nowait(
{
"type": "add_request",
"request_id": request_id,
"prompt": prompt,
"original_prompt": original_prompt,
"sampling_params_list": sampling_params_list,
"final_stage_id": final_stage_id,
}
)
async def _enqueue_abort_request(orchestrator_fixture: OrchestratorFixture, request_ids: list[str]) -> None:
orchestrator_fixture.request_sync_q.put_nowait(
{
"type": "abort",
"request_ids": request_ids,
}
)
@pytest.fixture
def orchestrator_factory():
fixtures: list[OrchestratorFixture] = []
def _factory(*args, **kwargs) -> OrchestratorFixture:
fixture = _build_harness(*args, **kwargs)
fixtures.append(fixture)
return fixture
yield _factory
for fixture in fixtures:
if fixture.thread.is_alive():
fixture.request_sync_q.put_nowait({"type": "shutdown"})
fixture.thread.join(timeout=5)
for q in fixture.queues:
q.close()
@pytest.mark.asyncio
async def test_run_two_stage_llm(orchestrator_factory) -> None:
stage0 = FakeStageClient(stage_type="llm", final_output=False)
stage1 = FakeStageClient(
stage_type="llm",
final_output=True,
next_inputs=[{"prompt_token_ids": [7, 8, 9]}],
)
processors = [
FakeOutputProcessor(request_outputs=[_build_request_output("req-llm", token_ids=[3, 4], finished=True)]),
FakeOutputProcessor(request_outputs=[_build_request_output("req-llm", token_ids=[10, 11], finished=True)]),
]
orchestrator_fixture = orchestrator_factory([stage0, stage1], output_processors=processors)
request = SimpleNamespace(request_id="req-llm", prompt_token_ids=[1, 2, 3])
try:
await _enqueue_add_request(
orchestrator_fixture,
request_id="req-llm",
prompt=request,
original_prompt={"prompt": "hello"},
sampling_params_list=[_sampling_params(), _sampling_params()],
final_stage_id=1,
)
await _wait_for(lambda: len(stage0.add_request_calls) == 1)
stage0.push_engine_core_outputs(_engine_core_outputs("stage0-raw", 1.0))
await _wait_for(lambda: len(stage1.add_request_calls) == 1)
stage1_request = stage1.add_request_calls[0][0]
assert stage1_request.request_id == "req-llm"
assert stage1_request.prompt_token_ids == [7, 8, 9]
stage1.push_engine_core_outputs(_engine_core_outputs("stage1-raw", 2.0))
output_msg = await _get_output_message(orchestrator_fixture)
assert output_msg["request_id"] == "req-llm"
assert output_msg["stage_id"] == 1
assert output_msg["finished"] is True
assert output_msg["engine_outputs"].request_id == "req-llm"
assert "req-llm" not in orchestrator_fixture.orchestrator.request_states
finally:
await _shutdown_orchestrator(orchestrator_fixture)
@pytest.mark.asyncio
async def test_run_single_stage_diffusion(orchestrator_factory) -> None:
stage0 = FakeStageClient(stage_type="diffusion", final_output=True, final_output_type="image")
orchestrator_fixture = orchestrator_factory([stage0])
params = OmniDiffusionSamplingParams()
try:
await _enqueue_add_request(
orchestrator_fixture,
request_id="req-diff",
prompt={"prompt": "draw a cat"},
original_prompt={"prompt": "draw a cat"},
sampling_params_list=[params],
final_stage_id=0,
)
await _wait_for(lambda: len(stage0.add_request_calls) == 1)
stage0.push_diffusion_output(
OmniRequestOutput.from_diffusion(
request_id="req-diff",
images=[],
final_output_type="image",
)
)
output_msg = await _get_output_message(orchestrator_fixture)
assert output_msg["request_id"] == "req-diff"
assert output_msg["stage_id"] == 0
assert output_msg["finished"] is True
assert output_msg["engine_outputs"].request_id == "req-diff"
assert "req-diff" not in orchestrator_fixture.orchestrator.request_states
finally:
await _shutdown_orchestrator(orchestrator_fixture)
@pytest.mark.asyncio
async def test_run_llm_to_diffusion(orchestrator_factory) -> None:
stage0 = FakeStageClient(stage_type="llm", final_output=False)
stage1 = FakeStageClient(stage_type="diffusion", final_output=True, final_output_type="image")
processors = [
FakeOutputProcessor(request_outputs=[_build_request_output("req-img", token_ids=[3, 4], finished=True)]),
FakeOutputProcessor(),
]
orchestrator_fixture = orchestrator_factory([stage0, stage1], output_processors=processors)
request = SimpleNamespace(request_id="req-img", prompt_token_ids=[1, 2, 3])
params = OmniDiffusionSamplingParams()
original_prompt = {"prompt": "draw a fox"}
try:
await _enqueue_add_request(
orchestrator_fixture,
request_id="req-img",
prompt=request,
original_prompt=original_prompt,
sampling_params_list=[_sampling_params(), params],
final_stage_id=1,
)
await _wait_for(lambda: len(stage0.add_request_calls) == 1)
stage0.push_engine_core_outputs(_engine_core_outputs("stage0-raw", 1.0))
await _wait_for(lambda: len(stage1.add_request_calls) == 1)
assert stage1.add_request_calls[0] == ("req-img", original_prompt, params)
stage1.push_diffusion_output(
OmniRequestOutput.from_diffusion(
request_id="req-img",
images=[],
final_output_type="image",
)
)
output_msg = await _get_output_message(orchestrator_fixture)
assert output_msg["request_id"] == "req-img"
assert output_msg["stage_id"] == 1
assert output_msg["finished"] is True
assert output_msg["engine_outputs"].request_id == "req-img"
assert "req-img" not in orchestrator_fixture.orchestrator.request_states
finally:
await _shutdown_orchestrator(orchestrator_fixture)
@pytest.mark.asyncio
async def test_run_async_chunk(orchestrator_factory) -> None:
stage0 = FakeStageClient(stage_type="llm", final_output=False)
stage1 = FakeStageClient(stage_type="llm", final_output=True)
processors = [
FakeOutputProcessor(request_outputs=[_build_request_output("req-async", token_ids=[1], finished=True)]),
FakeOutputProcessor(request_outputs=[_build_request_output("req-async", token_ids=[20, 21], finished=True)]),
]
orchestrator_fixture = orchestrator_factory(
[stage0, stage1],
output_processors=processors,
async_chunk=True,
)
request = SimpleNamespace(request_id="req-async", prompt_token_ids=[1, 2, 3, 4])
try:
await _enqueue_add_request(
orchestrator_fixture,
request_id="req-async",
prompt=request,
original_prompt={"prompt": "hello async"},
sampling_params_list=[_sampling_params(), _sampling_params()],
final_stage_id=1,
)
await _wait_for(lambda: len(stage1.add_request_calls) == 1)
prewarmed_request = stage1.add_request_calls[0][0]
assert prewarmed_request.request_id == "req-async"
assert prewarmed_request.prompt_token_ids
assert all(token_id == 0 for token_id in prewarmed_request.prompt_token_ids)
stage1.push_engine_core_outputs(_engine_core_outputs("stage1-final", 3.0))
output_msg = await _get_output_message(orchestrator_fixture)
assert output_msg["request_id"] == "req-async"
assert output_msg["stage_id"] == 1
assert output_msg["finished"] is True
assert "req-async" not in orchestrator_fixture.orchestrator.request_states
finally:
await _shutdown_orchestrator(orchestrator_fixture)
@pytest.mark.asyncio
async def test_run_shutdown(orchestrator_factory) -> None:
stages = [
FakeStageClient(stage_type="llm", final_output=False),
FakeStageClient(stage_type="diffusion", final_output=True, final_output_type="image"),
]
orchestrator_fixture = orchestrator_factory(stages)
await _shutdown_orchestrator(orchestrator_fixture)
assert not orchestrator_fixture.thread.is_alive()
for stage in stages:
assert stage.shutdown_calls == 1
@pytest.mark.asyncio
async def test_run_abort(orchestrator_factory) -> None:
stages = [
FakeStageClient(stage_type="llm", final_output=False),
FakeStageClient(stage_type="llm", final_output=True),
]
processors = [
FakeOutputProcessor(request_outputs=[_build_request_output("req-abort", token_ids=[1], finished=True)]),
FakeOutputProcessor(request_outputs=[_build_request_output("req-abort", token_ids=[2], finished=True)]),
]
orchestrator_fixture = orchestrator_factory(stages, output_processors=processors)
request = SimpleNamespace(request_id="req-abort", prompt_token_ids=[1, 2, 3])
try:
await _enqueue_add_request(
orchestrator_fixture,
request_id="req-abort",
prompt=request,
original_prompt={"prompt": "cancel me"},
sampling_params_list=[_sampling_params(), _sampling_params()],
final_stage_id=1,
)
await _wait_for(lambda: len(stages[0].add_request_calls) == 1)
await _enqueue_abort_request(orchestrator_fixture, ["req-abort"])
await _wait_for(lambda: all(stage.abort_calls for stage in stages))
for stage in stages:
assert stage.abort_calls == [["req-abort"]]
assert "req-abort" not in orchestrator_fixture.orchestrator.request_states
finally:
await _shutdown_orchestrator(orchestrator_fixture)
@@ -0,0 +1,207 @@
import asyncio
from types import SimpleNamespace
import pytest
from vllm import SamplingParams
from vllm_omni.engine.orchestrator import Orchestrator, OrchestratorRequestState
from vllm_omni.engine.stage_engine_core_client import StageEngineCoreClient
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
class _DummySenderStage:
def __init__(self, sender_info):
self._sender_info = sender_info
self.engine_outputs = None
def set_engine_outputs(self, outputs):
self.engine_outputs = outputs
def get_kv_sender_info(self):
return self._sender_info
class _DummyDiffusionStage:
stage_type = "diffusion"
custom_process_input_func = None
def __init__(self, engine_input_source=None):
self.engine_input_source = engine_input_source or [0]
self.calls = []
async def add_request_async(self, request_id, prompt, sampling_params, kv_sender_info=None):
self.calls.append(
{
"request_id": request_id,
"prompt": prompt,
"sampling_params": sampling_params,
"kv_sender_info": kv_sender_info,
}
)
def test_stage_engine_core_client_builds_kv_sender_info_from_tcp_address():
client = object.__new__(StageEngineCoreClient)
client.stage_id = 0
client.client_addresses = {"input_address": "tcp://10.20.30.40:1234"}
client._omni_kv_config = None
client._kv_sender_info = None
client._kv_sender_initialized = False
client._kv_sender_host = client._resolve_contact_host()
client._initialize_kv_sender_endpoint()
assert client.get_kv_sender_info() == {
"host": "10.20.30.40",
"zmq_port": 50151,
}
def test_stage_engine_core_client_falls_back_to_detected_ip_for_loopback(monkeypatch):
client = object.__new__(StageEngineCoreClient)
client.stage_id = 1
client.client_addresses = {"input_address": "tcp://127.0.0.1:1234"}
client._omni_kv_config = None
client._kv_sender_info = None
client._kv_sender_initialized = False
monkeypatch.setattr(client, "_detect_local_ip", lambda: "192.168.0.12")
client._kv_sender_host = client._resolve_contact_host()
client._initialize_kv_sender_endpoint()
assert client.get_kv_sender_info() == {
"host": "192.168.0.12",
"zmq_port": 50152,
}
def test_stage_engine_core_client_uses_connector_config_for_sender_port():
client = object.__new__(StageEngineCoreClient)
client.stage_id = 3
client.client_addresses = {"input_address": "tcp://10.20.30.40:1234"}
client._kv_sender_info = None
client._kv_sender_initialized = False
client._omni_kv_config = {
"omni_from_stage": "3",
"connector_config": {
"type": "MooncakeTransferEngineConnector",
"role": "sender",
"host": "10.20.30.99",
"zmq_port": 51000,
},
}
client._kv_sender_host = client._resolve_contact_host()
client._initialize_kv_sender_endpoint()
assert client.get_kv_sender_info() == {
"host": "10.20.30.99",
"zmq_port": 51103,
}
def test_stage_engine_core_client_preserves_explicit_loopback_sender_host():
client = object.__new__(StageEngineCoreClient)
client.stage_id = 2
client.client_addresses = {"input_address": "tcp://10.20.30.40:1234"}
client._kv_sender_info = None
client._kv_sender_initialized = False
client._omni_kv_config = {
"omni_from_stage": "2",
"connector_config": {
"type": "MooncakeTransferEngineConnector",
"role": "sender",
"host": "127.0.0.1",
"zmq_port": 51000,
},
}
client._kv_sender_host = client._resolve_contact_host()
client._initialize_kv_sender_endpoint()
assert client.get_kv_sender_info() == {
"host": "127.0.0.1",
"zmq_port": 51102,
}
def test_forward_to_diffusion_attaches_kv_sender_info():
orchestrator = object.__new__(Orchestrator)
sender_stage = _DummySenderStage({"host": "10.0.0.2", "zmq_port": 50151})
diffusion_stage = _DummyDiffusionStage(engine_input_source=[0])
orchestrator.num_stages = 2
orchestrator.stage_clients = [sender_stage, diffusion_stage]
orchestrator._companion_map = {}
orchestrator.stage_vllm_configs = [None, None]
orchestrator.output_processors = [None, None]
params = OmniDiffusionSamplingParams()
req_state = OrchestratorRequestState(
request_id="req-1",
prompt={"prompt": "hello"},
sampling_params_list=[SamplingParams(max_tokens=4), params],
final_stage_id=1,
)
output = SimpleNamespace(request_id="req-1", finished=True)
asyncio.run(Orchestrator._forward_to_next_stage(orchestrator, "req-1", 0, output, req_state))
assert sender_stage.engine_outputs == [output]
assert diffusion_stage.calls[0]["request_id"] == "req-1"
assert diffusion_stage.calls[0]["kv_sender_info"] == {
0: {"host": "10.0.0.2", "zmq_port": 50151},
}
assert req_state.stage_submit_ts[1] > 0
def test_forward_to_diffusion_uses_engine_input_source_for_kv_sender_info():
orchestrator = object.__new__(Orchestrator)
source_stage = _DummySenderStage({"host": "10.0.0.2", "zmq_port": 50151})
previous_stage = _DummySenderStage({"host": "10.0.0.9", "zmq_port": 59999})
diffusion_stage = _DummyDiffusionStage(engine_input_source=[0])
orchestrator.num_stages = 3
orchestrator.stage_clients = [source_stage, previous_stage, diffusion_stage]
orchestrator._companion_map = {}
orchestrator.stage_vllm_configs = [None, None, None]
orchestrator.output_processors = [None, None, None]
params = OmniDiffusionSamplingParams()
req_state = OrchestratorRequestState(
request_id="req-3",
prompt={"prompt": "hello"},
sampling_params_list=[SamplingParams(max_tokens=4), SamplingParams(max_tokens=4), params],
final_stage_id=2,
)
output = SimpleNamespace(request_id="req-3", finished=True)
asyncio.run(Orchestrator._forward_to_next_stage(orchestrator, "req-3", 1, output, req_state))
assert previous_stage.engine_outputs == [output]
assert diffusion_stage.calls[0]["kv_sender_info"] == {
0: {"host": "10.0.0.2", "zmq_port": 50151},
}
def test_prewarm_diffusion_attaches_kv_sender_info():
orchestrator = object.__new__(Orchestrator)
sender_stage = _DummySenderStage({"host": "10.0.0.3", "zmq_port": 50151})
diffusion_stage = _DummyDiffusionStage(engine_input_source=[0])
orchestrator.stage_clients = [sender_stage, diffusion_stage]
orchestrator.num_stages = 2
req_state = OrchestratorRequestState(
request_id="req-2",
prompt={"prompt": "hello"},
sampling_params_list=[SamplingParams(max_tokens=4), OmniDiffusionSamplingParams()],
final_stage_id=1,
)
stage0_request = SimpleNamespace(prompt_token_ids=[1, 2, 3])
asyncio.run(Orchestrator._prewarm_async_chunk_stages(orchestrator, "req-2", stage0_request, req_state))
assert diffusion_stage.calls[0]["request_id"] == "req-2"
assert diffusion_stage.calls[0]["kv_sender_info"] == {
0: {"host": "10.0.0.3", "zmq_port": 50151},
}
assert req_state.stage_submit_ts[1] > 0
+1
View File
@@ -12,6 +12,7 @@ import pytest
import torch
# ── Load modules without triggering vllm_omni.__init__ ─────────────
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
_ENGINE_DIR = Path(__file__).resolve().parents[2] / "vllm_omni" / "engine"
@@ -178,7 +178,7 @@ def test_client(mock_async_diffusion):
)
app.state.args = Namespace(
default_sampling_params='{"0": {"num_inference_steps":4, "guidance_scale":7.5}}',
max_generated_image_size=4096, # 64*64
max_generated_image_size=1024 * 1792,
)
return TestClient(app)
@@ -245,7 +245,7 @@ def async_omni_stage_configs_only_client():
# AsyncOmni exposes stage_configs on the engine instance.
app.state.args = Namespace(
default_sampling_params='{"1": {"num_inference_steps":4, "guidance_scale":7.5}}',
max_generated_image_size=4096, # 64*64
max_generated_image_size=1024 * 1792,
)
return TestClient(app)
@@ -392,6 +392,18 @@ def test_image_edits_async_omni_stage_configs_only(async_omni_stage_configs_only
assert len(captured) == 2
def test_generate_images_max_size_rejected(async_omni_test_client):
"""Test that a size exceeding max_generated_image_size returns 400."""
response = async_omni_test_client.post(
"/v1/images/generations",
json={
"prompt": "a cat",
"size": "2048x2048", # 4,194,304 pixels > max_generated_image_size (1,048,576)
},
)
assert response.status_code == 400
def test_generate_multiple_images(test_client):
"""Test generating multiple images"""
response = test_client.post(
@@ -982,12 +994,13 @@ def test_image_edit_parameter_default_single_stage(test_client):
assert captured_sampling_params.num_inference_steps == 4
assert captured_sampling_params.guidance_scale == 7.5
# Size exceeding max_generated_image_size (1024*1792) returns 400
response = test_client.post(
"/v1/images/edits",
files=[("image", img_bytes_1)],
data={
"prompt": "hello world.",
"size": "96x96",
"size": "2048x2048",
},
)
assert response.status_code == 400
@@ -385,3 +385,18 @@ class TestStreamingSpeechWebSocket:
speech_service.engine_client.abort.assert_awaited_once_with("req-abort")
assert websocket.send_json.await_count == 2
class TestGeneratePcmChunksContract:
"""Guard: _generate_pcm_chunks must exist on OmniOpenAIServingSpeech.
The WebSocket handler calls speech_service._generate_pcm_chunks()
at runtime. If the method is removed, all WS TTS streaming breaks
with an AttributeError. This test catches that at CI time.
"""
def test_generate_pcm_chunks_defined(self):
assert hasattr(OmniOpenAIServingSpeech, "_generate_pcm_chunks")
assert asyncio.iscoroutinefunction(OmniOpenAIServingSpeech._generate_pcm_chunks) or callable(
OmniOpenAIServingSpeech._generate_pcm_chunks
)
@@ -4,7 +4,7 @@ import pytest
from vllm_omni.entrypoints.openai.text_splitter import SentenceSplitter
pytestmark = [pytest.mark.openai, pytest.mark.speech]
pytestmark = [pytest.mark.openai, pytest.mark.speech, pytest.mark.core_model, pytest.mark.cpu]
class TestSentenceSplitterEnglish:
+8 -5
View File
@@ -6,8 +6,6 @@ from pytest_mock import MockerFixture
from vllm_omni.entrypoints.stage_utils import set_stage_devices
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
def _make_dummy_torch(call_log):
class _Props:
@@ -55,6 +53,8 @@ def _make_mock_platform(mocker, device_type: str = "cuda", env_var: str = "CUDA_
return mock_platform
@pytest.mark.core_model
@pytest.mark.cpu
@pytest.mark.usefixtures("clean_gpu_memory_between_tests")
def test_set_stage_devices_respects_logical_ids(mocker: MockerFixture, monkeypatch: pytest.MonkeyPatch):
# Preserve an existing logical mapping and ensure devices "0,1" map through it.
@@ -75,6 +75,8 @@ def test_set_stage_devices_respects_logical_ids(mocker: MockerFixture, monkeypat
assert os.environ["CUDA_VISIBLE_DEVICES"] == "6,7"
@pytest.mark.core_model
@pytest.mark.cpu
@pytest.mark.usefixtures("clean_gpu_memory_between_tests")
def test_set_stage_devices_handles_not_enough_devices(mocker: MockerFixture, monkeypatch: pytest.MonkeyPatch):
# Preserve an existing logical mapping and ensure devices "0,1" map through it.
@@ -90,9 +92,10 @@ def test_set_stage_devices_handles_not_enough_devices(mocker: MockerFixture, mon
mock_platform,
)
# Raise since we need 4 GPUs, but we only have 2 visible
with pytest.raises(ValueError):
set_stage_devices(stage_id=0, devices="0,1,2,3")
# Keep the logical mapping and resolve to the visible subset.
set_stage_devices(stage_id=0, devices="0,1,2,3")
assert os.environ["CUDA_VISIBLE_DEVICES"] == "6,7"
@pytest.mark.usefixtures("clean_gpu_memory_between_tests")
@@ -8,6 +8,8 @@ import pytest
import torch
import torch.nn as nn
from tests.utils import hardware_test
class TestPreLookaheadLayer:
"""Tests for PreLookaheadLayer."""
@@ -18,6 +20,8 @@ class TestPreLookaheadLayer:
return PreLookaheadLayer(in_channels=512, channels=512, pre_lookahead_len=3)
@pytest.mark.core_model
@pytest.mark.cpu
def test_forward_shape(self, layer):
"""Test that output shape matches input shape."""
batch, seq_len, channels = 2, 10, 512
@@ -27,6 +31,8 @@ class TestPreLookaheadLayer:
assert out.shape == x.shape
@pytest.mark.core_model
@pytest.mark.cpu
def test_forward_with_context(self, layer):
"""Test forward with context for streaming."""
batch, seq_len, channels = 1, 10, 512
@@ -38,6 +44,8 @@ class TestPreLookaheadLayer:
assert out.shape == x.shape
@pytest.mark.core_model
@pytest.mark.cpu
def test_residual_connection(self, layer):
"""Test that residual connection is applied."""
batch, seq_len, channels = 1, 5, 512
@@ -59,6 +67,8 @@ class TestDiTAttention:
return DiTAttention(dim=512, heads=8, dim_head=64, dropout=0.0)
@pytest.mark.core_model
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_forward_shape(self, attention):
"""Test attention output shape."""
batch, seq_len, dim = 2, 16, 512
@@ -68,6 +78,8 @@ class TestDiTAttention:
assert out.shape == x.shape
@pytest.mark.core_model
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_forward_with_mask(self, attention):
"""Test attention with mask."""
batch, seq_len, dim = 2, 16, 512
@@ -81,6 +93,8 @@ class TestDiTAttention:
# Masked positions should be zero
assert torch.allclose(out[:, -3:], torch.zeros_like(out[:, -3:]))
@pytest.mark.core_model
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_qkv_projections(self, attention):
"""Test that Q/K/V projections exist and have correct dimensions."""
assert hasattr(attention, "to_q")
@@ -100,6 +114,8 @@ class TestDiTBlock:
return DiTBlock(dim=512, heads=8, dim_head=64, ff_mult=4, dropout=0.0)
@pytest.mark.core_model
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_forward_shape(self, block):
"""Test block output shape."""
batch, seq_len, dim = 2, 16, 512
@@ -110,6 +126,8 @@ class TestDiTBlock:
assert out.shape == x.shape
@pytest.mark.core_model
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_adalayernorm_modulation(self, block):
"""Test that AdaLayerNorm modulates based on timestep."""
batch, seq_len, dim = 1, 8, 512
@@ -144,6 +162,8 @@ class TestDiT:
long_skip_connection=True,
)
@pytest.mark.core_model
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_forward_shape(self, dit):
"""Test DiT forward output shape."""
batch, mel_dim, seq_len = 1, 80, 32
@@ -158,6 +178,8 @@ class TestDiT:
assert out.shape == (batch, mel_dim, seq_len)
@pytest.mark.core_model
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_timestep_embedding(self, dit):
"""Test that different timesteps produce different outputs."""
batch, mel_dim, seq_len = 1, 80, 16
@@ -190,6 +212,8 @@ class TestCFM:
return DummyEstimator()
@pytest.mark.core_model
@pytest.mark.cpu
def test_causal_conditional_cfm_forward(self, dummy_estimator):
"""Test CausalConditionalCFM forward pass."""
from omegaconf import DictConfig
@@ -228,6 +252,8 @@ class TestCFM:
class TestSDPAFallback:
"""Test SDPA fallback for float32 inputs."""
@pytest.mark.core_model
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_float32_uses_sdpa(self):
"""Test that float32 inputs use SDPA fallback."""
from vllm_omni.diffusion.attention.layer import Attention
@@ -5,6 +5,8 @@
import pytest
import torch
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
class TestMakePadMask:
"""Tests for make_pad_mask utility."""
@@ -7,6 +7,7 @@ Regression tests for https://github.com/vllm-project/vllm-omni/issues/1862
from collections.abc import Mapping
import pytest
import torch
from vllm_omni.config.stage_config import StageConfigFactory
@@ -15,6 +16,8 @@ from vllm_omni.diffusion.data import (
OmniDiffusionConfig,
)
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
def _roundtrip_diffusion_config(**kwargs) -> OmniDiffusionConfig:
"""Simulate the real path: create_default_diffusion → OmniDiffusionConfig.
-185
View File
@@ -1,185 +0,0 @@
"""Tests for CUDAGraphWrapper.__getattr__ performance optimization.
This module tests that the patched CUDAGraphWrapper avoids expensive __repr__
calls when hasattr() is used for non-existent attributes. The original vLLM
implementation includes {self.runnable} in the AttributeError message, which
triggers model tree traversal and can take ~6ms on large models.
"""
import time
import pytest
import torch
import torch.nn as nn
from vllm_omni.worker.gpu_model_runner import CUDAGraphWrapper
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
class SlowReprModel(nn.Module):
"""A mock model with artificially slow __repr__ to detect unwanted calls."""
def __init__(self, repr_delay_ms: float = 10.0):
super().__init__()
self.linear = nn.Linear(16, 16)
self.repr_delay_ms = repr_delay_ms
self.repr_call_count = 0
def forward(self, x):
return self.linear(x)
def __repr__(self):
self.repr_call_count += 1
# Simulate expensive repr by sleeping
time.sleep(self.repr_delay_ms / 1000.0)
return f"SlowReprModel(delay={self.repr_delay_ms}ms)"
class MockCUDAGraphWrapper:
"""A minimal mock that mimics CUDAGraphWrapper structure for CPU testing."""
def __init__(self, runnable):
# Store in __dict__ directly to avoid triggering __getattr__
object.__setattr__(self, "runnable", runnable)
def __getattr__(self, key: str):
# This is the optimized implementation we're testing
runnable = object.__getattribute__(self, "runnable")
if hasattr(runnable, key):
return getattr(runnable, key)
# Key optimization: DO NOT include {self.runnable} in error message
# as it triggers expensive __repr__ on large models
raise AttributeError(f"Attribute {key} not exists in the runnable of cudagraph wrapper")
def test_hasattr_nonexistent_does_not_trigger_repr():
"""Verify that hasattr for non-existent attributes doesn't call __repr__."""
model = SlowReprModel(repr_delay_ms=100.0) # Very slow repr
wrapper = MockCUDAGraphWrapper(model)
# Reset counter
model.repr_call_count = 0
# Call hasattr for non-existent attribute multiple times
for _ in range(10):
result = hasattr(wrapper, "nonexistent_attribute_xyz")
assert result is False
# __repr__ should never have been called
assert model.repr_call_count == 0, (
f"__repr__ was called {model.repr_call_count} times when checking "
"for non-existent attributes. This indicates the AttributeError "
"message contains {self.runnable} which triggers expensive repr."
)
def test_hasattr_nonexistent_is_fast():
"""Verify that hasattr for non-existent attributes is fast (<1ms per call)."""
model = SlowReprModel(repr_delay_ms=100.0)
wrapper = MockCUDAGraphWrapper(model)
num_iterations = 100
start = time.perf_counter()
for _ in range(num_iterations):
hasattr(wrapper, "nonexistent_attribute_xyz")
elapsed_ms = (time.perf_counter() - start) * 1000
avg_ms = elapsed_ms / num_iterations
# If __repr__ were being called, each would take ~100ms
# We expect <1ms per call with the fix
assert avg_ms < 1.0, (
f"hasattr for non-existent attribute took {avg_ms:.2f}ms on average. "
"Expected <1ms. This suggests __repr__ is being triggered."
)
def test_hasattr_existing_attribute_works():
"""Verify that hasattr for existing attributes returns True and works correctly."""
model = SlowReprModel()
wrapper = MockCUDAGraphWrapper(model)
# 'forward' exists on nn.Module
assert hasattr(wrapper, "forward") is True
# 'linear' exists on our model
assert hasattr(wrapper, "linear") is True
# Can actually access the attribute
linear = wrapper.linear
assert isinstance(linear, nn.Linear)
def test_getattr_existing_attribute_returns_value():
"""Verify that getattr for existing attributes returns the correct value."""
model = SlowReprModel()
wrapper = MockCUDAGraphWrapper(model)
# Access forward method
forward_method = wrapper.forward
assert callable(forward_method)
# Access linear layer
linear = wrapper.linear
assert isinstance(linear, nn.Linear)
assert linear.in_features == 16
assert linear.out_features == 16
def test_getattr_nonexistent_raises_attribute_error():
"""Verify that getattr for non-existent attributes raises AttributeError."""
model = SlowReprModel()
wrapper = MockCUDAGraphWrapper(model)
with pytest.raises(AttributeError) as exc_info:
_ = wrapper.nonexistent_attribute
# Verify error message format (should NOT contain model repr)
error_msg = str(exc_info.value)
assert "nonexistent_attribute" in error_msg
assert "cudagraph wrapper" in error_msg
# Should NOT contain the slow repr output
assert "SlowReprModel(delay=" not in error_msg
def test_attribute_error_message_does_not_contain_runnable_repr():
"""Explicitly verify the error message doesn't trigger runnable repr."""
model = SlowReprModel(repr_delay_ms=100.0)
wrapper = MockCUDAGraphWrapper(model)
model.repr_call_count = 0
try:
_ = wrapper.nonexistent_attr
except AttributeError:
pass
# __repr__ should not have been called during error construction
assert model.repr_call_count == 0, (
"AttributeError message construction triggered __repr__. The error message should not include {self.runnable}."
)
@pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA not available")
def test_real_cudagraph_wrapper_hasattr_performance():
"""Test the actual CUDAGraphWrapper from vllm_omni (requires CUDA)."""
from vllm.config import CUDAGraphMode
model = SlowReprModel(repr_delay_ms=50.0).cuda()
model.repr_call_count = 0
# Create actual CUDAGraphWrapper
try:
wrapper = CUDAGraphWrapper(model, runtime_mode=CUDAGraphMode.NONE)
except Exception:
pytest.skip("Could not create CUDAGraphWrapper")
# Test hasattr performance
num_iterations = 50
start = time.perf_counter()
for _ in range(num_iterations):
hasattr(wrapper, "nonexistent_xyz")
elapsed_ms = (time.perf_counter() - start) * 1000
avg_ms = elapsed_ms / num_iterations
assert avg_ms < 1.0, f"Real CUDAGraphWrapper hasattr took {avg_ms:.2f}ms avg. Expected <1ms with the optimization."
assert model.repr_call_count == 0, f"__repr__ called {model.repr_call_count} times"
+17 -1
View File
@@ -24,11 +24,27 @@ except ModuleNotFoundError as exc: # pragma: no cover - optional dependency
from vllm_omni.transformers_utils import configs as _configs # noqa: F401, E402
from .config import OmniModelConfig
from .entrypoints import AsyncOmni, Omni
from .version import __version__, __version_tuple__ # isort:skip
def __getattr__(name: str):
# Lazy import for AsyncOmni and Omni to avoid pulling in heavy
# dependencies (vllm model_loader → fused_moe → pynvml) at package
# import time. This prevents crashes in lightweight subprocesses
# (e.g. model-architecture inspection) that lack a CUDA context.
# See: https://github.com/vllm-project/vllm-omni/issues/1793
if name == "AsyncOmni":
from .entrypoints.async_omni import AsyncOmni
return AsyncOmni
if name == "Omni":
from .entrypoints.omni import Omni
return Omni
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
__all__ = [
"__version__",
"__version_tuple__",
+1 -1
View File
@@ -185,7 +185,7 @@ def calculate_metrics(
# Note : this may inflate the output token count slightly
output_len = len(tokenizer(outputs[i].generated_text, add_special_tokens=False).input_ids)
actual_output_lens.append(output_len)
total_input += input_requests[i].prompt_len
total_input += outputs[i].prompt_len
tpot = 0
if output_len > 1:
latency_minus_ttft = outputs[i].text_latency - outputs[i].ttft
+4
View File
@@ -190,6 +190,10 @@ async def async_request_openai_chat_omni_completions(
if metrics := data.get("metrics"):
output.output_tokens = metrics.get("num_tokens_out", 0)
if usage := data.get("usage"):
if (pt := usage.get("prompt_tokens")) is not None:
output.prompt_len = pt
output.latency = timestamp - st
output.generated_text = generated_text
if generated_audio is not None:
@@ -209,3 +209,13 @@ class FlashAttentionImpl(AttentionImpl):
layout="BNSD",
)
return output
def forward_musa(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attn_metadata: AttentionMetadata = None,
) -> torch.Tensor:
# XXX (MUSA): MUSA uses the same implementation as XPU (mate only provides flash_attn_varlen_func)
return self.forward_xpu(query, key, value, attn_metadata)
@@ -12,6 +12,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# Adapted from https://github.com/huggingface/transformers/blob/main/src/transformers/modeling_flash_attention_utils.py
from functools import lru_cache
import torch
import torch.nn.functional as F
@@ -38,8 +40,10 @@ elif current_omni_platform.is_xpu():
except (ImportError, ModuleNotFoundError):
pass
elif current_omni_platform.is_musa():
# XXX (MUSA): Add MUSA-specific Flash Attention when available
pass
try:
from mate import flash_attn_varlen_func # noqa: F401
except (ImportError, ModuleNotFoundError):
pass
else:
# CUDA: try FA3 -> FA2 fallback chain
# Try FA3 from fa3-fwd PyPI package
@@ -76,6 +80,12 @@ else:
HAS_FLASH_ATTN = flash_attn_func is not None or flash_attn_varlen_func is not None
@lru_cache(maxsize=1)
def is_mate_available() -> bool:
"""Check if MATE (MUSA Flash Attention) is available."""
return current_omni_platform.is_musa() and flash_attn_varlen_func is not None
def _index_first_axis(tensor, indices):
"""
A local implementation of the PyTorch indexing operation `tensor[indices]` on the first axis,
-9
View File
@@ -48,16 +48,7 @@ def enable_bagel_teacache(pipeline: Any, config: DiffusionCacheConfig) -> None:
coefficients=config.coefficients,
)
transformer = pipeline.bagel
original_forward_flow = transformer._forward_flow
import types
def forward_alias(self, *args, **kwargs):
return original_forward_flow(*args, **kwargs)
transformer.forward = types.MethodType(forward_alias, transformer)
apply_teacache_hook(transformer, teacache_config)
transformer._forward_flow = transformer.forward
pipeline.transformer = transformer
logger.info(
@@ -1,7 +1,6 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import types
from typing import Any
import numpy as np
@@ -74,15 +73,8 @@ class BagelAdapter:
@staticmethod
def install_hook(transformer: Any, hook: DataCollectionHook) -> None:
original_forward_flow = transformer._forward_flow
def forward_alias(self, *args, **kwargs):
return original_forward_flow(*args, **kwargs)
transformer.forward = types.MethodType(forward_alias, transformer)
registry = HookRegistry.get_or_create(transformer)
registry.register_hook(hook._HOOK_NAME, hook)
transformer._forward_flow = transformer.forward
class StableAudioAdapter:
+7 -4
View File
@@ -9,6 +9,7 @@ from dataclasses import dataclass, field, fields
from typing import TYPE_CHECKING, Any
import torch
from PIL import Image
from pydantic import model_validator
from typing_extensions import Self
from vllm.config.utils import config
@@ -701,10 +702,12 @@ class DiffusionOutput:
Final output (after pipeline completion)
"""
output: torch.Tensor | None = None
trajectory_timesteps: list[torch.Tensor] | None = None
trajectory_latents: torch.Tensor | None = None
trajectory_decoded: list[torch.Tensor] | None = None
# Fields may be replaced with SHM handle dicts by ipc.pack_diffusion_output_shm
output: torch.Tensor | dict | None = None
trajectory_timesteps: torch.Tensor | dict | None = None
trajectory_latents: torch.Tensor | dict | None = None
trajectory_log_probs: torch.Tensor | dict | None = None
trajectory_decoded: list[Image.Image] | None = None
error: str | None = None
aborted: bool = False
abort_message: str | None = None
+28
View File
@@ -145,8 +145,12 @@ class DiffusionEngine:
postprocess_start_time = time.perf_counter()
outputs = self.post_process_func(output_data) if self.post_process_func is not None else output_data
audio_payload = None
model_audio_sample_rate = None
model_fps = None
if isinstance(outputs, dict):
audio_payload = outputs.get("audio")
model_audio_sample_rate = outputs.get("audio_sample_rate")
model_fps = outputs.get("fps")
outputs = outputs.get("video", outputs)
postprocess_time = time.perf_counter() - postprocess_start_time
logger.info(f"Post-processing completed in {postprocess_time:.4f} seconds")
@@ -192,6 +196,10 @@ class DiffusionEngine:
prompt=prompt,
metrics=metrics,
latents=output.trajectory_latents,
trajectory_latents=output.trajectory_latents,
trajectory_timesteps=output.trajectory_timesteps,
trajectory_log_probs=output.trajectory_log_probs,
trajectory_decoded=output.trajectory_decoded,
multimodal_output={"audio": request_audio_payload},
final_output_type="audio",
stage_durations=output.stage_durations,
@@ -202,6 +210,10 @@ class DiffusionEngine:
mm_output = {}
if audio_payload is not None:
mm_output["audio"] = audio_payload
if model_audio_sample_rate is not None:
mm_output["audio_sample_rate"] = model_audio_sample_rate
if model_fps is not None:
mm_output["fps"] = model_fps
return [
OmniRequestOutput.from_diffusion(
request_id=request_id,
@@ -209,6 +221,10 @@ class DiffusionEngine:
prompt=prompt,
metrics=metrics,
latents=output.trajectory_latents,
trajectory_latents=output.trajectory_latents,
trajectory_timesteps=output.trajectory_timesteps,
trajectory_log_probs=output.trajectory_log_probs,
trajectory_decoded=output.trajectory_decoded,
custom_output=output.custom_output or {},
multimodal_output=mm_output,
stage_durations=output.stage_durations,
@@ -240,6 +256,10 @@ class DiffusionEngine:
prompt=prompt,
metrics=metrics,
latents=output.trajectory_latents,
trajectory_latents=output.trajectory_latents,
trajectory_timesteps=output.trajectory_timesteps,
trajectory_log_probs=output.trajectory_log_probs,
trajectory_decoded=output.trajectory_decoded,
multimodal_output={"audio": request_audio_payload},
final_output_type="audio",
stage_durations=output.stage_durations,
@@ -260,6 +280,10 @@ class DiffusionEngine:
if num_outputs == 1:
sliced_audio = sliced_audio[0]
mm_output["audio"] = sliced_audio
if model_audio_sample_rate is not None:
mm_output["audio_sample_rate"] = model_audio_sample_rate
if model_fps is not None:
mm_output["fps"] = model_fps
results.append(
OmniRequestOutput.from_diffusion(
request_id=request_id,
@@ -267,6 +291,10 @@ class DiffusionEngine:
prompt=prompt,
metrics=metrics,
latents=output.trajectory_latents,
trajectory_latents=output.trajectory_latents,
trajectory_timesteps=output.trajectory_timesteps,
trajectory_log_probs=output.trajectory_log_probs,
trajectory_decoded=output.trajectory_decoded,
custom_output=output.custom_output or {},
multimodal_output=mm_output,
stage_durations=output.stage_durations,
@@ -1,6 +1,7 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from contextlib import nullcontext
from typing import Any
import torch
@@ -15,11 +16,38 @@ from vllm_omni.diffusion.distributed.autoencoders.distributed_vae_executor impor
GridSpec,
TileTask,
)
from vllm_omni.platforms import current_omni_platform
logger = init_logger(__name__)
class DistributedAutoencoderKLWan(AutoencoderKLWan, DistributedVaeMixin):
class OmniAutoencoderKLWan(AutoencoderKLWan):
def _execution_context(self):
try:
first_param = next(self.parameters())
except StopIteration:
return nullcontext()
dtype = first_param.dtype
if dtype not in (torch.float16, torch.bfloat16):
return nullcontext()
return current_omni_platform.create_autocast_context(
device_type=first_param.device.type,
dtype=dtype,
enabled=True,
)
def encode(self, x: torch.Tensor, return_dict: bool = True):
with self._execution_context():
return super().encode(x, return_dict=return_dict)
def decode(self, z: torch.Tensor, return_dict: bool = True):
with self._execution_context():
return super().decode(z, return_dict=return_dict)
class DistributedAutoencoderKLWan(OmniAutoencoderKLWan, DistributedVaeMixin):
@classmethod
def from_pretrained(cls, *args: Any, **kwargs: Any):
model = super().from_pretrained(*args, **kwargs)
@@ -84,11 +112,12 @@ class DistributedAutoencoderKLWan(AutoencoderKLWan, DistributedVaeMixin):
"""Decode a single latent tile into RGB space."""
self.clear_cache()
time = []
for k in range(len(task.tensor)):
self._conv_idx = [0]
tile = self.post_quant_conv(task.tensor[k])
decoded = self.decoder(tile, feat_cache=self._feat_map, feat_idx=self._conv_idx, first_chunk=(k == 0))
time.append(decoded)
with self._execution_context():
for k in range(len(task.tensor)):
self._conv_idx = [0]
tile = self.post_quant_conv(task.tensor[k])
decoded = self.decoder(tile, feat_cache=self._feat_map, feat_idx=self._conv_idx, first_chunk=(k == 0))
time.append(decoded)
result = torch.cat(time, dim=2)
return result
+5
View File
@@ -7,6 +7,7 @@ from typing import TYPE_CHECKING, Any
from vllm.logger import init_logger
from vllm_omni.diffusion.attention.backends.utils.fa import is_mate_available
from vllm_omni.platforms import current_omni_platform
if TYPE_CHECKING:
@@ -52,6 +53,10 @@ class PackagesEnvChecker:
"""Check if flash attention is available and compatible."""
platform = current_omni_platform
# MUSA uses MATE for flash attention
if platform.is_musa():
return is_mate_available()
# Flash attention requires CUDA-like platforms (CUDA or ROCm)
if not platform.is_cuda_alike():
return False
+24 -8
View File
@@ -78,13 +78,29 @@ def _tensor_from_shm(handle: dict[str, Any]) -> torch.Tensor:
return tensor
def _pack_tensor_if_large(val: torch.Tensor) -> torch.Tensor | dict:
"""Replace a tensor with an SHM handle if it exceeds the threshold."""
if val.nelement() * val.element_size() > _SHM_TENSOR_THRESHOLD:
return _tensor_to_shm(val)
return val
def _unpack_if_shm_handle(val: object) -> object:
"""Reconstruct a tensor from an SHM handle dict, or return as-is."""
if isinstance(val, dict) and val.get("__tensor_shm__"):
return _tensor_from_shm(val)
return val
def _pack_diffusion_fields(output: DiffusionOutput) -> DiffusionOutput:
if output.output is not None and isinstance(output.output, torch.Tensor):
if output.output.nelement() * output.output.element_size() > _SHM_TENSOR_THRESHOLD:
output.output = _tensor_to_shm(output.output)
output.output = _pack_tensor_if_large(output.output)
if output.trajectory_latents is not None and isinstance(output.trajectory_latents, torch.Tensor):
if output.trajectory_latents.nelement() * output.trajectory_latents.element_size() > _SHM_TENSOR_THRESHOLD:
output.trajectory_latents = _tensor_to_shm(output.trajectory_latents)
output.trajectory_latents = _pack_tensor_if_large(output.trajectory_latents)
if output.trajectory_timesteps is not None and isinstance(output.trajectory_timesteps, torch.Tensor):
output.trajectory_timesteps = _pack_tensor_if_large(output.trajectory_timesteps)
if output.trajectory_log_probs is not None and isinstance(output.trajectory_log_probs, torch.Tensor):
output.trajectory_log_probs = _pack_tensor_if_large(output.trajectory_log_probs)
return output
@@ -104,10 +120,10 @@ def pack_diffusion_output_shm(output: object) -> object:
def _unpack_diffusion_fields(output: DiffusionOutput) -> DiffusionOutput:
if isinstance(output.output, dict) and output.output.get("__tensor_shm__"):
output.output = _tensor_from_shm(output.output)
if isinstance(output.trajectory_latents, dict) and output.trajectory_latents.get("__tensor_shm__"):
output.trajectory_latents = _tensor_from_shm(output.trajectory_latents)
output.output = _unpack_if_shm_handle(output.output)
output.trajectory_latents = _unpack_if_shm_handle(output.trajectory_latents)
output.trajectory_timesteps = _unpack_if_shm_handle(output.trajectory_timesteps)
output.trajectory_log_probs = _unpack_if_shm_handle(output.trajectory_log_probs)
return output
+8
View File
@@ -145,6 +145,14 @@ class RotaryEmbedding(CustomOp):
) -> torch.Tensor:
return self.forward_native(x, cos, sin)
def forward_musa(
self,
x: torch.Tensor,
cos: torch.Tensor,
sin: torch.Tensor,
) -> torch.Tensor:
return self.forward_native(x, cos, sin)
def forward_native(
self,
x: torch.Tensor,
@@ -1655,6 +1655,9 @@ class Bagel(nn.Module):
cfg_img_past_key_values: NaiveCache | None = None,
cfg_img_key_values_lens: torch.IntTensor | None = None,
cfg_img_packed_key_value_indexes: torch.LongTensor | None = None,
return_trajectory_latents: bool = False,
scheduler: object | None = None,
scheduler_kwargs: dict | None = None,
):
x_t = packed_init_noises
@@ -1663,6 +1666,14 @@ class Bagel(nn.Module):
dts = timesteps[:-1] - timesteps[1:]
timesteps = timesteps[:-1]
# Optional trajectory recording for RL rollout data collection
trajectory_latents: list[torch.Tensor] | None = [] if return_trajectory_latents else None
trajectory_timesteps: list[torch.Tensor] | None = [] if return_trajectory_latents else None
trajectory_log_probs: list[torch.Tensor] | None = (
[] if (return_trajectory_latents and scheduler is not None) else None
)
_sched_kw = scheduler_kwargs or {}
use_cfg_text = cfg_text_scale > 1.0
use_cfg_img = cfg_img_scale > 1.0
@@ -1699,6 +1710,9 @@ class Bagel(nn.Module):
cfg_img_past_key_values=cfg_img_past_key_values,
cfg_img_key_values_lens=cfg_img_key_values_lens,
cfg_img_packed_key_value_indexes=cfg_img_packed_key_value_indexes,
return_trajectory_latents=return_trajectory_latents,
scheduler=scheduler,
scheduler_kwargs=scheduler_kwargs,
)
# ── SP + CFG: sequential single-branch forwards ──
@@ -1720,7 +1734,7 @@ class Bagel(nn.Module):
packed_seqlens=packed_seqlens,
)
v_t = self._forward_flow_single_branch(
v_t = self.forward_single_branch(
**common,
packed_indexes=packed_indexes,
packed_position_ids=packed_position_ids,
@@ -1730,7 +1744,7 @@ class Bagel(nn.Module):
)
if cfg_text_scale_ > 1.0:
cfg_text_v_t = self._forward_flow_single_branch(
cfg_text_v_t = self.forward_single_branch(
**common,
packed_indexes=cfg_text_packed_query_indexes,
packed_position_ids=cfg_text_packed_position_ids,
@@ -1740,7 +1754,7 @@ class Bagel(nn.Module):
)
cfg_img_v_t = None
if cfg_img_scale_ > 1.0:
cfg_img_v_t = self._forward_flow_single_branch(
cfg_img_v_t = self.forward_single_branch(
**common,
packed_indexes=cfg_img_packed_query_indexes,
packed_position_ids=cfg_img_packed_position_ids,
@@ -1758,16 +1772,25 @@ class Bagel(nn.Module):
cfg_renorm_min,
)
x_t = x_t - v_t.to(x_t.device) * dts[i]
if scheduler is not None:
out = scheduler.step(v_t.to(x_t.device), timesteps[i], x_t, dts[i], **_sched_kw)
x_t = out.prev_sample
if trajectory_log_probs is not None and out.log_prob is not None:
trajectory_log_probs.append(out.log_prob)
else:
x_t = x_t - v_t.to(x_t.device) * dts[i]
if return_trajectory_latents:
trajectory_latents.append(x_t.clone())
trajectory_timesteps.append(timesteps[i] - dts[i])
unpacked_latent = x_t.split((packed_seqlens - 2).tolist())
return unpacked_latent
return unpacked_latent, trajectory_latents, trajectory_timesteps, trajectory_log_probs
# ── SP without CFG: direct single-branch loop ──
if use_sp:
for i, t in enumerate(timesteps):
timestep = torch.tensor([t] * x_t.shape[0], device=x_t.device)
v_t = self._forward_flow_single_branch(
v_t = self.forward_single_branch(
x_t=x_t,
timestep=timestep,
packed_vae_token_indexes=packed_vae_token_indexes,
@@ -1781,10 +1804,20 @@ class Bagel(nn.Module):
past_key_values=past_key_values,
packed_key_value_indexes=packed_key_value_indexes,
)
x_t = x_t - v_t.to(x_t.device) * dts[i]
if scheduler is not None:
out = scheduler.step(v_t.to(x_t.device), timesteps[i], x_t, dts[i], **_sched_kw)
x_t = out.prev_sample
out_log_prob = getattr(out, "log_prob", None)
if trajectory_log_probs is not None and out_log_prob is not None:
trajectory_log_probs.append(out_log_prob)
else:
x_t = x_t - v_t.to(x_t.device) * dts[i]
if return_trajectory_latents:
trajectory_latents.append(x_t.clone())
trajectory_timesteps.append(timesteps[i] - dts[i])
unpacked_latent = x_t.split((packed_seqlens - 2).tolist())
return unpacked_latent
return unpacked_latent, trajectory_latents, trajectory_timesteps, trajectory_log_probs
# ── Batched CFG mode (cfg_parallel_size=1, no SP) ──
cfg_batched = None
@@ -1850,7 +1883,7 @@ class Bagel(nn.Module):
else:
cfg_text_scale_ = 1.0
cfg_img_scale_ = 1.0
v_t = self._forward_flow(
v_t = self.forward(
x_t=x_t,
timestep=timestep,
packed_vae_token_indexes=packed_vae_token_indexes,
@@ -1870,10 +1903,19 @@ class Bagel(nn.Module):
cfg_batched=cfg_batched,
)
x_t = x_t - v_t.to(x_t.device) * dts[i] # velocity pointing from data to noise
if scheduler is not None:
out = scheduler.step(v_t.to(x_t.device), timesteps[i], x_t, dts[i], **_sched_kw)
x_t = out.prev_sample
if trajectory_log_probs is not None and out.log_prob is not None:
trajectory_log_probs.append(out.log_prob)
else:
x_t = x_t - v_t.to(x_t.device) * dts[i] # velocity pointing from data to noise
if return_trajectory_latents:
trajectory_latents.append(x_t.clone())
trajectory_timesteps.append(timesteps[i] - dts[i])
unpacked_latent = x_t.split((packed_seqlens - 2).tolist())
return unpacked_latent
return unpacked_latent, trajectory_latents, trajectory_timesteps, trajectory_log_probs
def _generate_image_parallel(
self,
@@ -1905,6 +1947,9 @@ class Bagel(nn.Module):
cfg_img_past_key_values: NaiveCache | None,
cfg_img_key_values_lens: torch.IntTensor | None,
cfg_img_packed_key_value_indexes: torch.LongTensor | None,
return_trajectory_latents: bool = False,
scheduler: object | None = None,
scheduler_kwargs: dict | None = None,
):
"""CFG parallel denoising loop: each rank computes one CFG branch.
@@ -1961,13 +2006,20 @@ class Bagel(nn.Module):
else:
raise RuntimeError(f"Unexpected cfg_rank={cfg_rank} for Bagel 3-branch CFG parallel")
trajectory_latents: list[torch.Tensor] | None = [] if return_trajectory_latents else None
trajectory_timesteps: list[torch.Tensor] | None = [] if return_trajectory_latents else None
trajectory_log_probs: list[torch.Tensor] | None = (
[] if (return_trajectory_latents and scheduler is not None) else None
)
_sched_kw = scheduler_kwargs or {}
for i, t in enumerate(timesteps):
timestep = torch.tensor([t] * x_t.shape[0], device=x_t.device)
use_cfg_this_step = t > cfg_interval[0] and t <= cfg_interval[1] and cfg_text_scale > 1.0
if use_cfg_this_step:
# CFG interval: each rank computes its own branch
local_v_t = self._forward_flow_single_branch(
local_v_t = self.forward_single_branch(
x_t=x_t,
timestep=timestep,
packed_vae_token_indexes=packed_vae_token_indexes,
@@ -1994,7 +2046,7 @@ class Bagel(nn.Module):
)
else:
# Outside CFG interval: all ranks compute with gen inputs, no comm
v_t = self._forward_flow_single_branch(
v_t = self.forward_single_branch(
x_t=x_t,
timestep=timestep,
packed_vae_token_indexes=packed_vae_token_indexes,
@@ -2009,10 +2061,19 @@ class Bagel(nn.Module):
packed_key_value_indexes=packed_key_value_indexes,
)
x_t = x_t - v_t.to(x_t.device) * dts[i]
if scheduler is not None:
out = scheduler.step(v_t.to(x_t.device), timesteps[i], x_t, dts[i], **_sched_kw)
x_t = out.prev_sample
if trajectory_log_probs is not None and out.log_prob is not None:
trajectory_log_probs.append(out.log_prob)
else:
x_t = x_t - v_t.to(x_t.device) * dts[i]
if return_trajectory_latents:
trajectory_latents.append(x_t.clone())
trajectory_timesteps.append(timesteps[i] - dts[i])
unpacked_latent = x_t.split((packed_seqlens - 2).tolist())
return unpacked_latent
return unpacked_latent, trajectory_latents, trajectory_timesteps, trajectory_log_probs
@staticmethod
def _combine_cfg(
@@ -2067,7 +2128,7 @@ class Bagel(nn.Module):
return v_t
def _forward_flow_single_branch(
def forward_single_branch(
self,
x_t: torch.Tensor,
timestep: torch.LongTensor,
@@ -2197,7 +2258,7 @@ class Bagel(nn.Module):
v_t = v_t[packed_vae_token_indexes]
return v_t
def _forward_flow(
def forward(
self,
x_t: torch.Tensor,
timestep: torch.LongTensor,
@@ -159,6 +159,9 @@ class BagelPipeline(nn.Module, DiffusionPipelineProfilerMixin):
self.od_config = od_config
self.device = get_local_device()
self.scheduler: object | None = None
self.scheduler_kwargs: dict = {}
model = od_config.model
local_files_only = os.path.exists(model)
if local_files_only:
@@ -630,7 +633,7 @@ class BagelPipeline(nn.Module, DiffusionPipelineProfilerMixin):
enabled=self.device.type != "cpu",
dtype=self.od_config.dtype,
):
latents = self.bagel.generate_image(
latents, trajectory_latents, trajectory_timesteps, trajectory_log_probs = self.bagel.generate_image(
past_key_values=gen_context["past_key_values"],
cfg_text_past_key_values=cfg_text_context["past_key_values"],
cfg_img_past_key_values=cfg_img_context["past_key_values"],
@@ -650,11 +653,36 @@ class BagelPipeline(nn.Module, DiffusionPipelineProfilerMixin):
cfg_img_packed_query_indexes=generation_input_cfg_img["cfg_packed_query_indexes"],
cfg_img_key_values_lens=generation_input_cfg_img["cfg_key_values_lens"],
cfg_img_packed_key_value_indexes=generation_input_cfg_img["cfg_packed_key_value_indexes"],
return_trajectory_latents=req.sampling_params.return_trajectory_latents,
scheduler=self.scheduler,
scheduler_kwargs=self.scheduler_kwargs,
)
img = self._decode_image_from_latent(self.bagel, self.vae, latents[0], image_shape)
# Build trajectory output when requested
trajectory_latents_stacked: torch.Tensor | None = None
trajectory_timesteps_stacked: torch.Tensor | None = None
trajectory_decoded: list[Image.Image] | None = None
if trajectory_latents:
trajectory_latents_stacked = torch.stack(trajectory_latents)
trajectory_timesteps_stacked = torch.stack(trajectory_timesteps)
if req.sampling_params.return_trajectory_decoded:
trajectory_decoded = [
self._decode_image_from_latent(self.bagel, self.vae, lat, image_shape) for lat in trajectory_latents
]
trajectory_log_probs_stacked: torch.Tensor | None = None
if trajectory_log_probs:
trajectory_log_probs_stacked = torch.stack(trajectory_log_probs)
return DiffusionOutput(
output=img, stage_durations=self.stage_durations if hasattr(self, "stage_durations") else None
output=img,
trajectory_latents=trajectory_latents_stacked,
trajectory_timesteps=trajectory_timesteps_stacked,
trajectory_log_probs=trajectory_log_probs_stacked,
trajectory_decoded=trajectory_decoded,
stage_durations=self.stage_durations if hasattr(self, "stage_durations") else None,
)
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
@@ -582,6 +582,7 @@ class LongCatImageTransformer2DModel(nn.Module):
"""
_repeated_blocks = ["LongCatImageTransformerBlock", "LongCatImageSingleTransformerBlock"]
_layerwise_offload_blocks_attrs = ["transformer_blocks", "single_transformer_blocks"]
# Sequence Parallelism for LongCat (following diffusers' _cp_plan pattern)
_sp_plan = {
+148 -268
View File
@@ -28,8 +28,6 @@ from vllm.model_executor.models.utils import AutoWeightsLoader
from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig
from vllm_omni.diffusion.distributed.cfg_parallel import CFGParallelMixin
from vllm_omni.diffusion.distributed.parallel_state import (
get_cfg_group,
get_classifier_free_guidance_rank,
get_classifier_free_guidance_world_size,
)
from vllm_omni.diffusion.distributed.utils import get_local_device
@@ -122,6 +120,31 @@ def calculate_shift(
return mu
class _VideoAudioScheduler:
"""Composite scheduler dispatching to video and audio schedulers."""
def __init__(self, video_scheduler, audio_scheduler):
self.video_scheduler = video_scheduler
self.audio_scheduler = audio_scheduler
def step(self, noise_pred, t, latents, return_dict=False, generator=None):
video_out = self.video_scheduler.step(
noise_pred[0],
t[0],
latents[0],
return_dict=False,
generator=generator,
)[0]
audio_out = self.audio_scheduler.step(
noise_pred[1],
t[1],
latents[1],
return_dict=False,
generator=generator,
)[0]
return ((video_out, audio_out),)
class LTX2Pipeline(nn.Module, CFGParallelMixin, ProgressBarMixin):
def __init__(
self,
@@ -542,6 +565,10 @@ class LTX2Pipeline(nn.Module, CFGParallelMixin, ProgressBarMixin):
latents = latents.unflatten(2, (-1, num_mel_bins)).transpose(1, 2)
return latents
@staticmethod
def _unpad_audio_latents(latents: torch.Tensor, num_frames: int) -> torch.Tensor:
return latents[:, :num_frames]
def prepare_latents(
self,
batch_size: int = 1,
@@ -597,25 +624,49 @@ class LTX2Pipeline(nn.Module, CFGParallelMixin, ProgressBarMixin):
noise_scale: float = 0.0,
dtype: torch.dtype | None = None,
device: torch.device | None = None,
generator: torch.Generator | None = None,
generator: torch.Generator | list[torch.Generator] | None = None,
latents: torch.Tensor | None = None,
) -> tuple[torch.Tensor, int]:
) -> tuple[torch.Tensor, int, int]:
original_latent_length = audio_latent_length
padded_latent_length = original_latent_length
latent_mel_bins = num_mel_bins // self.audio_vae_mel_compression_ratio
sp_size = getattr(self.od_config.parallel_config, "sequence_parallel_size", 1)
if sp_size > 1:
padded_latent_length += (sp_size - (original_latent_length % sp_size)) % sp_size
if latents is not None:
if latents.ndim == 4:
# latents are of shape [B, C, L, M], need to be packed
latents = self._pack_audio_latents(latents)
if latents.ndim != 3:
raise ValueError(
f"Provided `latents` tensor has shape {latents.shape}, but the expected shape is [batch_size, num_seq, num_features]." # noqa
f"Provided `latents` tensor has shape {latents.shape}, but the expected shape is "
"[batch_size, num_seq, num_features] or [batch_size, num_channels, audio_length, mel_bins]."
)
latents = self._normalize_audio_latents(latents, self.audio_vae.latents_mean, self.audio_vae.latents_std)
latents = self._create_noised_state(latents, noise_scale, generator)
return latents.to(device=device, dtype=dtype)
# TODO: confirm whether this logic is correct
latent_mel_bins = num_mel_bins // self.audio_vae_mel_compression_ratio
if latents.shape[1] not in {original_latent_length, padded_latent_length}:
raise ValueError(
"Provided `audio_latents` has incompatible audio frame count "
f"{latents.shape[1]}; expected {original_latent_length} or {padded_latent_length}."
)
shape = (batch_size, num_channels_latents, audio_latent_length, latent_mel_bins)
if latents.shape[1] == original_latent_length and padded_latent_length > original_latent_length:
padding = torch.zeros(
latents.shape[0],
padded_latent_length - original_latent_length,
latents.shape[2],
dtype=latents.dtype,
device=latents.device,
)
latents = torch.cat([latents, padding], dim=1)
return latents.to(device=device, dtype=dtype), original_latent_length, padded_latent_length
shape = (batch_size, num_channels_latents, padded_latent_length, latent_mel_bins)
if isinstance(generator, list) and len(generator) != batch_size:
raise ValueError(
@@ -625,7 +676,7 @@ class LTX2Pipeline(nn.Module, CFGParallelMixin, ProgressBarMixin):
latents = randn_tensor(shape, generator=generator, device=device, dtype=dtype)
latents = self._pack_audio_latents(latents)
return latents
return latents, original_latent_length, padded_latent_length
@property
def guidance_scale(self):
@@ -655,147 +706,44 @@ class LTX2Pipeline(nn.Module, CFGParallelMixin, ProgressBarMixin):
def interrupt(self):
return self._interrupt
def _is_cfg_parallel_enabled(self, do_true_cfg: bool) -> bool:
return do_true_cfg and get_classifier_free_guidance_world_size() > 1
def _transformer_cache_context(self, context_name: str):
cache_context = getattr(self.transformer, "cache_context", None)
if callable(cache_context):
return cache_context(context_name)
return nullcontext()
def _predict_noise_av(self, **kwargs) -> tuple[torch.Tensor, torch.Tensor]:
def predict_noise(self, **kwargs):
with self._transformer_cache_context("cond_uncond"):
noise_pred_video, noise_pred_audio = self.transformer(**kwargs)
return noise_pred_video, noise_pred_audio
def predict_noise_av_maybe_with_cfg(
self,
do_true_cfg: bool,
true_cfg_scale: float,
positive_kwargs: dict[str, Any],
negative_kwargs: dict[str, Any] | None,
guidance_rescale: float = 0.0,
cfg_normalize: bool = False,
) -> tuple[torch.Tensor | None, torch.Tensor | None]:
if do_true_cfg:
cfg_parallel_ready = get_classifier_free_guidance_world_size() > 1
if cfg_parallel_ready:
cfg_group = get_cfg_group()
cfg_rank = get_classifier_free_guidance_rank()
if cfg_rank == 0:
noise_pred_video, noise_pred_audio = self._predict_noise_av(**positive_kwargs)
else:
noise_pred_video, noise_pred_audio = self._predict_noise_av(**negative_kwargs)
noise_pred_video = noise_pred_video.float()
noise_pred_audio = noise_pred_audio.float()
gathered_video = cfg_group.all_gather(noise_pred_video, separate_tensors=True)
gathered_audio = cfg_group.all_gather(noise_pred_audio, separate_tensors=True)
if cfg_rank == 0:
noise_pred_video_text = gathered_video[0]
noise_pred_video_uncond = gathered_video[1]
noise_pred_audio_text = gathered_audio[0]
noise_pred_audio_uncond = gathered_audio[1]
noise_pred_video = self.combine_cfg_noise(
noise_pred_video_text,
noise_pred_video_uncond,
true_cfg_scale,
cfg_normalize,
)
noise_pred_audio = self.combine_cfg_noise(
noise_pred_audio_text,
noise_pred_audio_uncond,
true_cfg_scale,
cfg_normalize,
)
if guidance_rescale > 0:
noise_pred_video = rescale_noise_cfg(
noise_pred_video,
noise_pred_video_text,
guidance_rescale=guidance_rescale,
)
noise_pred_audio = rescale_noise_cfg(
noise_pred_audio,
noise_pred_audio_text,
guidance_rescale=guidance_rescale,
)
return noise_pred_video, noise_pred_audio
return None, None
noise_pred_video_text, noise_pred_audio_text = self._predict_noise_av(**positive_kwargs)
noise_pred_video_uncond, noise_pred_audio_uncond = self._predict_noise_av(**negative_kwargs)
noise_pred_video_text = noise_pred_video_text.float()
noise_pred_audio_text = noise_pred_audio_text.float()
noise_pred_video_uncond = noise_pred_video_uncond.float()
noise_pred_audio_uncond = noise_pred_audio_uncond.float()
noise_pred_video = self.combine_cfg_noise(
noise_pred_video_text,
noise_pred_video_uncond,
true_cfg_scale,
cfg_normalize,
)
noise_pred_audio = self.combine_cfg_noise(
noise_pred_audio_text,
noise_pred_audio_uncond,
true_cfg_scale,
cfg_normalize,
)
if guidance_rescale > 0:
noise_pred_video = rescale_noise_cfg(
noise_pred_video,
noise_pred_video_text,
guidance_rescale=guidance_rescale,
)
noise_pred_audio = rescale_noise_cfg(
noise_pred_audio,
noise_pred_audio_text,
guidance_rescale=guidance_rescale,
)
return noise_pred_video, noise_pred_audio
noise_pred_video, noise_pred_audio = self._predict_noise_av(**positive_kwargs)
return noise_pred_video.float(), noise_pred_audio.float()
def _scheduler_step_video_audio_maybe_with_cfg(
def combine_cfg_noise(self, positive_noise_pred, negative_noise_pred, true_cfg_scale, cfg_normalize=False):
"""Per-element CFG combine with guidance_rescale support."""
(video_pos, audio_pos) = positive_noise_pred
(video_neg, audio_neg) = negative_noise_pred
video_combined = super().combine_cfg_noise(video_pos, video_neg, true_cfg_scale, cfg_normalize)
audio_combined = super().combine_cfg_noise(audio_pos, audio_neg, true_cfg_scale, cfg_normalize)
if self._guidance_rescale and self._guidance_rescale > 0:
video_combined = rescale_noise_cfg(video_combined, video_pos, guidance_rescale=self._guidance_rescale)
audio_combined = rescale_noise_cfg(audio_combined, audio_pos, guidance_rescale=self._guidance_rescale)
return (video_combined, audio_combined)
def _synchronize_cfg_parallel_step_output(
self,
noise_pred_video: torch.Tensor | None,
noise_pred_audio: torch.Tensor | None,
t: torch.Tensor,
latents: torch.Tensor,
audio_latents: torch.Tensor,
audio_scheduler: FlowMatchEulerDiscreteScheduler,
latents: tuple[torch.Tensor, torch.Tensor],
do_true_cfg: bool,
) -> tuple[torch.Tensor, torch.Tensor]:
cfg_parallel_ready = self._is_cfg_parallel_enabled(do_true_cfg)
if not (do_true_cfg and get_classifier_free_guidance_world_size() > 1):
return latents
if cfg_parallel_ready:
cfg_group = get_cfg_group()
cfg_rank = get_classifier_free_guidance_rank()
if cfg_rank == 0:
latents = self.scheduler.step(noise_pred_video, t, latents, return_dict=False)[0]
audio_latents = audio_scheduler.step(noise_pred_audio, t, audio_latents, return_dict=False)[0]
latents = latents.contiguous()
audio_latents = audio_latents.contiguous()
cfg_group.broadcast(latents, src=0)
cfg_group.broadcast(audio_latents, src=0)
return latents, audio_latents
latents = self.scheduler.step(noise_pred_video, t, latents, return_dict=False)[0]
audio_latents = audio_scheduler.step(noise_pred_audio, t, audio_latents, return_dict=False)[0]
return latents, audio_latents
# Without this sync, CUDA async execution causes non-deterministic
# numerical drift across denoising steps in CFG parallel mode,
# producing different video outputs across runs.
latents = tuple(tensor.contiguous() for tensor in latents)
device = next((tensor.device for tensor in latents if tensor.is_cuda), None)
if device is not None:
torch.cuda.current_stream(device).synchronize()
return latents
@torch.no_grad()
def forward(
@@ -828,6 +776,8 @@ class LTX2Pipeline(nn.Module, CFGParallelMixin, ProgressBarMixin):
attention_kwargs: dict[str, Any] | None = None,
max_sequence_length: int | None = None,
) -> DiffusionOutput:
# Extract prompt/negative_prompt from request.
# Input format: req.prompts is a list of str or dict with "prompt"/"negative_prompt" keys.
prompt = [p if isinstance(p, str) else (p.get("prompt") or "") for p in req.prompts] or prompt
if all(isinstance(p, str) or p.get("negative_prompt") is None for p in req.prompts):
negative_prompt = None
@@ -869,6 +819,7 @@ class LTX2Pipeline(nn.Module, CFGParallelMixin, ProgressBarMixin):
else req.sampling_params.extra_args.get("audio_latents", audio_latents)
)
# Override with pre-computed embeddings if provided in request.
req_prompt_embeds = [_get_prompt_field(p, "prompt_embeds") for p in req.prompts]
if any(p is not None for p in req_prompt_embeds):
prompt_embeds = torch.stack(req_prompt_embeds) # type: ignore[arg-type]
@@ -939,20 +890,17 @@ class LTX2Pipeline(nn.Module, CFGParallelMixin, ProgressBarMixin):
max_sequence_length=max_sequence_length,
device=device,
)
cfg_parallel_ready = self._is_cfg_parallel_enabled(self.do_classifier_free_guidance)
if self.do_classifier_free_guidance and not cfg_parallel_ready:
prompt_embeds = torch.cat([negative_prompt_embeds, prompt_embeds], dim=0)
prompt_attention_mask = torch.cat([negative_prompt_attention_mask, prompt_attention_mask], dim=0)
# Compute positive prompt connectors
additive_attention_mask = (1 - prompt_attention_mask.to(prompt_embeds.dtype)) * -1000000.0
connector_prompt_embeds, connector_audio_prompt_embeds, connector_attention_mask = self.connectors(
prompt_embeds, additive_attention_mask, additive_mask=True
)
# Compute negative prompt connectors when CFG is enabled
negative_connector_prompt_embeds = None
negative_connector_audio_prompt_embeds = None
negative_connector_attention_mask = None
if cfg_parallel_ready:
if self.do_classifier_free_guidance:
negative_additive_attention_mask = (
1 - negative_prompt_attention_mask.to(negative_prompt_embeds.dtype)
) * -1000000.0
@@ -1027,20 +975,7 @@ class LTX2Pipeline(nn.Module, CFGParallelMixin, ProgressBarMixin):
num_channels_latents_audio = (
self.audio_vae.config.latent_channels if getattr(self, "audio_vae", None) is not None else 8
)
# padding audio_latents if needed
sp_size = getattr(self.od_config.parallel_config, "sequence_parallel_size", 1)
if sp_size > 1:
pad_len = (sp_size - (audio_num_frames % sp_size)) % sp_size
if pad_len > 0:
if audio_latents is not None:
pad_shape = list(audio_latents.shape)
pad_shape[2] = pad_len
padding = torch.zeros(pad_shape, dtype=audio_latents.dtype, device=audio_latents.device)
audio_latents = torch.cat([audio_latents, padding], dim=2)
audio_num_frames += pad_len
audio_latents = self.prepare_audio_latents(
audio_latents, original_audio_num_frames, padded_audio_num_frames = self.prepare_audio_latents(
batch_size * num_videos_per_prompt,
num_channels_latents=num_channels_latents_audio,
audio_latent_length=audio_num_frames,
@@ -1061,6 +996,7 @@ class LTX2Pipeline(nn.Module, CFGParallelMixin, ProgressBarMixin):
self.scheduler.config.get("max_shift", 2.05),
)
audio_scheduler = copy.deepcopy(self.scheduler)
video_audio_scheduler = _VideoAudioScheduler(self.scheduler, audio_scheduler)
_ = retrieve_timesteps(
audio_scheduler,
num_inference_steps,
@@ -1083,12 +1019,10 @@ class LTX2Pipeline(nn.Module, CFGParallelMixin, ProgressBarMixin):
latents.shape[0], latent_num_frames, latent_height, latent_width, latents.device, fps=frame_rate
)
audio_coords = self.transformer.audio_rope.prepare_audio_coords(
audio_latents.shape[0], audio_num_frames, audio_latents.device
audio_latents.shape[0], padded_audio_num_frames, audio_latents.device
)
# Duplicate the positional ids as well if using CFG
if self.do_classifier_free_guidance and not cfg_parallel_ready:
video_coords = video_coords.repeat((2,) + (1,) * (video_coords.ndim - 1)) # Repeat twice in batch dim
audio_coords = audio_coords.repeat((2,) + (1,) * (audio_coords.ndim - 1))
# No coord duplication needed: mixin handles CFG via separate forward calls,
# not batch=2. Each forward gets batch=1 coords directly.
with self.progress_bar(total=len(timesteps)) as pbar:
for i, t in enumerate(timesteps):
@@ -1097,119 +1031,60 @@ class LTX2Pipeline(nn.Module, CFGParallelMixin, ProgressBarMixin):
self._current_timestep = t
if cfg_parallel_ready:
latent_model_input = latents.to(prompt_embeds.dtype)
audio_latent_model_input = audio_latents.to(prompt_embeds.dtype)
timestep = t.expand(latent_model_input.shape[0])
latent_model_input = latents.to(prompt_embeds.dtype)
audio_latent_model_input = audio_latents.to(prompt_embeds.dtype)
timestep = t.expand(latent_model_input.shape[0])
do_true_cfg = self.do_classifier_free_guidance
positive_kwargs = {
"hidden_states": latent_model_input,
"audio_hidden_states": audio_latent_model_input,
"encoder_hidden_states": connector_prompt_embeds,
"audio_encoder_hidden_states": connector_audio_prompt_embeds,
"timestep": timestep,
"encoder_attention_mask": connector_attention_mask,
"audio_encoder_attention_mask": connector_attention_mask,
"num_frames": latent_num_frames,
"height": latent_height,
"width": latent_width,
"fps": frame_rate,
"audio_num_frames": audio_num_frames,
"video_coords": video_coords,
"audio_coords": audio_coords,
"attention_kwargs": attention_kwargs,
"return_dict": False,
}
negative_kwargs = {
"hidden_states": latent_model_input,
"audio_hidden_states": audio_latent_model_input,
positive_kwargs = {
"hidden_states": latent_model_input,
"audio_hidden_states": audio_latent_model_input,
"encoder_hidden_states": connector_prompt_embeds,
"audio_encoder_hidden_states": connector_audio_prompt_embeds,
"timestep": timestep,
"encoder_attention_mask": connector_attention_mask,
"audio_encoder_attention_mask": connector_attention_mask,
"num_frames": latent_num_frames,
"height": latent_height,
"width": latent_width,
"fps": frame_rate,
"audio_num_frames": padded_audio_num_frames,
"video_coords": video_coords,
"audio_coords": audio_coords,
"attention_kwargs": attention_kwargs,
"return_dict": False,
}
negative_kwargs = (
{
**positive_kwargs,
"encoder_hidden_states": negative_connector_prompt_embeds,
"audio_encoder_hidden_states": negative_connector_audio_prompt_embeds,
"timestep": timestep,
"encoder_attention_mask": negative_connector_attention_mask,
"audio_encoder_attention_mask": negative_connector_attention_mask,
"num_frames": latent_num_frames,
"height": latent_height,
"width": latent_width,
"fps": frame_rate,
"audio_num_frames": audio_num_frames,
"video_coords": video_coords,
"audio_coords": audio_coords,
"attention_kwargs": attention_kwargs,
"return_dict": False,
}
if do_true_cfg
else None
)
noise_pred_video, noise_pred_audio = self.predict_noise_av_maybe_with_cfg(
do_true_cfg=True,
true_cfg_scale=guidance_scale,
positive_kwargs=positive_kwargs,
negative_kwargs=negative_kwargs,
guidance_rescale=guidance_rescale,
cfg_normalize=False,
)
noise_pred_video, noise_pred_audio = self.predict_noise_maybe_with_cfg(
do_true_cfg=do_true_cfg,
true_cfg_scale=guidance_scale,
positive_kwargs=positive_kwargs,
negative_kwargs=negative_kwargs,
cfg_normalize=False,
)
latents, audio_latents = self._scheduler_step_video_audio_maybe_with_cfg(
noise_pred_video,
noise_pred_audio,
t,
latents,
audio_latents,
audio_scheduler,
do_true_cfg=True,
)
else:
latent_model_input = torch.cat([latents] * 2) if self.do_classifier_free_guidance else latents
latent_model_input = latent_model_input.to(prompt_embeds.dtype)
audio_latent_model_input = (
torch.cat([audio_latents] * 2) if self.do_classifier_free_guidance else audio_latents
)
audio_latent_model_input = audio_latent_model_input.to(prompt_embeds.dtype)
timestep = t.expand(latent_model_input.shape[0])
with self._transformer_cache_context("cond_uncond"):
noise_pred_video, noise_pred_audio = self.transformer(
hidden_states=latent_model_input,
audio_hidden_states=audio_latent_model_input,
encoder_hidden_states=connector_prompt_embeds,
audio_encoder_hidden_states=connector_audio_prompt_embeds,
timestep=timestep,
encoder_attention_mask=connector_attention_mask,
audio_encoder_attention_mask=connector_attention_mask,
num_frames=latent_num_frames,
height=latent_height,
width=latent_width,
fps=frame_rate,
audio_num_frames=audio_num_frames,
video_coords=video_coords,
audio_coords=audio_coords,
attention_kwargs=attention_kwargs,
return_dict=False,
)
noise_pred_video = noise_pred_video.float()
noise_pred_audio = noise_pred_audio.float()
if self.do_classifier_free_guidance:
noise_pred_video_uncond, noise_pred_video_text = noise_pred_video.chunk(2)
noise_pred_video = noise_pred_video_uncond + guidance_scale * (
noise_pred_video_text - noise_pred_video_uncond
)
noise_pred_audio_uncond, noise_pred_audio_text = noise_pred_audio.chunk(2)
noise_pred_audio = noise_pred_audio_uncond + guidance_scale * (
noise_pred_audio_text - noise_pred_audio_uncond
)
if guidance_rescale > 0:
noise_pred_video = rescale_noise_cfg(
noise_pred_video, noise_pred_video_text, guidance_rescale=guidance_rescale
)
noise_pred_audio = rescale_noise_cfg(
noise_pred_audio, noise_pred_audio_text, guidance_rescale=guidance_rescale
)
latents = self.scheduler.step(noise_pred_video, t, latents, return_dict=False)[0]
audio_latents = audio_scheduler.step(noise_pred_audio, t, audio_latents, return_dict=False)[0]
latents, audio_latents = self.scheduler_step_maybe_with_cfg(
(noise_pred_video, noise_pred_audio),
(t, t),
(latents, audio_latents),
do_true_cfg=do_true_cfg,
per_request_scheduler=video_audio_scheduler,
)
latents, audio_latents = self._synchronize_cfg_parallel_step_output(
(latents, audio_latents),
do_true_cfg=do_true_cfg,
)
pbar.update()
@@ -1225,10 +1100,15 @@ class LTX2Pipeline(nn.Module, CFGParallelMixin, ProgressBarMixin):
latents, self.vae.latents_mean, self.vae.latents_std, self.vae.config.scaling_factor
)
audio_latents = self._unpad_audio_latents(audio_latents, original_audio_num_frames)
audio_latents = self._denormalize_audio_latents(
audio_latents, self.audio_vae.latents_mean, self.audio_vae.latents_std
)
audio_latents = self._unpack_audio_latents(audio_latents, audio_num_frames, num_mel_bins=latent_mel_bins)
audio_latents = self._unpack_audio_latents(
audio_latents,
original_audio_num_frames,
num_mel_bins=latent_mel_bins,
)
if output_type == "latent":
video = latents
@@ -14,7 +14,7 @@ import torch
import torch.nn as nn
from diffusers import FlowMatchEulerDiscreteScheduler
from diffusers.pipelines.ltx2.utils import DISTILLED_SIGMA_VALUES, STAGE_2_DISTILLED_SIGMA_VALUES
from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion import rescale_noise_cfg, retrieve_timesteps
from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion import retrieve_timesteps
from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion_img2img import retrieve_latents
from diffusers.utils.torch_utils import randn_tensor
from diffusers.video_processor import VideoProcessor
@@ -22,7 +22,6 @@ from vllm.logger import init_logger
from vllm.model_executor.models.utils import AutoWeightsLoader
from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig
from vllm_omni.diffusion.distributed.parallel_state import get_cfg_group, get_classifier_free_guidance_rank
from vllm_omni.diffusion.distributed.utils import get_local_device
from vllm_omni.diffusion.lora.manager import DiffusionLoRAManager
from vllm_omni.diffusion.model_loader.diffusers_loader import DiffusersPipelineLoader
@@ -46,6 +45,32 @@ def get_ltx2_post_process_func(od_config: OmniDiffusionConfig):
return _get_ltx2_post_process_func(od_config)
class _I2VVideoAudioScheduler:
"""Composite scheduler for I2V: uses _step_video_latents_i2v for video, standard step for audio."""
def __init__(self, pipeline, audio_scheduler, latent_num_frames, latent_height, latent_width):
self.video_scheduler = pipeline.scheduler
self.audio_scheduler = audio_scheduler
self._pipeline = pipeline
self._latent_num_frames = latent_num_frames
self._latent_height = latent_height
self._latent_width = latent_width
def step(self, noise_pred, t, latents, return_dict=False, generator=None):
video_out = self._pipeline._step_video_latents_i2v(
noise_pred[0],
latents[0],
t[0],
self._latent_num_frames,
self._latent_height,
self._latent_width,
)
audio_out = self.audio_scheduler.step(noise_pred[1], t[1], latents[1], return_dict=False, generator=generator)[
0
]
return ((video_out, audio_out),)
class LTX2ImageToVideoPipeline(LTX2Pipeline):
support_image_input = True
@@ -287,6 +312,8 @@ class LTX2ImageToVideoPipeline(LTX2Pipeline):
attention_kwargs: dict[str, Any] | None = None,
max_sequence_length: int | None = None,
) -> DiffusionOutput:
# Extract prompt/negative_prompt from request.
# Input format: req.prompts is a list of str or dict with "prompt"/"negative_prompt" keys.
prompt = [p if isinstance(p, str) else (p.get("prompt") or "") for p in req.prompts] or prompt
if all(isinstance(p, str) or p.get("negative_prompt") is None for p in req.prompts):
negative_prompt = None
@@ -328,6 +355,7 @@ class LTX2ImageToVideoPipeline(LTX2Pipeline):
else req.sampling_params.extra_args.get("audio_latents", audio_latents)
)
# Override with pre-computed embeddings if provided in request.
req_prompt_embeds = [_get_prompt_field(p, "prompt_embeds") for p in req.prompts]
if any(p is not None for p in req_prompt_embeds):
prompt_embeds = torch.stack(req_prompt_embeds) # type: ignore[arg-type]
@@ -429,20 +457,17 @@ class LTX2ImageToVideoPipeline(LTX2Pipeline):
max_sequence_length=max_sequence_length,
device=device,
)
cfg_parallel_ready = self._is_cfg_parallel_enabled(self.do_classifier_free_guidance)
if self.do_classifier_free_guidance and not cfg_parallel_ready:
prompt_embeds = torch.cat([negative_prompt_embeds, prompt_embeds], dim=0)
prompt_attention_mask = torch.cat([negative_prompt_attention_mask, prompt_attention_mask], dim=0)
# Compute positive prompt connectors
additive_attention_mask = (1 - prompt_attention_mask.to(prompt_embeds.dtype)) * -1000000.0
connector_prompt_embeds, connector_audio_prompt_embeds, connector_attention_mask = self.connectors(
prompt_embeds, additive_attention_mask, additive_mask=True
)
# Compute negative prompt connectors when CFG is enabled
negative_connector_prompt_embeds = None
negative_connector_audio_prompt_embeds = None
negative_connector_attention_mask = None
if cfg_parallel_ready:
if self.do_classifier_free_guidance:
negative_additive_attention_mask = (
1 - negative_prompt_attention_mask.to(negative_prompt_embeds.dtype)
) * -1000000.0
@@ -500,8 +525,6 @@ class LTX2ImageToVideoPipeline(LTX2Pipeline):
generator,
latents,
)
if self.do_classifier_free_guidance and not cfg_parallel_ready:
conditioning_mask = torch.cat([conditioning_mask, conditioning_mask])
duration_s = num_frames / frame_rate
audio_latents_per_second = (
@@ -529,20 +552,7 @@ class LTX2ImageToVideoPipeline(LTX2Pipeline):
num_channels_latents_audio = (
self.audio_vae.config.latent_channels if getattr(self, "audio_vae", None) is not None else 8
)
# padding audio_latents if needed
sp_size = getattr(self.od_config.parallel_config, "sequence_parallel_size", 1)
if sp_size > 1:
pad_len = (sp_size - (audio_num_frames % sp_size)) % sp_size
if pad_len > 0:
if audio_latents is not None:
pad_shape = list(audio_latents.shape)
pad_shape[2] = pad_len
padding = torch.zeros(pad_shape, dtype=audio_latents.dtype, device=audio_latents.device)
audio_latents = torch.cat([audio_latents, padding], dim=2)
audio_num_frames += pad_len
audio_latents = self.prepare_audio_latents(
audio_latents, original_audio_num_frames, padded_audio_num_frames = self.prepare_audio_latents(
batch_size * num_videos_per_prompt,
num_channels_latents=num_channels_latents_audio,
audio_latent_length=audio_num_frames,
@@ -585,12 +595,17 @@ class LTX2ImageToVideoPipeline(LTX2Pipeline):
latents.shape[0], latent_num_frames, latent_height, latent_width, latents.device, fps=frame_rate
)
audio_coords = self.transformer.audio_rope.prepare_audio_coords(
audio_latents.shape[0], audio_num_frames, audio_latents.device
audio_latents.shape[0], padded_audio_num_frames, audio_latents.device
)
# Duplicate the positional ids as well if using CFG
if self.do_classifier_free_guidance and not cfg_parallel_ready:
video_coords = video_coords.repeat((2,) + (1,) * (video_coords.ndim - 1)) # Repeat twice in batch dim
audio_coords = audio_coords.repeat((2,) + (1,) * (audio_coords.ndim - 1))
i2v_scheduler = _I2VVideoAudioScheduler(
pipeline=self,
audio_scheduler=audio_scheduler,
latent_num_frames=latent_num_frames,
latent_height=latent_height,
latent_width=latent_width,
)
# No coord duplication needed: mixin handles CFG via separate forward calls.
with self.progress_bar(total=len(timesteps)) as pbar:
for i, t in enumerate(timesteps):
@@ -599,140 +614,62 @@ class LTX2ImageToVideoPipeline(LTX2Pipeline):
self._current_timestep = t
if cfg_parallel_ready:
latent_model_input = latents.to(prompt_embeds.dtype)
audio_latent_model_input = audio_latents.to(prompt_embeds.dtype)
latent_model_input = latents.to(prompt_embeds.dtype)
audio_latent_model_input = audio_latents.to(prompt_embeds.dtype)
timestep = t.expand(latent_model_input.shape[0])
video_timestep = timestep.unsqueeze(-1) * (1 - conditioning_mask)
do_true_cfg = self.do_classifier_free_guidance
timestep = t.expand(latent_model_input.shape[0])
video_timestep = timestep.unsqueeze(-1) * (1 - conditioning_mask)
positive_kwargs = {
"hidden_states": latent_model_input,
"audio_hidden_states": audio_latent_model_input,
"encoder_hidden_states": connector_prompt_embeds,
"audio_encoder_hidden_states": connector_audio_prompt_embeds,
"timestep": video_timestep,
"audio_timestep": timestep,
"encoder_attention_mask": connector_attention_mask,
"audio_encoder_attention_mask": connector_attention_mask,
"num_frames": latent_num_frames,
"height": latent_height,
"width": latent_width,
"fps": frame_rate,
"audio_num_frames": audio_num_frames,
"video_coords": video_coords,
"audio_coords": audio_coords,
"attention_kwargs": attention_kwargs,
"return_dict": False,
}
negative_kwargs = {
"hidden_states": latent_model_input,
"audio_hidden_states": audio_latent_model_input,
positive_kwargs = {
"hidden_states": latent_model_input,
"audio_hidden_states": audio_latent_model_input,
"encoder_hidden_states": connector_prompt_embeds,
"audio_encoder_hidden_states": connector_audio_prompt_embeds,
"timestep": video_timestep,
"audio_timestep": timestep,
"encoder_attention_mask": connector_attention_mask,
"audio_encoder_attention_mask": connector_attention_mask,
"num_frames": latent_num_frames,
"height": latent_height,
"width": latent_width,
"fps": frame_rate,
"audio_num_frames": padded_audio_num_frames,
"video_coords": video_coords,
"audio_coords": audio_coords,
"attention_kwargs": attention_kwargs,
"return_dict": False,
}
negative_kwargs = (
{
**positive_kwargs,
"encoder_hidden_states": negative_connector_prompt_embeds,
"audio_encoder_hidden_states": negative_connector_audio_prompt_embeds,
"timestep": video_timestep,
"audio_timestep": timestep,
"encoder_attention_mask": negative_connector_attention_mask,
"audio_encoder_attention_mask": negative_connector_attention_mask,
"num_frames": latent_num_frames,
"height": latent_height,
"width": latent_width,
"fps": frame_rate,
"audio_num_frames": audio_num_frames,
"video_coords": video_coords,
"audio_coords": audio_coords,
"attention_kwargs": attention_kwargs,
"return_dict": False,
}
if do_true_cfg
else None
)
noise_pred_video, noise_pred_audio = self.predict_noise_av_maybe_with_cfg(
do_true_cfg=True,
true_cfg_scale=guidance_scale,
positive_kwargs=positive_kwargs,
negative_kwargs=negative_kwargs,
guidance_rescale=guidance_rescale,
cfg_normalize=False,
)
noise_pred_video, noise_pred_audio = self.predict_noise_maybe_with_cfg(
do_true_cfg=do_true_cfg,
true_cfg_scale=guidance_scale,
positive_kwargs=positive_kwargs,
negative_kwargs=negative_kwargs,
cfg_normalize=False,
)
if get_classifier_free_guidance_rank() == 0:
latents = self._step_video_latents_i2v(
noise_pred_video,
latents,
t,
latent_num_frames,
latent_height,
latent_width,
)
audio_latents = audio_scheduler.step(noise_pred_audio, t, audio_latents, return_dict=False)[0]
cfg_group = get_cfg_group()
latents = latents.contiguous()
audio_latents = audio_latents.contiguous()
cfg_group.broadcast(latents, src=0)
cfg_group.broadcast(audio_latents, src=0)
else:
latent_model_input = torch.cat([latents] * 2) if self.do_classifier_free_guidance else latents
latent_model_input = latent_model_input.to(prompt_embeds.dtype)
audio_latent_model_input = (
torch.cat([audio_latents] * 2) if self.do_classifier_free_guidance else audio_latents
)
audio_latent_model_input = audio_latent_model_input.to(prompt_embeds.dtype)
timestep = t.expand(latent_model_input.shape[0])
video_timestep = timestep.unsqueeze(-1) * (1 - conditioning_mask)
with self._transformer_cache_context("cond_uncond"):
noise_pred_video, noise_pred_audio = self.transformer(
hidden_states=latent_model_input,
audio_hidden_states=audio_latent_model_input,
encoder_hidden_states=connector_prompt_embeds,
audio_encoder_hidden_states=connector_audio_prompt_embeds,
timestep=video_timestep,
audio_timestep=timestep,
encoder_attention_mask=connector_attention_mask,
audio_encoder_attention_mask=connector_attention_mask,
num_frames=latent_num_frames,
height=latent_height,
width=latent_width,
fps=frame_rate,
audio_num_frames=audio_num_frames,
video_coords=video_coords,
audio_coords=audio_coords,
attention_kwargs=attention_kwargs,
return_dict=False,
)
noise_pred_video = noise_pred_video.float()
noise_pred_audio = noise_pred_audio.float()
if self.do_classifier_free_guidance:
noise_pred_video_uncond, noise_pred_video_text = noise_pred_video.chunk(2)
noise_pred_video = noise_pred_video_uncond + guidance_scale * (
noise_pred_video_text - noise_pred_video_uncond
)
noise_pred_audio_uncond, noise_pred_audio_text = noise_pred_audio.chunk(2)
noise_pred_audio = noise_pred_audio_uncond + guidance_scale * (
noise_pred_audio_text - noise_pred_audio_uncond
)
if guidance_rescale > 0:
noise_pred_video = rescale_noise_cfg(
noise_pred_video, noise_pred_video_text, guidance_rescale=guidance_rescale
)
noise_pred_audio = rescale_noise_cfg(
noise_pred_audio, noise_pred_audio_text, guidance_rescale=guidance_rescale
)
latents = self._step_video_latents_i2v(
noise_pred_video,
latents,
t,
latent_num_frames,
latent_height,
latent_width,
)
audio_latents = audio_scheduler.step(noise_pred_audio, t, audio_latents, return_dict=False)[0]
latents, audio_latents = self.scheduler_step_maybe_with_cfg(
(noise_pred_video, noise_pred_audio),
(t, t),
(latents, audio_latents),
do_true_cfg=do_true_cfg,
per_request_scheduler=i2v_scheduler,
)
latents, audio_latents = self._synchronize_cfg_parallel_step_output(
(latents, audio_latents),
do_true_cfg=do_true_cfg,
)
pbar.update()
@@ -748,10 +685,15 @@ class LTX2ImageToVideoPipeline(LTX2Pipeline):
latents, self.vae.latents_mean, self.vae.latents_std, self.vae.config.scaling_factor
)
audio_latents = self._unpad_audio_latents(audio_latents, original_audio_num_frames)
audio_latents = self._denormalize_audio_latents(
audio_latents, self.audio_vae.latents_mean, self.audio_vae.latents_std
)
audio_latents = self._unpack_audio_latents(audio_latents, audio_num_frames, num_mel_bins=latent_mel_bins)
audio_latents = self._unpack_audio_latents(
audio_latents,
original_audio_num_frames,
num_mel_bins=latent_mel_bins,
)
if output_type == "latent":
video = latents
@@ -1624,7 +1624,12 @@ def get_magi_human_post_process_func(*args, **kwargs):
def post_process(output):
if isinstance(output, tuple) and len(output) == 2:
video, audio = output
return {"video": video, "audio": audio}
return {
"video": video,
"audio": audio,
"audio_sample_rate": 44100,
"fps": 25,
}
return output
return post_process
@@ -114,6 +114,8 @@ class NextStepConfig(LlamaConfig):
class NextStepModel(nn.Module):
_layerwise_offload_blocks_attrs = ["layers"]
def __init__(self, config: NextStepConfig):
super().__init__()
self.config = config
@@ -366,6 +366,7 @@ class OvisImageTransformer2DModel(nn.Module):
"""
_repeated_blocks = ["OvisImageTransformerBlock", "OvisImageSingleTransformerBlock"]
_layerwise_offload_blocks_attrs = ["transformer_blocks", "single_transformer_blocks"]
def __init__(
self,
@@ -387,6 +387,7 @@ class SD3Transformer2DModel(nn.Module):
"""
_repeated_blocks = ["SD3TransformerBlock"]
_layerwise_offload_blocks_attrs = ["transformer_blocks"]
def __init__(
self,
@@ -272,7 +272,7 @@ class Wan22Pipeline(nn.Module, CFGParallelMixin, ProgressBarMixin, DiffusionPipe
model, subfolder="text_encoder", torch_dtype=dtype, local_files_only=local_files_only
).to(self.device)
self.vae = DistributedAutoencoderKLWan.from_pretrained(
model, subfolder="vae", torch_dtype=torch.float32, local_files_only=local_files_only
model, subfolder="vae", torch_dtype=dtype, local_files_only=local_files_only
).to(self.device)
# Initialize transformers with correct config (weights loaded via load_weights)
@@ -217,7 +217,7 @@ class Wan22I2VPipeline(
# VAE
self.vae = DistributedAutoencoderKLWan.from_pretrained(
model, subfolder="vae", torch_dtype=torch.float32, local_files_only=local_files_only
model, subfolder="vae", torch_dtype=dtype, local_files_only=local_files_only
).to(self.device)
# Transformers (weights loaded via load_weights)
@@ -24,13 +24,13 @@ from typing import Any, cast
import numpy as np
import PIL.Image
import torch
from diffusers import AutoencoderKLWan
from diffusers.utils.torch_utils import randn_tensor
from torch import nn
from transformers import AutoTokenizer, UMT5EncoderModel
from vllm.model_executor.models.utils import AutoWeightsLoader
from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig
from vllm_omni.diffusion.distributed.autoencoders.autoencoder_kl_wan import OmniAutoencoderKLWan
from vllm_omni.diffusion.distributed.cfg_parallel import CFGParallelMixin
from vllm_omni.diffusion.distributed.utils import get_local_device
from vllm_omni.diffusion.model_loader.diffusers_loader import DiffusersPipelineLoader
@@ -174,8 +174,8 @@ class Wan22TI2VPipeline(nn.Module, SupportImageInput, CFGParallelMixin, Progress
).to(self.device)
# VAE
self.vae = AutoencoderKLWan.from_pretrained(
model, subfolder="vae", torch_dtype=torch.float32, local_files_only=local_files_only
self.vae = OmniAutoencoderKLWan.from_pretrained(
model, subfolder="vae", torch_dtype=dtype, local_files_only=local_files_only
).to(self.device)
# Single transformer (TI2V uses dense 5B model, not MoE)
@@ -21,9 +21,10 @@ import os
from collections.abc import Callable, Iterable
from typing import Any
import PIL.Image
import torch
import torch.nn as nn
from diffusers.image_processor import VaeImageProcessor
from diffusers.image_processor import PipelineImageInput, VaeImageProcessor
from diffusers.schedulers import FlowMatchEulerDiscreteScheduler
from diffusers.utils import logging
from diffusers.utils.torch_utils import randn_tensor
@@ -59,7 +60,7 @@ def get_post_process_func(
vae_config = json.load(f)
vae_scale_factor = 2 ** (len(vae_config["block_out_channels"]) - 1) if "block_out_channels" in vae_config else 8
image_processor = VaeImageProcessor(vae_scale_factor=vae_scale_factor * 2)
image_processor = VaeImageProcessor(vae_scale_factor=vae_scale_factor * 2, do_convert_rgb=True)
def post_process_func(
images: torch.Tensor,
@@ -83,6 +84,20 @@ def calculate_shift(
return mu
# Copied from diffusers
def retrieve_latents(
encoder_output: torch.Tensor, generator: torch.Generator | None = None, sample_mode: str = "sample"
):
if hasattr(encoder_output, "latent_dist") and sample_mode == "sample":
return encoder_output.latent_dist.sample(generator)
elif hasattr(encoder_output, "latent_dist") and sample_mode == "argmax":
return encoder_output.latent_dist.mode()
elif hasattr(encoder_output, "latents"):
return encoder_output.latents
else:
raise AttributeError("Could not access latents of provided encoder_output")
# Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.retrieve_timesteps
def retrieve_timesteps(
scheduler,
@@ -187,6 +202,8 @@ class ZImagePipeline(nn.Module, DiffusionPipelineProfilerMixin):
enable_diffusion_pipeline_profiler=self.od_config.enable_diffusion_pipeline_profiler
)
self.image_processor = VaeImageProcessor(vae_scale_factor=self.vae_scale_factor * 2, do_convert_rgb=True)
def encode_prompt(
self,
prompt: str | list[str],
@@ -282,12 +299,45 @@ class ZImagePipeline(nn.Module, DiffusionPipelineProfilerMixin):
device,
generator,
latents=None,
image=None,
timestep=None,
):
height = 2 * (int(height) // (self.vae_scale_factor * 2))
width = 2 * (int(width) // (self.vae_scale_factor * 2))
shape = (batch_size, num_channels_latents, height, width)
if image is not None:
if latents is not None:
return latents.to(device=device, dtype=dtype)
image = image.to(device=device, dtype=dtype)
if image.shape[1] != num_channels_latents:
if isinstance(generator, list):
image_latents = [
retrieve_latents(self.vae.encode(image[i : i + 1]), generator=generator[i])
for i in range(image.shape[0])
]
image_latents = torch.cat(image_latents, dim=0)
else:
image_latents = retrieve_latents(self.vae.encode(image), generator=generator)
image_latents = (image_latents - self.vae.config.shift_factor) * self.vae.config.scaling_factor
else:
image_latents = image
if batch_size > image_latents.shape[0] and batch_size % image_latents.shape[0] == 0:
additional_image_per_prompt = batch_size // image_latents.shape[0]
image_latents = torch.cat([image_latents] * additional_image_per_prompt, dim=0)
elif batch_size > image_latents.shape[0] and batch_size % image_latents.shape[0] != 0:
raise ValueError(
f"Cannot duplicate `image` of batch size {image_latents.shape[0]} to {batch_size} text prompts."
)
noise = randn_tensor(shape, generator=generator, device=device, dtype=dtype)
latents = self.scheduler.scale_noise(image_latents, timestep, noise)
return latents
if latents is None:
latents = randn_tensor(shape, generator=generator, device=device, dtype=dtype)
else:
@@ -296,6 +346,14 @@ class ZImagePipeline(nn.Module, DiffusionPipelineProfilerMixin):
latents = latents.to(device)
return latents
def get_timesteps(self, num_inference_steps, strength, device):
init_timestep = min(num_inference_steps * strength, num_inference_steps)
t_start = int(max(num_inference_steps - init_timestep, 0))
timesteps = self.scheduler.timesteps[t_start * self.scheduler.order :]
if hasattr(self.scheduler, "set_begin_index"):
self.scheduler.set_begin_index(t_start * self.scheduler.order)
return timesteps, num_inference_steps - t_start
@property
def guidance_scale(self):
return self._guidance_scale
@@ -320,6 +378,8 @@ class ZImagePipeline(nn.Module, DiffusionPipelineProfilerMixin):
self,
req: OmniDiffusionRequest,
prompt: str | list[str] | None = None,
image: PipelineImageInput = None,
strength: float = 0.6,
height: int = 1024,
width: int = 1024,
num_inference_steps: int = 50,
@@ -347,6 +407,11 @@ class ZImagePipeline(nn.Module, DiffusionPipelineProfilerMixin):
prompt (`str` or `list[str]`, *optional*):
The prompt or prompts to guide the image generation. If not defined, one has to pass `prompt_embeds`.
instead.
image (`PipelineImageInput`, *optional*):
The image to use for img2img generation. If provided, the pipeline
will perform img2img instead of text-to-image.
strength (`float`, *optional*, defaults to 0.6):
Indicates extent to transform the reference `image`. Must be between 0 and 1.
height (`int`, *optional*, defaults to 1024):
The height in pixels of the generated image.
width (`int`, *optional*, defaults to 1024):
@@ -425,6 +490,34 @@ class ZImagePipeline(nn.Module, DiffusionPipelineProfilerMixin):
elif req.prompts:
negative_prompt = ["" if isinstance(p, str) else (p.get("negative_prompt") or "") for p in req.prompts]
# Handle img2img: extract image from request
if image is None and req.prompts:
if len(req.prompts) > 1:
logger.warning(
"This model only supports a single prompt for img2img, not a batched request. "
"Taking only the first image for now."
)
first_prompt = req.prompts[0]
if not isinstance(first_prompt, str):
raw_image = first_prompt.get("multi_modal_data", {}).get("image")
if raw_image is not None:
if isinstance(raw_image, list):
image = [PIL.Image.open(im) if isinstance(im, str) else raw_image[0] for im in raw_image[:1]]
else:
image = PIL.Image.open(raw_image) if isinstance(raw_image, str) else raw_image
# strength is currently only applicable for Z-Image I2I; other pipelines ignore this parameter
strength = req.sampling_params.strength if req.sampling_params.strength is not None else strength
if strength is not None and image is None:
logger.warning(
"strength parameter (%.2f) is only applicable for image-to-image (I2I) generation. "
"It will be ignored for text-to-image (T2I) generation.",
strength,
)
strength = None
if image is not None and strength is not None and (strength < 0 or strength > 1):
raise ValueError(f"The value of strength should be in [0.0, 1.0] but is {strength}")
height = req.sampling_params.height or height
width = req.sampling_params.width or width
num_inference_steps = req.sampling_params.num_inference_steps or num_inference_steps
@@ -491,16 +584,71 @@ class ZImagePipeline(nn.Module, DiffusionPipelineProfilerMixin):
# 4. Prepare latent variables
num_channels_latents = self.transformer.in_channels
latents = self.prepare_latents(
batch_size * num_images_per_prompt,
num_channels_latents,
height,
width,
torch.float32,
device,
generator,
latents,
)
# img2img mode: prepare latents from input image
if image is not None:
# Handle image list - take first image
if isinstance(image, list):
image = image[0]
# Prepare image for VAE encoding using image_processor
if not isinstance(image, torch.Tensor):
init_image = self.image_processor.preprocess(image, height, width)
image = init_image.to(dtype=torch.float32, device=device)
# Initialize scheduler kwargs for img2img
mu = calculate_shift(
(height // self.vae_scale_factor // 2) * (width // self.vae_scale_factor // 2),
self.scheduler.config.get("base_image_seq_len", 256),
self.scheduler.config.get("max_image_seq_len", 4096),
self.scheduler.config.get("base_shift", 0.5),
self.scheduler.config.get("max_shift", 1.15),
)
self.scheduler.sigma_min = 0.0
scheduler_kwargs = {"mu": mu}
# First initialize timesteps in scheduler
timesteps, num_inference_steps = retrieve_timesteps(
self.scheduler,
num_inference_steps,
device,
sigmas=sigmas,
**scheduler_kwargs,
)
# Then adjust timesteps based on strength
timesteps, num_inference_steps = self.get_timesteps(num_inference_steps, strength, device)
if num_inference_steps < 1:
raise ValueError(
f"After adjusting the num_inference_steps by strength parameter: "
f"{strength}, the number of pipeline steps is {num_inference_steps} "
f"which is < 1 and not appropriate for this pipeline."
)
latent_timestep = timesteps[:1].repeat(batch_size * num_images_per_prompt)
latents = self.prepare_latents(
batch_size * num_images_per_prompt,
num_channels_latents,
height,
width,
prompt_embeds[0].dtype,
device,
generator,
latents,
image,
latent_timestep,
)
else:
latents = self.prepare_latents(
batch_size * num_images_per_prompt,
num_channels_latents,
height,
width,
torch.float32,
device,
generator,
latents,
)
# Repeat prompt_embeds for num_images_per_prompt
if num_images_per_prompt > 1:
@@ -509,25 +657,28 @@ class ZImagePipeline(nn.Module, DiffusionPipelineProfilerMixin):
negative_prompt_embeds = [npe for npe in negative_prompt_embeds for _ in range(num_images_per_prompt)]
actual_batch_size = batch_size * num_images_per_prompt
image_seq_len = (latents.shape[2] // 2) * (latents.shape[3] // 2)
# 5. Prepare timesteps
mu = calculate_shift(
image_seq_len,
self.scheduler.config.get("base_image_seq_len", 256),
self.scheduler.config.get("max_image_seq_len", 4096),
self.scheduler.config.get("base_shift", 0.5),
self.scheduler.config.get("max_shift", 1.15),
)
self.scheduler.sigma_min = 0.0
scheduler_kwargs = {"mu": mu}
timesteps, num_inference_steps = retrieve_timesteps(
self.scheduler,
num_inference_steps,
device,
sigmas=sigmas,
**scheduler_kwargs,
)
if image is None:
image_seq_len = (latents.shape[2] // 2) * (latents.shape[3] // 2)
mu = calculate_shift(
image_seq_len,
self.scheduler.config.get("base_image_seq_len", 256),
self.scheduler.config.get("max_image_seq_len", 4096),
self.scheduler.config.get("base_shift", 0.5),
self.scheduler.config.get("max_shift", 1.15),
)
self.scheduler.sigma_min = 0.0
scheduler_kwargs = {"mu": mu}
timesteps, num_inference_steps = retrieve_timesteps(
self.scheduler,
num_inference_steps,
device,
sigmas=sigmas,
**scheduler_kwargs,
)
num_warmup_steps = max(len(timesteps) - num_inference_steps * self.scheduler.order, 0)
self._num_timesteps = len(timesteps)
@@ -21,7 +21,7 @@ class PipelineModules:
class ModuleDiscovery:
"""Discovers pipeline components for offloading"""
DIT_ATTRS = ["transformer", "transformer_2", "dit", "sr_dit", "language_model", "transformer_blocks"]
DIT_ATTRS = ["transformer", "transformer_2", "dit", "sr_dit", "language_model", "transformer_blocks", "model"]
ENCODER_ATTRS = ["text_encoder", "text_encoder_2", "text_encoder_3", "image_encoder"]
VAE_ATTRS = ["vae", "audio_vae"]
+2
View File
@@ -26,6 +26,8 @@ class OmniDiffusionRequest:
sampling_params: OmniDiffusionSamplingParams
request_ids: list[str] = field(default_factory=list)
request_id: str | None = None
kv_sender_info: dict | None = None
def __post_init__(self):
"""Initialize dependent fields after dataclass initialization."""
+11 -1
View File
@@ -179,6 +179,7 @@ class StageDiffusionClient:
request_id: str,
prompt: OmniPromptType,
sampling_params: OmniDiffusionSamplingParams,
kv_sender_info: dict[int, dict[str, Any]] | None = None,
) -> None:
self._request_socket.send(
self._encoder.encode(
@@ -187,6 +188,7 @@ class StageDiffusionClient:
"request_id": request_id,
"prompt": prompt,
"sampling_params": self._sampling_params_to_dict(sampling_params),
"kv_sender_info": kv_sender_info,
}
)
)
@@ -198,6 +200,7 @@ class StageDiffusionClient:
request_id: str,
prompts: list[OmniPromptType],
sampling_params: OmniDiffusionSamplingParams,
kv_sender_info: dict[int, dict[str, Any]] | None = None,
) -> None:
"""Submit a list of prompts as a single batched engine call.
@@ -206,7 +209,12 @@ class StageDiffusionClient:
*request_id*.
"""
task = asyncio.create_task(
self._run_batch(request_id, prompts, sampling_params),
self._run_batch(
request_id,
prompts,
sampling_params,
kv_sender_info,
),
name=f"diffusion-batch-{request_id}",
)
self._tasks[request_id] = task
@@ -216,6 +224,7 @@ class StageDiffusionClient:
request_id: str,
prompts: list[OmniPromptType],
sampling_params: OmniDiffusionSamplingParams,
kv_sender_info: dict[int, dict[str, Any]] | None = None,
) -> None:
try:
self._request_socket.send(
@@ -225,6 +234,7 @@ class StageDiffusionClient:
"request_id": request_id,
"prompts": prompts,
"sampling_params": self._sampling_params_to_dict(sampling_params),
"kv_sender_info": kv_sender_info,
}
)
)
+54 -5
View File
@@ -14,8 +14,10 @@ from multiprocessing.process import BaseProcess
from typing import TYPE_CHECKING, Any
import msgspec
import torch
import zmq
import zmq.asyncio
from PIL import Image
from vllm.logger import init_logger
from vllm.transformers_utils.config import get_hf_file_to_dict
from vllm.utils.network_utils import get_open_zmq_ipc_path, zmq_socket_ctx
@@ -128,6 +130,7 @@ class StageDiffusionProc:
request_id: str,
prompt: Any,
sampling_params_dict: dict,
kv_sender_info: dict[str, Any] | None = None,
) -> OmniRequestOutput:
"""Build a diffusion request and run DiffusionEngine.step()."""
sampling_params = self._reconstruct_sampling_params(sampling_params_dict)
@@ -136,6 +139,8 @@ class StageDiffusionProc:
prompts=[prompt],
sampling_params=sampling_params,
request_ids=[request_id],
request_id=request_id,
kv_sender_info=kv_sender_info,
)
loop = asyncio.get_running_loop()
@@ -150,6 +155,7 @@ class StageDiffusionProc:
request_id: str,
prompts: list[Any],
sampling_params_dict: dict,
kv_sender_info: dict[str, Any] | None = None,
) -> OmniRequestOutput:
"""Build a batched diffusion request and run DiffusionEngine.step().
@@ -163,7 +169,9 @@ class StageDiffusionProc:
request = OmniDiffusionRequest(
prompts=prompts,
sampling_params=sampling_params,
request_ids=[request_id] * len(prompts),
request_ids=[f"{request_id}-{i}" for i in range(len(prompts))],
request_id=request_id,
kv_sender_info=kv_sender_info,
)
loop = asyncio.get_running_loop()
@@ -174,8 +182,13 @@ class StageDiffusionProc:
merged_mm: dict[str, Any] = {}
merged_metrics: dict[str, Any] = {}
merged_durations: dict[str, float] = {}
merged_custom: dict[str, Any] = {}
peak_mem = 0.0
latents = None
trajectory_latents: list[torch.Tensor] | None = None
trajectory_timesteps: list[torch.Tensor] | None = None
trajectory_log_probs: torch.Tensor | None = None
trajectory_decoded: list[Image.Image] | None = None
final_output_type = "image"
for r in results:
@@ -183,9 +196,18 @@ class StageDiffusionProc:
merged_mm.update(r._multimodal_output)
merged_metrics.update(r.metrics)
merged_durations.update(r.stage_durations)
merged_custom.update(r._custom_output)
peak_mem = max(peak_mem, r.peak_memory_mb)
if latents is None and r.latents is not None:
latents = r.latents
if trajectory_latents is None:
trajectory_latents = r.trajectory_latents
if trajectory_timesteps is None:
trajectory_timesteps = r.trajectory_timesteps
if trajectory_log_probs is None:
trajectory_log_probs = r.trajectory_log_probs
if trajectory_decoded is None:
trajectory_decoded = r.trajectory_decoded
if r.final_output_type != "image":
final_output_type = r.final_output_type
@@ -195,6 +217,11 @@ class StageDiffusionProc:
prompt=prompts[0] if len(prompts) == 1 else None,
metrics=merged_metrics,
latents=latents,
trajectory_latents=trajectory_latents,
trajectory_timesteps=trajectory_timesteps,
trajectory_log_probs=trajectory_log_probs,
trajectory_decoded=trajectory_decoded,
custom_output=merged_custom or None,
multimodal_output=merged_mm or None,
final_output_type=final_output_type,
stage_durations=merged_durations,
@@ -325,10 +352,20 @@ class StageDiffusionProc:
tasks: dict[str, asyncio.Task] = {}
async def _dispatch_request(request_id: str, prompt: Any, sampling_params_dict: dict) -> None:
async def _dispatch_request(
request_id: str,
prompt: Any,
sampling_params_dict: dict,
kv_sender_info: dict[str, Any] | None = None,
) -> None:
"""Process a single diffusion request and send the response."""
try:
result = await self._process_request(request_id, prompt, sampling_params_dict)
result = await self._process_request(
request_id,
prompt,
sampling_params_dict,
kv_sender_info=kv_sender_info,
)
await response_socket.send(encoder.encode({"type": "result", "output": result}))
except DiffusionRequestAbortedError as e:
logger.info(
@@ -363,6 +400,7 @@ class StageDiffusionProc:
request_id,
msg["prompt"],
msg["sampling_params"],
msg.get("kv_sender_info"),
)
)
tasks[request_id] = task
@@ -370,9 +408,19 @@ class StageDiffusionProc:
elif msg_type == "add_batch_request":
request_id = msg["request_id"]
async def _dispatch_batch(rid: str, prompts: list, sp_dict: dict) -> None:
async def _dispatch_batch(
rid: str,
prompts: list,
sp_dict: dict,
kv_sender_info: dict[str, Any] | None = None,
) -> None:
try:
result = await self._process_batch_request(rid, prompts, sp_dict)
result = await self._process_batch_request(
rid,
prompts,
sp_dict,
kv_sender_info=kv_sender_info,
)
await response_socket.send(encoder.encode({"type": "result", "output": result}))
except DiffusionRequestAbortedError as e:
logger.info(
@@ -399,6 +447,7 @@ class StageDiffusionProc:
request_id,
msg["prompts"],
msg["sampling_params"],
msg.get("kv_sender_info"),
)
)
tasks[request_id] = task
@@ -277,13 +277,15 @@ class MooncakeTransferEngineConnector(OmniConnectorBase):
}
self.config = config
host_config = config.get("host", "127.0.0.1")
# Support "auto" to auto-detect local IP address
if host_config.lower() == "auto":
host_config = config.get("host")
host_value = "auto" if host_config is None else str(host_config)
# Default sender/receiver bootstrap to a routable local IP so the
# advertised endpoint matches the interface Mooncake binds.
if host_value.lower() == "auto" or host_value in {"", "*", "0.0.0.0", "::"}:
self.host = self._get_local_ip()
logger.info(f"Auto-detected local IP for RDMA: {self.host}")
else:
self.host = host_config
self.host = host_value
self.zmq_port = config.get("zmq_port", 50051)
self.protocol = config.get("protocol", "rdma")
@@ -2,6 +2,8 @@
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Unified OmniConnector and KV cache transfer management."""
import json
import struct
import time
from collections.abc import Callable
from dataclasses import asdict, dataclass
@@ -12,12 +14,36 @@ from vllm.logger import init_logger
from .factory import OmniConnectorFactory
from .utils.config import ConnectorSpec
from .utils.initialization import KV_TRANSFER_PORT_OFFSET
from .utils.kv_utils import normalize_layer_kv
logger = init_logger(__name__)
LayerKV = torch.Tensor | tuple[torch.Tensor, torch.Tensor]
_SAFE_TORCH_DTYPES = {
name: dtype
for name in (
"bool",
"uint8",
"int8",
"int16",
"int32",
"int64",
"float16",
"float32",
"float64",
"bfloat16",
"complex64",
"complex128",
"float8_e4m3fn",
"float8_e4m3fnuz",
"float8_e5m2",
"float8_e5m2fnuz",
)
if isinstance((dtype := getattr(torch, name, None)), torch.dtype)
}
@dataclass
class OmniKVCacheConfig:
@@ -46,6 +72,242 @@ class KVCacheTransferData:
"""Convert to dictionary for serialization."""
return asdict(self)
def to_bytes(self) -> bytes:
"""Convert to compact binary format for fast transfer."""
tensors_desc: list[dict[str, Any]] = []
tensor_bufs: list[bytes] = []
data_offset = 0
for cache_name in ("key_cache", "value_cache"):
cache_list = self.layer_blocks.get(cache_name, [])
for layer_idx, tensor in enumerate(cache_list):
if tensor is None:
tensors_desc.append({"n": f"{cache_name}_{layer_idx}", "x": True})
continue
t = tensor.detach().cpu().contiguous()
dtype_str = str(t.dtype).removeprefix("torch.")
raw = t.view(torch.uint8).numpy().tobytes()
tensors_desc.append(
{
"n": f"{cache_name}_{layer_idx}",
"i": layer_idx,
"d": dtype_str,
"s": list(t.shape),
"o": data_offset,
"b": len(raw),
}
)
tensor_bufs.append(raw)
data_offset += len(raw)
header = json.dumps(
{
"rid": self.request_id,
"bids": self.block_ids,
"meta": self.metadata,
"td": tensors_desc,
"nl": len(self.layer_blocks.get("key_cache", [])),
},
separators=(",", ":"),
).encode("utf-8")
return b"".join([struct.pack(">I", len(header)), header] + tensor_bufs)
def to_gpu_tensor(self) -> torch.Tensor:
"""Convert to a packed GPU tensor for raw-data connectors."""
tensors_desc: list[dict[str, Any]] = []
gpu_tensors: list[torch.Tensor] = []
data_offset = 0
device = None
for cache_name in ("key_cache", "value_cache"):
cache_list = self.layer_blocks.get(cache_name, [])
for layer_idx, tensor in enumerate(cache_list):
if tensor is None:
tensors_desc.append({"n": f"{cache_name}_{layer_idx}", "x": True})
continue
t = tensor.detach().contiguous()
if device is None and t.is_cuda:
device = t.device
dtype_str = str(t.dtype).removeprefix("torch.")
nbytes = t.numel() * t.element_size()
tensors_desc.append(
{
"n": f"{cache_name}_{layer_idx}",
"i": layer_idx,
"d": dtype_str,
"s": list(t.shape),
"o": data_offset,
"b": nbytes,
}
)
gpu_tensors.append(t.view(torch.uint8).flatten())
data_offset += nbytes
if device is None:
raise RuntimeError("No CUDA tensors found, use to_bytes() instead")
header = json.dumps(
{
"rid": self.request_id,
"bids": self.block_ids,
"meta": self.metadata,
"td": tensors_desc,
"nl": len(self.layer_blocks.get("key_cache", [])),
},
separators=(",", ":"),
).encode("utf-8")
header_prefix = struct.pack(">I", len(header)) + header
total_size = len(header_prefix) + data_offset
output = torch.empty(total_size, dtype=torch.uint8, device=device)
header_tensor = torch.frombuffer(bytearray(header_prefix), dtype=torch.uint8)
output[: len(header_prefix)].copy_(header_tensor)
pos = len(header_prefix)
for t_flat in gpu_tensors:
n = t_flat.numel()
output[pos : pos + n].copy_(t_flat)
pos += n
return output
@staticmethod
def _load_header_from_memoryview(raw_mv: memoryview) -> tuple[dict[str, Any], memoryview]:
if len(raw_mv) < 4:
raise ValueError("Corrupted KV payload: missing 4-byte header length")
header_len = struct.unpack(">I", raw_mv[:4])[0]
if header_len > len(raw_mv) - 4:
raise ValueError(f"Corrupted KV payload: header_len={header_len} exceeds buffer size={len(raw_mv)}")
return json.loads(bytes(raw_mv[4 : 4 + header_len])), raw_mv[4 + header_len :]
@staticmethod
def _load_header_from_tensor(gpu_tensor: torch.Tensor) -> tuple[dict[str, Any], int]:
if gpu_tensor.dtype != torch.uint8 or gpu_tensor.dim() != 1:
raise ValueError("Packed GPU KV payload must be a 1-D uint8 tensor")
total_bytes = int(gpu_tensor.numel())
if total_bytes < 4:
raise ValueError("Corrupted KV payload: missing 4-byte header length")
header_len = struct.unpack(">I", gpu_tensor[:4].cpu().numpy().tobytes())[0]
if header_len > total_bytes - 4:
raise ValueError(f"Corrupted KV payload: header_len={header_len} exceeds buffer size={total_bytes}")
header_bytes = gpu_tensor[4 : 4 + header_len].cpu().numpy().tobytes()
return json.loads(header_bytes), 4 + header_len
@staticmethod
def _validate_tensor_span(name: str, info: dict[str, Any], tensor_data_bytes: int) -> tuple[int, int]:
offset = info["o"]
nbytes = info["b"]
if offset < 0 or nbytes < 0 or offset + nbytes > tensor_data_bytes:
raise ValueError(
f"Corrupted KV payload tensor span for {name}: "
f"offset={offset}, bytes={nbytes}, tensor_data_bytes={tensor_data_bytes}"
)
return offset, nbytes
@staticmethod
def _resolve_torch_dtype(dtype_name: Any) -> torch.dtype:
torch_dtype = _SAFE_TORCH_DTYPES.get(str(dtype_name))
if torch_dtype is None:
raise ValueError(f"Unsupported dtype in KV payload: {dtype_name}")
return torch_dtype
@staticmethod
def _resolve_layer_idx(info: dict[str, Any], num_layers: int) -> int:
layer_idx = info.get("i")
if layer_idx is None:
name = info.get("n")
if isinstance(name, str) and name.startswith("key_cache_"):
layer_idx = int(name.removeprefix("key_cache_"))
elif isinstance(name, str) and name.startswith("value_cache_"):
layer_idx = int(name.removeprefix("value_cache_"))
else:
raise ValueError(f"Invalid KV tensor name in payload: {name}")
if not isinstance(layer_idx, int):
raise ValueError(f"Invalid layer index in KV payload: {layer_idx}")
if layer_idx < 0 or layer_idx >= num_layers:
raise ValueError(f"Invalid layer index in KV payload: {layer_idx} (num_layers={num_layers})")
return layer_idx
@staticmethod
def from_bytes(raw: "bytes | bytearray | memoryview") -> dict[str, Any]:
"""Reconstruct KV cache data from the packed bytes format."""
raw_mv = memoryview(raw) if not isinstance(raw, memoryview) else raw
header, tensor_data_mv = KVCacheTransferData._load_header_from_memoryview(raw_mv)
num_layers = header["nl"]
key_cache: list[torch.Tensor | None] = [None] * num_layers
value_cache: list[torch.Tensor | None] = [None] * num_layers
for info in header["td"]:
if info.get("x"):
continue
name: str = info["n"]
torch_dtype = KVCacheTransferData._resolve_torch_dtype(info["d"])
offset, nbytes = KVCacheTransferData._validate_tensor_span(name, info, len(tensor_data_mv))
t = (
torch.frombuffer(
tensor_data_mv,
dtype=torch.uint8,
offset=offset,
count=nbytes,
)
.view(torch_dtype)
.reshape(info["s"])
)
layer_idx = KVCacheTransferData._resolve_layer_idx(info, num_layers)
if name.startswith("key_cache_"):
key_cache[layer_idx] = t
elif name.startswith("value_cache_"):
value_cache[layer_idx] = t
return {
"request_id": header["rid"],
"layer_blocks": {"key_cache": key_cache, "value_cache": value_cache},
"block_ids": header["bids"],
"metadata": header["meta"],
}
@staticmethod
def from_bytes_gpu(gpu_tensor: torch.Tensor) -> dict[str, Any]:
"""Reconstruct KV cache data from a packed GPU tensor."""
header, data_start = KVCacheTransferData._load_header_from_tensor(gpu_tensor)
num_layers = header["nl"]
key_cache: list[torch.Tensor | None] = [None] * num_layers
value_cache: list[torch.Tensor | None] = [None] * num_layers
tensor_data_bytes = int(gpu_tensor.numel()) - data_start
for info in header["td"]:
if info.get("x"):
continue
name: str = info["n"]
torch_dtype = KVCacheTransferData._resolve_torch_dtype(info["d"])
offset, nbytes = KVCacheTransferData._validate_tensor_span(name, info, tensor_data_bytes)
t = gpu_tensor[data_start + offset : data_start + offset + nbytes].clone()
t = t.view(torch_dtype).reshape(info["s"])
layer_idx = KVCacheTransferData._resolve_layer_idx(info, num_layers)
if name.startswith("key_cache_"):
key_cache[layer_idx] = t
elif name.startswith("value_cache_"):
value_cache[layer_idx] = t
return {
"request_id": header["rid"],
"layer_blocks": {"key_cache": key_cache, "value_cache": value_cache},
"block_ids": header["bids"],
"metadata": header["meta"],
}
class OmniKVTransferManager:
"""Unified management for OmniConnector and KV cache transfer.
@@ -79,6 +341,13 @@ class OmniKVTransferManager:
else (None, None)
)
if config.need_send_cache and config.connector_config:
try:
_ = self.connector
logger.info("Sender connector eagerly initialized")
except Exception as e:
logger.warning("Failed to eagerly initialize sender connector: %s", e)
@classmethod
def _create(cls, cfg: dict | None) -> "OmniKVTransferManager":
"""Create manager from raw config dict."""
@@ -140,8 +409,39 @@ class OmniKVTransferManager:
cfg = self.config.connector_config
if cfg and (c_type := cfg.get("type")):
try:
logger.info(f"Initializing OmniConnector with config: {cfg}")
c_extra = {k: v for k, v in cfg.items() if k != "type"}
if c_type == "MooncakeTransferEngineConnector":
base_port = c_extra.get("zmq_port", 50051)
c_extra["from_stage"] = (
str(self.config.from_stage) if self.config.from_stage is not None else "0"
)
c_extra["to_stage"] = str(self.config.to_stage) if self.config.to_stage is not None else "1"
if self.config.need_send_cache:
c_extra["role"] = "sender"
from_stage = self.config.from_stage
if from_stage is not None:
try:
c_extra["zmq_port"] = base_port + KV_TRANSFER_PORT_OFFSET + int(from_stage)
except (TypeError, ValueError):
c_extra["zmq_port"] = base_port + KV_TRANSFER_PORT_OFFSET
elif self.config.need_recv_cache:
c_extra["role"] = "receiver"
from_stage = self.config.from_stage
sender_port = base_port + KV_TRANSFER_PORT_OFFSET
if from_stage is not None:
try:
sender_port = base_port + KV_TRANSFER_PORT_OFFSET + int(from_stage)
except (TypeError, ValueError):
pass
c_extra.setdefault("sender_host", c_extra.get("host", "127.0.0.1"))
c_extra.setdefault("sender_zmq_port", sender_port)
logger.info(
"Initializing OmniConnector (purpose=kv_transfer) with config: %s, role: %s",
cfg,
c_extra.get("role", "N/A"),
)
self._connector = OmniConnectorFactory.create_connector(ConnectorSpec(name=c_type, extra=c_extra))
except Exception as e:
logger.error(f"Failed to initialize OmniConnector: {e}")
@@ -157,6 +457,85 @@ class OmniKVTransferManager:
"""Get connector (compatibility wrapper for existing code)."""
return self.connector
def _resolve_sender_info(
self, sender_info: dict[str, Any], sender_stage_id: str | int | None = None
) -> dict[str, Any] | None:
if not sender_info:
return None
if "host" in sender_info:
return sender_info
if not isinstance(sender_info, dict):
return None
preferred_keys: list[str | int] = []
if sender_stage_id is None:
recv_from, _ = self.recv_stages
sender_stage_id = recv_from
if sender_stage_id is not None:
preferred_keys.append(sender_stage_id)
preferred_keys.append(str(sender_stage_id))
try:
preferred_keys.append(int(sender_stage_id))
except (TypeError, ValueError):
pass
for key in dict.fromkeys(preferred_keys):
info = sender_info.get(key)
if isinstance(info, dict) and "host" in info:
return info
candidates = [info for info in sender_info.values() if isinstance(info, dict) and "host" in info]
if len(candidates) == 1:
return candidates[0]
if candidates:
logger.warning(
"Ambiguous sender_info for sender_stage_id=%s: "
"expected caller to resolve a single sender entry, got %s",
sender_stage_id,
sender_info,
)
return None
@staticmethod
def _clone_received_payload_tensors(data: dict[str, Any]) -> dict[str, Any]:
if not isinstance(data, dict) or "layer_blocks" not in data:
return data
layer_blocks = data["layer_blocks"]
for cache_name in ("key_cache", "value_cache"):
cache_list = layer_blocks.get(cache_name, [])
for idx, tensor in enumerate(cache_list):
if isinstance(tensor, torch.Tensor):
cache_list[idx] = tensor.clone()
return data
def update_sender_info(self, sender_info: dict[str, Any], sender_stage_id: str | int | None = None) -> None:
"""Update receiver-side sender info before loading remote KV cache."""
if not self.config.need_recv_cache:
return
actual_info = self._resolve_sender_info(sender_info, sender_stage_id=sender_stage_id)
if not actual_info or "host" not in actual_info:
logger.warning("Invalid sender_info format: %s", sender_info)
return
if self.config.connector_config:
self.config.connector_config["sender_host"] = actual_info.get("host")
self.config.connector_config["sender_zmq_port"] = actual_info.get("zmq_port")
if self._connector and hasattr(self._connector, "update_sender_info"):
try:
self._connector.update_sender_info(actual_info.get("host"), actual_info.get("zmq_port"))
except Exception:
if hasattr(self._connector, "sender_host"):
self._connector.sender_host = actual_info.get("host")
if hasattr(self._connector, "sender_zmq_port"):
self._connector.sender_zmq_port = actual_info.get("zmq_port")
def handle_finished_requests_kv_transfer(
self,
finished_reqs: dict[str, dict[str, Any]],
@@ -203,7 +582,8 @@ class OmniKVTransferManager:
custom_metadata = data.get("custom_metadata")
# Extract KV cache from GPU blocks -> CPU tensors
# Extract KV cache from GPU blocks and keep it on-device when
# possible so raw-data connectors can use the fast path.
kv_data = self._extract_kv_cache(
req_id, block_ids, seq_len, kv_caches, block_size, cache_dtype, custom_metadata
)
@@ -280,9 +660,8 @@ class OmniKVTransferManager:
flat_k = flat_k[:seq_len]
flat_v = flat_v[:seq_len]
# Move to CPU
key_cache[layer_idx] = flat_k.detach().cpu().contiguous()
value_cache[layer_idx] = flat_v.detach().cpu().contiguous()
key_cache[layer_idx] = flat_k.detach().contiguous()
value_cache[layer_idx] = flat_v.detach().contiguous()
if not any(k is not None for k in key_cache):
return None
@@ -311,14 +690,40 @@ class OmniKVTransferManager:
if not from_stage or not to_stage:
raise ValueError("Transfer stages (omni_from_stage, omni_to_stage) not configured")
# Prepare data and transfer with retry
data_dict = kv_data.to_dict()
data_dict["request_id"] = transfer_req_id
kv_data.request_id = transfer_req_id
serialization_start = time.perf_counter()
transfer_data: torch.Tensor | bytes | dict[str, Any]
supports_raw = getattr(self.connector, "supports_raw_data", False)
success, size, _ = self._transfer_with_retry(from_stage, to_stage, f"kv_cache_{transfer_req_id}", data_dict)
try:
if supports_raw:
transfer_data = kv_data.to_gpu_tensor()
else:
raise RuntimeError("Connector does not support raw tensor")
except Exception:
try:
transfer_data = kv_data.to_bytes()
except Exception:
data_dict = kv_data.to_dict()
data_dict["request_id"] = transfer_req_id
transfer_data = data_dict
serialization_ms = (time.perf_counter() - serialization_start) * 1000
logger.info("KV cache serialized for %s in %.1f ms", transfer_req_id, serialization_ms)
transfer_start = time.perf_counter()
success, size, _ = self._transfer_with_retry(from_stage, to_stage, f"kv_cache_{transfer_req_id}", transfer_data)
elapsed = time.perf_counter() - transfer_start
if success:
logger.info(f"KV transfer OK: {transfer_req_id}, {size} bytes")
mbps = (size / 1024 / 1024) / elapsed if elapsed > 0 else 0
logger.info(
"KV transfer OK: %s, %s bytes, %.3fs, %.1f MB/s",
transfer_req_id,
size,
elapsed,
mbps,
)
else:
logger.error(f"KV transfer FAILED: {transfer_req_id}")
@@ -327,7 +732,7 @@ class OmniKVTransferManager:
from_stage: str,
to_stage: str,
request_id: str,
data: dict[str, Any],
data: "dict[str, Any] | bytes | torch.Tensor",
max_retries: int = 3,
) -> tuple[bool, int, dict[str, Any] | None]:
"""Transfer data with retry and exponential backoff.
@@ -393,6 +798,8 @@ class OmniKVTransferManager:
timeout = self.config.recv_timeout
start_time = time.time()
poll_interval = 0.01
max_poll_interval = 0.5
logger.info(f"Wait for KV cache for request {request_id} from stage {from_stage} to {to_stage}...")
@@ -400,33 +807,74 @@ class OmniKVTransferManager:
while True:
# Build the full key for connector
full_request_id = f"omni_{from_stage}_to_{to_stage}_kv_cache_{request_id}"
link_start = time.perf_counter()
result = self.connector.get(
from_stage=from_stage,
to_stage=to_stage,
get_key=full_request_id,
)
if result:
data, size = result
logger.info(f"Successfully received KV cache for {request_id}, {size} bytes")
raw_data, size = result
elapsed = time.time() - start_time
link_ms = (time.perf_counter() - link_start) * 1000
managed_buffer = None
# Move tensors to target device if specified
if target_device is not None and isinstance(data, dict) and "layer_blocks" in data:
layer_blocks = data["layer_blocks"]
for cache_list in [
layer_blocks.get("key_cache", []),
layer_blocks.get("value_cache", []),
]:
for i, tensor in enumerate(cache_list):
if isinstance(tensor, torch.Tensor) and tensor.device != target_device:
cache_list[i] = tensor.to(target_device).contiguous()
if hasattr(raw_data, "tensor") and hasattr(raw_data, "release"):
managed_buffer = raw_data
try:
buf_tensor = raw_data.tensor
if buf_tensor.is_cuda:
data = KVCacheTransferData.from_bytes_gpu(buf_tensor)
raw_data.release()
managed_buffer = None
else:
data = KVCacheTransferData.from_bytes(memoryview(buf_tensor.numpy()))
data = self._clone_received_payload_tensors(data)
raw_data.release()
managed_buffer = None
except Exception as e:
logger.error("Failed to deserialize KV cache from ManagedBuffer: %s", e)
if managed_buffer is not None:
raw_data.release()
return None, 0
elif isinstance(raw_data, (bytes, bytearray)):
data = KVCacheTransferData.from_bytes(raw_data)
elif isinstance(raw_data, torch.Tensor) and raw_data.dtype == torch.uint8 and raw_data.dim() == 1:
data = KVCacheTransferData.from_bytes(raw_data.cpu().numpy().tobytes())
else:
data = raw_data
try:
if isinstance(data, dict) and "layer_blocks" in data:
layer_blocks = data["layer_blocks"]
for cache_list in [
layer_blocks.get("key_cache", []),
layer_blocks.get("value_cache", []),
]:
for i, tensor in enumerate(cache_list):
if not isinstance(tensor, torch.Tensor):
continue
if target_device is not None and tensor.device != target_device:
cache_list[i] = tensor.to(target_device).contiguous()
finally:
if managed_buffer is not None:
managed_buffer.release()
logger.info(
"Successfully received KV cache for %s, %s bytes, wait=%.3fs, link=%.1fms",
request_id,
size,
elapsed,
link_ms,
)
return data, size
if time.time() - start_time > timeout:
logger.error(f"Timeout waiting for KV cache for request {request_id} after {timeout}s")
return None, 0
time.sleep(0.5)
time.sleep(poll_interval)
poll_interval = min(poll_interval * 2, max_poll_interval)
except Exception as e:
logger.error(f"Error receiving KV cache for {request_id}: {e}")
@@ -459,6 +907,16 @@ class OmniKVTransferManager:
if hasattr(req, "sampling_params") and req.sampling_params is not None:
req.sampling_params.kv_metadata = data["metadata"]
@staticmethod
def _resolve_request_id(req: Any) -> str | None:
"""Resolve the logical request ID used for KV transfer lookups."""
request_id = getattr(req, "request_id", None)
if request_id:
return request_id
if hasattr(req, "request_ids") and req.request_ids:
return req.request_ids[0]
return None
# Legacy compatibility method
def receive_kv_cache(self, req: Any, target_device: torch.device | None = None) -> bool:
"""Receive KV cache and populate request object (legacy interface).
@@ -470,11 +928,11 @@ class OmniKVTransferManager:
Returns:
True if successful, False otherwise
"""
request_id = getattr(req, "request_id", None)
if not request_id and hasattr(req, "request_ids") and req.request_ids:
# Adaptation for new OmniDiffusionRequest which has list of prompts/ids
request_id = req.request_ids[0]
kv_sender_info = getattr(req, "kv_sender_info", None)
if kv_sender_info:
self.update_sender_info(kv_sender_info, sender_stage_id=self.recv_stages[0])
request_id = self._resolve_request_id(req)
if not request_id:
logger.warning("Request has no ID, cannot receive KV cache")
return False
@@ -513,9 +971,7 @@ class OmniKVTransferManager:
cfg_ids = getattr(getattr(req, "sampling_params", None), "cfg_kv_request_ids", None)
if cfg_ids and cfg_kv_collect_func:
request_id = getattr(req, "request_id", None) or (
req.request_ids[0] if hasattr(req, "request_ids") and req.request_ids else None
)
request_id = self._resolve_request_id(req)
try:
cfg_kvs = cfg_kv_collect_func(
request_id,
@@ -19,9 +19,17 @@ else:
logger = get_connector_logger(__name__)
# Reserve a separate port range for KV-transfer sockets so they do not
# collide with request-forwarding endpoints that share the same base port.
KV_TRANSFER_PORT_OFFSET = 100
def initialize_connectors_from_config(
config_path: str | Path | None = None, default_shm_threshold: int = 65536
config_path: str | Path | None = None,
default_shm_threshold: int = 65536,
purpose: str = "request_forwarding",
caller_stage_id: int | str | None = None,
is_sender: bool | None = None,
) -> tuple[OmniTransferConfig | None, dict[tuple[str, str], OmniConnectorBase]]:
"""
Initialize connectors from configuration file.
@@ -36,12 +44,20 @@ def initialize_connectors_from_config(
return None, {}
# create connectors from config
connectors = create_connectors_from_config(transfer_config.connectors)
connectors = create_connectors_from_config(
transfer_config.connectors,
purpose=purpose,
caller_stage_id=caller_stage_id,
is_sender=is_sender,
)
return transfer_config, connectors
def create_connectors_from_config(
connectors_config: dict[tuple[str, str], ConnectorSpec],
purpose: str = "request_forwarding",
caller_stage_id: int | str | None = None,
is_sender: bool | None = None,
) -> dict[tuple[str, str], OmniConnectorBase]:
"""
Create connectors from config.
@@ -52,12 +68,59 @@ def create_connectors_from_config(
Returns:
A dictionary of connectors.
"""
purpose_port_offsets = {
"request_forwarding": 0,
"kv_transfer": KV_TRANSFER_PORT_OFFSET,
}
port_offset = purpose_port_offsets.get(purpose, 0)
orchestrator_port_offset = 200
connectors = {}
for edge_key, connector_spec in connectors_config.items():
from_stage, to_stage = edge_key
try:
connector = OmniConnectorFactory.create_connector(connector_spec)
if connector_spec.name == "MooncakeTransferEngineConnector":
extra = dict(connector_spec.extra) if connector_spec.extra else {}
base_port = extra.get("zmq_port", 50051)
try:
stage_offset = int(from_stage)
except (TypeError, ValueError):
stage_offset = 0
if str(caller_stage_id) == "orchestrator":
adjusted_port = base_port + orchestrator_port_offset + stage_offset
else:
adjusted_port = base_port + port_offset + stage_offset
extra["zmq_port"] = adjusted_port
if is_sender is not None:
extra["role"] = "sender" if is_sender else "receiver"
if not is_sender:
extra.setdefault("sender_host", extra.get("host", "127.0.0.1"))
extra.setdefault("sender_zmq_port", adjusted_port)
elif caller_stage_id is not None:
caller_str = str(caller_stage_id)
if caller_str == from_stage:
extra["role"] = "sender"
elif caller_str == to_stage:
extra["role"] = "receiver"
extra.setdefault("sender_host", extra.get("host", "127.0.0.1"))
extra.setdefault("sender_zmq_port", adjusted_port)
else:
extra["role"] = "sender"
else:
extra["role"] = extra.get("role", "auto")
connector = OmniConnectorFactory.create_connector(ConnectorSpec(name=connector_spec.name, extra=extra))
else:
connector = OmniConnectorFactory.create_connector(connector_spec)
connectors[edge_key] = connector
logger.info(f"Created connector for {edge_key[0]} -> {edge_key[1]}: {type(connector).__name__}")
logger.info(
"Created connector for %s -> %s: %s",
from_stage,
to_stage,
type(connector).__name__,
)
except Exception as e:
raise RuntimeError(f"Failed to initialize connector for edge {edge_key}: {e}") from e
@@ -289,7 +352,11 @@ def initialize_orchestrator_connectors(
else:
default_shm_threshold = max(0, shm_threshold_bytes)
transfer_config, connectors = initialize_connectors_from_config(
config_path, default_shm_threshold=default_shm_threshold
config_path,
default_shm_threshold=default_shm_threshold,
purpose="request_forwarding",
caller_stage_id="orchestrator",
is_sender=True,
)
return transfer_config, connectors
@@ -316,6 +383,7 @@ def get_stage_connector_config(
def build_stage_connectors(
stage_id: int,
connectors_config: dict[str, Any],
purpose: str = "request_forwarding",
) -> dict[tuple[str, str], Any] | None:
"""Instantiate OmniConnectors for a stage based on config."""
if not connectors_config:
@@ -352,7 +420,12 @@ def build_stage_connectors(
try:
# Use unified connector creation logic
connectors = create_connectors_from_config(stage_connector_specs)
connectors = create_connectors_from_config(
stage_connector_specs,
purpose=purpose,
caller_stage_id=stage_id,
is_sender=False,
)
except Exception as exc: # pragma: no cover - defensive logging
# Fail fast so the stage does not start with missing connectors.
logger.exception("[Stage-%s] Failed to initialize connectors: %s", stage_id, exc)
+5 -3
View File
@@ -395,15 +395,17 @@ class AsyncOmniEngine:
proc=proc,
addresses=addresses,
)
logger.info("[AsyncOmniEngine] Stage %s engine launch started", metadata.stage_id)
# Keep the stage-specific device visibility until vLLM
# finishes starting all child processes.
complete_stage_handshake(proc, handshake_address, addresses, vllm_config)
logger.info("[AsyncOmniEngine] Stage %s engine startup completed", metadata.stage_id)
finally:
if previous_visible_devices is None:
current_omni_platform.unset_device_control_env_var()
else:
current_omni_platform.set_device_control_env_var(previous_visible_devices)
logger.info("[AsyncOmniEngine] Stage %s engine launch started", metadata.stage_id)
complete_stage_handshake(proc, handshake_address, addresses, vllm_config)
logger.info("[AsyncOmniEngine] Stage %s engine startup completed", metadata.stage_id)
assert started_stage is not None
return started_stage
except Exception:
+42 -3
View File
@@ -79,7 +79,7 @@ def build_engine_core_request_from_tokens(
sampling_params=sampling_params,
pooling_params=pooling_params,
arrival_time=arrival_time,
lora_request=None,
lora_request=getattr(params, "lora_request", None),
cache_salt=None,
data_parallel_rank=None,
prompt_embeds=prompt_embeds,
@@ -477,6 +477,30 @@ class Orchestrator:
),
)
def _build_kv_sender_info(self, sender_stage_ids: list[int]) -> dict[int, dict[str, Any]] | None:
"""Build per-request sender info for diffusion KV-transfer receivers."""
sender_infos: dict[int, dict[str, Any]] = {}
for sender_stage_id in dict.fromkeys(sender_stage_ids):
if sender_stage_id < 0 or sender_stage_id >= self.num_stages:
continue
sender_stage = self.stage_clients[sender_stage_id]
get_sender_info = getattr(sender_stage, "get_kv_sender_info", None)
if not callable(get_sender_info):
continue
sender_info = get_sender_info()
if not sender_info:
logger.warning(
"[Orchestrator] Stage-%s has no KV sender info available",
sender_stage_id,
)
continue
sender_infos[sender_stage_id] = sender_info
return sender_infos or None
async def _forward_to_next_stage(
self,
req_id: str,
@@ -522,14 +546,22 @@ class Orchestrator:
req_id,
)
source_stage_ids = list(getattr(next_client, "engine_input_source", None) or [stage_id])
kv_sender_info = self._build_kv_sender_info(sender_stage_ids=source_stage_ids)
if isinstance(diffusion_prompt, list):
await next_client.add_batch_request_async(
req_id,
diffusion_prompt,
params,
kv_sender_info=kv_sender_info,
)
else:
await next_client.add_request_async(req_id, diffusion_prompt, params)
await next_client.add_request_async(
req_id,
diffusion_prompt,
params,
kv_sender_info=kv_sender_info,
)
req_state.stage_submit_ts[next_stage_id] = _time.time()
return
@@ -731,7 +763,14 @@ class Orchestrator:
params = req_state.sampling_params_list[next_stage_id]
if next_client.stage_type == "diffusion":
await next_client.add_request_async(request_id, req_state.prompt, params)
source_stage_ids = list(getattr(next_client, "engine_input_source", None) or [next_stage_id - 1])
kv_sender_info = self._build_kv_sender_info(sender_stage_ids=source_stage_ids)
await next_client.add_request_async(
request_id,
req_state.prompt,
params,
kv_sender_info=kv_sender_info,
)
req_state.stage_submit_ts[next_stage_id] = _time.time()
continue
Loaded 100 of 129 files, more files were not shown because too many files have changed in this diff. Show more