[Rebase] Align vLLM-Omni with vLLM v0.31.0 (#8459)

Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
This commit is contained in:
Zhou Taichang authored and GitHub committed 2026-10-05 14:11:01 +08:00
1 parent 1f47fa2f29
commit 0b0d2d7009
141 files changed
+2885 -721

No files matched your search

+4 -3
View File
@@ -102,6 +102,7 @@ CI_SOURCE_FILE_DEPENDENCIES_PATH = ROOT / ".buildkite/common/ci_source_file_depe
SOURCE_FILTER_FALLBACK_KEY = "source_filter_fallback" SOURCE_FILTER_FALLBACK_KEY = "source_filter_fallback"
CUDA_HF_TOKEN_ENV = "VLLM_CI_HF_TOKEN" CUDA_HF_TOKEN_ENV = "VLLM_CI_HF_TOKEN"
CUDA_HF_TOKEN_EXPORT = f'if [ -n "$${{{CUDA_HF_TOKEN_ENV}:-}}" ]; then export HF_TOKEN="$${{{CUDA_HF_TOKEN_ENV}}}"; fi' CUDA_HF_TOKEN_EXPORT = f'if [ -n "$${{{CUDA_HF_TOKEN_ENV}:-}}" ]; then export HF_TOKEN="$${{{CUDA_HF_TOKEN_ENV}}}"; fi'
CUDA_RUNTIME_CHECK = "python3 .buildkite/cuda/scripts/check_vllm_runtime.py"
# Bootstrap Buildkite ``if`` expressions. # Bootstrap Buildkite ``if`` expressions.
# ``*_MAIN_IF``: main + env schedule. ``*_LABEL_IF``: PR label (and/or composed with MAIN). # ``*_MAIN_IF``: main + env schedule. ``*_LABEL_IF``: PR label (and/or composed with MAIN).
@@ -620,11 +621,11 @@ def _expand_mirror_hardwares(step: dict[str, Any]) -> dict[str, Any] | None:
if any(preset_name == chip or preset_name.startswith(f"{chip}_") for chip in _cuda_mirror_chips()): if any(preset_name == chip or preset_name.startswith(f"{chip}_") for chip in _cuda_mirror_chips()):
commands = merged.get("commands") commands = merged.get("commands")
if isinstance(commands, list): if isinstance(commands, list):
merged["commands"] = [CUDA_HF_TOKEN_EXPORT, *commands] merged["commands"] = [CUDA_HF_TOKEN_EXPORT, CUDA_RUNTIME_CHECK, *commands]
elif commands is None: elif commands is None:
merged["commands"] = [CUDA_HF_TOKEN_EXPORT] merged["commands"] = [CUDA_HF_TOKEN_EXPORT, CUDA_RUNTIME_CHECK]
else: else:
merged["commands"] = [CUDA_HF_TOKEN_EXPORT, commands] merged["commands"] = [CUDA_HF_TOKEN_EXPORT, CUDA_RUNTIME_CHECK, commands]
# Preset retry (K8S_RETRY on l4_*) must not clobber a step that opted out. # Preset retry (K8S_RETRY on l4_*) must not clobber a step that opted out.
if "retry" in step: if "retry" in step:
merged["retry"] = step["retry"] merged["retry"] = step["retry"]
@@ -0,0 +1,38 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Verify the pinned vLLM and compiled CUDA imports on a GPU CI worker."""
from __future__ import annotations
import importlib
import re
from pathlib import Path
def main() -> None:
dockerfile = Path(__file__).resolve().parents[3] / "docker" / "Dockerfile.ci"
match = re.search(
r"^ARG VLLM_VERSION=([0-9]+\.[0-9]+\.[0-9]+)$",
dockerfile.read_text(),
re.MULTILINE,
)
if match is None:
raise RuntimeError(f"Missing vLLM release version in {dockerfile}")
expected_version = match.group(1)
import torch
import vllm
assert vllm.__version__ == expected_version, vllm.__version__
assert torch.cuda.is_available(), "vLLM runtime check requires a GPU worker"
for module in ("flashinfer", "vllm._C_stable_libtorch"):
importlib.import_module(module)
print(
f"Verified vLLM CUDA runtime: vllm={vllm.__version__} "
f"expected={expected_version} torch={torch.__version__} "
f"cuda={torch.version.cuda} device={torch.cuda.get_device_name()}"
)
if __name__ == "__main__":
main()
+7
View File
@@ -48,6 +48,8 @@ steps:
- label: ":full_moon: Omni · Doc Test with H100 · 2-GPU" - label: ":full_moon: Omni · Doc Test with H100 · 2-GPU"
source_file_dependencies: omni_qwen3_omni_doc source_file_dependencies: omni_qwen3_omni_doc
timeout_in_minutes: 90 timeout_in_minutes: 90
artifact_paths:
- audio_*.wav
commands: commands:
- pytest -s -v tests/examples/ -m "full_model and omni and H100 and B200 and cards_2" --run-level "full_model" - pytest -s -v tests/examples/ -m "full_model and omni and H100 and B200 and cards_2" --run-level "full_model"
@@ -156,6 +158,11 @@ steps:
- label: ":full_moon: TTS · Function Test with L4" - label: ":full_moon: TTS · Function Test with L4"
source_file_dependencies: tts_qwen3_tts_function source_file_dependencies: tts_qwen3_tts_function
timeout_in_minutes: 120 timeout_in_minutes: 120
env:
VLLM_OMNI_FAILED_SPEECH_AUDIO_DIR: .
artifact_paths:
- failed_speech_*.wav
- failed_speech_*.json
commands: commands:
- export VLLM_ALLOW_LONG_MAX_MODEL_LEN="1" - export VLLM_ALLOW_LONG_MAX_MODEL_LEN="1"
- export VLLM_USE_DEEP_GEMM="0" - export VLLM_USE_DEEP_GEMM="0"
+10 -7
View File
@@ -1,10 +1,7 @@
# Target vLLM version: v0.30.0 (tag @ ced6857afa0e). # Use the official release image with its matching CUDA/PyTorch stack.
# Use the matching release image and its coherent CUDA/PyTorch/FlashInfer ARG VLLM_VERSION=0.31.0
# runtime. Do not reinstall a commit wheel or repair dependencies on top of
# an older base image at a release boundary. For a future unreleased target,
# restore an explicit wheel pin and validate its complete runtime stack.
ARG VLLM_BASE_IMAGE=vllm/vllm-openai ARG VLLM_BASE_IMAGE=vllm/vllm-openai
ARG VLLM_BASE_TAG=v0.30.0 ARG VLLM_BASE_TAG=v${VLLM_VERSION}
# We use registry-based layer caching for the dependencies, # We use registry-based layer caching for the dependencies,
# where the tag is determined based on the hash of the contents # where the tag is determined based on the hash of the contents
@@ -12,6 +9,7 @@ ARG VLLM_BASE_TAG=v0.30.0
# everything except the actual project dir, since that should not # everything except the actual project dir, since that should not
# change much across branches. # change much across branches.
FROM ${VLLM_BASE_IMAGE}:${VLLM_BASE_TAG} AS deps FROM ${VLLM_BASE_IMAGE}:${VLLM_BASE_TAG} AS deps
ARG VLLM_VERSION
# Keep APP_DIR outside /workspace so k8s Buildkite workspace mounts cannot # Keep APP_DIR outside /workspace so k8s Buildkite workspace mounts cannot
# shadow the editable install target (#5364). Docker WORKDIR creates /opt if # shadow the editable install target (#5364). Docker WORKDIR creates /opt if
# needed. # needed.
@@ -36,12 +34,17 @@ RUN apt-get update && \
# for CI # for CI
COPY pyproject.toml setup.py ./ COPY pyproject.toml setup.py ./
COPY requirements/ requirements/ COPY requirements/ requirements/
ENV SETUPTOOLS_SCM_PRETEND_VERSION=0.0.0 # Scope the override to Omni; dependency source builds need their own versions.
ENV VLLM_OMNI_VERSION_OVERRIDE=0.0.0
# Editable install: deps + project metadata are cached in this layer, while # Editable install: deps + project metadata are cached in this layer, while
# the package itself is linked to APP_DIR (populated later by COPY --link). # the package itself is linked to APP_DIR (populated later by COPY --link).
# Do NOT use a non-editable install here — that copies an empty stub into # Do NOT use a non-editable install here — that copies an empty stub into
# site-packages and breaks ``import vllm_omni.entrypoints``. # site-packages and breaks ``import vllm_omni.entrypoints``.
RUN uv pip install --system -e ".[dev]" RUN uv pip install --system -e ".[dev]"
# Fail the image build if dependency resolution replaced the selected vLLM.
# The CPU builder has no NVIDIA driver; CUDA imports are checked at the start
# of each GPU test job by .buildkite/cuda/scripts/check_vllm_runtime.py.
RUN python3 -c "import vllm; assert vllm.__version__ == '${VLLM_VERSION}', vllm.__version__"
# The final stage just copies the working dir on top of the # The final stage just copies the working dir on top of the
# dependencies image. This uses --link so that we can skip # dependencies image. This uses --link so that we can skip
+1 -1
View File
@@ -1,6 +1,6 @@
# NOTE: this is largely taken from vLLM # NOTE: this is largely taken from vLLM
# Exclude .git for CI builds — version is set via SETUPTOOLS_SCM_PRETEND_VERSION # Exclude .git for CI builds — version is set via VLLM_OMNI_VERSION_OVERRIDE
.git .git
.venv .venv
/build /build
+1 -1
View File
@@ -1,4 +1,4 @@
ARG BASE_IMAGE=vllm/vllm-openai:v0.30.0 ARG BASE_IMAGE=vllm/vllm-openai:v0.31.0
FROM ${BASE_IMAGE} FROM ${BASE_IMAGE}
ARG COMMON_WORKDIR=/app ARG COMMON_WORKDIR=/app
+5 -4
View File
@@ -1,7 +1,7 @@
# Keep the ROCm runtime aligned with the vLLM release targeted by # Keep the ROCm runtime aligned with the vLLM release targeted by
# docker/Dockerfile.ci. A stale base image can build successfully while failing # docker/Dockerfile.ci. A stale base image can build successfully while failing
# at runtime when vLLM-Omni imports APIs added by the newer vLLM release. # at runtime when vLLM-Omni imports APIs added by the newer vLLM release.
ARG BASE_IMAGE=vllm/vllm-openai-rocm:v0.30.0 ARG BASE_IMAGE=vllm/vllm-openai-rocm:v0.31.0
# Keep the dependency-heavy portion independent from the source tree. The # Keep the dependency-heavy portion independent from the source tree. The
# registry cache is keyed on these inputs, while the final source copy can be # registry cache is keyed on these inputs, while the final source copy can be
@@ -12,11 +12,11 @@ FROM ${BASE_IMAGE} AS deps
# BASE_IMAGE pins the default prebuilt runtime when USE_NIGHTLY_BUILD=0. # BASE_IMAGE pins the default prebuilt runtime when USE_NIGHTLY_BUILD=0.
# VLLM_VERSION_OR_COMMIT_HASH is consumed only by the opt-in source rebuild when # VLLM_VERSION_OR_COMMIT_HASH is consumed only by the opt-in source rebuild when
# USE_NIGHTLY_BUILD=1. Keep both aligned with docker/Dockerfile.ci's # USE_NIGHTLY_BUILD=1. Keep both aligned with docker/Dockerfile.ci's
# VLLM_BASE_TAG. This is enforced by tests/buildkite/test_rocm_dockerfile.py. An # release version. This is enforced by tests/buildkite/test_rocm_dockerfile.py. An
# intentional custom source pin must update that regression in the same change, # intentional custom source pin must update that regression in the same change,
# and should return to the stable release as soon as one is available. # and should return to the stable release as soon as one is available.
ARG USE_NIGHTLY_BUILD=0 ARG USE_NIGHTLY_BUILD=0
ARG VLLM_VERSION_OR_COMMIT_HASH=v0.30.0 ARG VLLM_VERSION_OR_COMMIT_HASH=v0.31.0
ARG ARG_PYTORCH_ROCM_ARCH ARG ARG_PYTORCH_ROCM_ARCH
ENV PYTORCH_ROCM_ARCH=${ARG_PYTORCH_ROCM_ARCH:-${PYTORCH_ROCM_ARCH}} ENV PYTORCH_ROCM_ARCH=${ARG_PYTORCH_ROCM_ARCH:-${PYTORCH_ROCM_ARCH}}
@@ -75,7 +75,8 @@ WORKDIR ${COMMON_WORKDIR}/vllm-omni
RUN mkdir -p vllm_omni && touch vllm_omni/__init__.py RUN mkdir -p vllm_omni && touch vllm_omni/__init__.py
COPY pyproject.toml setup.py ./ COPY pyproject.toml setup.py ./
COPY requirements/ requirements/ COPY requirements/ requirements/
ENV SETUPTOOLS_SCM_PRETEND_VERSION=0.0.0 # Scope the override to Omni; dependency source builds need their own versions.
ENV VLLM_OMNI_VERSION_OVERRIDE=0.0.0
RUN uv pip install --python "$(python3 -c 'import sys; print(sys.executable)')" \ RUN uv pip install --python "$(python3 -c 'import sys; print(sys.executable)')" \
--no-cache-dir -e ".[dev]" --no-build-isolation && \ --no-cache-dir -e ".[dev]" --no-build-isolation && \
+1 -1
View File
@@ -2,7 +2,7 @@
This section lists the most common options for running vLLM-Omni. This section lists the most common options for running vLLM-Omni.
For options within a vLLM Engine, please refer to the [vLLM 0.30 configuration guide](https://docs.vllm.ai/en/v0.30.0/configuration/index.html). For options within a vLLM Engine, please refer to the [vLLM 0.31 configuration guide](https://docs.vllm.ai/en/v0.31.0/configuration/index.html).
Each model defines fixed topology in a registered `PipelineConfig` and runtime Each model defines fixed topology in a registered `PipelineConfig` and runtime
overrides in a deploy YAML. overrides in a deploy YAML.
+1 -1
View File
@@ -186,7 +186,7 @@ their keys only.
## Inherited vLLM variables ## Inherited vLLM variables
vLLM-Omni also reads variables through its aligned vLLM dependency. Refer to vLLM-Omni also reads variables through its aligned vLLM dependency. Refer to
the [vLLM 0.30 environment-variable reference](https://docs.vllm.ai/en/v0.30.0/configuration/env_vars.html) the [vLLM 0.31 environment-variable reference](https://docs.vllm.ai/en/v0.31.0/configuration/env_vars.html)
for their definitions. This includes vLLM launch, cache, logging, plugin, ROCm, for their definitions. This includes vLLM launch, cache, logging, plugin, ROCm,
XPU, ModelScope, and FlashInfer workspace settings. XPU, ModelScope, and FlashInfer workspace settings.
+1 -1
View File
@@ -1,7 +1,7 @@
# Installation # Installation
!!! important !!! important
vLLM-Omni is released against the matching upstream vLLM major/minor version. The vLLM-Omni 0.30.x release line uses vLLM 0.30.x; the source-install instructions pin vLLM 0.30.0. vLLM-Omni is released against the matching upstream vLLM major/minor version. This source checkout targets vLLM 0.31.0. The published vLLM-Omni 0.30.0 wheel instructions use vLLM 0.30.0; choose the installation path that matches your Omni revision.
vLLM-Omni supports the following hardware platforms: vLLM-Omni supports the following hardware platforms:
@@ -1,11 +1,13 @@
# --8<-- [start:requirements] # --8<-- [start:requirements]
- GPU: compute capability 7.0 or higher (e.g., V100, T4, RTX20xx, A100, L4, H100, etc.) - GPU: compute capability 7.5 or higher (e.g., T4, RTX20xx, A100, L4, H100, etc.)
# --8<-- [end:requirements] # --8<-- [end:requirements]
# --8<-- [start:set-up-using-python] # --8<-- [start:set-up-using-python]
vLLM-Omni depends on the matching major/minor release of vLLM. The vLLM-Omni 0.30.x release line uses vLLM 0.30.x. vLLM-Omni depends on the matching major/minor release of vLLM. This source
checkout targets vLLM 0.31.0. The pre-built Omni wheel instructions below install
the published 0.30.0 release with its matching vLLM dependency.
!!! note !!! note
PyTorch installed via `conda` will statically link `NCCL` library, which can cause issues when vLLM tries to use `NCCL`. See <gh-issue:8420> for more details. PyTorch installed via `conda` will statically link `NCCL` library, which can cause issues when vLLM tries to use `NCCL`. See <gh-issue:8420> for more details.
@@ -41,13 +43,17 @@ uv pip install 'vllm-omni[demo]==0.30.0'
# --8<-- [start:build-wheel-from-source] # --8<-- [start:build-wheel-from-source]
#### Installation of vLLM #### Installation of vLLM
If you do not need to modify source code of vLLM, you can directly install the stable 0.30.0 release version of the library If you do not need to modify source code of vLLM, install the final 0.31.0
release wheel from its commit-pinned index:
```bash ```bash
uv pip install vllm==0.30.0 --torch-backend=auto uv pip install vllm==0.31.0 --torch-backend=auto \
--extra-index-url https://wheels.vllm.ai/db9527a46873454610df6dbedf79a36d6bf1a7f6
``` ```
The 0.30.0 release of vLLM ships CUDA 13.0-compatible binaries by default. If you need a different CUDA variant or want to reuse an existing PyTorch installation, build vLLM from source instead. The default 0.31.0 wheel contains CUDA 12.9 binaries. If you need a different
CUDA variant or want to reuse an existing PyTorch installation, build vLLM
from source instead.
#### Installation of vLLM-Omni #### Installation of vLLM-Omni
Since vllm-omni is rapidly evolving, it's recommended to install it from source Since vllm-omni is rapidly evolving, it's recommended to install it from source
@@ -68,16 +74,16 @@ If you want to check, modify or debug with source code of vLLM, install the libr
```bash ```bash
git clone https://github.com/vllm-project/vllm.git git clone https://github.com/vllm-project/vllm.git
cd vllm cd vllm
git checkout v0.30.0 git checkout v0.31.0
``` ```
Set up environment variables to get pre-built wheels. If there are internet problems, just download the whl file manually. And set `VLLM_PRECOMPILED_WHEEL_LOCATION` as your local absolute path of whl file. Set up environment variables to get pre-built wheels. If there are internet problems, just download the whl file manually. And set `VLLM_PRECOMPILED_WHEEL_LOCATION` as your local absolute path of whl file.
```bash ```bash
#For CUDA 13.0 (the default for v0.30.0; the wheel filename has no `+cu130` suffix) # Default CUDA 12.9 wheel for v0.31.0 on x86_64
export VLLM_PRECOMPILED_WHEEL_LOCATION=https://github.com/vllm-project/vllm/releases/download/v0.30.0/vllm-0.30.0-cp38-abi3-manylinux_2_28_x86_64.whl export VLLM_PRECOMPILED_WHEEL_LOCATION=https://wheels.vllm.ai/db9527a46873454610df6dbedf79a36d6bf1a7f6/vllm-0.31.0-cp38-abi3-manylinux_2_28_x86_64.whl
``` ```
Install vllm with command below (If you have no existing PyTorch). Install vllm with command below (If you have no existing PyTorch).
```bash ```bash
uv pip install --editable . VLLM_USE_PRECOMPILED=1 uv pip install --editable . --torch-backend=auto
``` ```
Install vllm with command below (If you already have PyTorch). Install vllm with command below (If you already have PyTorch).
```bash ```bash
@@ -128,12 +134,13 @@ docker run --runtime nvidia --gpus 2 \
DOCKER_BUILDKIT=1 docker build -f docker/Dockerfile.cuda -t vllm-omni-cuda . DOCKER_BUILDKIT=1 docker build -f docker/Dockerfile.cuda -t vllm-omni-cuda .
``` ```
If you want to specify the base vLLM version: The source build defaults to `vllm/vllm-openai:v0.31.0`. To select that
upstream release image explicitly:
```bash ```bash
DOCKER_BUILDKIT=1 docker build \ DOCKER_BUILDKIT=1 docker build \
-f docker/Dockerfile.cuda \ -f docker/Dockerfile.cuda \
--build-arg BASE_IMAGE=vllm/vllm-openai:v0.30.0 \ --build-arg BASE_IMAGE=vllm/vllm-openai:v0.31.0 \
-t vllm-omni-cuda . -t vllm-omni-cuda .
``` ```
@@ -10,8 +10,9 @@
For ROCm, vLLM-Omni currently recommends the setup steps through Docker Images. For ROCm, vLLM-Omni currently recommends the setup steps through Docker Images.
vLLM-Omni depends on the matching major/minor release of vLLM. The vLLM-Omni vLLM-Omni depends on the matching major/minor release of vLLM. This source
0.30.x release line uses vLLM 0.30.x. checkout targets vLLM 0.31.0. The pre-built Omni wheel instructions below install
the published 0.30.0 release with its matching vLLM dependency.
The Dockerfile's `BASE_IMAGE` pin applies only to Docker builds. The The Dockerfile's `BASE_IMAGE` pin applies only to Docker builds. The
`vllm-omni` package does not install vLLM as a dependency, so non-Docker source `vllm-omni` package does not install vLLM as a dependency, so non-Docker source
@@ -49,13 +50,14 @@ uv pip install onnxruntime-rocm
#### Installation of vLLM #### Installation of vLLM
If you do not need to modify source code of vLLM, you can directly install the stable 0.30.0 release version of the library If you do not need to modify source code of vLLM, install the final 0.31.0
release wheel for ROCm 7.2.3:
```bash ```bash
uv pip install vllm==0.30.0+rocm723 --extra-index-url https://wheels.vllm.ai/rocm/0.30.0/rocm723 uv pip install vllm==0.31.0+rocm723 --extra-index-url https://wheels.vllm.ai/rocm/0.31.0/rocm723
``` ```
The pre-built 0.30.0 vLLM wheel targets ROCm 7.2.3. If you need a different ROCm stack or want to reuse an existing PyTorch installation, build vLLM from source instead. The pre-built 0.31.0 vLLM wheel targets ROCm 7.2.3. If you need a different ROCm stack or want to reuse an existing PyTorch installation, build vLLM from source instead.
#### Installation of vLLM-Omni #### Installation of vLLM-Omni
@@ -75,7 +77,7 @@ If you want to check, modify or debug with source code of vLLM, install the libr
```bash ```bash
git clone https://github.com/vllm-project/vllm.git git clone https://github.com/vllm-project/vllm.git
cd vllm cd vllm
git checkout v0.30.0 git checkout v0.31.0
python3 -m pip install -r requirements/rocm.txt python3 -m pip install -r requirements/rocm.txt
python3 setup.py develop python3 setup.py develop
``` ```
@@ -87,7 +89,7 @@ python3 setup.py develop
#### Build docker image #### Build docker image
The source build defaults to the published upstream base image The source build defaults to the published upstream base image
`vllm/vllm-openai-rocm:v0.30.0`, aligned with the vLLM release used by CI. `vllm/vllm-openai-rocm:v0.31.0`, aligned with this source checkout.
Older or custom bases must provide the vLLM APIs checked by the Dockerfile's Older or custom bases must provide the vLLM APIs checked by the Dockerfile's
image-build canary. This upstream base is distinct from the prebuilt image-build canary. This upstream base is distinct from the prebuilt
`vllm/vllm-omni-rocm` images discussed below; their published-tag availability `vllm/vllm-omni-rocm` images discussed below; their published-tag availability
@@ -102,7 +104,7 @@ To select the upstream ROCm base explicitly, pass `BASE_IMAGE`:
```bash ```bash
DOCKER_BUILDKIT=1 docker build \ DOCKER_BUILDKIT=1 docker build \
-f docker/Dockerfile.rocm \ -f docker/Dockerfile.rocm \
--build-arg BASE_IMAGE=vllm/vllm-openai-rocm:v0.30.0 \ --build-arg BASE_IMAGE=vllm/vllm-openai-rocm:v0.31.0 \
-t vllm-omni-rocm . -t vllm-omni-rocm .
``` ```
@@ -20,12 +20,16 @@ vLLM-Omni currently recommends using the Docker image setup steps below.
#### Build docker image #### Build docker image
```bash ```bash
DOCKER_BUILDKIT=1 docker build -f docker/Dockerfile.xpu -t vllm-omni-xpu --shm-size=4g . export VLLM_VERSION=v0.31.0
DOCKER_BUILDKIT=1 docker build -f docker/Dockerfile.xpu -t vllm-omni-xpu --shm-size=4g \
--build-arg VLLM_VERSION="$VLLM_VERSION" .
``` ```
This layers vLLM-Omni on top of the published `vllm/vllm-openai-xpu:<VLLM_VERSION>` This layers vLLM-Omni on top of the published `vllm/vllm-openai-xpu:<VLLM_VERSION>`
base image (v0.30.0 by default), which Docker pulls automatically. To target a different vLLM release, base image, which Docker pulls automatically. The Dockerfile still defaults to
pass `--build-arg VLLM_VERSION=<tag>`. If that tag has not been published yet, build v0.30.0; the command above explicitly selects v0.31.0 to match this source
checkout. To target a different vLLM release, pass
`--build-arg VLLM_VERSION=<tag>`. If that tag has not been published yet, build
the base from upstream's own Dockerfile first and point the build at it: the base from upstream's own Dockerfile first and point the build at it:
```bash ```bash
@@ -71,10 +71,14 @@ We are keeping [issue #886](https://github.com/vllm-project/vllm-omni/issues/886
# --8<-- [start:installation-main] # --8<-- [start:installation-main]
You can also install vLLM-Omni from the latest main branch if you want to use the latest features or bug fixes. Use the aligned vLLM-Ascend v0.30.0 image described above, and check [issue #886](https://github.com/vllm-project/vllm-omni/issues/886) for the status of the latest vLLM-Omni commit on NPU. You can also install vLLM-Omni from source if you want to use the latest features
or bug fixes. This source checkout targets vLLM 0.31.0 and needs a matching
vLLM-Ascend runtime. The v0.30.0 images above are for the published Omni v0.30.0
release. Check [issue #886](https://github.com/vllm-project/vllm-omni/issues/886)
for aligned versions and NPU validation before selecting a development image.
```bash ```bash
# Inside the vLLM-Ascend v0.30.0 container, install vLLM-Omni from main # Inside a container with matching vLLM and vLLM-Ascend, install Omni from source
cd /vllm-workspace cd /vllm-workspace
git clone https://github.com/vllm-project/vllm-omni.git git clone https://github.com/vllm-project/vllm-omni.git
cd vllm-omni cd vllm-omni
+6 -3
View File
@@ -19,17 +19,20 @@ uv venv --python 3.12 --seed
source .venv/bin/activate source .venv/bin/activate
# On CUDA # On CUDA
uv pip install vllm==0.30.0 --torch-backend=auto uv pip install vllm==0.31.0 --torch-backend=auto \
--extra-index-url https://wheels.vllm.ai/db9527a46873454610df6dbedf79a36d6bf1a7f6
# On ROCm # On ROCm
uv pip install vllm==0.30.0+rocm723 --extra-index-url https://wheels.vllm.ai/rocm/0.30.0/rocm723 uv pip install vllm==0.31.0+rocm723 --extra-index-url https://wheels.vllm.ai/rocm/0.31.0/rocm723
git clone https://github.com/vllm-project/vllm-omni.git git clone https://github.com/vllm-project/vllm-omni.git
cd vllm-omni cd vllm-omni
uv pip install -e . uv pip install -e .
``` ```
For additional installation methods — please see the [installation guide](installation/README.md). These commands install vLLM 0.31.0 for this source checkout. The CUDA index is
pinned to the final release commit. For published vLLM-Omni wheels and additional
installation methods, see the [installation guide](installation/README.md).
!!! note !!! note
It is important to install the same major & minor version of vLLM and vLLM Omni, otherwise things may not work as expected. If the versions are misaligned, you will see a warning when you import vLLM Omni. It is important to install the same major & minor version of vLLM and vLLM Omni, otherwise things may not work as expected. If the versions are misaligned, you will see a warning when you import vLLM Omni.
+1 -1
View File
@@ -2,7 +2,7 @@
# kernels==0.16.1 requires the LayerRepository version=/revision= integration # kernels==0.16.1 requires the LayerRepository version=/revision= integration
# introduced in transformers 5.13. Keep the tested upper bound: 5.15 has # introduced in transformers 5.13. Keep the tested upper bound: 5.15 has
# a reported model-construction regression; later versions need validation. # a reported model-construction regression; later versions need validation.
# This range also satisfies vLLM 0.30.0's transformers>=5.10.4 requirement. # This range also satisfies vLLM 0.31.0's transformers>=5.10.4,<5.18.0 requirement.
# MOSS-TTS-Nano's list/set weight-loading compatibility is handled by # MOSS-TTS-Nano's list/set weight-loading compatibility is handled by
# transformers_keys_to_ignore_compat in model_executor/models/utils.py. # transformers_keys_to_ignore_compat in model_executor/models/utils.py.
transformers >= 5.13.0, < 5.15 transformers >= 5.13.0, < 5.15
+13 -8
View File
@@ -10,7 +10,8 @@ pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
REPO_ROOT = Path(__file__).resolve().parents[2] REPO_ROOT = Path(__file__).resolve().parents[2]
AMD_TEMPLATE = REPO_ROOT / ".buildkite/amd/test-template-amd-omni.j2" AMD_TEMPLATE = REPO_ROOT / ".buildkite/amd/test-template-amd-omni.j2"
AMD_BUILD_SCRIPT = REPO_ROOT / ".buildkite/amd/scripts/build-ci-image.sh" AMD_BUILD_SCRIPT = REPO_ROOT / ".buildkite/amd/scripts/build-ci-image.sh"
CI_DOCKERFILE = REPO_ROOT / "docker/Dockerfile.ci" CUDA_RELEASE_DOCKERFILE = REPO_ROOT / "docker/Dockerfile.cuda"
CUDA_CI_DOCKERFILE = REPO_ROOT / "docker/Dockerfile.ci"
ROCM_DOCKERFILE = REPO_ROOT / "docker/Dockerfile.rocm" ROCM_DOCKERFILE = REPO_ROOT / "docker/Dockerfile.rocm"
ROCM_DOCKERIGNORE = REPO_ROOT / "docker/Dockerfile.rocm.dockerignore" ROCM_DOCKERIGNORE = REPO_ROOT / "docker/Dockerfile.rocm.dockerignore"
@@ -30,14 +31,18 @@ def _line_index(lines: list[str], prefix: str) -> int:
return matches[0] return matches[0]
def test_rocm_base_tracks_ci_vllm_release() -> None: def test_rocm_base_tracks_cuda_vllm_release() -> None:
ci_release = _docker_arg(CI_DOCKERFILE, "VLLM_BASE_TAG") cuda_base = _docker_arg(CUDA_RELEASE_DOCKERFILE, "BASE_IMAGE")
cuda_image, _, cuda_release = cuda_base.rpartition(":")
rocm_base = _docker_arg(ROCM_DOCKERFILE, "BASE_IMAGE") rocm_base = _docker_arg(ROCM_DOCKERFILE, "BASE_IMAGE")
image_ref, separator, image_tag = rocm_base.rpartition(":") image_ref, separator, image_tag = rocm_base.rpartition(":")
assert separator, f"expected a tagged ROCm base image, got {rocm_base}" assert separator, f"expected a tagged ROCm base image, got {rocm_base}"
assert image_ref.rsplit("/", 1)[-1] == "vllm-openai-rocm", image_ref assert cuda_image == "vllm/vllm-openai"
assert image_tag == ci_release assert image_ref == "vllm/vllm-openai-rocm"
assert image_tag == cuda_release == f"v{_docker_arg(CUDA_CI_DOCKERFILE, 'VLLM_VERSION')}"
assert _docker_arg(CUDA_CI_DOCKERFILE, "VLLM_BASE_IMAGE") == cuda_image
assert _docker_arg(CUDA_CI_DOCKERFILE, "VLLM_BASE_TAG") == "v${VLLM_VERSION}"
def test_rocm_defaults_to_prebuilt_base_image() -> None: def test_rocm_defaults_to_prebuilt_base_image() -> None:
@@ -103,11 +108,11 @@ def test_rocm_build_context_excludes_git_history() -> None:
assert ".git" in patterns assert ".git" in patterns
def test_rocm_source_ref_tracks_ci_vllm_release() -> None: def test_rocm_source_ref_tracks_cuda_vllm_release() -> None:
ci_release = _docker_arg(CI_DOCKERFILE, "VLLM_BASE_TAG") cuda_release = _docker_arg(CUDA_CI_DOCKERFILE, "VLLM_VERSION")
rocm_source_ref = _docker_arg(ROCM_DOCKERFILE, "VLLM_VERSION_OR_COMMIT_HASH") rocm_source_ref = _docker_arg(ROCM_DOCKERFILE, "VLLM_VERSION_OR_COMMIT_HASH")
assert rocm_source_ref == ci_release assert rocm_source_ref == f"v{cuda_release}"
def test_rocm_dockerfile_contains_vllm_api_canary() -> None: def test_rocm_dockerfile_contains_vllm_api_canary() -> None:
+9 -1
View File
@@ -172,7 +172,11 @@ def test_mirror_hardwares_l4_1_expands_to_agents_and_plugins(monkeypatch: pytest
env_names = {item["name"] for item in container["env"]} env_names = {item["name"] for item in container["env"]}
assert "VLLM_CI_HF_TOKEN" in env_names assert "VLLM_CI_HF_TOKEN" in env_names
assert "HF_TOKEN" not in env_names assert "HF_TOKEN" not in env_names
assert step["commands"] == [CUDA_HF_TOKEN_EXPORT, "pytest -sv tests/example"] assert step["commands"] == [
CUDA_HF_TOKEN_EXPORT,
"python3 .buildkite/cuda/scripts/check_vllm_runtime.py",
"pytest -sv tests/example",
]
def test_mirror_hardwares_l4_preserves_explicit_retry() -> None: def test_mirror_hardwares_l4_preserves_explicit_retry() -> None:
@@ -239,6 +243,10 @@ def test_all_cuda_mirror_hardwares_restore_hf_token_at_runtime() -> None:
assert "VLLM_CI_HF_TOKEN" in env_names assert "VLLM_CI_HF_TOKEN" in env_names
assert "HF_TOKEN" not in env_names assert "HF_TOKEN" not in env_names
assert step["commands"][0] == CUDA_HF_TOKEN_EXPORT assert step["commands"][0] == CUDA_HF_TOKEN_EXPORT
assert step["commands"][1:] == [
"python3 .buildkite/cuda/scripts/check_vllm_runtime.py",
"pytest -sv tests/example",
]
def _gpu_limit(step: dict) -> int: def _gpu_limit(step: dict) -> int:
@@ -171,6 +171,7 @@ def test_cancelled_notification_is_filtered_before_coordinator(monkeypatch, mock
s, _ = scheduler(monkeypatch, [event]) s, _ = scheduler(monkeypatch, [event])
s._generation_max_wait_s = 0 s._generation_max_wait_s = 0
s.waiting = [] s.waiting = []
s.kv_holding_waiting = []
s.input_coordinator._async_chunk = True s.input_coordinator._async_chunk = True
s.input_coordinator.update_request_metadata = mocker.Mock() s.input_coordinator.update_request_metadata = mocker.Mock()
s.input_coordinator.process_pending_chunks = mocker.Mock() s.input_coordinator.process_pending_chunks = mocker.Mock()
@@ -178,7 +179,11 @@ def test_cancelled_notification_is_filtered_before_coordinator(monkeypatch, mock
s.input_coordinator.update_request_metadata.assert_called_once_with( s.input_coordinator.update_request_metadata.assert_called_once_with(
s.requests, {"0": {"code_predictor_codes": [7]}}, model_mode="generation" s.requests, {"0": {"code_predictor_codes": [7]}}, model_mode="generation"
) )
s.input_coordinator.process_pending_chunks.assert_called_once_with(s.waiting, s.running, {"0"}, set()) s.input_coordinator.process_pending_chunks.assert_called_once()
waiting, running, ready, finished = s.input_coordinator.process_pending_chunks.call_args.args
assert list(waiting) == []
assert running is s.running
assert ready == {"0"} and finished == set()
@pytest.mark.parametrize("target,wait", [(0, 1), (257, 1), (4, -1), (4, float("nan")), (4, float("inf"))]) @pytest.mark.parametrize("target,wait", [(0, 1), (257, 1), (4, -1), (4, float("nan")), (4, float("inf"))])
@@ -11,7 +11,9 @@ from types import SimpleNamespace
import pytest import pytest
import torch import torch
from vllm import SamplingParams from vllm import SamplingParams
from vllm.distributed.aux_output_connector.connector import AuxOutputSchedulerConnector
from vllm.v1.core.sched.interface import PauseState from vllm.v1.core.sched.interface import PauseState
from vllm.v1.core.sched.output import CachedRequestData
from vllm.v1.core.sched.request_queue import SchedulingPolicy, create_request_queue from vllm.v1.core.sched.request_queue import SchedulingPolicy, create_request_queue
from vllm.v1.request import Request, RequestStatus from vllm.v1.request import Request, RequestStatus
@@ -53,12 +55,14 @@ class FakeAdapter:
def _make_generation_scheduler(waiting_request, *, use_v2_model_runner=False): def _make_generation_scheduler(waiting_request, *, use_v2_model_runner=False):
scheduler = OmniGenerationScheduler.__new__(OmniGenerationScheduler) scheduler = OmniGenerationScheduler.__new__(OmniGenerationScheduler)
scheduler.max_num_scheduled_tokens = 8 scheduler.max_num_scheduled_tokens = 8
scheduler.max_num_active_reqs = 1
scheduler.max_num_running_reqs = 1 scheduler.max_num_running_reqs = 1
scheduler._pause_state = PauseState.UNPAUSED scheduler._pause_state = PauseState.UNPAUSED
scheduler.running = [] scheduler.running = []
scheduler.waiting = create_request_queue(SchedulingPolicy.FCFS) scheduler.waiting = create_request_queue(SchedulingPolicy.FCFS)
scheduler.waiting.add_request(waiting_request) scheduler.waiting.add_request(waiting_request)
scheduler.skipped_waiting = create_request_queue(SchedulingPolicy.FCFS) scheduler.kv_holding_waiting = create_request_queue(SchedulingPolicy.FCFS)
scheduler.deferred_waiting = set()
scheduler.requests = {waiting_request.request_id: waiting_request} scheduler.requests = {waiting_request.request_id: waiting_request}
scheduler.policy = SchedulingPolicy.FCFS scheduler.policy = SchedulingPolicy.FCFS
scheduler.chunk_transfer_adapter = FakeAdapter() scheduler.chunk_transfer_adapter = FakeAdapter()
@@ -86,6 +90,7 @@ def _make_generation_scheduler(waiting_request, *, use_v2_model_runner=False):
get_manager_metadata=lambda: None, get_manager_metadata=lambda: None,
) )
scheduler.connector = None scheduler.connector = None
scheduler.aux_output_connector = None
scheduler.ec_connector = None scheduler.ec_connector = None
scheduler.prev_step_scheduled_req_ids = set() scheduler.prev_step_scheduled_req_ids = set()
scheduler._pending_finish_reqs = [] scheduler._pending_finish_reqs = []
@@ -105,6 +110,45 @@ def _make_generation_scheduler(waiting_request, *, use_v2_model_runner=False):
return scheduler return scheduler
def test_generation_scheduler_preserves_auxiliary_output_metadata() -> None:
request = Request("aux-contract", [1, 2], SamplingParams(max_tokens=2), pooling_params=None)
request.block_hashes.append(b"block-hash")
scheduler = _make_generation_scheduler(request, use_v2_model_runner=True)
scheduler.aux_output_connector = AuxOutputSchedulerConnector()
output = scheduler._build_generation_scheduler_output(
new_reqs_data=[],
cached_reqs_data=CachedRequestData.make_empty(),
num_scheduled_tokens={request.request_id: 1},
scheduled_spec_decode_tokens={},
scheduled_encoder_inputs={},
num_common_prefix_blocks=[0],
)
metadata = output.aux_output_connector_metadata
assert metadata is not None
assert metadata.requests == {request.request_id: 0}
assert list(metadata.block_hashes[request.request_id]) == request.block_hashes
@pytest.mark.parametrize(("runner_slots", "active_limit"), [(1, 2), (2, 1)])
def test_generation_admits_kv_holder_before_fresh_requests(runner_slots, active_limit) -> None:
fresh = Request("fresh", [1, 2], SamplingParams(max_tokens=2), pooling_params=None)
holding = Request("holding", [3, 4], SamplingParams(max_tokens=2), pooling_params=None)
holding.num_computed_tokens = 1
scheduler = _make_generation_scheduler(fresh)
scheduler.max_num_running_reqs = runner_slots
scheduler.max_num_active_reqs = active_limit
scheduler.requests[holding.request_id] = holding
scheduler.kv_holding_waiting.add_request(holding)
output = scheduler.schedule()
assert set(output.num_scheduled_tokens) == {holding.request_id}
assert list(scheduler.waiting) == [fresh]
assert list(scheduler.kv_holding_waiting) == []
def _chunk_request(request_id, **kwargs): def _chunk_request(request_id, **kwargs):
defaults = dict( defaults = dict(
request_id=request_id, request_id=request_id,
@@ -123,7 +167,13 @@ def _chunk_request(request_id, **kwargs):
record_event=lambda *args, **kwargs: None, record_event=lambda *args, **kwargs: None,
) )
defaults.update(kwargs) defaults.update(kwargs)
return SimpleNamespace(**defaults) return _HashableChunkRequest(**defaults)
class _HashableChunkRequest(SimpleNamespace):
# Deferred queues require identity hashing; SimpleNamespace types it as None.
__hash__ = object.__hash__ # type: ignore[assignment]
__eq__ = object.__eq__
def test_chunk_lifecycle_no_resubmit_and_state_survives_requeue(monkeypatch) -> None: def test_chunk_lifecycle_no_resubmit_and_state_survives_requeue(monkeypatch) -> None:
@@ -138,7 +188,8 @@ def test_chunk_lifecycle_no_resubmit_and_state_survives_requeue(monkeypatch) ->
) )
scheduler.running = [] scheduler.running = []
scheduler.waiting = create_request_queue(scheduler.policy) scheduler.waiting = create_request_queue(scheduler.policy)
scheduler.skipped_waiting = create_request_queue(scheduler.policy) scheduler.kv_holding_waiting = create_request_queue(scheduler.policy)
scheduler.deferred_waiting = set()
completed = Request("completed", [1, 2], SamplingParams(max_tokens=4), pooling_params=None) completed = Request("completed", [1, 2], SamplingParams(max_tokens=4), pooling_params=None)
completed.status = RequestStatus.RUNNING completed.status = RequestStatus.RUNNING
@@ -292,6 +343,7 @@ def _express_scheduler():
scheduler = _make_generation_scheduler(continuation, use_v2_model_runner=True) scheduler = _make_generation_scheduler(continuation, use_v2_model_runner=True)
scheduler.waiting.add_request(first) scheduler.waiting.add_request(first)
scheduler.requests[first.request_id] = first scheduler.requests[first.request_id] = first
scheduler.max_num_active_reqs = 4
scheduler.max_num_running_reqs = 4 scheduler.max_num_running_reqs = 4
scheduler._native_data_plane = True scheduler._native_data_plane = True
scheduler.chunk_transfer_adapter = None scheduler.chunk_transfer_adapter = None
@@ -9,7 +9,7 @@ handing control to upstream, because upstream ``Scheduler.schedule()`` raises
admits in a finished state -- which kills the stage's engine core, not just admits in a finished state -- which kills the stage's engine core, not just
the request. the request.
Regression for the sweep missing ``skipped_waiting``, the third queue Regression for the sweep missing ``kv_holding_waiting``, the third queue
upstream admits from. An aborted duplex session parked there was re-selected upstream admits from. An aborted duplex session parked there was re-selected
by ``_select_waiting_queue_for_scheduling`` on a later tick and crashed the by ``_select_waiting_queue_for_scheduling`` on a later tick and crashed the
stage. stage.
@@ -49,28 +49,30 @@ class _StubRequest:
return (self.priority, self.arrival_time) < (other.priority, other.arrival_time) return (self.priority, self.arrival_time) < (other.priority, other.arrival_time)
def _make_scheduler(policy: SchedulingPolicy, *, waiting=(), skipped_waiting=(), running=()): def _make_scheduler(policy: SchedulingPolicy, *, waiting=(), kv_holding_waiting=(), running=()):
"""Bare scheduler with just the surface the sweep reads.""" """Bare scheduler with just the surface the sweep reads."""
scheduler = OmniARScheduler.__new__(OmniARScheduler) scheduler = OmniARScheduler.__new__(OmniARScheduler)
scheduler.waiting = create_request_queue(policy) scheduler.waiting = create_request_queue(policy)
for req in waiting: for req in waiting:
scheduler.waiting.add_request(req) scheduler.waiting.add_request(req)
scheduler.skipped_waiting = create_request_queue(policy) scheduler.kv_holding_waiting = create_request_queue(policy)
for req in skipped_waiting: scheduler.deferred_waiting = set()
scheduler.skipped_waiting.add_request(req) for req in kv_holding_waiting:
scheduler.kv_holding_waiting.add_request(req)
scheduler.running = list(running) scheduler.running = list(running)
return scheduler return scheduler
@pytest.mark.parametrize("policy", _POLICIES) @pytest.mark.parametrize("policy", _POLICIES)
def test_schedule_sweeps_skipped_waiting_before_upstream_selection( def test_schedule_sweeps_kv_holding_waiting_before_upstream_selection(
monkeypatch: pytest.MonkeyPatch, monkeypatch: pytest.MonkeyPatch,
policy: SchedulingPolicy, policy: SchedulingPolicy,
) -> None: ) -> None:
"""The public schedule path must remove aborted skipped requests first.""" """The public schedule path must remove aborted skipped requests first."""
aborted = _StubRequest("req-aborted", RequestStatus.FINISHED_ABORTED) aborted = _StubRequest("req-aborted", RequestStatus.FINISHED_ABORTED)
scheduler = _make_scheduler(policy, skipped_waiting=[aborted]) scheduler = _make_scheduler(policy, kv_holding_waiting=[aborted])
scheduler.chunk_transfer_adapter = None scheduler.chunk_transfer_adapter = None
scheduler.max_num_active_reqs = 8
scheduler.max_num_running_reqs = 8 scheduler.max_num_running_reqs = 8
scheduler.input_coordinator = None scheduler.input_coordinator = None
scheduler.num_waiting_for_streaming_input = 0 scheduler.num_waiting_for_streaming_input = 0
@@ -85,7 +87,7 @@ def test_schedule_sweeps_skipped_waiting_before_upstream_selection(
sentinel = object() sentinel = object()
def upstream_schedule(_self, _throttle_prefills: bool = False): def upstream_schedule(_self, _throttle_prefills: bool = False):
assert list(scheduler.skipped_waiting) == [] assert list(scheduler.kv_holding_waiting) == []
return sentinel return sentinel
monkeypatch.setattr(ar_sched_mod.VLLMScheduler, "schedule", upstream_schedule) monkeypatch.setattr(ar_sched_mod.VLLMScheduler, "schedule", upstream_schedule)
@@ -185,10 +185,15 @@ def test_stage_zero_final_skips_inter_stage_output(mocker):
def test_streaming_session_update_invalidates_omits_kv_transfer_cache(): def test_streaming_session_update_invalidates_omits_kv_transfer_cache():
scheduler = OmniARScheduler.__new__(OmniARScheduler) scheduler = OmniARScheduler.__new__(OmniARScheduler)
scheduler._omits_kv_transfer_cache = {} scheduler._omits_kv_transfer_cache = {}
scheduler.skipped_waiting = [] scheduler.deferred_waiting = set()
scheduler.kv_holding_waiting = []
scheduler.log_stats = False scheduler.log_stats = False
tagged = apply_omni_final_stage_metadata(_make_engine_request(), final_stage_id=0) tagged = apply_omni_final_stage_metadata(_make_engine_request(), final_stage_id=0)
session = SimpleNamespace(
class Session(SimpleNamespace):
__hash__ = object.__hash__
session = Session(
request_id="req", request_id="req",
additional_information=tagged.additional_information, additional_information=tagged.additional_information,
status=RequestStatus.WAITING, status=RequestStatus.WAITING,
@@ -11,12 +11,14 @@ from types import MethodType, SimpleNamespace
import numpy as np import numpy as np
import pytest import pytest
import torch
from vllm.v1.engine import FinishReason from vllm.v1.engine import FinishReason
from vllm.v1.outputs import LogprobsLists from vllm.v1.outputs import LogprobsLists
from vllm.v1.request import RequestStatus from vllm.v1.request import RequestStatus
from vllm_omni.core.sched.omni_ar_scheduler import OmniARScheduler, _slice_sampled_logprobs from vllm_omni.core.sched.omni_ar_scheduler import OmniARScheduler, _slice_sampled_logprobs
from vllm_omni.core.sched.omni_scheduler_mixin import OmniSchedulerMixin from vllm_omni.core.sched.omni_scheduler_mixin import OmniSchedulerMixin
from vllm_omni.outputs import OmniModelRunnerOutput
pytestmark = [pytest.mark.core_model, pytest.mark.cpu] pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
@@ -132,11 +134,13 @@ def _make_scheduler_stub(requests: list[_Request]) -> SimpleNamespace:
scheduler = SimpleNamespace( scheduler = SimpleNamespace(
perf_metrics=None, perf_metrics=None,
connector=None, connector=None,
aux_output_connector=None,
chunk_transfer_adapter=None, chunk_transfer_adapter=None,
requests={request.request_id: request for request in requests}, requests={request.request_id: request for request in requests},
running=list(requests), running=list(requests),
waiting=_RequestQueue(), waiting=_RequestQueue(),
skipped_waiting=_RequestQueue(), kv_holding_waiting=_RequestQueue(),
deferred_waiting=set(),
structured_output_manager=SimpleNamespace(accept_tokens=lambda _request, _token_ids: True), structured_output_manager=SimpleNamespace(accept_tokens=lambda _request, _token_ids: True),
transfer_triggered_requests=set(), transfer_triggered_requests=set(),
active_kv_transfers=set(), active_kv_transfers=set(),
@@ -292,6 +296,36 @@ def test_invalid_logprobs_finish_only_the_affected_scheduler_request() -> None:
np.testing.assert_array_equal(output_by_id["good"].new_logprobs.logprob_token_ids[:, 0], [8]) np.testing.assert_array_equal(output_by_id["good"].new_logprobs.logprob_token_ids[:, 0], [8])
@pytest.mark.parametrize("scored", [False, True])
def test_fixed_token_scores_reach_engine_output(scored) -> None:
request = _Request("req")
request.sampling_params.num_logprobs = None
scheduler = _make_scheduler_stub([request])
_bind_request_lifecycle(scheduler, update_request=lambda req, tokens: (tokens, False))
scheduler_output = SimpleNamespace(
num_scheduled_tokens={"req": 3},
scheduled_spec_decode_tokens={},
num_invalid_spec_tokens=0,
)
scores = torch.tensor([[-0.5, -1.5], [-0.2, -2.2]])
model_runner_output = OmniModelRunnerOutput(
req_ids=["req"],
req_id_to_index={"req": 0},
sampled_token_ids=[[7]],
prompt_logprobs_dict={},
prompt_token_id_logprobs_dict={"req": scores} if scored else {},
)
outputs = OmniARScheduler.update_from_output(scheduler, scheduler_output, model_runner_output)
(output,) = outputs[0].outputs
assert output.new_token_ids == [7]
if scored:
torch.testing.assert_close(output.prompt_token_id_logprobs, scores)
else:
assert output.prompt_token_id_logprobs is None
def _pooling_model_runner_output(pooler_tensor) -> SimpleNamespace: def _pooling_model_runner_output(pooler_tensor) -> SimpleNamespace:
return SimpleNamespace( return SimpleNamespace(
sampled_token_ids=[[]], sampled_token_ids=[[]],
@@ -73,7 +73,8 @@ def _replace_streaming_session(session: Request) -> None:
sched.num_waiting_for_streaming_input = 0 sched.num_waiting_for_streaming_input = 0
sched.log_stats = False sched.log_stats = False
sched.chunk_transfer_adapter = None sched.chunk_transfer_adapter = None
sched.skipped_waiting = set() sched.kv_holding_waiting = set()
sched.deferred_waiting = set()
session.status = RequestStatus.WAITING_FOR_STREAMING_REQ session.status = RequestStatus.WAITING_FOR_STREAMING_REQ
update = StreamingUpdate( update = StreamingUpdate(
mm_features=None, mm_features=None,
@@ -43,7 +43,8 @@ def _make_scheduler(*, stage_id: int = 0, session_mode: str = "turn") -> OmniARS
sched.num_waiting_for_streaming_input = 0 sched.num_waiting_for_streaming_input = 0
sched.log_stats = False sched.log_stats = False
sched.chunk_transfer_adapter = None sched.chunk_transfer_adapter = None
sched.skipped_waiting = set() sched.kv_holding_waiting = set()
sched.deferred_waiting = set()
sched._free_request_blocks = MagicMock() sched._free_request_blocks = MagicMock()
sched.encoder_cache_manager = MagicMock() sched.encoder_cache_manager = MagicMock()
sched._inflight_prefills = set() sched._inflight_prefills = set()
@@ -1185,7 +1186,8 @@ def test_chunk_segment_cleanup_keeps_requeued_resumable_receiver() -> None:
sched.vllm_config = SimpleNamespace(model_config=SimpleNamespace(session_mode="duplex")) sched.vllm_config = SimpleNamespace(model_config=SimpleNamespace(session_mode="duplex"))
sched.running = [] sched.running = []
sched.waiting = queue() sched.waiting = queue()
sched.skipped_waiting = queue(session) sched.kv_holding_waiting = queue(session)
sched.deferred_waiting = set()
sched.num_waiting_for_streaming_input = 1 sched.num_waiting_for_streaming_input = 1
sched.chunk_transfer_adapter = SimpleNamespace( sched.chunk_transfer_adapter = SimpleNamespace(
receives_chunks=True, receives_chunks=True,
@@ -1197,7 +1199,7 @@ def test_chunk_segment_cleanup_keeps_requeued_resumable_receiver() -> None:
assert session.status == RequestStatus.WAITING assert session.status == RequestStatus.WAITING
assert session in sched.waiting.requests assert session in sched.waiting.requests
assert session not in sched.skipped_waiting.requests assert session not in sched.kv_holding_waiting.requests
assert sched.num_waiting_for_streaming_input == 0 assert sched.num_waiting_for_streaming_input == 0
assert session.request_id not in sched.chunk_transfer_adapter.segment_finished_requests assert session.request_id not in sched.chunk_transfer_adapter.segment_finished_requests
@@ -1224,7 +1226,8 @@ def test_chunk_segment_cleanup_keeps_explicit_update_stage_parked(
receives_chunks=receives_chunks, receives_chunks=receives_chunks,
segment_finished_requests={session.request_id}, segment_finished_requests={session.request_id},
) )
sched.skipped_waiting = MagicMock() sched.kv_holding_waiting = MagicMock()
sched.deferred_waiting = set()
sched._enqueue_waiting_request = MagicMock() sched._enqueue_waiting_request = MagicMock()
sched._resume_downstream_chunk_receiver(session) sched._resume_downstream_chunk_receiver(session)
@@ -1232,7 +1235,7 @@ def test_chunk_segment_cleanup_keeps_explicit_update_stage_parked(
assert session.status == RequestStatus.WAITING_FOR_STREAMING_REQ assert session.status == RequestStatus.WAITING_FOR_STREAMING_REQ
assert sched.num_waiting_for_streaming_input == 1 assert sched.num_waiting_for_streaming_input == 1
assert session.request_id not in sched.chunk_transfer_adapter.segment_finished_requests assert session.request_id not in sched.chunk_transfer_adapter.segment_finished_requests
sched.skipped_waiting.remove_requests.assert_not_called() sched.kv_holding_waiting.remove_requests.assert_not_called()
sched._enqueue_waiting_request.assert_not_called() sched._enqueue_waiting_request.assert_not_called()
@@ -1288,7 +1291,8 @@ def _park_session(sched: OmniARScheduler, session: Request) -> None:
session.num_computed_tokens = 6 session.num_computed_tokens = 6
session.num_output_placeholders = 0 session.num_output_placeholders = 0
session.status = RequestStatus.WAITING_FOR_STREAMING_REQ session.status = RequestStatus.WAITING_FOR_STREAMING_REQ
sched.skipped_waiting.add_request(session) sched.kv_holding_waiting.add_request(session)
sched.deferred_waiting.add(session)
sched.num_waiting_for_streaming_input = 1 sched.num_waiting_for_streaming_input = 1
@@ -1311,7 +1315,7 @@ def test_stage0_streaming_update_that_overflows_max_model_len_finishes_the_sessi
assert session.status == RequestStatus.FINISHED_ERROR assert session.status == RequestStatus.FINISHED_ERROR
assert session.request_id not in sched.requests assert session.request_id not in sched.requests
assert len(sched.waiting) == 0 assert len(sched.waiting) == 0
assert len(sched.skipped_waiting) == 0 assert len(sched.kv_holding_waiting) == 0
assert sched.num_waiting_for_streaming_input == 0 assert sched.num_waiting_for_streaming_input == 0
sched._free_request_blocks.assert_called_once_with(session) sched._free_request_blocks.assert_called_once_with(session)
assert sched.finished_req_ids == {session.request_id} assert sched.finished_req_ids == {session.request_id}
@@ -1419,7 +1423,8 @@ def _make_admission_scheduler(*, max_model_len: int) -> OmniARScheduler:
sched.max_model_len = max_model_len sched.max_model_len = max_model_len
sched.policy = SchedulingPolicy.FCFS sched.policy = SchedulingPolicy.FCFS
sched.waiting = create_request_queue(sched.policy) sched.waiting = create_request_queue(sched.policy)
sched.skipped_waiting = create_request_queue(sched.policy) sched.kv_holding_waiting = create_request_queue(sched.policy)
sched.deferred_waiting = set()
sched.running = [] sched.running = []
sched.requests = {} sched.requests = {}
sched._free_request = MagicMock() # needs a KV manager; not what this covers sched._free_request = MagicMock() # needs a KV manager; not what this covers
@@ -1448,7 +1453,7 @@ def test_queued_streaming_update_that_overflows_does_not_return_to_admission() -
assert finished is True assert finished is True
assert len(sched.waiting) == 0 assert len(sched.waiting) == 0
assert len(sched.skipped_waiting) == 0 assert len(sched.kv_holding_waiting) == 0
assert session.status == RequestStatus.FINISHED_ERROR assert session.status == RequestStatus.FINISHED_ERROR
assert session.resumable is False assert session.resumable is False
assert "context_length_exceeded: " in sched._streaming_context_overflow[session.request_id][1] assert "context_length_exceeded: " in sched._streaming_context_overflow[session.request_id][1]
@@ -1467,7 +1472,8 @@ def test_queued_streaming_update_that_fits_still_resumes_the_session() -> None:
finished = sched._handle_stopped_request(session) finished = sched._handle_stopped_request(session)
assert finished is False assert finished is False
assert len(sched.waiting) == 1 assert list(sched.kv_holding_waiting) == [session]
assert session not in sched.deferred_waiting
assert session.status == RequestStatus.WAITING assert session.status == RequestStatus.WAITING
assert not getattr(sched, "_streaming_context_overflow", {}) assert not getattr(sched, "_streaming_context_overflow", {})
sched._free_request.assert_not_called() sched._free_request.assert_not_called()
@@ -1489,7 +1495,7 @@ def test_queued_stop_with_a_recorded_overflow_still_leaves_admission() -> None:
assert finished is True assert finished is True
assert len(sched.waiting) == 0 assert len(sched.waiting) == 0
assert len(sched.skipped_waiting) == 0 assert len(sched.kv_holding_waiting) == 0
assert session.status == RequestStatus.FINISHED_ERROR assert session.status == RequestStatus.FINISHED_ERROR
assert session.resumable is False assert session.resumable is False
sched._free_request.assert_not_called() sched._free_request.assert_not_called()
@@ -1512,7 +1518,7 @@ def test_parked_stop_with_a_recorded_overflow_keeps_the_streaming_counter_balanc
finished = sched._handle_stopped_request(session) finished = sched._handle_stopped_request(session)
assert finished is True assert finished is True
assert len(sched.skipped_waiting) == 0 assert len(sched.kv_holding_waiting) == 0
assert sched.num_waiting_for_streaming_input == 0 assert sched.num_waiting_for_streaming_input == 0
assert session.status == RequestStatus.FINISHED_ERROR assert session.status == RequestStatus.FINISHED_ERROR
sched._free_request.assert_not_called() sched._free_request.assert_not_called()
@@ -1545,6 +1551,7 @@ def test_async_chunk_reserves_parked_slots_during_ar_admission(monkeypatch, nati
sched.running = [] sched.running = []
sched._native_data_plane = native sched._native_data_plane = native
sched.use_v2_model_runner = native sched.use_v2_model_runner = native
sched.max_num_active_reqs = 8
sched.max_num_running_reqs = 8 sched.max_num_running_reqs = 8
sched.input_coordinator = ( sched.input_coordinator = (
SimpleNamespace(_async_chunk=True, _waiting_for_chunk_running=[parked], restore_queues=lambda _w, _r: None) SimpleNamespace(_async_chunk=True, _waiting_for_chunk_running=[parked], restore_queues=lambda _w, _r: None)
@@ -1571,7 +1578,7 @@ def test_async_chunk_reserves_parked_slots_during_ar_admission(monkeypatch, nati
observed_limits: list[int] = [] observed_limits: list[int] = []
def fake_schedule(self, _throttle_prefills=False): def fake_schedule(self, _throttle_prefills=False):
observed_limits.append(self.max_num_running_reqs) observed_limits.append(self.max_num_active_reqs)
return SimpleNamespace(scheduled_new_reqs=[]) return SimpleNamespace(scheduled_new_reqs=[])
monkeypatch.setattr(VLLMScheduler, "schedule", fake_schedule) monkeypatch.setattr(VLLMScheduler, "schedule", fake_schedule)
@@ -1579,4 +1586,4 @@ def test_async_chunk_reserves_parked_slots_during_ar_admission(monkeypatch, nati
sched.schedule() sched.schedule()
assert observed_limits == [7 if native else 8] assert observed_limits == [7 if native else 8]
assert sched.max_num_running_reqs == 8 assert sched.max_num_active_reqs == 8
@@ -79,16 +79,19 @@ def _make_scheduler(
else: else:
hf_config.voxcpm2_runtime_config = runtime_config hf_config.voxcpm2_runtime_config = runtime_config
sched.vllm_config = SimpleNamespace(model_config=model_config) sched.vllm_config = SimpleNamespace(model_config=model_config)
# Upstream ``Scheduler.__init__`` always creates ``skipped_waiting``; the # Upstream ``Scheduler.__init__`` always creates ``kv_holding_waiting``; the
# abort sweep at the top of ``schedule()`` reads it. # abort sweep at the top of ``schedule()`` reads it.
sched.skipped_waiting = _MockQueue() sched.kv_holding_waiting = _MockQueue()
sched.deferred_waiting = set()
return sched return sched
def test_voxcpm2_unified_decode_graph_defers_waiting_when_decode_ready() -> None: @pytest.mark.parametrize("queue_name", ["waiting", "kv_holding_waiting"])
def test_voxcpm2_unified_decode_graph_defers_waiting_when_decode_ready(queue_name) -> None:
scheduler = _make_scheduler() scheduler = _make_scheduler()
scheduler.running = [_MockRequest("decode")] scheduler.running = [_MockRequest("decode")]
scheduler.waiting = _MockQueue([_MockRequest("prefill", status=RequestStatus.WAITING)]) scheduler.waiting = _MockQueue()
setattr(scheduler, queue_name, _MockQueue([_MockRequest("prefill", status=RequestStatus.WAITING)]))
assert scheduler._should_defer_waiting_for_unified_decode_graph() assert scheduler._should_defer_waiting_for_unified_decode_graph()
@@ -144,12 +147,17 @@ def test_voxcpm2_unified_decode_graph_does_not_defer_without_cuda_graph(monkeypa
def test_unified_decode_graph_deferral_restores_waiting_queue(monkeypatch) -> None: def test_unified_decode_graph_deferral_restores_waiting_queue(monkeypatch) -> None:
scheduler = _make_scheduler() scheduler = _make_scheduler()
scheduler.max_num_active_reqs = 8
scheduler.max_num_running_reqs = 8 scheduler.max_num_running_reqs = 8
scheduler.running = [_MockRequest("decode")] scheduler.running = [_MockRequest("decode")]
original_waiting_req = _MockRequest("waiting", status=RequestStatus.WAITING) original_waiting_req = _MockRequest("waiting", status=RequestStatus.WAITING)
deferred_by_upstream = _MockRequest("deferred-by-upstream", status=RequestStatus.WAITING) deferred_by_upstream = _MockRequest("deferred-by-upstream", status=RequestStatus.WAITING)
original_waiting = _MockQueue([original_waiting_req]) original_waiting = _MockQueue([original_waiting_req])
scheduler.waiting = original_waiting scheduler.waiting = original_waiting
kv_holder = _MockRequest("kv-holder", status=RequestStatus.WAITING)
original_kv_holding = _MockQueue([kv_holder])
scheduler.kv_holding_waiting = original_kv_holding
preempted_by_upstream = _MockRequest("preempted-by-upstream", status=RequestStatus.PREEMPTED)
scheduler.policy = "fcfs" scheduler.policy = "fcfs"
scheduler.chunk_transfer_adapter = None scheduler.chunk_transfer_adapter = None
scheduler.input_coordinator = None scheduler.input_coordinator = None
@@ -160,7 +168,10 @@ def test_unified_decode_graph_deferral_restores_waiting_queue(monkeypatch) -> No
def fake_upstream_schedule(self, throttle_prefills: bool = False): def fake_upstream_schedule(self, throttle_prefills: bool = False):
assert self.waiting is not original_waiting assert self.waiting is not original_waiting
assert not self.waiting assert not self.waiting
assert self.kv_holding_waiting is not original_kv_holding
assert not self.kv_holding_waiting
self.waiting.add_request(deferred_by_upstream) self.waiting.add_request(deferred_by_upstream)
self.kv_holding_waiting.add_request(preempted_by_upstream)
raise RuntimeError("stop before output wrapping") raise RuntimeError("stop before output wrapping")
monkeypatch.setattr(scheduler_mod.VLLMScheduler, "schedule", fake_upstream_schedule) monkeypatch.setattr(scheduler_mod.VLLMScheduler, "schedule", fake_upstream_schedule)
@@ -169,3 +180,6 @@ def test_unified_decode_graph_deferral_restores_waiting_queue(monkeypatch) -> No
scheduler.schedule() scheduler.schedule()
assert scheduler.waiting._items == [deferred_by_upstream, original_waiting_req] assert scheduler.waiting._items == [deferred_by_upstream, original_waiting_req]
assert scheduler.waiting is original_waiting
assert scheduler.kv_holding_waiting is original_kv_holding
assert scheduler.kv_holding_waiting._items == [preempted_by_upstream, kv_holder]
@@ -23,10 +23,12 @@ def _scheduler(monkeypatch, waiting, running=()):
scheduler.waiting = create_request_queue(scheduler.policy) scheduler.waiting = create_request_queue(scheduler.policy)
for request in waiting: for request in waiting:
scheduler.waiting.add_request(request) scheduler.waiting.add_request(request)
scheduler.skipped_waiting = create_request_queue(scheduler.policy) scheduler.kv_holding_waiting = create_request_queue(scheduler.policy)
scheduler.deferred_waiting = set()
scheduler.running = list(running) scheduler.running = list(running)
scheduler.requests = {request.request_id: request for request in [*waiting, *running]} scheduler.requests = {request.request_id: request for request in [*waiting, *running]}
scheduler.max_model_len = 128 scheduler.max_model_len = 128
scheduler.max_num_active_reqs = 2
scheduler.max_num_running_reqs = 2 scheduler.max_num_running_reqs = 2
scheduler.max_num_scheduled_tokens = 256 scheduler.max_num_scheduled_tokens = 256
scheduler.block_size = 16 scheduler.block_size = 16
@@ -52,10 +52,11 @@ class _SchedulerStub(OmniGenerationScheduler):
self.num_waiting_for_streaming_input = 0 self.num_waiting_for_streaming_input = 0
self.log_stats = log_stats self.log_stats = log_stats
self.chunk_transfer_adapter = _ChunkTransferAdapterStub() self.chunk_transfer_adapter = _ChunkTransferAdapterStub()
self.skipped_waiting = _SkippedWaitingStub() self.kv_holding_waiting = _SkippedWaitingStub()
self.deferred_waiting: set[Request] = set()
def _enqueue_waiting_request(self, session: Request) -> None: def _enqueue_waiting_request(self, session: Request) -> None:
raise AssertionError("unexpected enqueue for skipped_waiting miss") raise AssertionError("unexpected enqueue for kv_holding_waiting miss")
class _AsyncChunkStopSchedulerStub(OmniGenerationScheduler): class _AsyncChunkStopSchedulerStub(OmniGenerationScheduler):
@@ -134,7 +135,8 @@ def test_resumable_generation_stop_marks_segment_boundary() -> None:
sched._handle_stopped_request.side_effect = rearm_resumable_request sched._handle_stopped_request.side_effect = rearm_resumable_request
sched.running = [session] sched.running = [session]
sched.waiting = MagicMock() sched.waiting = MagicMock()
sched.skipped_waiting = MagicMock() sched.kv_holding_waiting = MagicMock()
sched.deferred_waiting = set()
sched.structured_output_manager.accept_tokens.return_value = True sched.structured_output_manager.accept_tokens.return_value = True
# Async scheduling can observe the segment boundary in the next schedule() # Async scheduling can observe the segment boundary in the next schedule()
# before this model output is applied and defer the same request for finish. # before this model output is applied and defer the same request for finish.
@@ -206,7 +208,8 @@ def test_rejected_grammar_finishes_and_frees_generation_request(prompt_complete:
OmniSchedulerMixin._remove_stopped_requests_from_queues(sched, running, preempted) OmniSchedulerMixin._remove_stopped_requests_from_queues(sched, running, preempted)
) )
sched.waiting = MagicMock() sched.waiting = MagicMock()
sched.skipped_waiting = MagicMock() sched.kv_holding_waiting = MagicMock()
sched.deferred_waiting = set()
sched.structured_output_manager.accept_tokens.return_value = False sched.structured_output_manager.accept_tokens.return_value = False
sched._pending_finish_reqs = [] sched._pending_finish_reqs = []
sched.recompute_kv_load_failures = False sched.recompute_kv_load_failures = False
@@ -350,6 +353,8 @@ class _RealignSchedulerStub(OmniSchedulerMixin):
self.requests = requests self.requests = requests
self.running = running self.running = running
self.waiting = waiting self.waiting = waiting
self.kv_holding_waiting: list[Request] = []
self.deferred_waiting: set[Request] = set()
class TestRealignRequestStatusToQueues: class TestRealignRequestStatusToQueues:
@@ -123,6 +123,7 @@ def _make_fast_path_sched(*, defer_block_free: bool) -> tuple[MagicMock, Request
sched = MagicMock() sched = MagicMock()
sched.max_num_scheduled_tokens = 8 sched.max_num_scheduled_tokens = 8
sched.max_num_active_reqs = 8
sched.max_num_running_reqs = 8 sched.max_num_running_reqs = 8
sched._native_data_plane = False sched._native_data_plane = False
sched._retains_state_across_chunks = False sched._retains_state_across_chunks = False
@@ -135,6 +136,8 @@ def _make_fast_path_sched(*, defer_block_free: bool) -> tuple[MagicMock, Request
sched.requests = {request.request_id: request} sched.requests = {request.request_id: request}
sched.running = [request] sched.running = [request]
sched.waiting = [] # falsy: the waiting loop never runs sched.waiting = [] # falsy: the waiting loop never runs
sched.kv_holding_waiting = []
sched.deferred_waiting = set()
sched.policy = SchedulingPolicy.FCFS sched.policy = SchedulingPolicy.FCFS
sched.chunk_transfer_adapter = None sched.chunk_transfer_adapter = None
sched.scheduler_config = MagicMock(enable_chunked_prefill=True) sched.scheduler_config = MagicMock(enable_chunked_prefill=True)
@@ -1,3 +1,6 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Integration tests for the post-finish defensive purge of """Integration tests for the post-finish defensive purge of
``self.running`` placed in ``self.running`` placed in
``OmniARScheduler.finish_requests`` / ``OmniARScheduler.finish_requests`` /
@@ -52,6 +55,8 @@ def _make_scheduler(scheduler_cls, *, requests, running, waiting):
scheduler.requests = requests scheduler.requests = requests
scheduler.running = running scheduler.running = running
scheduler.waiting = waiting scheduler.waiting = waiting
scheduler.kv_holding_waiting = []
scheduler.deferred_waiting = set()
return scheduler return scheduler
@@ -52,6 +52,8 @@ def test_finish_requests_cleans_input_coordinator_for_finished_ids(
scheduler.requests = {} scheduler.requests = {}
scheduler.running = [] scheduler.running = []
scheduler.waiting = [] scheduler.waiting = []
scheduler.kv_holding_waiting = []
scheduler.deferred_waiting = set()
def fake_finish_requests(self, request_ids, finished_status): def fake_finish_requests(self, request_ids, finished_status):
assert request_ids == ["req-a", "req-b"] assert request_ids == ["req-a", "req-b"]
@@ -7,7 +7,10 @@ from typing import Any
import pytest import pytest
from vllm.config import SchedulerConfig, VllmConfig from vllm.config import SchedulerConfig, VllmConfig
from vllm.sampling_params import SamplingParams
from vllm.v1.core.sched.scheduler import Scheduler
from vllm.v1.engine import FinishReason from vllm.v1.engine import FinishReason
from vllm.v1.request import Request
from vllm_omni.config.model import OmniModelConfig from vllm_omni.config.model import OmniModelConfig
from vllm_omni.core.sched import omni_scheduler_mixin from vllm_omni.core.sched import omni_scheduler_mixin
@@ -18,7 +21,7 @@ pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
class _Scheduler(OmniSchedulerMixin): class _Scheduler(OmniSchedulerMixin):
pass _is_blocked_waiting_status = staticmethod(Scheduler._is_blocked_waiting_status)
def test_async_chunk_adapter_initializes_for_stage_zero_sender_and_stage_one_receiver(monkeypatch): def test_async_chunk_adapter_initializes_for_stage_zero_sender_and_stage_one_receiver(monkeypatch):
@@ -120,7 +123,11 @@ def test_native_data_plane_uses_the_selected_input_protocol(async_chunk, stage_i
def test_schedule_lifecycle_helpers_process_and_restore_both_input_paths(): def test_schedule_lifecycle_helpers_process_and_restore_both_input_paths():
calls: list[tuple[Any, ...]] = [] calls: list[tuple[Any, ...]] = []
scheduler = _Scheduler() scheduler = _Scheduler()
scheduler.waiting = ["waiting"] waiting = Request("waiting", [1], SamplingParams(max_tokens=1), pooling_params=None)
holding = Request("holding", [1, 2], SamplingParams(max_tokens=1), pooling_params=None)
holding.num_computed_tokens = 1
scheduler.waiting = [waiting]
scheduler.kv_holding_waiting = [holding]
scheduler.running = ["running"] scheduler.running = ["running"]
scheduler.requests = {"request": object()} scheduler.requests = {"request": object()}
scheduler._consume_pending_connector_output = lambda mode: calls.append(("consume", mode)) scheduler._consume_pending_connector_output = lambda mode: calls.append(("consume", mode))
@@ -137,16 +144,16 @@ def test_schedule_lifecycle_helpers_process_and_restore_both_input_paths():
scheduler.chunk_transfer_adapter = SimpleNamespace( scheduler.chunk_transfer_adapter = SimpleNamespace(
receives_chunks=True, receives_chunks=True,
process_pending_chunks=lambda waiting, running, scheduler_requests: calls.append( process_pending_chunks=lambda waiting, running, scheduler_requests: calls.append(
("process", waiting, running, scheduler_requests) ("process", list(waiting), running, scheduler_requests)
), ),
restore_queues=lambda waiting, running, scheduler_requests: calls.append( restore_queues=lambda waiting, running, scheduler_requests: calls.append(
("restore-chunks", waiting, running, scheduler_requests) ("restore-chunks", list(waiting), running, scheduler_requests)
), ),
collect_timed_out_request_ids=_collect_timed_out, collect_timed_out_request_ids=_collect_timed_out,
collect_failed_send_request_ids=_collect_failed_sends, collect_failed_send_request_ids=_collect_failed_sends,
) )
scheduler.input_coordinator = SimpleNamespace( scheduler.input_coordinator = SimpleNamespace(
restore_queues=lambda waiting, running: calls.append(("restore-full", waiting, running)) restore_queues=lambda waiting, running: calls.append(("restore-full", list(waiting), running))
) )
scheduler._process_pending_omni_inputs("ar") scheduler._process_pending_omni_inputs("ar")
@@ -155,13 +162,13 @@ def test_schedule_lifecycle_helpers_process_and_restore_both_input_paths():
assert calls == [ assert calls == [
("consume", "ar"), ("consume", "ar"),
("timeouts",), ("timeouts",),
("process", scheduler.waiting, scheduler.running, scheduler.requests), ("process", [holding, waiting], scheduler.running, scheduler.requests),
# The chunk deadline runs after chunks are applied, so a chunk that # The chunk deadline runs after chunks are applied, so a chunk that
# arrived this cycle resets the clock before it is measured (R1.1). # arrived this cycle resets the clock before it is measured (R1.1).
("chunk-timeouts", omni_scheduler_mixin.DEFAULT_INPUT_WAIT_TIMEOUT_S), ("chunk-timeouts", omni_scheduler_mixin.DEFAULT_INPUT_WAIT_TIMEOUT_S),
("failed-sends",), ("failed-sends",),
("restore-chunks", scheduler.waiting, scheduler.running, scheduler.requests), ("restore-chunks", [holding, waiting], scheduler.running, scheduler.requests),
("restore-full", scheduler.waiting, scheduler.running), ("restore-full", [holding, waiting], scheduler.running),
] ]
@@ -5,8 +5,13 @@
from types import SimpleNamespace from types import SimpleNamespace
import pytest import pytest
from vllm import SamplingParams
from vllm.v1.core.sched.request_queue import SchedulingPolicy, create_request_queue
from vllm.v1.request import Request, RequestStatus
from vllm_omni.core.sched.omni_ar_scheduler import OmniARScheduler
from vllm_omni.core.sched.omni_scheduler_mixin import OmniSchedulerMixin from vllm_omni.core.sched.omni_scheduler_mixin import OmniSchedulerMixin
from vllm_omni.core.sched.omni_scheduling_coordinator import OmniSchedulingCoordinator
from vllm_omni.outputs import OmniConnectorOutput from vllm_omni.outputs import OmniConnectorOutput
pytestmark = [pytest.mark.core_model, pytest.mark.cpu] pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
@@ -15,6 +20,8 @@ pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
def test_ready_inbox_coalesces_live_events_and_drops_cancelled(mocker): def test_ready_inbox_coalesces_live_events_and_drops_cancelled(mocker):
scheduler = OmniSchedulerMixin() scheduler = OmniSchedulerMixin()
scheduler.requests, scheduler.waiting, scheduler.running = {"r": object()}, [], [] scheduler.requests, scheduler.waiting, scheduler.running = {"r": object()}, [], []
scheduler.kv_holding_waiting = []
scheduler._is_blocked_waiting_status = OmniARScheduler._is_blocked_waiting_status
coordinator = SimpleNamespace( coordinator = SimpleNamespace(
_async_chunk=True, update_request_metadata=mocker.Mock(), process_pending_chunks=mocker.Mock() _async_chunk=True, update_request_metadata=mocker.Mock(), process_pending_chunks=mocker.Mock()
) )
@@ -32,7 +39,92 @@ def test_ready_inbox_coalesces_live_events_and_drops_cancelled(mocker):
coordinator.update_request_metadata.assert_called_once_with( coordinator.update_request_metadata.assert_called_once_with(
scheduler.requests, {"r": {"decode_token_end": 2}}, model_mode="ar" scheduler.requests, {"r": {"decode_token_end": 2}}, model_mode="ar"
) )
coordinator.process_pending_chunks.assert_called_once_with([], [], {"r"}, {"r"}) coordinator.process_pending_chunks.assert_called_once_with(mocker.ANY, [], {"r"}, {"r"})
assert list(coordinator.process_pending_chunks.call_args.args[0]) == []
scheduler._consume_pending_connector_output(model_mode="ar") scheduler._consume_pending_connector_output(model_mode="ar")
assert coordinator.update_request_metadata.call_count == 1 assert coordinator.update_request_metadata.call_count == 1
coordinator.process_pending_chunks.assert_called_with([], [], set(), set()) coordinator.process_pending_chunks.assert_called_with(mocker.ANY, [], set(), set())
assert list(coordinator.process_pending_chunks.call_args.args[0]) == []
@pytest.mark.parametrize("policy", [SchedulingPolicy.FCFS, SchedulingPolicy.PRIORITY])
@pytest.mark.parametrize("async_chunk", [False, True])
def test_native_input_gate_parks_and_restores_kv_holders(policy, async_chunk):
scheduler = OmniARScheduler.__new__(OmniARScheduler)
scheduler.waiting = create_request_queue(policy)
scheduler.kv_holding_waiting = create_request_queue(policy)
scheduler.deferred_waiting = set()
scheduler.running = []
scheduler.chunk_transfer_adapter = None
scheduler.input_coordinator = OmniSchedulingCoordinator(
stage_id=1, scheduler_max_num_seqs=2, async_chunk=async_chunk
)
scheduler._init_omni_connector_output_inbox()
holder = Request("holder", [1, 2, 3], SamplingParams(max_tokens=1), pooling_params=None)
holder.num_computed_tokens = 2
fresh = Request("fresh", [1], SamplingParams(max_tokens=1), pooling_params=None)
scheduler.requests = {request.request_id: request for request in (holder, fresh)}
scheduler._enqueue_waiting_request(fresh)
scheduler._enqueue_waiting_request(holder)
scheduler._consume_pending_connector_output(model_mode="ar")
assert not scheduler.waiting and not scheduler.kv_holding_waiting
expected_status = RequestStatus.WAITING_FOR_CHUNK if async_chunk else RequestStatus.WAITING_FOR_INPUT
assert holder.status == fresh.status == expected_status
coordinator = scheduler.input_coordinator
registrations = coordinator.pending_chunk_registrations if async_chunk else coordinator.pending_input_registrations
assert [entry.request_id for entry in registrations] == ["holder", "fresh"]
scheduler._restore_omni_wait_queues()
assert list(scheduler.kv_holding_waiting) == [holder]
assert list(scheduler.waiting) == [fresh]
ready_output = (
OmniConnectorOutput(chunk_ready_req_ids={"holder"})
if async_chunk
else OmniConnectorOutput(stage_recv_req_ids={"holder"})
)
scheduler.enqueue_omni_connector_output(ready_output)
scheduler._consume_pending_connector_output(model_mode="ar")
assert holder.status == RequestStatus.WAITING
assert fresh.status == expected_status
assert list(scheduler.kv_holding_waiting) == [holder]
assert not scheduler.waiting
scheduler._restore_omni_wait_queues()
assert list(scheduler.kv_holding_waiting) == [holder]
assert list(scheduler.waiting) == [fresh]
@pytest.mark.parametrize("async_chunk", [False, True])
@pytest.mark.parametrize(
"status",
[
RequestStatus.WAITING_FOR_STREAMING_REQ,
RequestStatus.WAITING_FOR_REMOTE_KVS,
RequestStatus.WAITING_FOR_STRUCTURED_OUTPUT_GRAMMAR,
],
)
def test_native_input_gate_preserves_upstream_blocked_waits(async_chunk, status):
scheduler = OmniARScheduler.__new__(OmniARScheduler)
scheduler.waiting = create_request_queue(SchedulingPolicy.FCFS)
scheduler.kv_holding_waiting = create_request_queue(SchedulingPolicy.FCFS)
scheduler.deferred_waiting = set()
scheduler.running = []
request = Request("blocked", [1, 2, 3], SamplingParams(max_tokens=1), pooling_params=None)
request.status = status
request.num_computed_tokens = 2
scheduler.requests = {request.request_id: request}
scheduler._enqueue_waiting_request(request)
scheduler.input_coordinator = OmniSchedulingCoordinator(
stage_id=1, scheduler_max_num_seqs=1, async_chunk=async_chunk
)
scheduler._init_omni_connector_output_inbox()
scheduler._consume_pending_connector_output(model_mode="ar")
assert request.status == status
assert list(scheduler.kv_holding_waiting) == [request]
assert scheduler.deferred_waiting == {request}
assert not scheduler.input_coordinator.pending_chunk_registrations
assert not scheduler.input_coordinator.pending_input_registrations
@@ -59,7 +59,8 @@ def _make_scheduler(scheduler_cls, *, requests, running, waiting, counter, skipp
scheduler.requests = requests scheduler.requests = requests
scheduler.running = running scheduler.running = running
scheduler.waiting = waiting scheduler.waiting = waiting
scheduler.skipped_waiting = skipped if skipped is not None else [] scheduler.kv_holding_waiting = skipped if skipped is not None else []
scheduler.deferred_waiting = set()
scheduler.num_waiting_for_streaming_input = counter scheduler.num_waiting_for_streaming_input = counter
return scheduler return scheduler
@@ -97,8 +98,8 @@ def test_finish_requests_clears_counter_left_by_a_stomped_status(
assert scheduler.num_waiting_for_streaming_input == 0 assert scheduler.num_waiting_for_streaming_input == 0
def test_resync_counts_parked_requests_in_skipped_waiting() -> None: def test_resync_counts_parked_requests_in_kv_holding_waiting() -> None:
"""``get_num_unfinished_requests`` sums ``waiting`` and ``skipped_waiting``, """``get_num_unfinished_requests`` sums ``waiting`` and ``kv_holding_waiting``,
so the resync has to look at both or it under-counts and reintroduces the so the resync has to look at both or it under-counts and reintroduces the
spin the counter exists to prevent.""" spin the counter exists to prevent."""
parked = _StubRequest("req-skipped", RequestStatus.WAITING_FOR_STREAMING_REQ) parked = _StubRequest("req-skipped", RequestStatus.WAITING_FOR_STREAMING_REQ)
@@ -128,9 +129,11 @@ def test_ar_schedule_resyncs_before_delegating_upstream(monkeypatch: pytest.Monk
scheduler.requests = {parked.request_id: parked} scheduler.requests = {parked.request_id: parked}
scheduler.running = [parked] scheduler.running = [parked]
scheduler.waiting = [] scheduler.waiting = []
scheduler.skipped_waiting = [] scheduler.kv_holding_waiting = []
scheduler.deferred_waiting = set()
scheduler.num_waiting_for_streaming_input = 1 # leaked by the status rewrite scheduler.num_waiting_for_streaming_input = 1 # leaked by the status rewrite
scheduler.policy = "fcfs" scheduler.policy = "fcfs"
scheduler.max_num_active_reqs = 8
scheduler.max_num_running_reqs = 8 scheduler.max_num_running_reqs = 8
seen: dict[str, int] = {} seen: dict[str, int] = {}
@@ -161,12 +164,13 @@ def test_generation_scheduler_sweep_removes_aborted_requests_from_all_queues() -
scheduler = OmniGenerationScheduler.__new__(OmniGenerationScheduler) scheduler = OmniGenerationScheduler.__new__(OmniGenerationScheduler)
scheduler.waiting = create_request_queue(SchedulingPolicy.FCFS) scheduler.waiting = create_request_queue(SchedulingPolicy.FCFS)
scheduler.waiting.add_request(aborted) scheduler.waiting.add_request(aborted)
scheduler.skipped_waiting = create_request_queue(SchedulingPolicy.FCFS) scheduler.kv_holding_waiting = create_request_queue(SchedulingPolicy.FCFS)
scheduler.skipped_waiting.add_request(aborted) scheduler.deferred_waiting = set()
scheduler.kv_holding_waiting.add_request(aborted)
scheduler.running = [aborted] scheduler.running = [aborted]
scheduler._drop_aborted_queued_requests() scheduler._drop_aborted_queued_requests()
assert list(scheduler.waiting) == [] assert list(scheduler.waiting) == []
assert list(scheduler.skipped_waiting) == [] assert list(scheduler.kv_holding_waiting) == []
assert scheduler.running == [] assert scheduler.running == []
@@ -216,44 +216,40 @@ def test_video_chat_stream_invalid_requests(
# WS /v1/realtime # WS /v1/realtime
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
# Resolved in ``test_realtime_invalid_requests`` (skip ``session.created`` handshake).
_REALTIME_WS_MODEL_MISMATCH = object()
_REALTIME_WS_INVALID_AUDIO_APPEND = object()
@hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2) @hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2)
@pytest.mark.parametrize( @pytest.mark.parametrize(
"send_frames_spec, ws_error_code, err_message", "send_frames_spec, ws_error_code, err_message",
[ [
pytest.param("{not-json", "invalid_json", "Invalid JSON", id="invalid_json"), pytest.param("{not-json", "invalid_event", "Invalid or unrecognized client event", id="invalid_json"),
pytest.param( pytest.param(
json.dumps({"type": "session.update"}), json.dumps({"type": "session.update"}),
"invalid_event", "invalid_event",
("model", "Missing required field"), "Invalid or unrecognized client event",
id="session_update_missing_model", id="session_update_missing_session",
), ),
pytest.param( pytest.param(
_REALTIME_WS_MODEL_MISMATCH, json.dumps({"type": "session.update", "session": []}),
"model_not_found", "invalid_event",
("model", "does not exist"), "Invalid or unrecognized client event",
id="session_update_model_mismatch", id="session_update_invalid_session",
), ),
pytest.param( pytest.param(
json.dumps({"type": "input_audio_buffer.commit", "final": False}), json.dumps({"type": "input_audio_buffer.commit"}),
"model_not_validated", "invalid_request_error",
("session.update", "validate the model"), "Input audio buffer is empty",
id="commit_without_model_validation", id="commit_without_audio",
), ),
pytest.param( pytest.param(
json.dumps({"type": "unknown.event"}), json.dumps({"type": "unknown.event"}),
"unknown_event", "invalid_event",
"Unknown event type", "Invalid or unrecognized client event",
id="unknown_event_type", id="unknown_event_type",
), ),
pytest.param( pytest.param(
_REALTIME_WS_INVALID_AUDIO_APPEND, json.dumps({"type": "input_audio_buffer.append", "audio": "not-valid-base64!!!"}),
"invalid_audio", "invalid_request_error",
"Invalid audio data", "Invalid base64 audio data",
id="invalid_audio_append", id="invalid_audio_append",
), ),
], ],
@@ -266,27 +262,12 @@ def test_realtime_invalid_requests(
ws_error_code: str, ws_error_code: str,
err_message: str | tuple[str, ...], err_message: str | tuple[str, ...],
) -> None: ) -> None:
send_frames: Any
if send_frames_spec is _REALTIME_WS_MODEL_MISMATCH:
send_frames = json.dumps(
{
"type": "session.update",
"model": "this-model-is-not-served-on-this-instance",
}
)
elif send_frames_spec is _REALTIME_WS_INVALID_AUDIO_APPEND:
send_frames = [
json.dumps({"type": "session.update", "model": omni_server.model}),
json.dumps({"type": "input_audio_buffer.append", "audio": "not-valid-base64!!!"}),
]
else:
send_frames = send_frames_spec
online_client.send_realtime_ws_request( online_client.send_realtime_ws_request(
{ {
"send_frames": send_frames, "send_frames": send_frames_spec,
"timeout": 120, "timeout": 120,
"ws_max_size": None, "ws_max_size": None,
"ws_skip_types": ["session.created"], "ws_skip_types": ["session.created", "conversation.created"],
"ws_json_type": "error", "ws_json_type": "error",
"ws_error_code": ws_error_code, "ws_error_code": ws_error_code,
"err_message": err_message, "err_message": err_message,
+3 -1
View File
@@ -329,7 +329,9 @@ def _uneven_cfg_sharded_worker(
init_method=init_method, init_method=init_method,
rank=rank, rank=rank,
world_size=4, world_size=4,
timeout=datetime.timedelta(seconds=10), # Spawned workers import torch/vLLM before joining; allow startup
# skew without changing the collective deadlock checks below.
timeout=datetime.timedelta(seconds=60),
) )
try: try:
fs_groups = [ fs_groups = [
@@ -103,7 +103,7 @@ def test_block_hashing_uses_upstream_extra_keys_directly(monkeypatch):
# Independent expected native extras: a crossing item has a negative # Independent expected native extras: a crossing item has a negative
# relative offset; a block can include the end of one item and the next. # relative offset; a block can include the end of one item and the next.
first, second = (feature.identifier for feature in row.mm_features) first, second = (feature.identifier for feature in row.mm_features)
extras = [((first, 2),), ((first, -2), (second, 3)), ((second, -1),)] extras = [(("mm", first, 2),), (("mm", first, -2), ("mm", second, 3)), (("mm", second, -1),)]
parent = BlockHash(hash_fn(("diffusion-prefix-root-v1", row.cache_namespace))) parent = BlockHash(hash_fn(("diffusion-prefix-root-v1", row.cache_namespace)))
expected = [] expected = []
for start, keys in zip(range(0, 12, 4), extras, strict=True): for start, keys in zip(range(0, 12, 4), extras, strict=True):
@@ -31,6 +31,10 @@ def producer(monkeypatch):
# vLLM 0.30 send_kv_to_decode also reads the PP size (ce08bb5b34, # vLLM 0.30 send_kv_to_decode also reads the PP size (ce08bb5b34,
# #56033), the MLA/Mamba flags and the head-resharding validator. # #56033), the MLA/Mamba flags and the head-resharding validator.
worker.pp_size = 1 worker.pp_size = 1
worker.opaque_packed_storages = set()
worker._prepared_transfer_regions = {}
worker.region_shared_groups = []
worker.region_row_offsets = []
worker.use_mla = False worker.use_mla = False
worker.kv_cache_config = SimpleNamespace(has_mamba_layers=False) worker.kv_cache_config = SimpleNamespace(has_mamba_layers=False)
worker._validate_head_resharding_layout = Mock(return_value=None) worker._validate_head_resharding_layout = Mock(return_value=None)
@@ -1253,14 +1253,15 @@ def test_layer_adapter_accepts_platform_native_backend_and_uses_rank_local_heads
speculative_config=None, speculative_config=None,
) )
def select_backend(*, backend, attn_selector_config, num_heads): def select_backend(*, backend, attn_selector_config, num_heads, _run_kv_cache_dtype):
# Exercise the real 0.29 selector, replacing only platform resolution. # Exercise the real upstream selector, replacing only platform resolution.
# Ulysses has already gathered tokens and sharded heads before the # Ulysses has already gathered tokens and sharded heads before the
# native kernel; the MoE PCP mapping must not request PCP attention. # native kernel; the MoE PCP mapping must not request PCP attention.
assert not attn_selector_config.use_pcp assert not attn_selector_config.use_pcp
assert not attn_selector_config.use_dcp assert not attn_selector_config.use_dcp
assert attn_selector_config.use_non_causal assert attn_selector_config.use_non_causal
assert num_heads == 4 assert num_heads == 4
assert _run_kv_cache_dtype == config.cache_config.cache_dtype
selected_backends.append((backend, config.attention_config.backend_per_kind)) selected_backends.append((backend, config.attention_config.backend_per_kind))
if selection_fails: if selection_fails:
raise ValueError("test backend unavailable") raise ValueError("test backend unavailable")
+15 -4
View File
@@ -14,6 +14,7 @@ from typing import Literal
import pytest import pytest
import torch import torch
from tests.helpers.distributed import configure_cpu_collective_worker, start_collective_workers
from tests.helpers.mark import hardware_marks from tests.helpers.mark import hardware_marks
from vllm_omni.diffusion.distributed.comm import RingComm, SeqAllToAll4D, SeqAllToAll5D from vllm_omni.diffusion.distributed.comm import RingComm, SeqAllToAll4D, SeqAllToAll5D
from vllm_omni.diffusion.distributed.parallel_state import ( from vllm_omni.diffusion.distributed.parallel_state import (
@@ -56,6 +57,8 @@ def _init_worker(
"MASTER_PORT": str(master_port), "MASTER_PORT": str(master_port),
} }
) )
if device_kind == "cpu":
configure_cpu_collective_worker()
backend = "gloo" if device_kind == "cpu" else None backend = "gloo" if device_kind == "cpu" else None
init_distributed_environment(backend=backend) init_distributed_environment(backend=backend)
@@ -227,6 +230,10 @@ def _run_ring_p2p(
atol=atol, atol=atol,
msg=f"[Rank {local_rank}] Ring P2P data mismatch", msg=f"[Rank {local_rank}] Ring P2P data mismatch",
) )
if device_kind == "cpu":
# P2P completion only synchronizes neighbors. Keep fast CPU ranks
# alive until every rank has finished constructing and using groups.
torch.distributed.barrier()
finally: finally:
destroy_distributed_env() destroy_distributed_env()
@@ -243,7 +250,7 @@ def _spawn_4d_identity(
device_kind: DeviceKind, device_kind: DeviceKind,
master_port: int, master_port: int,
) -> None: ) -> None:
torch.multiprocessing.spawn( start_collective_workers(
_run_4d_identity, _run_4d_identity,
args=( args=(
world_size, world_size,
@@ -257,6 +264,7 @@ def _spawn_4d_identity(
master_port, master_port,
), ),
nprocs=world_size, nprocs=world_size,
device_kind=device_kind,
) )
@@ -272,7 +280,7 @@ def _spawn_5d_identity(
device_kind: DeviceKind, device_kind: DeviceKind,
master_port: int, master_port: int,
) -> None: ) -> None:
torch.multiprocessing.spawn( start_collective_workers(
_run_5d_identity, _run_5d_identity,
args=( args=(
world_size, world_size,
@@ -286,6 +294,7 @@ def _spawn_5d_identity(
master_port, master_port,
), ),
nprocs=world_size, nprocs=world_size,
device_kind=device_kind,
) )
@@ -299,10 +308,11 @@ def _spawn_ring_p2p(
device_kind: DeviceKind, device_kind: DeviceKind,
master_port: int, master_port: int,
) -> None: ) -> None:
torch.multiprocessing.spawn( start_collective_workers(
_run_ring_p2p, _run_ring_p2p,
args=(world_size, dtype, batch_size, num_heads, head_size, device_kind, master_port), args=(world_size, dtype, batch_size, num_heads, head_size, device_kind, master_port),
nprocs=world_size, nprocs=world_size,
device_kind=device_kind,
) )
@@ -499,8 +509,9 @@ def _run_fused_qkv_matches_three_4d(
@pytest.mark.cpu @pytest.mark.cpu
@pytest.mark.parametrize("world_size", [2, 4]) @pytest.mark.parametrize("world_size", [2, 4])
def test_fused_qkv_all_to_all_matches_three_4d_exchanges_cpu(world_size: int) -> None: def test_fused_qkv_all_to_all_matches_three_4d_exchanges_cpu(world_size: int) -> None:
torch.multiprocessing.spawn( start_collective_workers(
_run_fused_qkv_matches_three_4d, _run_fused_qkv_matches_three_4d,
args=(world_size, "cpu", 29613), args=(world_size, "cpu", 29613),
nprocs=world_size, nprocs=world_size,
device_kind="cpu",
) )
@@ -19,6 +19,7 @@ from vllm.utils.network_utils import get_file_store_init_method
from vllm.v1.worker.gpu_worker import AsyncIntermediateTensors from vllm.v1.worker.gpu_worker import AsyncIntermediateTensors
import vllm_omni.diffusion.distributed.pipeline_parallel as pp_module import vllm_omni.diffusion.distributed.pipeline_parallel as pp_module
from tests.helpers.distributed import collective_context, start_collective_workers
from tests.helpers.mark import hardware_marks from tests.helpers.mark import hardware_marks
from vllm_omni.diffusion.distributed.cfg_parallel import CFGParallelMixin from vllm_omni.diffusion.distributed.cfg_parallel import CFGParallelMixin
from vllm_omni.diffusion.distributed.parallel_state import ( from vllm_omni.diffusion.distributed.parallel_state import (
@@ -39,6 +40,25 @@ DeviceKind = Literal["cpu", "cuda"]
_UNIT_MARKS = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cpu] _UNIT_MARKS = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cpu]
@pytest.fixture(autouse=True)
def _restore_collective_platforms():
"""CPU baselines run in the parent process; retain its original platforms."""
import vllm.platforms
from vllm_omni.diffusion.distributed import comm, group_coordinator, parallel_state
original_platform = vllm.platforms.current_platform
original_omni_platforms = {
module: module.current_omni_platform for module in (comm, group_coordinator, parallel_state)
}
try:
yield
finally:
vllm.platforms.current_platform = original_platform
for module, platform in original_omni_platforms.items():
module.current_omni_platform = platform
def _cleanup_distributed() -> None: def _cleanup_distributed() -> None:
"""Destroy omni distributed state left dangling after a test. """Destroy omni distributed state left dangling after a test.
@@ -409,6 +429,10 @@ def init_dist(
device = _worker_device(local_rank, device_kind) device = _worker_device(local_rank, device_kind)
if device_kind == "cuda": if device_kind == "cuda":
current_omni_platform.set_device(device) current_omni_platform.set_device(device)
else:
from tests.helpers.distributed import configure_cpu_collective_worker
configure_cpu_collective_worker()
backend = "gloo" if device_kind == "cpu" else None backend = "gloo" if device_kind == "cpu" else None
init_distributed_environment( init_distributed_environment(
world_size=world_size, world_size=world_size,
@@ -500,13 +524,14 @@ def isend_irecv_worker(
def _run_isend_irecv(pp_size: int, device_kind: DeviceKind, init_method: str) -> None: def _run_isend_irecv(pp_size: int, device_kind: DeviceKind, init_method: str) -> None:
mp_context = torch.multiprocessing.get_context("spawn") mp_context = collective_context(device_kind)
manager = mp_context.Manager() manager = mp_context.Manager()
q = manager.Queue() q = manager.Queue()
torch.multiprocessing.spawn( start_collective_workers(
isend_irecv_worker, isend_irecv_worker,
args=(pp_size, init_method, device_kind, q), args=(pp_size, init_method, device_kind, q),
nprocs=pp_size, nprocs=pp_size,
device_kind=device_kind,
) )
results = {label: tensor for label, tensor in [q.get(), q.get()]} results = {label: tensor for label, tensor in [q.get(), q.get()]}
torch.testing.assert_close( torch.testing.assert_close(
@@ -656,15 +681,16 @@ def _run_predict_noise(
baseline_out = compute_single_gpu_baseline(test_config, dtype, do_true_cfg, device_kind) baseline_out = compute_single_gpu_baseline(test_config, dtype, do_true_cfg, device_kind)
mp_context = torch.multiprocessing.get_context("spawn") mp_context = collective_context(device_kind)
manager = mp_context.Manager() manager = mp_context.Manager()
pp_q = manager.Queue() pp_q = manager.Queue()
world_size = pp_size * cfg_size world_size = pp_size * cfg_size
torch.multiprocessing.spawn( start_collective_workers(
predict_noise_worker, predict_noise_worker,
args=(world_size, init_method, pp_size, cfg_size, do_true_cfg, dtype, test_config, device_kind, pp_q), args=(world_size, init_method, pp_size, cfg_size, do_true_cfg, dtype, test_config, device_kind, pp_q),
nprocs=world_size, nprocs=world_size,
device_kind=device_kind,
) )
pp_out = pp_q.get() pp_out = pp_q.get()
@@ -900,15 +926,16 @@ def _run_scheduler_step(
baseline = compute_scheduler_step_baseline(test_config, do_true_cfg, device_kind) baseline = compute_scheduler_step_baseline(test_config, do_true_cfg, device_kind)
mp_context = torch.multiprocessing.get_context("spawn") mp_context = collective_context(device_kind)
manager = mp_context.Manager() manager = mp_context.Manager()
q = manager.Queue() q = manager.Queue()
world_size = pp_size * cfg_size world_size = pp_size * cfg_size
torch.multiprocessing.spawn( start_collective_workers(
scheduler_step_worker, scheduler_step_worker,
args=(world_size, init_method, pp_size, cfg_size, do_true_cfg, test_config, device_kind, q), args=(world_size, init_method, pp_size, cfg_size, do_true_cfg, test_config, device_kind, q),
nprocs=world_size, nprocs=world_size,
device_kind=device_kind,
) )
result = q.get() result = q.get()
@@ -239,11 +239,11 @@ def test_constructor_rejects_unsupported_mllm_quantization(mock_dependencies, mo
) )
def test_constructor_routes_mllm_quantization(mock_dependencies, quantization_config, quantize_mllm): def test_constructor_routes_mllm_quantization(mock_dependencies, quantization_config, quantize_mllm):
from transformers import AutoModel from transformers import AutoModel
from vllm.model_executor.layers.quantization.fp8 import Fp8Config
from vllm.model_executor.layers.quantization.online.base import OnlineQuantizationConfig from vllm.model_executor.layers.quantization.online.base import OnlineQuantizationConfig
from vllm.model_executor.layers.quantization.utils.quant_utils import kFp8Static128BlockSym from vllm.model_executor.layers.quantization.utils.quant_utils import kFp8Static128BlockSym
from vllm_omni.diffusion.models.boogu_image.pipeline_boogu_image import BooguImagePipeline from vllm_omni.diffusion.models.boogu_image.pipeline_boogu_image import BooguImagePipeline
from vllm_omni.quantization.fp8_config import DiffusionFp8Config
od_config = OmniDiffusionConfig( od_config = OmniDiffusionConfig(
model="dummy-boogu", model="dummy-boogu",
@@ -287,15 +287,14 @@ def test_constructor_routes_mllm_quantization(mock_dependencies, quantization_co
mock_dependencies["mllm_config_loader"].assert_not_called() mock_dependencies["mllm_config_loader"].assert_not_called()
mock_dependencies["mllm_builder"].assert_not_called() mock_dependencies["mllm_builder"].assert_not_called()
assert [source.subfolder for source in pipeline.weights_sources] == ["transformer"] assert [source.subfolder for source in pipeline.weights_sources] == ["transformer"]
assert isinstance(mock_dependencies["transformer_cls"].call_args.kwargs["quant_config"], Fp8Config) assert isinstance(mock_dependencies["transformer_cls"].call_args.kwargs["quant_config"], DiffusionFp8Config)
def test_constructor_preserves_mllm_ignored_layers(mock_dependencies): def test_constructor_preserves_mllm_ignored_layers(mock_dependencies):
from vllm.model_executor.layers.quantization.fp8 import Fp8Config
from vllm_omni.diffusion.models.boogu_image.pipeline_boogu_image import BooguImagePipeline from vllm_omni.diffusion.models.boogu_image.pipeline_boogu_image import BooguImagePipeline
from vllm_omni.quantization.fp8_config import DiffusionFp8Config
quant_config = Fp8Config( quant_config = DiffusionFp8Config(
ignored_layers=["mllm.language_model.layers.0.self_attn.q_proj", "transformer.blocks.0.attn.to_q"] ignored_layers=["mllm.language_model.layers.0.self_attn.q_proj", "transformer.blocks.0.attn.to_q"]
) )
od_config = OmniDiffusionConfig( od_config = OmniDiffusionConfig(
@@ -16,17 +16,13 @@ class _RecordingBlock(nn.Module):
def test_flux2_propagates_online_quant_config_to_all_transformer_blocks(monkeypatch): def test_flux2_propagates_online_quant_config_to_all_transformer_blocks(monkeypatch):
from vllm.model_executor.layers.quantization.fp8 import Fp8Config
from vllm_omni.diffusion.models.flux2 import flux2_transformer from vllm_omni.diffusion.models.flux2 import flux2_transformer
from vllm_omni.quantization import build_quant_config
monkeypatch.setattr(flux2_transformer, "Flux2TransformerBlock", _RecordingBlock) monkeypatch.setattr(flux2_transformer, "Flux2TransformerBlock", _RecordingBlock)
monkeypatch.setattr(flux2_transformer, "Flux2SingleTransformerBlock", _RecordingBlock) monkeypatch.setattr(flux2_transformer, "Flux2SingleTransformerBlock", _RecordingBlock)
quant_config = Fp8Config( quant_config = build_quant_config("fp8")
is_checkpoint_fp8_serialized=False,
activation_scheme="dynamic",
)
model = flux2_transformer.Flux2Transformer2DModel( model = flux2_transformer.Flux2Transformer2DModel(
in_channels=4, in_channels=4,
out_channels=4, out_channels=4,
@@ -71,12 +71,12 @@ def _small_od_config(*, class_name=None):
def test_fp8_scope_and_prefix_propagation(monkeypatch): def test_fp8_scope_and_prefix_propagation(monkeypatch):
from vllm.model_executor.layers.linear import LinearBase, UnquantizedLinearMethod from vllm.model_executor.layers.linear import LinearBase, UnquantizedLinearMethod
from vllm.model_executor.layers.quantization.fp8 import Fp8Config
from vllm.model_executor.layers.quantization.utils.quant_utils import ( from vllm.model_executor.layers.quantization.utils.quant_utils import (
is_layer_skipped, is_layer_skipped,
) )
from vllm_omni.diffusion.models.minimax_h3 import minimax_h3_transformer as h3 from vllm_omni.diffusion.models.minimax_h3 import minimax_h3_transformer as h3
from vllm_omni.quantization.fp8_config import DiffusionFp8Config
monkeypatch.setattr(h3, "ColumnParallelLinear", _FakeLinear) monkeypatch.setattr(h3, "ColumnParallelLinear", _FakeLinear)
monkeypatch.setattr(h3, "MergedColumnParallelLinear", _FakeLinear) monkeypatch.setattr(h3, "MergedColumnParallelLinear", _FakeLinear)
@@ -92,7 +92,7 @@ def test_fp8_scope_and_prefix_propagation(monkeypatch):
"blocks.0.adaln_proj.linear", "blocks.0.adaln_proj.linear",
"final_layer.adaln_proj.linear", "final_layer.adaln_proj.linear",
} }
fp8_config = Fp8Config(ignored_layers=sorted(ignored_layers)) fp8_config = DiffusionFp8Config(ignored_layers=sorted(ignored_layers))
model = h3.MiniMaxH3DiTModel( model = h3.MiniMaxH3DiTModel(
_small_od_config(), _small_od_config(),
quant_config=fp8_config, quant_config=fp8_config,
+78 -13
View File
@@ -15,6 +15,74 @@ def test_build_quant_config_fp8():
assert config is not None assert config is not None
assert config.get_name() == "fp8" assert config.get_name() == "fp8"
assert config.activation_scheme == "dynamic" assert config.activation_scheme == "dynamic"
assert not config.is_checkpoint_fp8_serialized
def test_build_quant_config_fp8_uses_native_online_quantization():
from vllm.model_executor.layers.quantization.online.base import OnlineQuantizationConfig
from vllm.model_executor.layers.quantization.utils.quant_utils import kFp8StaticTensorSym
from vllm_omni.quantization import build_quant_config
config = build_quant_config("fp8", ignored_layers=["proj_out"])
assert isinstance(config, OnlineQuantizationConfig)
assert config.args.linear.weight == kFp8StaticTensorSym
assert config.args.moe.weight == kFp8StaticTensorSym
assert config.ignored_layers == ["proj_out"]
def test_build_quant_config_upstream_online_fp8_shorthand():
from vllm.model_executor.layers.quantization.online.base import OnlineQuantizationConfig
from vllm.model_executor.layers.quantization.utils.quant_utils import kFp8StaticTensorSym
from vllm_omni.quantization import build_quant_config
config = build_quant_config("fp8_per_tensor", ignore=["proj_out"])
assert isinstance(config, OnlineQuantizationConfig)
assert config.args.linear.weight == kFp8StaticTensorSym
assert config.ignored_layers == ["proj_out"]
def test_online_fp8_rejects_static_activation():
from vllm_omni.quantization import build_quant_config
with pytest.raises(ValueError, match="activation_scheme='dynamic'"):
build_quant_config("fp8", activation_scheme="static")
def test_build_online_quant_config_preserves_explicit_native_args():
from vllm.config.quantization import QuantizationConfigArgs, QuantSpec
from vllm.model_executor.layers.quantization.online.base import OnlineQuantizationConfig
from vllm_omni.quantization import build_quant_config
args = QuantizationConfigArgs(linear=QuantSpec(weight="fp8_per_tensor_static"), ignore=["proj_out"])
config = build_quant_config("online", args=args)
assert isinstance(config, OnlineQuantizationConfig)
assert config.args is args
assert config.ignored_layers == ["proj_out"]
def test_build_online_quant_config_requires_args():
from vllm_omni.quantization import build_quant_config
with pytest.raises(ValueError, match="requires quantization config arguments"):
build_quant_config("online")
def test_serialized_checkpoint_replaces_online_fp8_config():
from vllm.model_executor.layers.quantization.fp8 import Fp8Config
from vllm_omni.quantization import build_quant_config
from vllm_omni.quantization.factory import resolve_quant_config_from_disk
config = resolve_quant_config_from_disk(
build_quant_config("fp8"),
{"quant_method": "fp8", "is_checkpoint_fp8_serialized": True, "activation_scheme": "static"},
)
assert isinstance(config, Fp8Config)
assert config.is_checkpoint_fp8_serialized
assert config.activation_scheme == "static"
def test_build_quant_config_none(): def test_build_quant_config_none():
@@ -39,7 +107,7 @@ def test_build_quant_config_invalid():
def test_build_quant_config_dict(): def test_build_quant_config_dict():
from vllm_omni.quantization import build_quant_config from vllm_omni.quantization import build_quant_config
config = build_quant_config({"method": "fp8", "activation_scheme": "static"}) config = build_quant_config({"method": "fp8", "is_checkpoint_fp8_serialized": True, "activation_scheme": "static"})
assert config is not None assert config is not None
assert config.get_name() == "fp8" assert config.get_name() == "fp8"
assert config.activation_scheme == "static" assert config.activation_scheme == "static"
@@ -48,7 +116,7 @@ def test_build_quant_config_dict():
def test_build_quant_config_dict_not_mutated(): def test_build_quant_config_dict_not_mutated():
from vllm_omni.quantization import build_quant_config from vllm_omni.quantization import build_quant_config
original = {"method": "fp8", "activation_scheme": "static"} original = {"method": "fp8", "is_checkpoint_fp8_serialized": True, "activation_scheme": "static"}
copy = original.copy() copy = original.copy()
build_quant_config(original) build_quant_config(original)
assert original == copy assert original == copy
@@ -99,7 +167,7 @@ def test_build_quant_config_per_component_inner_dict_not_mutated():
"""Inner component dicts should not be mutated by build_quant_config.""" """Inner component dicts should not be mutated by build_quant_config."""
from vllm_omni.quantization import build_quant_config from vllm_omni.quantization import build_quant_config
inner = {"method": "fp8", "activation_scheme": "static"} inner = {"method": "fp8", "is_checkpoint_fp8_serialized": True, "activation_scheme": "static"}
original = inner.copy() original = inner.copy()
build_quant_config({"transformer": inner, "vae": None}) build_quant_config({"transformer": inner, "vae": None})
assert inner == original assert inner == original
@@ -115,20 +183,18 @@ def test_flat_dict_not_misdetected_as_per_component():
def test_build_quant_config_passthrough(): def test_build_quant_config_passthrough():
from vllm.model_executor.layers.quantization.fp8 import Fp8Config
from vllm_omni.quantization import build_quant_config from vllm_omni.quantization import build_quant_config
from vllm_omni.quantization.fp8_config import DiffusionFp8Config
fp8 = Fp8Config(is_checkpoint_fp8_serialized=False, activation_scheme="dynamic") fp8 = DiffusionFp8Config()
assert build_quant_config(fp8) is fp8 assert build_quant_config(fp8) is fp8
def test_component_config_routing(): def test_component_config_routing():
from vllm.model_executor.layers.quantization.fp8 import Fp8Config
from vllm_omni.quantization import ComponentQuantizationConfig from vllm_omni.quantization import ComponentQuantizationConfig
from vllm_omni.quantization.fp8_config import DiffusionFp8Config
fp8 = Fp8Config(is_checkpoint_fp8_serialized=False, activation_scheme="dynamic") fp8 = DiffusionFp8Config()
config = ComponentQuantizationConfig( config = ComponentQuantizationConfig(
component_configs={"transformer": fp8, "vae": None}, component_configs={"transformer": fp8, "vae": None},
) )
@@ -140,11 +206,10 @@ def test_component_config_routing():
def test_component_config_with_default(): def test_component_config_with_default():
from vllm.model_executor.layers.quantization.fp8 import Fp8Config
from vllm_omni.quantization import ComponentQuantizationConfig from vllm_omni.quantization import ComponentQuantizationConfig
from vllm_omni.quantization.fp8_config import DiffusionFp8Config
fp8 = Fp8Config(is_checkpoint_fp8_serialized=False, activation_scheme="dynamic") fp8 = DiffusionFp8Config()
config = ComponentQuantizationConfig( config = ComponentQuantizationConfig(
component_configs={"vae": None}, component_configs={"vae": None},
default_config=fp8, default_config=fp8,
@@ -167,7 +232,7 @@ def test_integration_dict():
config = OmniDiffusionConfig( config = OmniDiffusionConfig(
model="test", model="test",
quantization_config={"method": "fp8", "activation_scheme": "static"}, quantization_config={"method": "fp8", "is_checkpoint_fp8_serialized": True, "activation_scheme": "static"},
) )
assert config.quantization_config is not None assert config.quantization_config is not None
assert config.quantization_config.get_name() == "fp8" assert config.quantization_config.get_name() == "fp8"
@@ -0,0 +1,72 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Exercise online FP8 loading and inference through the native CUDA path."""
import pytest
import torch
from torch.nn import functional as F
from vllm.config import set_current_vllm_config
from vllm.distributed import (
destroy_distributed_environment,
destroy_model_parallel,
init_distributed_environment,
initialize_model_parallel,
)
from vllm.model_executor.layers.linear import ReplicatedLinear, UnquantizedLinearMethod
from vllm.model_executor.layers.quantization.online.fp8 import Fp8PerTensorOnlineLinearMethod
from vllm.utils.network_utils import get_open_port
from vllm.utils.torch_utils import set_default_torch_dtype
from tests.helpers.mark import hardware_test
from vllm_omni.diffusion.data import OmniDiffusionConfig
from vllm_omni.diffusion.vllm_config import create_diffusion_vllm_config
from vllm_omni.quantization import build_quant_config
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion]
@pytest.mark.parametrize("method", ["fp8", "fp8_per_tensor"])
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_online_fp8_loads_bf16_weights_and_runs_native_kernel(method):
ignored_key = "ignored_layers" if method == "fp8" else "ignore"
config = build_quant_config(method, **{ignored_key: ["skip"]})
od_config = OmniDiffusionConfig(model="test", dtype=torch.bfloat16, quantization_config=config)
vllm_config = create_diffusion_vllm_config(torch.device("cuda:0"), od_config)
try:
init_distributed_environment(
world_size=1,
rank=0,
local_rank=0,
distributed_init_method=f"tcp://127.0.0.1:{get_open_port()}",
backend="nccl",
)
with set_current_vllm_config(vllm_config), set_default_torch_dtype(torch.bfloat16), torch.device("cuda:0"):
initialize_model_parallel(tensor_model_parallel_size=1, pipeline_model_parallel_size=1)
layer = ReplicatedLinear(
128, 128, bias=False, params_dtype=torch.bfloat16, quant_config=config, prefix="linear", disable_tp=True
)
assert isinstance(layer.quant_method, Fp8PerTensorOnlineLinearMethod)
assert layer.weight.is_meta
generator = torch.Generator(device="cuda").manual_seed(42)
weights = torch.randn(128, 128, generator=generator, dtype=torch.bfloat16) / 128**0.5
inputs = torch.randn(32, 128, generator=generator, dtype=torch.bfloat16)
expected = F.linear(inputs, weights).float()
# This is the loader installed by the native online quantizer.
# It materializes and quantizes the BF16 checkpoint in one pass.
layer.weight.weight_loader(layer.weight, weights)
assert layer.weight.dtype == torch.float8_e4m3fn
actual = layer(inputs)[0].float()
relative_mse = (actual - expected).square().mean() / expected.square().mean()
assert relative_mse.item() < 0.002
ignored = ReplicatedLinear(
128, 128, bias=False, params_dtype=torch.bfloat16, quant_config=config, prefix="skip", disable_tp=True
)
assert isinstance(ignored.quant_method, UnquantizedLinearMethod)
assert ignored.weight.dtype == torch.bfloat16
finally:
destroy_model_parallel()
destroy_distributed_environment()
@@ -11,6 +11,7 @@ import pytest
import torch import torch
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from vllm.sampling_params import SamplingParams from vllm.sampling_params import SamplingParams
from vllm.v1.core.sched.request_queue import SchedulingPolicy, create_request_queue
from vllm.v1.core.sched.scheduler import Scheduler as VLLMScheduler from vllm.v1.core.sched.scheduler import Scheduler as VLLMScheduler
from vllm.v1.metrics.stats import PrefillStats, PromptTokenStats from vllm.v1.metrics.stats import PrefillStats, PromptTokenStats
from vllm.v1.request import Request, RequestStatus from vllm.v1.request import Request, RequestStatus
@@ -635,6 +636,45 @@ def build_adapter(monkeypatch, mocker: MockerFixture):
return _build return _build
@pytest.mark.parametrize("active_stream_window", [0, 2])
def test_scheduler_chunk_gate_preserves_kv_holding_queue(build_adapter, active_stream_window):
adapter, _ = build_adapter(active_stream_window=active_stream_window)
scheduler = OmniARScheduler.__new__(OmniARScheduler)
scheduler.waiting = create_request_queue(SchedulingPolicy.FCFS)
scheduler.kv_holding_waiting = create_request_queue(SchedulingPolicy.FCFS)
scheduler.deferred_waiting = set()
scheduler.running = []
scheduler.chunk_transfer_adapter = adapter
scheduler.input_coordinator = None
holder = Request("holder", [1, 2, 3], SamplingParams(max_tokens=1), pooling_params=None)
holder.num_computed_tokens = 2
fresh = Request("fresh", [1], SamplingParams(max_tokens=1), pooling_params=None)
scheduler.requests = {request.request_id: request for request in (holder, fresh)}
scheduler._enqueue_waiting_request(fresh)
scheduler._enqueue_waiting_request(holder)
scheduler._process_pending_omni_inputs(model_mode="ar")
assert not scheduler.waiting and not scheduler.kv_holding_waiting
assert list(adapter.waiting_for_chunk_waiting_requests) == [holder, fresh]
assert holder.status == fresh.status == RequestStatus.WAITING_FOR_CHUNK
scheduler._restore_omni_wait_queues()
assert list(scheduler.kv_holding_waiting) == [holder]
assert list(scheduler.waiting) == [fresh]
adapter._finished_load_reqs.add(holder.request_id)
scheduler._process_pending_omni_inputs(model_mode="ar")
assert holder.status == RequestStatus.WAITING
assert fresh.status == RequestStatus.WAITING_FOR_CHUNK
assert list(scheduler.kv_holding_waiting) == [holder]
assert not scheduler.waiting
assert adapter.requests_with_ready_chunks == {holder.request_id}
scheduler._restore_omni_wait_queues()
assert list(scheduler.kv_holding_waiting) == [holder]
assert list(scheduler.waiting) == [fresh]
def _dequeue_load_entry(adapter, request): def _dequeue_load_entry(adapter, request):
"""Register and take the entry as the background recv loop would.""" """Register and take the entry as the background recv loop would."""
adapter.load_async(request) adapter.load_async(request)
@@ -2445,7 +2485,8 @@ def test_finish_requests_reclaims_resumable_segment_and_reuses_capacity(
scheduler.requests = {request.request_id: request} scheduler.requests = {request.request_id: request}
scheduler.running = [request] if placement == "running" else [] scheduler.running = [request] if placement == "running" else []
scheduler.waiting = DummyWaitingQueue([request] if placement == "waiting" else []) scheduler.waiting = DummyWaitingQueue([request] if placement == "waiting" else [])
scheduler.skipped_waiting = DummyWaitingQueue([request] if placement.startswith("skipped") else []) scheduler.kv_holding_waiting = DummyWaitingQueue([request] if placement.startswith("skipped") else [])
scheduler.deferred_waiting = set()
scheduler.num_waiting_for_streaming_input = int(streaming_counter_owned) scheduler.num_waiting_for_streaming_input = int(streaming_counter_owned)
freed = [] freed = []
@@ -2469,7 +2510,7 @@ def test_finish_requests_reclaims_resumable_segment_and_reuses_capacity(
assert not adapter.waiting_for_chunk_running_requests assert not adapter.waiting_for_chunk_running_requests
assert scheduler.running == [] assert scheduler.running == []
assert list(scheduler.waiting) == [] assert list(scheduler.waiting) == []
assert list(scheduler.skipped_waiting) == [] assert list(scheduler.kv_holding_waiting) == []
assert scheduler.num_waiting_for_streaming_input == 0 assert scheduler.num_waiting_for_streaming_input == 0
fresh_a = _req("req-fresh-a", RequestStatus.WAITING) fresh_a = _req("req-fresh-a", RequestStatus.WAITING)
@@ -2492,7 +2533,8 @@ def test_finish_requests_does_not_reopen_off_queue_resumable_segment(build_adapt
scheduler.requests = {request.request_id: request} scheduler.requests = {request.request_id: request}
scheduler.running = [] scheduler.running = []
scheduler.waiting = DummyWaitingQueue() scheduler.waiting = DummyWaitingQueue()
scheduler.skipped_waiting = DummyWaitingQueue() scheduler.kv_holding_waiting = DummyWaitingQueue()
scheduler.deferred_waiting = set()
scheduler._free_request = lambda *args, **kwargs: pytest.fail("off-queue request was freed") scheduler._free_request = lambda *args, **kwargs: pytest.fail("off-queue request was freed")
assert ( assert (
@@ -2566,6 +2608,7 @@ def test_generation_scheduler_calls_cleanup_on_finished(monkeypatch, mocker: Moc
scheduler = mocker.MagicMock() scheduler = mocker.MagicMock()
scheduler._first_chunk_express = False scheduler._first_chunk_express = False
scheduler.aux_output_connector = None
scheduler._express_min_slack_s = 0.0 scheduler._express_min_slack_s = 0.0
scheduler.chunk_transfer_adapter = adapter_mock scheduler.chunk_transfer_adapter = adapter_mock
scheduler.connector = None scheduler.connector = None
@@ -2646,6 +2689,7 @@ def test_ar_scheduler_defers_cleanup_and_queues_save_on_finished(mocker: MockerF
adapter_mock.save_async = lambda *a, **kw: save_calls.append((a, kw)) adapter_mock.save_async = lambda *a, **kw: save_calls.append((a, kw))
scheduler = mocker.MagicMock() scheduler = mocker.MagicMock()
scheduler.aux_output_connector = None
scheduler.chunk_transfer_adapter = adapter_mock scheduler.chunk_transfer_adapter = adapter_mock
bind_omits_transfer_helpers(scheduler) bind_omits_transfer_helpers(scheduler)
scheduler.connector = None scheduler.connector = None
@@ -2744,6 +2788,8 @@ def test_omni_ar_scheduler_finish_requests(mocker: MockerFixture):
sched.requests = {} sched.requests = {}
sched.running = [] sched.running = []
sched.waiting = [] sched.waiting = []
sched.kv_holding_waiting = []
sched.deferred_waiting = set()
with patch.object(VLLMScheduler, "finish_requests", _super_finish): with patch.object(VLLMScheduler, "finish_requests", _super_finish):
OmniARScheduler.finish_requests(sched, ["r1"], RequestStatus.FINISHED_ABORTED) OmniARScheduler.finish_requests(sched, ["r1"], RequestStatus.FINISHED_ABORTED)
@@ -2759,7 +2805,8 @@ def _parked_sender_scheduler(adapter, session):
scheduler.requests = {session.request_id: session} scheduler.requests = {session.request_id: session}
scheduler.running = [] scheduler.running = []
scheduler.waiting = DummyWaitingQueue() scheduler.waiting = DummyWaitingQueue()
scheduler.skipped_waiting = DummyWaitingQueue([session]) scheduler.kv_holding_waiting = DummyWaitingQueue([session])
scheduler.deferred_waiting = set()
scheduler.num_waiting_for_streaming_input = 1 scheduler.num_waiting_for_streaming_input = 1
scheduler.finished_req_ids_dict = {} scheduler.finished_req_ids_dict = {}
+26
View File
@@ -0,0 +1,26 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
import pytest
@pytest.fixture
def sana_model_parallel(tmp_path):
"""Construct native parallel layers with real single-rank GPU groups."""
from vllm_omni.diffusion.distributed.parallel_state import (
destroy_distributed_env,
init_distributed_environment,
initialize_model_parallel,
)
init_distributed_environment(
world_size=1,
rank=0,
local_rank=0,
distributed_init_method=f"file://{tmp_path / 'sana-distributed'}",
)
try:
initialize_model_parallel()
yield
finally:
destroy_distributed_env()
@@ -202,6 +202,7 @@ def test_sana_video_transformer_matches_frozen_golden(
variant: str, variant: str,
_hardware, _hardware,
accuracy_artifact_root: Path, accuracy_artifact_root: Path,
sana_model_parallel,
) -> None: ) -> None:
del _hardware del _hardware
from vllm.config import LoadConfig from vllm.config import LoadConfig
@@ -189,6 +189,7 @@ def _cfg_prediction(
def test_sana_video_quick_pipeline_stages_match_diffusers( def test_sana_video_quick_pipeline_stages_match_diffusers(
variant: str, variant: str,
_hardware, _hardware,
sana_model_parallel,
) -> None: ) -> None:
"""Compare the first denoising step without replacing native attention. """Compare the first denoising step without replacing native attention.
@@ -23,7 +23,8 @@ BASE_FEATURE_MARKS = hardware_marks(res={"cuda": "L4"})
def _get_diffusion_feature_cases(model: str): def _get_diffusion_feature_cases(model: str):
return [ return [
pytest.param( pytest.param(
OmniServerParams(model=model), # The unsharded transformer alone exceeds the L4's 22 GiB.
OmniServerParams(model=model, server_args=["--enable-layerwise-offload"]),
id="base", id="base",
marks=BASE_FEATURE_MARKS, marks=BASE_FEATURE_MARKS,
), ),
@@ -28,7 +28,7 @@ import pytest
from tests.helpers.mark import hardware_test from tests.helpers.mark import hardware_test
from tests.helpers.runtime import OmniServerParams from tests.helpers.runtime import OmniServerParams
from tests.helpers.stage_config import get_deploy_config_path from tests.helpers.stage_config import get_deploy_config_path, modify_stage_config
pytestmark = [ pytestmark = [
pytest.mark.advanced_model, pytest.mark.advanced_model,
@@ -84,7 +84,12 @@ tts_server_params = [
pytest.param( pytest.param(
OmniServerParams( OmniServerParams(
model=MODEL, model=MODEL,
stage_config_path=get_deploy_config_path("moss_tts_realtime.yaml"), stage_config_path=modify_stage_config(
get_deploy_config_path("moss_tts_realtime.yaml"),
# Leave room on the single L4 for the codec weights, reference
# encoder, and speech-serving activations in addition to KV.
updates={"stages": {0: {"gpu_memory_utilization": 0.35}}},
),
server_args=["--disable-log-stats"], server_args=["--disable-log-stats"],
), ),
id="moss_tts_realtime", id="moss_tts_realtime",
@@ -21,7 +21,14 @@ models = ["Qwen/Qwen2.5-Omni-7B"]
# Single CI deploy YAML; rocm/xpu deltas are picked automatically via the # Single CI deploy YAML; rocm/xpu deltas are picked automatically via the
# platforms: section in vllm_omni/deploy/ci/qwen2_5_omni.yaml. # platforms: section in vllm_omni/deploy/ci/qwen2_5_omni.yaml.
stage_configs = [modify_stage_config(get_deploy_config_path("ci/qwen2_5_omni.yaml"))] stage_configs = [
modify_stage_config(
get_deploy_config_path("ci/qwen2_5_omni.yaml"),
# This suite requests two cards. Share the Talker's card with
# Code2Wav and leave room for its whole-sequence DiT workspace.
updates={"stages": {1: {"gpu_memory_utilization": 0.3}, 2: {"devices": "1"}}},
)
]
# Create parameter combinations for model and stage config # Create parameter combinations for model and stage config
test_params = [ test_params = [
+34
View File
@@ -20,7 +20,11 @@ import pytest
from omegaconf import OmegaConf from omegaconf import OmegaConf
from pydantic import ValidationError from pydantic import ValidationError
from transformers import PretrainedConfig, Qwen3OmniMoeConfig from transformers import PretrainedConfig, Qwen3OmniMoeConfig
from vllm.config import VllmConfig
from vllm.engine.arg_utils import EngineArgs from vllm.engine.arg_utils import EngineArgs
from vllm.exceptions import VLLMValidationError
from vllm.v1.engine.input_processor import InputProcessor
from vllm.v1.worker.gpu.sample.logit_bias import LogitBiasState
from tests.helpers.mock import patch_hf_snapshot_download from tests.helpers.mock import patch_hf_snapshot_download
from vllm_omni.config.model import OmniModelConfig from vllm_omni.config.model import OmniModelConfig
@@ -32,6 +36,36 @@ from vllm_omni.worker.omni_connector_model_runner_mixin import OmniConnectorMode
pytestmark = [pytest.mark.core_model, pytest.mark.cpu] pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
@pytest.mark.parametrize("use_v2", [False, True])
def test_stage_runner_selects_matching_input_processor_validator(monkeypatch, use_v2):
from vllm.sampling_params import SamplingParams
from vllm_omni.model_executor.models.audex.cfg import AudexCFGLogitsProcessor
# Upstream's process-wide preference must not override a stage's deploy choice.
monkeypatch.setattr("vllm.envs.VLLM_USE_V2_MODEL_RUNNER", not use_v2)
config = VllmConfig()
model_config = object.__new__(OmniModelConfig)
model_config.use_v2_model_runner = use_v2
model_config.runner_type = "generate"
model_config.logits_processors = [LogitBiasState if use_v2 else AudexCFGLogitsProcessor]
config.model_config = model_config
assert config.use_v2_model_runner is use_v2
processor = object.__new__(InputProcessor)
processor.vllm_config, processor.model_config = config, model_config
validator = processor._build_logits_processors_params_validator()
validator(SamplingParams(extra_args={"cfg_role": "cond", "cfg_scale": 1.5}))
if not use_v2:
with pytest.raises(VLLMValidationError, match="cfg_role"):
validator(SamplingParams(extra_args={"cfg_role": "invalid"}))
@pytest.mark.parametrize("use_v2", [False, True])
def test_plain_vllm_config_retains_upstream_runner_preference(monkeypatch, use_v2):
monkeypatch.setattr("vllm.envs.VLLM_USE_V2_MODEL_RUNNER", use_v2)
assert VllmConfig().use_v2_model_runner is use_v2
def test_sync_config_is_omni(): def test_sync_config_is_omni():
"""Ensure create_model_config gives the right type.""" """Ensure create_model_config gives the right type."""
cfg = OmniEngineArgs().create_model_config() cfg = OmniEngineArgs().create_model_config()
@@ -8,6 +8,7 @@ from __future__ import annotations
from dataclasses import fields from dataclasses import fields
import pytest import pytest
from vllm.config import AuxOutputConfig
from vllm_omni.engine.arg_utils import ( from vllm_omni.engine.arg_utils import (
SHARED_FIELDS, SHARED_FIELDS,
@@ -49,3 +50,26 @@ def test_internal_blacklist_keys_derived_from_orchestrator():
""" """
blacklist = internal_blacklist_keys() blacklist = internal_blacklist_keys()
assert blacklist == orchestrator_field_names() - SHARED_FIELDS assert blacklist == orchestrator_field_names() - SHARED_FIELDS
@pytest.mark.parametrize(
"aux_args",
[
{"enable_return_routed_experts": True},
{"aux_output_config": {"enable_return_routed_experts": True}},
{"aux_output_config": AuxOutputConfig(enable_return_routed_experts=True)},
],
)
@pytest.mark.parametrize("use_v2_model_runner", [False, True])
@pytest.mark.parametrize("is_npu", [False, True])
def test_aux_outputs_require_a_supported_omni_stage_runner(aux_args, use_v2_model_runner, is_npu, monkeypatch):
# Upstream runner selection reads this environment variable; Omni workers
# select their runner from the separate per-stage flag instead.
monkeypatch.setenv("VLLM_USE_V2_MODEL_RUNNER", "1")
monkeypatch.setattr("vllm_omni.engine.arg_utils.current_omni_platform.is_npu", lambda: is_npu)
if use_v2_model_runner or is_npu:
args = OmniEngineArgs(model="unused", use_v2_model_runner=use_v2_model_runner, **aux_args)
assert args.aux_output_config.enabled
else:
with pytest.raises(ValueError, match="use_v2_model_runner=True for this Omni stage"):
OmniEngineArgs(model="unused", **aux_args)
@@ -168,6 +168,37 @@ def test_build_add_request_message_preserves_additional_information(mocker: Mock
output_processor.add_request.assert_not_called() output_processor.add_request.assert_not_called()
def test_build_add_request_message_preserves_kv_hints_after_omni_payload_upgrade(mocker):
from vllm.v1.engine.input_processor import InputProcessor
from vllm.v1.kv_hints import KvHintsEnvelope
engine = object.__new__(AsyncOmniEngine)
params = SamplingParams(max_tokens=8)
engine.default_sampling_params_list = [params]
engine.stage_metadata = [StageRuntimeInfo(final_output=False, final_output_type=None, stage_type="llm")]
engine.supported_tasks = ("generate",)
processor = mocker.create_autospec(InputProcessor, instance=True)
hints = KvHintsEnvelope("1", "hint-message", [])
def process(**kwargs):
request = _make_engine_core_request()
request.kv_hints = kwargs["kv_hints"]
return request
processor.process_inputs.side_effect = process
engine.input_processor = processor
msg = engine._build_add_request_message(
request_id="req-1",
prompt={"prompt_token_ids": [1], "additional_information": {"speaker": "voice"}},
sampling_params_list=[params],
kv_hints=hints,
)
assert processor.process_inputs.call_args.kwargs["kv_hints"] is hints
assert isinstance(msg.prompt, OmniEngineCoreRequest)
assert msg.prompt.kv_hints is hints
assert msg.prompt.additional_information is not None
def test_build_add_request_message_injects_global_id_before_prompt_transform(mocker: MockerFixture): def test_build_add_request_message_injects_global_id_before_prompt_transform(mocker: MockerFixture):
engine = object.__new__(AsyncOmniEngine) engine = object.__new__(AsyncOmniEngine)
params = SamplingParams(max_tokens=8) params = SamplingParams(max_tokens=8)
@@ -296,11 +296,21 @@ def test_stage_engine_core_client_module_reload_keeps_forward_refs_deferred():
"""Regression test for forward references in make_async_mp_client.""" """Regression test for forward references in make_async_mp_client."""
import vllm_omni.engine.stage_engine_core_client as client_mod import vllm_omni.engine.stage_engine_core_client as client_mod
importlib.reload(client_mod) # Snapshot the module namespace so a reload cannot strand other test
# modules that captured the pre-reload class objects (isinstance checks
# against the stale class would then fail). ``importlib.reload`` mutates
# the module dict in place; restoring it leaves the runtime exactly as it
# was before this test ran.
saved = dict(client_mod.__dict__)
try:
importlib.reload(client_mod)
assert client_mod.StageEngineCoreClientBase.make_async_mp_client.__annotations__["return"] == ( assert client_mod.StageEngineCoreClientBase.make_async_mp_client.__annotations__["return"] == (
"StageEngineCoreClient | DPLBStageEngineCoreClient" "StageEngineCoreClient | DPLBStageEngineCoreClient"
) )
finally:
client_mod.__dict__.clear()
client_mod.__dict__.update(saved)
def test_async_omni_engine_initialize_stages_passes_log_stats_and_client_config_to_runtime(monkeypatch): def test_async_omni_engine_initialize_stages_passes_log_stats_and_client_config_to_runtime(monkeypatch):
+27 -1
View File
@@ -13,13 +13,16 @@ substitutable with the base class — including base-style calls that pass
import inspect import inspect
import msgspec
import numpy as np import numpy as np
import pytest import pytest
import torch import torch
from vllm.sampling_params import SamplingParams from vllm.sampling_params import SamplingParams
from vllm.v1.engine import EngineCoreRequest
from vllm.v1.kv_hints import KvHintAction, KvHintsEnvelope
from vllm.v1.request import Request from vllm.v1.request import Request
from vllm_omni.engine import PromptEmbedsPayload from vllm_omni.engine import OmniEngineCoreRequest, PromptEmbedsPayload
from vllm_omni.request import OmniRequest, OmniStreamingUpdate from vllm_omni.request import OmniRequest, OmniStreamingUpdate
pytestmark = [pytest.mark.core_model, pytest.mark.cpu] pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
@@ -111,3 +114,26 @@ def test_model_intermediate_buffer_round_trips_to_streaming_update():
assert update is not None assert update is not None
assert request.model_intermediate_buffer is buffer assert request.model_intermediate_buffer is buffer
assert update.model_intermediate_buffer is buffer assert update.model_intermediate_buffer is buffer
def test_kv_hints_survive_omni_upgrade_wire_transport_and_scheduler_conversion():
hints = KvHintsEnvelope("1", "hint-message", [KvHintAction("a", "prefetch", "1", {"blocks": [7]})])
core = EngineCoreRequest(
request_id="hint-request",
prompt_token_ids=[1, 2],
mm_features=None,
sampling_params=SamplingParams(max_tokens=2),
pooling_params=None,
arrival_time=0.0,
lora_request=None,
cache_salt=None,
data_parallel_rank=None,
session_id="session",
kv_hints=hints,
)
upgraded = OmniEngineCoreRequest.from_request(core)
decoded = msgspec.msgpack.decode(msgspec.msgpack.encode(upgraded), type=OmniEngineCoreRequest)
scheduled = OmniRequest.from_engine_core_request(decoded, block_hasher=None)
assert decoded.session_id == "session"
assert scheduled.kv_hints == hints
assert scheduled.session_id == "session"
+13
View File
@@ -2,6 +2,7 @@
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Tests for OmniRequestOutput class.""" """Tests for OmniRequestOutput class."""
import numpy as np
import pytest import pytest
from PIL import Image from PIL import Image
from vllm.outputs import CompletionOutput, RequestOutput from vllm.outputs import CompletionOutput, RequestOutput
@@ -157,6 +158,7 @@ class TestMsgpackRoundTrip:
) )
source = _make_text_request_output(text="round trip") source = _make_text_request_output(text="round trip")
source.prompt_token_id_logprobs = np.array([[-0.5, -1.5], [-0.2, -2.2]], dtype=np.float32)
out = OmniRequestOutput.from_stage_output( out = OmniRequestOutput.from_stage_output(
source, source,
request_id="rt-1", request_id="rt-1",
@@ -178,6 +180,7 @@ class TestMsgpackRoundTrip:
assert decoded.outputs[0].text == "round trip" assert decoded.outputs[0].text == "round trip"
assert decoded.prompt == "test prompt" assert decoded.prompt == "test prompt"
assert decoded.prompt_token_ids == [1, 2, 3] assert decoded.prompt_token_ids == [1, 2, 3]
np.testing.assert_array_equal(decoded.prompt_token_id_logprobs, source.prompt_token_id_logprobs)
def test_round_trip_diffusion_output(self): def test_round_trip_diffusion_output(self):
"""A diffusion output round-trips without data loss.""" """A diffusion output round-trips without data loss."""
@@ -302,6 +305,16 @@ class TestRequestOutputFieldParity:
assert omni.num_cached_tokens is None assert omni.num_cached_tokens is None
assert omni.num_cache_creation_tokens is None assert omni.num_cache_creation_tokens is None
def test_fixed_token_scores_survive_stage_output_wrapping(self) -> None:
source = _make_text_request_output()
source.prompt_token_id_logprobs = np.array([[-0.5, -1.5], [-0.2, -2.2]], dtype=np.float32)
omni = OmniRequestOutput.from_stage_output(source, stage_id=0)
forwarded = OmniRequestOutput.from_stage_output(omni, stage_id=1)
np.testing.assert_array_equal(forwarded.prompt_token_id_logprobs, source.prompt_token_id_logprobs)
assert OmniRequestOutput(request_id="unscored").prompt_token_id_logprobs is None
def test_transfer_param_fields_are_copied_from_the_stage_output(self) -> None: def test_transfer_param_fields_are_copied_from_the_stage_output(self) -> None:
"""Declaring the fields is not enough — they must also be copied. """Declaring the fields is not enough — they must also be copied.
@@ -1282,6 +1282,7 @@ async def test_cache_reset_rpc_failure_is_nonfatal_and_visits_all_replicas(metho
[ [
("reset_mm_cache", {}, True), ("reset_mm_cache", {}, True),
("sleep", {}, True), ("sleep", {}, True),
("release_kv_cache_memory", {}, True),
("pause_scheduler", {"clear_cache": True}, True), ("pause_scheduler", {"clear_cache": True}, True),
("pause_scheduler", {"clear_cache": False}, False), ("pause_scheduler", {"clear_cache": False}, False),
("reset_prefix_cache", {}, False), ("reset_prefix_cache", {}, False),
@@ -7,8 +7,9 @@ from unittest.mock import MagicMock
import pytest import pytest
import torch import torch
from vllm.sampling_params import RequestOutputKind from vllm.sampling_params import RequestOutputKind, SamplingParams
from vllm.v1.engine import FinishReason from vllm.v1.engine import EngineCoreRequest, FinishReason
from vllm.v1.engine.logprobs import LogprobsProcessor
from vllm_omni.engine import OmniEngineCoreOutput from vllm_omni.engine import OmniEngineCoreOutput
from vllm_omni.engine.output_modality import OutputModality from vllm_omni.engine.output_modality import OutputModality
@@ -16,6 +17,7 @@ from vllm_omni.engine.output_processor import (
MultimodalOutputProcessor, MultimodalOutputProcessor,
OmniRequestState, OmniRequestState,
) )
from vllm_omni.outputs import OmniRequestOutput
pytestmark = [pytest.mark.core_model, pytest.mark.cpu] pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
@@ -109,6 +111,37 @@ def test_text_tokens_are_detokenized_when_mrv2_ar_output_has_pooling_payload():
assert not state.mm_accumulated.is_empty assert not state.mm_accumulated.is_empty
def test_fixed_token_scores_survive_output_processing_and_stage_wrapping():
processor, state = _make_processor(OutputModality.TEXT, _Detokenizer())
request = EngineCoreRequest(
request_id="r",
prompt_token_ids=[1, 2, 3],
mm_features=None,
sampling_params=SamplingParams(prompt_logprob_token_ids=[4, 5]),
pooling_params=None,
arrival_time=0.0,
lora_request=None,
cache_salt=None,
data_parallel_rank=None,
)
state.logprobs_processor = LogprobsProcessor.from_new_request(None, request)
scores = torch.tensor([[-0.5, -1.5], [-0.2, -2.2]])
processed = processor.process_outputs(
[
OmniEngineCoreOutput(
request_id="r",
new_token_ids=[42],
prompt_token_id_logprobs=scores,
finish_reason=FinishReason.STOP,
)
]
)
(raw_output,) = processed.request_outputs
final_output = OmniRequestOutput.from_stage_output(raw_output, stage_id=0)
torch.testing.assert_close(torch.from_numpy(final_output.prompt_token_id_logprobs), scores)
@pytest.mark.parametrize("streaming", [False, True]) @pytest.mark.parametrize("streaming", [False, True])
def test_generation_stage_mm_delivered_only_with_terminal(streaming): def test_generation_stage_mm_delivered_only_with_terminal(streaming):
# Non-terminal chunks accumulate; the terminal output carries the audio. # Non-terminal chunks accumulate; the terminal output carries the audio.
-1
View File
@@ -97,7 +97,6 @@ _OMNI_ONLY_LLM_STAGE_ENGINE_FIELDS = frozenset(
"enable_multithread_weight_load", "enable_multithread_weight_load",
"env", "env",
"has_sampling_extra_args", "has_sampling_extra_args",
"log_level",
"log_stats", "log_stats",
"model_arch", "model_arch",
"model_subdir", "model_subdir",
+52 -1
View File
@@ -5,11 +5,19 @@
from __future__ import annotations from __future__ import annotations
from types import SimpleNamespace from types import SimpleNamespace
from unittest.mock import patch
import pytest import pytest
from vllm import SamplingParams
from vllm.v1.engine.core_client import AsyncMPClient, DPLBAsyncMPClient
from vllm.v1.engine.exceptions import EngineDeadError from vllm.v1.engine.exceptions import EngineDeadError
from vllm_omni.engine.stage_engine_core_client import StageEngineCoreClient from vllm_omni.engine.stage_engine_core_client import (
DPLBStageEngineCoreClient,
StageEngineCoreClient,
StageEngineCoreClientBase,
)
from vllm_omni.engine.stage_init_utils import StageMetadata
pytestmark = [pytest.mark.core_model, pytest.mark.cpu] pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
@@ -30,3 +38,46 @@ def test_check_health_raises_when_resources_engine_dead():
client = _make_client(engine_dead=True) client = _make_client(engine_dead=True)
with pytest.raises(EngineDeadError, match="engine core is dead"): with pytest.raises(EngineDeadError, match="engine core is dead"):
client.check_health() client.check_health()
@pytest.mark.parametrize("dp_size, external_lb", [(1, False), (2, False), (2, True)])
@pytest.mark.parametrize("renderer", [None, object()])
def test_stage_client_forwards_renderer_to_the_actual_upstream_constructor(dp_size, external_lb, renderer):
config = SimpleNamespace(
model_config=None,
parallel_config=SimpleNamespace(data_parallel_size=dp_size, data_parallel_external_lb=external_lb),
)
metadata = StageMetadata(
stage_id=0,
stage_type="llm",
engine_output_type="text",
is_comprehension=True,
requires_multimodal_data=False,
engine_input_source=[],
final_output=True,
final_output_type="text",
default_sampling_params=SamplingParams(),
custom_process_input_func=None,
model_stage=None,
runtime_cfg=None,
)
internal_lb = dp_size > 1 and not external_lb
parent = DPLBAsyncMPClient if internal_lb else AsyncMPClient
# Keep Omni construction/factory routing real; intercept only upstream's
# subprocess/socket boundary, using its current signature via autospec.
with patch.object(parent, "__init__", autospec=True, return_value=None) as init:
addresses = {"input_address": "tcp://192.0.2.1:1"}
client = StageEngineCoreClientBase.make_async_mp_client(
config, object, metadata=metadata, client_addresses=addresses, renderer=renderer
)
assert isinstance(client, DPLBStageEngineCoreClient if internal_lb else StageEngineCoreClient)
init.assert_called_once_with(
client,
config,
object,
log_stats=False,
client_addresses=addresses,
client_count=1,
client_index=0,
renderer=renderer,
)
@@ -83,6 +83,27 @@ def test_collective_rpc_control_method_reraises_worker_error():
asyncio.run(run()) asyncio.run(run())
@pytest.mark.cpu
def test_kv_memory_release_uses_engine_core_and_propagates_pause_errors():
async def run() -> None:
release = AsyncMock(return_value=None)
pool, client = _make_pool(release_kv_cache_memory_async=release)
await pool.collective_rpc(0, "release_kv_cache_memory", timeout=2.0)
release.assert_awaited_once_with()
client.collective_rpc_async.assert_not_awaited()
release.side_effect = RuntimeError("requires a completed pause first")
with pytest.raises(RuntimeError, match="completed pause"):
await pool.collective_rpc(0, "release_kv_cache_memory")
del client.release_kv_cache_memory_async
result = await pool.collective_rpc(0, "release_kv_cache_memory")
assert result["supported"] is False
client.collective_rpc_async.assert_not_awaited()
asyncio.run(run())
@pytest.mark.cpu @pytest.mark.cpu
def test_collective_rpc_reset_caches_use_control_helpers(): def test_collective_rpc_reset_caches_use_control_helpers():
async def run() -> None: async def run() -> None:
+34
View File
@@ -10,6 +10,7 @@ import pytest
from vllm.engine.protocol import StreamingInput from vllm.engine.protocol import StreamingInput
from vllm.lora.request import LoRARequest from vllm.lora.request import LoRARequest
from vllm.sampling_params import RequestOutputKind, SamplingParams from vllm.sampling_params import RequestOutputKind, SamplingParams
from vllm.v1.kv_hints import KvHintsEnvelope
from tests.helpers.mark import hardware_test from tests.helpers.mark import hardware_test
from tests.helpers.stage_config import get_deploy_config_path from tests.helpers.stage_config import get_deploy_config_path
@@ -138,6 +139,39 @@ def test_generate_forwards_lora_request_to_engine():
asyncio.run(run()) asyncio.run(run())
@pytest.mark.cpu
@pytest.mark.parametrize("streaming", [None, "chunk", "empty"])
def test_generate_forwards_kv_hints_on_initial_stage_submission(streaming):
async def run():
submissions = []
async def add(**kwargs):
submissions.append(kwargs)
omni = get_async_omni_instance(fake_add_request=add)
omni.engine.add_streaming_update_async = _noop
omni.engine.stage_configs = [SimpleNamespace(is_comprehension=True, stage_type="llm")]
omni.engine.stage_vllm_configs = [SimpleNamespace(model_config=SimpleNamespace(is_encoder_decoder=False))]
hints = KvHintsEnvelope("1", "message", [])
params = SamplingParams(max_tokens=2, output_kind=RequestOutputKind.DELTA)
async def chunks():
if streaming == "chunk":
yield StreamingInput(prompt={"prompt_token_ids": [1]})
async for _ in omni.generate(
prompt=chunks() if streaming else {"prompt_token_ids": [1]},
request_id="hints",
sampling_params_list=[params],
kv_hints=hints,
):
pass
assert len(submissions) == 1
assert submissions[0]["kv_hints"] is hints
asyncio.run(run())
@pytest.mark.cpu @pytest.mark.cpu
@pytest.mark.parametrize( @pytest.mark.parametrize(
"req_ids,cancel_prefix,expected_cancel_count", "req_ids,cancel_prefix,expected_cancel_count",
@@ -194,6 +194,56 @@ def test_reset_encoder_cache_forwards_to_ar_stages():
asyncio.run(run()) asyncio.run(run())
@pytest.mark.cpu
def test_release_kv_memory_requires_pause_and_wakes_only_ar_kv_allocations():
async def run() -> None:
omni = _make_omni(stage_types=["llm", "diffusion", "llm"])
with pytest.raises(RuntimeError, match="completed pause"):
await omni.release_kv_cache_memory()
omni.collective_rpc.assert_not_awaited()
omni._paused = True
await omni.release_kv_cache_memory()
omni._clear_frontend_mm_cache.assert_awaited_once_with()
omni.collective_rpc.assert_awaited_once_with(
method="release_kv_cache_memory",
args=(),
kwargs=None,
stage_ids=[0, 2],
timeout=CACHE_RESET_TIMEOUT_S,
)
assert omni._stage_sleeping_tags == {0: {"kv_cache"}, 2: {"kv_cache"}}
assert not omni._level2_sleeping
omni.collective_rpc.reset_mock()
await omni.wake_up(stage_ids=[0, 2], tags=["kv_cache"])
omni.collective_rpc.assert_awaited_once_with(
method="wake_up", args=(), kwargs={"tags": ["kv_cache"]}, stage_ids=[0, 2]
)
assert not omni._sleeping_tags
assert omni._paused
asyncio.run(run())
@pytest.mark.cpu
def test_release_kv_memory_skips_diffusion_and_reports_engine_failures():
async def run() -> None:
diffusion = _make_omni(stage_types=["diffusion"])
await diffusion.release_kv_cache_memory()
diffusion.collective_rpc.assert_not_awaited()
assert not diffusion._paused
omni = _make_omni(stage_types=["llm"])
omni._paused = True
omni.collective_rpc.return_value = [{"supported": False, "error": "requires all executor memory resident"}]
with pytest.raises(RuntimeError, match="all executor memory resident"):
await omni.release_kv_cache_memory()
assert not omni._sleeping_tags
asyncio.run(run())
@pytest.mark.cpu @pytest.mark.cpu
def test_reset_mm_cache_clears_frontend_then_forwards_to_ar_stages(): def test_reset_mm_cache_clears_frontend_then_forwards_to_ar_stages():
async def run() -> None: async def run() -> None:
+41 -7
View File
@@ -4,9 +4,11 @@
"""Assertion and response validation helpers for tests.""" """Assertion and response validation helpers for tests."""
import base64 import base64
import hashlib
import io import io
import json import json
import math import math
import os
import tempfile import tempfile
import threading import threading
import wave import wave
@@ -999,6 +1001,31 @@ def _assert_transcript_matches(
) )
def _retain_failed_speech_audio(
audio_bytes: bytes | None, transcript: str, expected_text: Any, error: AssertionError
) -> None:
"""Retain the exact ASR input when a CI speech-content assertion fails."""
directory = os.environ.get("VLLM_OMNI_FAILED_SPEECH_AUDIO_DIR")
if not directory or not audio_bytes:
return
stem = f"failed_speech_{hashlib.sha256(audio_bytes).hexdigest()[:16]}"
root = Path(directory)
try:
root.mkdir(parents=True, exist_ok=True)
(root / f"{stem}.wav").write_bytes(audio_bytes)
(root / f"{stem}.json").write_text(
json.dumps(
{"transcript": transcript, "expected_text": str(expected_text), "assertion": str(error)},
ensure_ascii=False,
indent=2,
)
)
print(f"Retained failed speech ASR input: {root / f'{stem}.wav'}")
except OSError as exc:
# Diagnostic I/O must never replace or suppress the content failure.
print(f"Could not retain failed speech ASR input: {exc}")
def assert_audio_speech_response(response: Any, request_config: dict[str, Any], run_level: str | None = None) -> None: def assert_audio_speech_response(response: Any, request_config: dict[str, Any], run_level: str | None = None) -> None:
"""Validate speech API results from :class:`~tests.helpers.client.OmniResponse`. """Validate speech API results from :class:`~tests.helpers.client.OmniResponse`.
@@ -1063,14 +1090,21 @@ def assert_audio_speech_response(response: Any, request_config: dict[str, Any],
if expected_text: if expected_text:
print(f"audio content is: {transcript}") print(f"audio content is: {transcript}")
print(f"input text is: {expected_text}") print(f"input text is: {expected_text}")
_assert_transcript_matches( transcription_audio = _speech_audio_for_transcription(
transcript, getattr(response, "audio_bytes", None), request_config
_speech_audio_for_transcription(getattr(response, "audio_bytes", None), request_config),
expected_text,
threshold=0.9,
escalation_model=request_config.get("transcript_escalation_model"),
language=request_config.get("transcript_language"),
) )
try:
_assert_transcript_matches(
transcript,
transcription_audio,
expected_text,
threshold=0.9,
escalation_model=request_config.get("transcript_escalation_model"),
language=request_config.get("transcript_language"),
)
except AssertionError as exc:
_retain_failed_speech_audio(transcription_audio, transcript, expected_text, exc)
raise
_assert_preset_voice_gender_from_audio( _assert_preset_voice_gender_from_audio(
response.audio_bytes, response.audio_bytes,
request_config.get("voice"), request_config.get("voice"),
+19
View File
@@ -0,0 +1,19 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Import preload for the CPU-only collective forkserver.
This module is imported in the forkserver process, before it starts workers.
Its CUDA visibility does not alter the pytest process or CUDA parity workers.
"""
import os
os.environ["CUDA_VISIBLE_DEVICES"] = ""
import torch # noqa: E402
# Retain the expensive worker-module imports across CPU process groups.
from tests.diffusion.distributed import test_comm, test_pipeline_parallel # noqa: E402, F401
if torch.cuda.is_initialized():
raise RuntimeError("CPU collective forkserver must not initialize CUDA")
+62
View File
@@ -0,0 +1,62 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Process and platform setup for isolated collective test workers."""
from collections.abc import Callable
from multiprocessing.context import BaseContext
import torch
def collective_context(device_kind: str) -> BaseContext:
"""Reuse imports in a clean CPU forkserver; CUDA workers use spawn."""
if device_kind == "cpu" and "forkserver" in torch.multiprocessing.get_all_start_methods():
torch.multiprocessing.set_forkserver_preload(["tests.helpers.cpu_collective_server"])
return torch.multiprocessing.get_context("forkserver")
return torch.multiprocessing.get_context("spawn")
def start_collective_workers(
worker: Callable[..., None], *, args: tuple[object, ...], nprocs: int, device_kind: str
) -> None:
context = collective_context(device_kind)
if context.get_start_method() == "spawn":
torch.multiprocessing.spawn(worker, args=args, nprocs=nprocs)
else:
torch.multiprocessing.start_processes(worker, args=args, nprocs=nprocs, start_method=context.get_start_method())
def configure_cpu_collective_worker() -> None:
"""Select CPU platforms inside a spawned worker that only uses Gloo.
Upstream coordinators resolve local ranks through the selected
platform's visible devices. CPU ranks can outnumber visible GPUs, so they
must use the CPU platform even when the test host has CUDA installed.
The worker still creates real process groups and runs real collectives.
"""
import vllm.platforms
from vllm.platforms.cpu import CpuPlatform
from vllm_omni.diffusion.distributed import comm, group_coordinator, parallel_state
from vllm_omni.platforms.interface import UnspecifiedOmniPlatform
class CpuCollectivePlatform(UnspecifiedOmniPlatform):
dist_backend = "gloo"
@classmethod
def get_device_count(cls) -> int:
# CPU tensors share one logical device regardless of process rank.
return 1
@classmethod
def set_device(cls, device: torch.device) -> None:
assert device.type == "cpu"
@classmethod
def synchronize(cls) -> None:
pass
vllm.platforms.current_platform = CpuPlatform()
platform = CpuCollectivePlatform()
for module in (comm, group_coordinator, parallel_state):
module.current_omni_platform = platform
+58 -12
View File
@@ -659,6 +659,14 @@ class OmniRunner:
self.seed = seed self.seed = seed
self._prompt_len_estimate_cache: dict[str, Any] = {} self._prompt_len_estimate_cache: dict[str, Any] = {}
self.omni: Any = None self.omni: Any = None
self._runner_process = psutil.Process()
self._initial_child_ids: set[tuple[int, float]] = set()
self._owned_engine_processes: dict[int, float] = {}
for child in self._runner_process.children(recursive=True):
try:
self._initial_child_ids.add((child.pid, child.create_time()))
except psutil.Error:
pass
try: try:
from vllm_omni.entrypoints.omni import Omni from vllm_omni.entrypoints.omni import Omni
@@ -670,12 +678,14 @@ class OmniRunner:
deploy_config=deploy_config, deploy_config=deploy_config,
**kwargs, **kwargs,
) )
self._remember_engine_processes(startup=True)
startup_s = time.perf_counter() - startup_t0 startup_s = time.perf_counter() - startup_t0
if log_stats: if log_stats:
print(f"OmniRunner startup took {startup_s:.3f}s (model={model_name})", flush=True) print(f"OmniRunner startup took {startup_s:.3f}s (model={model_name})", flush=True)
except BaseException: except BaseException:
# ``with OmniRunner(...)`` never reaches ``__enter__``/``__exit__`` # ``with OmniRunner(...)`` never reaches ``__enter__``/``__exit__``
# when construction fails after worker processes have started. # when construction fails after worker processes have started.
self._remember_engine_processes(startup=True)
self.__exit__(None, None, None) self.__exit__(None, None, None)
raise raise
@@ -899,18 +909,51 @@ class OmniRunner:
def stop_profile(self, stages: list[int] | None = None) -> list[Any]: def stop_profile(self, stages: list[int] | None = None) -> list[Any]:
return self.omni.stop_profile(stages=stages) return self.omni.stop_profile(stages=stages)
def _remember_engine_processes(self, *, startup: bool = False) -> None:
"""Track startup workers, then only descendants of those workers.
Existing children may belong to another runner or a shared transcriber.
Record identities before close() can reparent surviving workers.
"""
try:
if startup:
candidates = self._runner_process.children(recursive=True)
else:
candidates = []
for pid, created in list(self._owned_engine_processes.items()):
try:
proc = psutil.Process(pid)
if proc.create_time() == created:
candidates.extend(proc.children(recursive=True))
except psutil.Error:
pass
for proc in candidates:
try:
identity = (proc.pid, proc.create_time())
if identity in self._initial_child_ids:
continue
if startup and any(
(parent.pid, parent.create_time()) in self._initial_child_ids for parent in proc.parents()
):
continue
cmdline = " ".join(proc.cmdline()).lower()
if "enginecore" in cmdline or "enginecore" in proc.name().lower():
self._owned_engine_processes[identity[0]] = identity[1]
except psutil.Error:
pass
except psutil.Error as error:
print(f"Warning: could not inspect runner workers: {error}")
def _cleanup_process(self): def _cleanup_process(self):
try: try:
keywords = ["enginecore"]
matched = [] matched = []
for proc in psutil.process_iter(["pid", "name", "cmdline", "username"]): for pid, created in self._owned_engine_processes.items():
try: try:
cmdline = " ".join(proc.cmdline()).lower() if proc.cmdline() else "" proc = psutil.Process(pid)
name = proc.name().lower() if proc.create_time() == created:
if any(k in cmdline for k in keywords) or any(k in name for k in keywords): print(f"Found owned vllm process: PID={proc.pid}")
print(f"Found vllm process: PID={proc.pid}, cmd={cmdline[:100]}")
matched.append(proc) matched.append(proc)
except (psutil.NoSuchProcess, psutil.AccessDenied): except psutil.Error:
pass pass
for proc in matched: for proc in matched:
try: try:
@@ -938,11 +981,14 @@ class OmniRunner:
return self return self
def __exit__(self, exc_type, exc_val, exc_tb): def __exit__(self, exc_type, exc_val, exc_tb):
omni = getattr(self, "omni", None) self._remember_engine_processes()
if omni is not None and hasattr(omni, "close"): try:
omni.close() omni = getattr(self, "omni", None)
self._cleanup_process() if omni is not None and hasattr(omni, "close"):
cleanup_test_environment() omni.close()
finally:
self._cleanup_process()
cleanup_test_environment()
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
+81
View File
@@ -2,7 +2,9 @@
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project # SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
import io import io
import json
import wave import wave
from contextlib import nullcontext
from types import SimpleNamespace from types import SimpleNamespace
import numpy as np import numpy as np
@@ -249,6 +251,85 @@ def test_pcm_transcript_escalation_uses_same_valid_wav_and_rejects_two_wrong_tra
_assert_wav_wraps_pcm(wav_bytes, pcm_bytes, 24_000) _assert_wav_wraps_pcm(wav_bytes, pcm_bytes, 24_000)
@pytest.mark.parametrize("response_format", ["wav", "pcm"])
@pytest.mark.parametrize("escalation_model", [None, "large-v3"])
def test_failed_speech_capture_retains_exact_asr_input_and_original_failure(
monkeypatch, tmp_path, response_format, escalation_model
):
pcm_bytes = _pcm_sine()
wav = io.BytesIO()
with wave.open(wav, "wb") as writer:
writer.setnchannels(1)
writer.setsampwidth(2)
writer.setframerate(24_000)
writer.writeframes(pcm_bytes)
audio_bytes = wav.getvalue() if response_format == "wav" else pcm_bytes
transcribed = []
def transcribe(audio_bytes, **kwargs):
transcribed.append(audio_bytes)
return "this is unrelated speech"
directory = tmp_path / "failed_audio"
monkeypatch.setenv("VLLM_OMNI_FAILED_SPEECH_AUDIO_DIR", str(directory))
monkeypatch.setattr(assertions, "convert_audio_bytes_to_text", transcribe)
config = {
"input": "please read this requested sentence accurately",
"response_format": response_format,
"transcript_pcm_sample_rate": 24_000,
"transcript_escalation_model": escalation_model,
}
response = OmniResponse(success=True, audio_bytes=audio_bytes, audio_format=f"audio/{response_format}")
with pytest.raises(AssertionError, match="Transcript doesn't match input") as error:
assert_audio_speech_response(response, config, "full_model")
clips = list(directory.glob("*.wav"))
assert len(clips) == 1
assert all(clip == clips[0].read_bytes() for clip in transcribed)
_assert_wav_wraps_pcm(clips[0].read_bytes(), pcm_bytes, 24_000)
metadata = json.loads(clips[0].with_suffix(".json").read_text())
assert metadata["expected_text"] == config["input"]
assert metadata["transcript"] == "this is unrelated speech"
assert metadata["assertion"] == str(error.value)
@pytest.mark.parametrize("capture_enabled", [False, True])
@pytest.mark.parametrize("content_failure", [False, True])
def test_speech_capture_is_opt_in_and_does_not_write_successes(monkeypatch, tmp_path, capture_enabled, content_failure):
directory = tmp_path / "failed_audio"
if capture_enabled:
monkeypatch.setenv("VLLM_OMNI_FAILED_SPEECH_AUDIO_DIR", str(directory))
else:
monkeypatch.delenv("VLLM_OMNI_FAILED_SPEECH_AUDIO_DIR", raising=False)
expected = "please read this requested sentence accurately"
monkeypatch.setattr(
assertions,
"convert_audio_bytes_to_text",
lambda *_args, **_kwargs: "this is unrelated speech" if content_failure else expected,
)
with pytest.raises(AssertionError, match="Transcript doesn't match input") if content_failure else nullcontext():
assert_audio_speech_response(
OmniResponse(success=True, audio_bytes=b"audio", audio_format="audio/wav"),
{"input": expected, "response_format": "wav"},
"full_model",
)
assert directory.exists() is (capture_enabled and content_failure)
assert len(list(tmp_path.rglob("*.wav"))) == int(capture_enabled and content_failure)
def test_speech_capture_io_failure_does_not_mask_content_failure(monkeypatch, tmp_path):
directory = tmp_path / "blocked"
directory.write_text("a file cannot be an output directory")
monkeypatch.setenv("VLLM_OMNI_FAILED_SPEECH_AUDIO_DIR", str(directory))
monkeypatch.setattr(assertions, "convert_audio_bytes_to_text", lambda *_args, **_kwargs: "this is unrelated speech")
with pytest.raises(AssertionError, match="Transcript doesn't match input"):
assert_audio_speech_response(
OmniResponse(success=True, audio_bytes=b"audio", audio_format="audio/wav"),
{"input": "please read this requested sentence accurately", "response_format": "wav"},
"full_model",
)
@pytest.mark.parametrize( @pytest.mark.parametrize(
"pcm_bytes", "pcm_bytes",
[ [
+117 -1
View File
@@ -1,10 +1,15 @@
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project # SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Tests for OmniRunner startup rollback.""" """Tests for OmniRunner startup rollback and worker ownership."""
from __future__ import annotations from __future__ import annotations
import subprocess
import sys
from collections.abc import Generator
import psutil
import pytest import pytest
from tests.helpers.runtime import OmniRunner from tests.helpers.runtime import OmniRunner
@@ -38,3 +43,114 @@ def test_runner_init_rolls_back_on_omni_startup_failure(monkeypatch: pytest.Monk
assert cleaned.count("process") == 1 assert cleaned.count("process") == 1
# Once at the start of ``__init__``, once from the constructor rollback. # Once at the start of ``__init__``, once from the constructor rollback.
assert cleaned.count("env") == 2 assert cleaned.count("env") == 2
@pytest.fixture
def live_workers() -> Generator[list[subprocess.Popen[bytes]], None, None]:
workers: list[subprocess.Popen[bytes]] = []
try:
yield workers
finally:
for worker in workers:
if worker.poll() is None:
worker.kill()
worker.wait(timeout=10)
if worker.stdout is not None:
worker.stdout.close()
def _start_engine_worker(
workers: list[subprocess.Popen[bytes]], *, ignore_terminate: bool = False
) -> subprocess.Popen[bytes]:
script = "import signal, time; "
if ignore_terminate:
script += "signal.signal(signal.SIGTERM, signal.SIG_IGN); "
script += "print('ready', flush=True); time.sleep(90)"
worker = subprocess.Popen([sys.executable, "-c", script, "enginecore"], stdout=subprocess.PIPE)
workers.append(worker)
assert worker.stdout is not None
assert worker.stdout.readline() == b"ready\n"
return worker
@pytest.mark.parametrize("close_raises", [False, True])
def test_runner_cleanup_preserves_unrelated_engine_workers(
monkeypatch: pytest.MonkeyPatch,
live_workers: list[subprocess.Popen[bytes]],
close_raises: bool,
) -> None:
existing = _start_engine_worker(live_workers)
owned: list[subprocess.Popen[bytes]] = []
late_unrelated: list[subprocess.Popen[bytes]] = []
monkeypatch.setattr("tests.helpers.runtime.cleanup_test_environment", lambda: None)
class _OwnedOmni:
def __init__(self, *args: object, **kwargs: object) -> None:
owned.append(_start_engine_worker(live_workers))
def close(self) -> None:
late_unrelated.append(_start_engine_worker(live_workers))
if close_raises:
raise RuntimeError("close failed")
monkeypatch.setattr("vllm_omni.entrypoints.omni.Omni", _OwnedOmni)
if close_raises:
with pytest.raises(RuntimeError, match="close failed"), OmniRunner("fake-model"):
pass
else:
with OmniRunner("fake-model"):
pass
assert owned[0].poll() is not None
assert existing.poll() is None
assert late_unrelated[0].poll() is None
def test_runner_startup_failure_cleans_only_new_workers(
monkeypatch: pytest.MonkeyPatch, live_workers: list[subprocess.Popen[bytes]]
) -> None:
unrelated = _start_engine_worker(live_workers)
owned: list[subprocess.Popen[bytes]] = []
monkeypatch.setattr("tests.helpers.runtime.cleanup_test_environment", lambda: None)
class _FailingOmni:
def __init__(self, *args: object, **kwargs: object) -> None:
owned.append(_start_engine_worker(live_workers))
raise RuntimeError("startup failed")
monkeypatch.setattr("vllm_omni.entrypoints.omni.Omni", _FailingOmni)
with pytest.raises(RuntimeError, match="startup failed"):
OmniRunner("fake-model")
assert owned[0].poll() is not None
assert unrelated.poll() is None
def test_runner_cleanup_rejects_stale_process_identity(live_workers: list[subprocess.Popen[bytes]]) -> None:
unrelated = _start_engine_worker(live_workers)
runner = object.__new__(OmniRunner)
runner._owned_engine_processes = {unrelated.pid: psutil.Process(unrelated.pid).create_time() + 1}
runner._cleanup_process()
assert unrelated.poll() is None
def test_runner_cleanup_kills_owned_worker_ignoring_terminate(
monkeypatch: pytest.MonkeyPatch, live_workers: list[subprocess.Popen[bytes]]
) -> None:
owned: list[subprocess.Popen[bytes]] = []
monkeypatch.setattr("tests.helpers.runtime.cleanup_test_environment", lambda: None)
class _StubbornOmni:
def __init__(self, *args: object, **kwargs: object) -> None:
owned.append(_start_engine_worker(live_workers, ignore_terminate=True))
def close(self) -> None:
pass
monkeypatch.setattr("vllm_omni.entrypoints.omni.Omni", _StubbornOmni)
with OmniRunner("fake-model"):
assert owned[0].poll() is None
assert owned[0].poll() is not None
+88 -6
View File
@@ -1,13 +1,17 @@
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project # SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
from importlib import import_module
from types import SimpleNamespace from types import SimpleNamespace
from unittest.mock import Mock from unittest.mock import Mock
import pytest import pytest
from transformers import BatchFeature from transformers import BatchFeature
from vllm.config.multimodal import MultiModalDummyOptions
from vllm.multimodal.parse import AudioProcessorItems, ImageProcessorItems, MultiModalDataItems
from vllm.multimodal.processing import BaseMultiModalProcessor
from vllm_omni.inputs.mm_processor import OmniMultiModalProcessor from vllm_omni.inputs.mm_processor import OmniDummyInputsBuilder, OmniMultiModalProcessor
pytestmark = [pytest.mark.core_model, pytest.mark.cpu] pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
@@ -24,10 +28,14 @@ def test_custom_processor_bridge_preserves_prompt_kwargs_and_passthrough(counts,
processor = object.__new__(Processor) processor = object.__new__(Processor)
processor.dummy_inputs = SimpleNamespace(get_dummy_text=Mock(return_value="dummy")) processor.dummy_inputs = SimpleNamespace(get_dummy_text=Mock(return_value="dummy"))
processor._get_hf_mm_data = Mock(return_value=({"audios": ["wave"]}, {"embedding": "preserve"}))
processor._call_hf_processor = Mock(return_value=BatchFeature({"features": "processed"})) processor._call_hf_processor = Mock(return_value=BatchFeature({"features": "processed"}))
items = Mock()
items.get_all_counts.return_value = counts class AudioItems(AudioProcessorItems):
def get_passthrough_data(self):
return {"embedding": "preserve"}
items = MultiModalDataItems({"audio": AudioItems(["wave"]), "image": ImageProcessorItems([])} if counts else {})
items.select = Mock(wraps=items.select)
kwargs = {"sampling_rate": 16000} kwargs = {"sampling_rate": 16000}
if original_text is not None: if original_text is not None:
kwargs[processor._OMNI_PROMPT_TEXT_KEY] = original_text kwargs[processor._OMNI_PROMPT_TEXT_KEY] = original_text
@@ -36,9 +44,83 @@ def test_custom_processor_bridge_preserves_prompt_kwargs_and_passthrough(counts,
items.select.assert_called_once_with({k for k, v in counts.items() if v > 0}) items.select.assert_called_once_with({k for k, v in counts.items() if v > 0})
processor._call_hf_processor.assert_called_once_with( processor._call_hf_processor.assert_called_once_with(
"dummy" if original_text is None else original_text, "dummy" if original_text is None else original_text,
{"audios": ["wave"]}, {"audios": ["wave"]} if counts else {},
{"sampling_rate": 16000}, {"sampling_rate": 16000},
{}, {},
) )
assert dict(result) == {"features": "processed", "embedding": "preserve"} assert dict(result) == ({"features": "processed", "embedding": "preserve"} if counts else {"features": "processed"})
assert kwargs == before assert kwargs == before
@pytest.mark.parametrize(
"module_name,builder_name,processor_name,expected_kwargs",
[
(
"cosyvoice3",
"CosyVoice3DummyInputsBuilder",
"CosyVoice3MultiModalProcessor",
{"prompt_text": "Testing my voices. Why should I not?"},
),
(
"glm_tts",
"GLMTTSDummyInputsBuilder",
"GLMTTSMultiModalProcessor",
{"prompt_text": "This is the reference voice."},
),
(
"omnivoice",
"OmniVoiceDummyInputsBuilder",
"OmniVoiceMultiModalProcessor",
{"ref_text": "Testing voice cloning."},
),
("mimo_audio", "MiMoAudioLLMDummyInputsBuilder", "MiMoAudioLLMMultiModalProcessor", {}),
],
)
def test_dummy_budget_preserves_model_conditioning(module_name, builder_name, processor_name, expected_kwargs):
"""The v0.31 budget path must reach each model's custom builder hook."""
module = import_module(f"vllm_omni.model_executor.models.{module_name}.{module_name}")
builder = object.__new__(getattr(module, builder_name))
assert isinstance(builder, OmniDummyInputsBuilder)
items = MultiModalDataItems({})
builder.info = SimpleNamespace(ctx=SimpleNamespace(tokenizer=None), parse_mm_data=Mock(return_value=items))
builder.get_dummy_mm_data = Mock(return_value={})
processor = object.__new__(getattr(module, processor_name))
processor.dummy_inputs = builder
inputs = processor.get_dummy_inputs(128, {"audio": 1}, MultiModalDummyOptions())
assert inputs.mm_data_items is items
assert inputs.hf_processor_mm_kwargs == expected_kwargs
assert inputs.prompt == ("<|empty|>" if module_name == "mimo_audio" else [])
def test_dummy_budget_keeps_upstream_path_for_standard_builders(monkeypatch):
class Processor(OmniMultiModalProcessor):
def _get_mm_fields_config(self, *args):
return {}
def _get_prompt_updates(self, *args):
return []
processor = object.__new__(Processor)
processor.dummy_inputs = SimpleNamespace()
expected = object()
upstream = Mock(return_value=expected)
monkeypatch.setattr(BaseMultiModalProcessor, "get_dummy_inputs", upstream)
options = MultiModalDummyOptions()
assert processor.get_dummy_inputs(128, {"audio": 1}, options) is expected
upstream.assert_called_once_with(128, {"audio": 1}, options)
def test_voxtral_dummy_budget_uses_audio_token_prompt():
from vllm_omni.model_executor.models.voxtral_tts.voxtral_tts_audio_generation import VoxtralTTSMultiModalProcessor
processor = object.__new__(VoxtralTTSMultiModalProcessor)
expected = SimpleNamespace(prompt=[1, 25, 24, 35])
builder = SimpleNamespace(get_dummy_processor_inputs=Mock(return_value=expected))
processor.dummy_inputs = builder
options = MultiModalDummyOptions()
assert processor.get_dummy_inputs(128, {"audio": 1}, options) is expected
builder.get_dummy_processor_inputs.assert_called_once_with(128, {"audio": 1}, options)
+32 -1
View File
@@ -8,7 +8,9 @@ from unittest.mock import AsyncMock, Mock
import pytest import pytest
import torch import torch
from vllm.inputs import tokens_input from vllm.exceptions import VLLMValidationError
from vllm.inputs import mm_input, tokens_input
from vllm.multimodal.inputs import PlaceholderRange
from vllm.pooling_params import PoolingParams from vllm.pooling_params import PoolingParams
from vllm.renderers import BaseRenderer from vllm.renderers import BaseRenderer
from vllm.renderers.params import TokenizeParams from vllm.renderers.params import TokenizeParams
@@ -115,6 +117,35 @@ def test_omni_renderer_is_a_real_subclass_created_once():
assert not hasattr(instance, "_renderer") assert not hasattr(instance, "_renderer")
@pytest.mark.parametrize("async_mode", [False, True])
@pytest.mark.parametrize("no_media_kwargs", [False, True])
@pytest.mark.parametrize("limit", [2, 4])
def test_renderer_applies_expanded_prompt_bound_without_splitting_media(renderer, async_mode, no_media_kwargs, limit):
renderer._process_multimodal.return_value = mm_input(
list(range(6)), {}, {"image": ["hash"]}, {"image": [PlaceholderRange(offset=1, length=2)]}
)
prompt = {"prompt_token_ids": [1, 2, 3], "additional_information": {"speaker": 1}}
if no_media_kwargs:
prompt["mm_processor_kwargs"] = {"target_h": 512}
else:
prompt["multi_modal_data"] = {"image": "image"}
params = TokenizeParams(max_total_tokens=128, truncate_prompt_tokens=limit, truncation_side="right")
def render():
if async_mode:
return asyncio.run(renderer.render_cmpl_async([prompt], params))
return renderer.render_cmpl([prompt], params)
if limit == 2:
with pytest.raises(VLLMValidationError, match="would split or drop"):
render()
else:
(result,) = render()
assert result["prompt_token_ids"] == [0, 1, 2, 3]
assert result["mm_placeholders"]["image"] == [PlaceholderRange(offset=1, length=2)]
assert result["additional_information"] == {"speaker": 1}
def test_concrete_renderer_overrides_are_not_suppressed(): def test_concrete_renderer_overrides_are_not_suppressed():
"""The old proxy bound BaseRenderer.render_cmpl itself and hid subclass overrides.""" """The old proxy bound BaseRenderer.render_cmpl itself and hid subclass overrides."""
+2
View File
@@ -426,6 +426,8 @@ def _make_scheduler_shell() -> OmniARScheduler:
obj.requests = {} obj.requests = {}
obj.running = [] obj.running = []
obj.waiting = [] obj.waiting = []
obj.kv_holding_waiting = []
obj.deferred_waiting = set()
obj._kv_wait_start_ts = {} obj._kv_wait_start_ts = {}
obj._omni_kv_config = None obj._omni_kv_config = None
return obj return obj
+1
View File
@@ -477,6 +477,7 @@ def stats_config():
observability_config=ObservabilityConfig(kv_cache_metrics=True), observability_config=ObservabilityConfig(kv_cache_metrics=True),
speculative_config=Mock(spec=SpeculativeConfig, num_speculative_tokens=3), speculative_config=Mock(spec=SpeculativeConfig, num_speculative_tokens=3),
kv_transfer_config=None, kv_transfer_config=None,
ec_transfer_config=None,
lora_config=None, lora_config=None,
) )
yield config yield config
@@ -7,7 +7,11 @@ from types import SimpleNamespace
import numpy as np import numpy as np
import pytest import pytest
import torch import torch
from vllm.config import VllmConfig
from vllm.sampling_params import SamplingParams from vllm.sampling_params import SamplingParams
from vllm.v1.worker.gpu.sample.bad_words import BadWordsState
from vllm.v1.worker.gpu.sample.logit_bias import LogitBiasState
from vllm.v1.worker.gpu.sample.penalties import PenaltiesState
from vllm.v1.worker.gpu.sample.sampler import Sampler from vllm.v1.worker.gpu.sample.sampler import Sampler
from vllm.v1.worker.gpu.states import RequestState from vllm.v1.worker.gpu.states import RequestState
@@ -27,11 +31,11 @@ def test_matches_real_sampler_for_mixed_prefill_decode_stop_and_reordering(seed,
pytest.skip("CUDA required") pytest.skip("CUDA required")
device = torch.device("cuda") device = torch.device("cuda")
reqs = RequestState(5, 32, 16, 0, 8, device) reqs = RequestState(5, 32, 16, 0, 8, device)
sampler = Sampler(5, 8, device, reqs) sampler = Sampler(VllmConfig(), 5, 8, device, reqs)
for i, length in enumerate([4, 1, 3]): for i, length in enumerate([4, 1, 3]):
reqs.add_request(str(i), length, [2] * length, 0, 8) reqs.add_request(str(i), length, [2] * length, 0, 8)
slot = reqs.req_id_to_index[str(i)] slot = reqs.req_id_to_index[str(i)]
sampler.add_request(slot, length, SamplingParams(temperature=temperature, top_k=3, top_p=0.8, seed=seed)) sampler.add_request(slot, SamplingParams(temperature=temperature, top_k=3, top_p=0.8, seed=seed))
reqs.apply_staged_writes() reqs.apply_staged_writes()
sampler.apply_staged_writes() sampler.apply_staged_writes()
state = _state(MossLocalModelState, device) state = _state(MossLocalModelState, device)
@@ -39,6 +43,7 @@ def test_matches_real_sampler_for_mixed_prefill_decode_stop_and_reordering(seed,
state.model._batch_state = None state.model._batch_state = None
batch = _batch(device, [2, 4, 3], [1, 2, 1]) batch = _batch(device, [2, 4, 3], [1, 2, 1])
batch.seq_lens = torch.tensor([5, 2, 2], device=device, dtype=torch.int32) batch.seq_lens = torch.tensor([5, 2, 2], device=device, dtype=torch.int32)
batch.seq_lens_cpu_upper_bound = torch.tensor([5, 2, 2], dtype=torch.int32)
batch.cu_num_logits_np = np.arange(4, dtype=np.int32) batch.cu_num_logits_np = np.arange(4, dtype=np.int32)
batch.cu_num_logits = torch.tensor(batch.cu_num_logits_np, device=device) batch.cu_num_logits = torch.tensor(batch.cu_num_logits_np, device=device)
batch.expanded_idx_mapping = batch.idx_mapping batch.expanded_idx_mapping = batch.idx_mapping
@@ -73,10 +78,10 @@ def test_real_sampler_constraints_disable_determined_tokens(constraint):
pytest.skip("CUDA required") pytest.skip("CUDA required")
device = torch.device("cuda") device = torch.device("cuda")
reqs = RequestState(5, 32, 16, 0, 8, device) reqs = RequestState(5, 32, 16, 0, 8, device)
sampler = Sampler(5, 8, device, reqs) sampler = Sampler(VllmConfig(), 5, 8, device, reqs)
reqs.add_request("constrained", 1, [2], 0, 8) reqs.add_request("constrained", 1, [2], 0, 8)
slot = reqs.req_id_to_index["constrained"] slot = reqs.req_id_to_index["constrained"]
sampler.add_request(slot, 1, SamplingParams(**constraint)) sampler.add_request(slot, SamplingParams(**constraint))
state = _state(MossLocalModelState, device) state = _state(MossLocalModelState, device)
state._direct_tokens = True state._direct_tokens = True
# Use the actual upstream request registration, including its combined # Use the actual upstream request registration, including its combined
@@ -85,16 +90,22 @@ def test_real_sampler_constraints_disable_determined_tokens(constraint):
@pytest.mark.cpu @pytest.mark.cpu
@pytest.mark.parametrize("feature", ["logprobs", "bias", "penalty", "bad_words", "thinking", "trace", "mask", "nans"]) @pytest.mark.parametrize(
"feature", ["logprobs", "bias", "penalty", "bad_words", "custom", "thinking", "trace", "mask", "nans"]
)
def test_distribution_features_fall_back_before_using_gpu(feature): def test_distribution_features_fall_back_before_using_gpu(feature):
sampler = object.__new__(Sampler) sampler = object.__new__(Sampler)
sampler.compute_nans = feature == "nans" sampler.compute_nans = feature == "nans"
sampler.return_sampling_mask = feature == "mask" sampler.return_sampling_mask = feature == "mask"
sampler.trace_replay_state = object() if feature == "trace" else None sampler.trace_replay_state = object() if feature == "trace" else None
sampler.get_logprobs_dims = lambda rows: (1, 0) if feature == "logprobs" else None sampler.get_logprobs_dims = lambda rows: (1, 0) if feature == "logprobs" else None
sampler.logit_bias_state = SimpleNamespace(use_logit_bias=np.array([feature == "bias"])) bias = object.__new__(LogitBiasState)
sampler.penalties_state = SimpleNamespace(use_penalty=np.array([feature == "penalty"])) bias.use_logit_bias = np.array([feature == "bias"])
sampler.bad_words_state = SimpleNamespace(num_bad_words=SimpleNamespace(np=np.array([feature == "bad_words"]))) penalties = object.__new__(PenaltiesState)
penalties.use_penalty = np.array([feature == "penalty"])
bad_words = object.__new__(BadWordsState)
bad_words.num_bad_words = SimpleNamespace(np=np.array([feature == "bad_words"]))
sampler.logits_processors = [bias, penalties, bad_words] + ([object()] if feature == "custom" else [])
sampler.thinking_budget_state = SimpleNamespace(enabled=True, use_thinking_budget=np.array([feature == "thinking"])) sampler.thinking_budget_state = SimpleNamespace(enabled=True, use_thinking_budget=np.array([feature == "thinking"]))
state = object.__new__(MossLocalModelState) state = object.__new__(MossLocalModelState)
state._direct_tokens = True state._direct_tokens = True
@@ -132,10 +132,10 @@ def test_first_prefill_through_runner_preserves_parent_eager_contract(monkeypatc
hidden_states=torch.zeros(2, 4), hidden_states=torch.zeros(2, 4),
finished_req_ids=set(), finished_req_ids=set(),
ec_connector_output=None, ec_connector_output=None,
routed_experts=None,
) )
runner._kv_extracted_req_ids = runner._last_aux_output = runner._last_multimodal_outputs = None runner._kv_extracted_req_ids = runner._last_aux_output = runner._last_multimodal_outputs = None
runner.is_last_pp_rank, runner.pp_handler, runner.check_ep_fault = True, None, False runner.is_last_pp_rank, runner.pp_handler, runner.check_ep_fault = True, None, False
runner.aux_output_connector = None
runner.model_config = SimpleNamespace(async_chunk=False) runner.model_config = SimpleNamespace(async_chunk=False)
runner.vllm_config = SimpleNamespace(model_config=SimpleNamespace(engine_output_type="text")) runner.vllm_config = SimpleNamespace(model_config=SimpleNamespace(engine_output_type="text"))
runner.model_state, runner.model = state, model runner.model_state, runner.model = state, model
@@ -154,7 +154,10 @@ def test_first_prefill_through_runner_preserves_parent_eager_contract(monkeypatc
torch.zeros(1), torch.zeros(1),
) )
) )
runner.prompt_logprobs_worker = SimpleNamespace(compute_prompt_logprobs=mocker.Mock(return_value={})) runner.prompt_logprobs_worker = SimpleNamespace(
compute_prompt_logprobs=mocker.Mock(return_value={}),
compute_prompt_token_id_logprobs=mocker.Mock(return_value={}),
)
runner.postprocess_sampled = mocker.Mock() runner.postprocess_sampled = mocker.Mock()
runner.kv_connector = SimpleNamespace(post_forward=mocker.Mock(return_value=None)) runner.kv_connector = SimpleNamespace(post_forward=mocker.Mock(return_value=None))
mock_out = SimpleNamespace(copy_event=None) mock_out = SimpleNamespace(copy_event=None)
@@ -42,10 +42,26 @@ def _make_processor(monkeypatch: pytest.MonkeyPatch) -> OmniVoiceMultiModalProce
return processor return processor
class _FakeProcessorItem:
def __init__(self, processor_data: dict[str, object]) -> None:
self._processor_data = processor_data
def get_processor_data(self) -> dict[str, object]:
return dict(self._processor_data)
def get_passthrough_data(self) -> dict[str, object]:
return {}
def _items(counts: dict[str, int], mm_data: dict[str, object]) -> SimpleNamespace: def _items(counts: dict[str, int], mm_data: dict[str, object]) -> SimpleNamespace:
class _Selected:
@staticmethod
def values():
return [_FakeProcessorItem(mm_data)] if mm_data else []
return SimpleNamespace( return SimpleNamespace(
get_all_counts=lambda: counts, get_all_counts=lambda: counts,
select=lambda _keys: mm_data, select=lambda _keys: _Selected(),
) )
@@ -75,9 +91,8 @@ def test_apply_tokenizes_the_prompt_once(monkeypatch: pytest.MonkeyPatch) -> Non
def test_media_path_encodes_audio_without_retokenizing(monkeypatch: pytest.MonkeyPatch) -> None: def test_media_path_encodes_audio_without_retokenizing(monkeypatch: pytest.MonkeyPatch) -> None:
processor = _make_processor(monkeypatch) processor = _make_processor(monkeypatch)
audio = np.zeros(2400, dtype=np.float32) audio = np.zeros(2400, dtype=np.float32)
monkeypatch.setattr(processor, "_get_hf_mm_data", lambda _items: ({"audios": [audio]}, {}))
out = processor._apply_hf_processor_main(_items({"audio": 1}, {}), {"ref_text": "reference"}) out = processor._apply_hf_processor_main(_items({"audio": 1}, {"audios": [audio]}), {"ref_text": "reference"})
assert tuple(out["ref_audio_tokens"].shape) == (8, 4) assert tuple(out["ref_audio_tokens"].shape) == (8, 4)
assert list(out["ref_audio_len"]) == [4] assert list(out["ref_audio_len"]) == [4]
@@ -89,7 +104,6 @@ def test_media_path_encodes_audio_without_retokenizing(monkeypatch: pytest.Monke
def test_media_path_with_no_audio_touches_no_tokenizer(monkeypatch: pytest.MonkeyPatch) -> None: def test_media_path_with_no_audio_touches_no_tokenizer(monkeypatch: pytest.MonkeyPatch) -> None:
"""Text-only requests and fully cached audio both reach this path with empty items.""" """Text-only requests and fully cached audio both reach this path with empty items."""
processor = _make_processor(monkeypatch) processor = _make_processor(monkeypatch)
monkeypatch.setattr(processor, "_get_hf_mm_data", lambda _items: ({}, {}))
out = processor._apply_hf_processor_main(_items({}, {}), {"ref_text": "reference"}) out = processor._apply_hf_processor_main(_items({}, {}), {"ref_text": "reference"})
@@ -99,4 +113,6 @@ def test_media_path_with_no_audio_touches_no_tokenizer(monkeypatch: pytest.Monke
def test_legacy_call_hf_processor_is_gone() -> None: def test_legacy_call_hf_processor_is_gone() -> None:
assert not hasattr(OmniVoiceMultiModalProcessor, "_call_hf_processor") # Upstream reintroduced a 2-arg `_call_hf_processor` on the base class; the
# omni-specific 4-arg hook must not be defined on the omni class itself.
assert "_call_hf_processor" not in OmniVoiceMultiModalProcessor.__dict__
@@ -262,6 +262,7 @@ def test_cached_apply_hf_processor_expands_pair_miss_for_audio_hit_video_miss():
"audio": 1, "audio": 1,
}[modality] }[modality]
mm_data_items.__getitem__ = MagicMock(side_effect=lambda modality: [f"{modality}-0"]) mm_data_items.__getitem__ = MagicMock(side_effect=lambda modality: [f"{modality}-0"])
mm_data_items.values = MagicMock(return_value=[])
inputs = MagicMock() inputs = MagicMock()
inputs.mm_data_items = mm_data_items inputs.mm_data_items = mm_data_items
@@ -275,6 +276,7 @@ def test_cached_apply_hf_processor_expands_pair_miss_for_audio_hit_video_miss():
cache = MagicMock() cache = MagicMock()
# Video miss, audio hit under pair-aware keys — expansion must force audio. # Video miss, audio hit under pair-aware keys — expansion must force audio.
cache.is_cached.side_effect = lambda keys: [False] if keys[0].startswith("v") else [True] cache.is_cached.side_effect = lambda keys: [False] if keys[0].startswith("v") else [True]
inputs.cache = cache
captured = {} captured = {}
@@ -309,6 +311,8 @@ def test_cached_apply_hf_processor_expands_pair_miss_for_audio_hit_video_miss():
fake_self._get_mm_fields_config = MagicMock(return_value={}) fake_self._get_mm_fields_config = MagicMock(return_value={})
fake_self._get_mm_prompt_updates = MagicMock(return_value={"video": [[object()]], "audio": [[object()]]}) fake_self._get_mm_prompt_updates = MagicMock(return_value={"video": [[object()]], "audio": [[object()]]})
fake_self._merge_mm_kwargs = capture_merge fake_self._merge_mm_kwargs = capture_merge
fake_self._postprocess_prompt = lambda prompt: prompt
inputs.prompt = [0, 1, 2]
with patch( with patch(
"vllm_omni.model_executor.models.qwen2_5_omni.qwen2_5_omni_thinker.MultiModalKwargsItems.from_hf_inputs", "vllm_omni.model_executor.models.qwen2_5_omni.qwen2_5_omni_thinker.MultiModalKwargsItems.from_hf_inputs",
@@ -549,24 +553,6 @@ def test_qwen2_5_prompt_updates_validate_mask_against_request_video_count():
) )
def test_qwen2_5_mm_only_dummy_counts_subtract_only_videos_using_audio():
captured_counts = {}
fake_self = SimpleNamespace(
dummy_inputs=SimpleNamespace(get_dummy_text=lambda counts: captured_counts.update(counts) or "dummy"),
_apply_hf_processor_text_mm=lambda **_kwargs: ([], {}, False),
)
mm_items = SimpleNamespace(get_all_counts=lambda: {"video": 2, "audio": 1})
Qwen2_5OmniThinkerMultiModalProcessor._apply_hf_processor_mm_only(
fake_self,
mm_items,
{"use_audio_in_video": [True, False]},
{},
)
assert captured_counts == {"video": 2, "audio": 0}
def test_get_video_use_audio_in_video_falls_back_to_prompt_updates_for_cache_hits(): def test_get_video_use_audio_in_video_falls_back_to_prompt_updates_for_cache_hits():
fake_self = _fake_processor() fake_self = _fake_processor()
update_with_audio = SimpleNamespace(content=SimpleNamespace(full=[VIDEO_TOKEN_ID, AUDIO_TOKEN_ID])) update_with_audio = SimpleNamespace(content=SimpleNamespace(full=[VIDEO_TOKEN_ID, AUDIO_TOKEN_ID]))
@@ -661,7 +647,12 @@ def test_qwen3_processor_inherits_vllm_omni_per_video_helpers():
Qwen3OmniMoeThinkerMultiModalProcessor, Qwen3OmniMoeThinkerMultiModalProcessor,
Qwen2_5OmniThinkerMultiModalProcessor, Qwen2_5OmniThinkerMultiModalProcessor,
) )
assert Qwen3OmniMoeThinkerMultiModalProcessor._get_hf_mm_data is UpstreamQwen3Processor._get_hf_mm_data # Keep Qwen3's native HF preprocessing while inheriting Omni pair handling.
assert Qwen3OmniMoeThinkerMultiModalProcessor._get_hf_mm_inputs is UpstreamQwen3Processor._get_hf_mm_inputs
assert (
Qwen3OmniMoeThinkerMultiModalProcessor._get_audio_in_video_pairs
is Qwen2_5OmniThinkerMultiModalProcessor._get_audio_in_video_pairs
)
mro = Qwen3OmniMoeThinkerMultiModalProcessor.__mro__ mro = Qwen3OmniMoeThinkerMultiModalProcessor.__mro__
assert mro.index(Qwen2_5OmniThinkerMultiModalProcessor) < mro.index(UpstreamQwen3Processor) assert mro.index(Qwen2_5OmniThinkerMultiModalProcessor) < mro.index(UpstreamQwen3Processor)
@@ -79,31 +79,16 @@ def get_text_token_prompts(
"""Return ``(text_prompt, token_prompt)`` for the given multimodal data.""" """Return ``(text_prompt, token_prompt)`` for the given multimodal data."""
dummy_inputs = processor.dummy_inputs dummy_inputs = processor.dummy_inputs
tokenizer: TokenizerLike = processor.info.get_tokenizer() tokenizer: TokenizerLike = processor.info.get_tokenizer()
model_config = processor.info.ctx.model_config
parsed_data = processor.info.parse_mm_data(mm_data) parsed_data = processor.info.parse_mm_data(mm_data)
mm_counts = {k: len(vs) for k, vs in parsed_data.items()} mm_counts = {k: len(vs) for k, vs in parsed_data.items()}
inputs = dummy_inputs.get_dummy_processor_inputs( text_prompt = dummy_inputs.get_dummy_text(mm_counts)
model_config.max_model_len, token_prompt = tokenizer.encode(
mm_counts, text_prompt,
mm_options={}, **processor.info.get_default_tok_params().get_encode_kwargs(),
) )
text_prompt: str | None
token_prompt: list[int]
if isinstance(inputs.prompt, list):
text_prompt = None
token_prompt = inputs.prompt
elif isinstance(inputs.prompt, str):
text_prompt = inputs.prompt
token_prompt = tokenizer.encode(
text_prompt,
**processor.info.get_default_tok_params().get_encode_kwargs(),
)
else:
raise TypeError(type(inputs.prompt))
return text_prompt, token_prompt return text_prompt, token_prompt
@@ -199,8 +184,8 @@ def _test_processing_correctness(
modality: _to_dummy_options(modality, count) for modality, count in limit_mm_per_prompt_ints.items() modality: _to_dummy_options(modality, count) for modality, count in limit_mm_per_prompt_ints.items()
} }
baseline_processor = factories.build_processor(ctx, cache=None) baseline_processor = factories.build_processor(ctx)
cached_processor = factories.build_processor(ctx, cache=cache) cached_processor = factories.build_processor(ctx)
rng = np.random.RandomState(0) rng = np.random.RandomState(0)
@@ -238,6 +223,7 @@ def _test_processing_correctness(
baseline_processor, baseline_processor,
cached_processor, cached_processor,
batch_idx, batch_idx,
cache,
) )
@@ -247,6 +233,7 @@ def _test_processing_correctness_one(
baseline_processor: BaseMultiModalProcessor, baseline_processor: BaseMultiModalProcessor,
cached_processor: BaseMultiModalProcessor, cached_processor: BaseMultiModalProcessor,
batch_idx: int, batch_idx: int,
cache: MultiModalProcessorOnlyCache,
): ):
model_type = model_config.hf_config.model_type model_type = model_config.hf_config.model_type
@@ -258,12 +245,14 @@ def _test_processing_correctness_one(
token_prompt, token_prompt,
mm_items=mm_items, mm_items=mm_items,
hf_processor_mm_kwargs={}, hf_processor_mm_kwargs={},
cache=None,
) )
cached_tokenized_result = cached_processor( cached_tokenized_result = cached_processor(
token_prompt, token_prompt,
mm_items=mm_items, mm_items=mm_items,
hf_processor_mm_kwargs={}, hf_processor_mm_kwargs={},
cache=cache,
) )
_assert_inputs_equal( _assert_inputs_equal(
@@ -278,11 +267,13 @@ def _test_processing_correctness_one(
text_prompt, text_prompt,
mm_items=mm_items, mm_items=mm_items,
hf_processor_mm_kwargs={}, hf_processor_mm_kwargs={},
cache=None,
) )
cached_text_result = cached_processor( cached_text_result = cached_processor(
text_prompt, text_prompt,
mm_items=mm_items, mm_items=mm_items,
hf_processor_mm_kwargs={}, hf_processor_mm_kwargs={},
cache=cache,
) )
_assert_inputs_equal( _assert_inputs_equal(
+40
View File
@@ -66,6 +66,8 @@ def _make_scheduler_cls() -> type:
class FakeScheduler: class FakeScheduler:
def __init__(self) -> None: def __init__(self) -> None:
self.waiting = _FakeWaitingQueue() self.waiting = _FakeWaitingQueue()
self.kv_holding_waiting = _FakeWaitingQueue()
self.deferred_waiting: set[_FakeRequest] = set()
self.running: list[_FakeRequest] = [] self.running: list[_FakeRequest] = []
self.requests: dict[str, _FakeRequest] = {} self.requests: dict[str, _FakeRequest] = {}
self.finished: list[set[str]] = [] self.finished: list[set[str]] = []
@@ -196,6 +198,44 @@ def test_lone_member_is_held_then_released(scheduler: Any) -> None:
assert scheduler.running == [] assert scheduler.running == []
def test_lone_kv_holding_member_is_held(scheduler: Any) -> None:
request = _FakeRequest("req-1", {"cfg_role": "cond"})
scheduler.add_request(request)
scheduler.waiting.remove_requests([request])
scheduler.kv_holding_waiting.append(request)
held = cfg_pairing._hold_incomplete_pairs(scheduler)
assert list(scheduler.kv_holding_waiting) == []
cfg_pairing._release_held(held)
assert list(scheduler.kv_holding_waiting) == [request]
def test_deferred_kv_partner_holds_the_fresh_member(scheduler: Any) -> None:
cond, uncond = _add_pair(scheduler)
scheduler.waiting.remove_requests([uncond])
scheduler.kv_holding_waiting.append(uncond)
scheduler.deferred_waiting.add(uncond)
held = cfg_pairing._hold_incomplete_pairs(scheduler)
assert list(scheduler.waiting) == []
assert list(scheduler.kv_holding_waiting) == [uncond]
cfg_pairing._release_held(held)
assert list(scheduler.waiting) == [cond]
def test_kv_holding_queue_keeps_pair_partners_adjacent(scheduler: Any) -> None:
cond, uncond = _add_pair(scheduler)
other = _FakeRequest("other")
scheduler.kv_holding_waiting.extend([cond, other, uncond])
scheduler.waiting.clear()
cfg_pairing._reorder_waiting_for_cfg(scheduler)
assert list(scheduler.kv_holding_waiting) == [cond, uncond, other]
def test_complete_pair_is_admitted_together(scheduler: Any) -> None: def test_complete_pair_is_admitted_together(scheduler: Any) -> None:
scheduler.add_request(_FakeRequest("req-1", {"cfg_role": "cond"})) scheduler.add_request(_FakeRequest("req-1", {"cfg_role": "cond"}))
scheduler.schedule() scheduler.schedule()
+161
View File
@@ -0,0 +1,161 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""CPU contract coverage for NPU routing capture and the native data plane."""
from types import SimpleNamespace
import numpy as np
import pytest
import torch
from vllm.config import AuxOutputConfig
from vllm.distributed.aux_output_connector import worker
from vllm.distributed.aux_output_connector.connector import (
AuxOutputConnectorMetadata,
AuxOutputSchedulerConnector,
PackedBlockHashes,
)
from vllm.model_executor.layers.fused_moe import routed_experts_capturer
from vllm.v1.kv_cache_interface import KVCacheConfig
from vllm_omni.platforms.npu.worker.aux_output import NPUAuxOutputMixin
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
class _Router:
def _select_experts(self, logical_ids):
return torch.ones_like(logical_ids, dtype=torch.float32), logical_ids
class _AscendMoE(torch.nn.Module):
__module__ = "vllm_ascend.ops.fused_moe"
def __init__(self):
super().__init__()
self.layer_id = 0
self.router = _Router()
class _Runner(NPUAuxOutputMixin):
def _sync_device(self):
self.synchronized += 1
@pytest.fixture
def runner(monkeypatch):
# Exercise the real capturer, block store, worker and scheduler connectors
# with CPU tensors; importing vllm-ascend or owning NPU hardware is unnecessary.
monkeypatch.setattr(routed_experts_capturer, "current_platform", SimpleNamespace(device_type="cpu"))
monkeypatch.setattr(routed_experts_capturer, "get_forward_context", lambda: SimpleNamespace(dp_metadata=None))
monkeypatch.setattr(worker, "get_tp_group", lambda: SimpleNamespace(is_first_rank=True))
runner = _Runner()
runner.model = torch.nn.Sequential(_AscendMoE())
runner.vllm_config = SimpleNamespace(
aux_output_config=AuxOutputConfig(enable_return_routed_experts=True),
model_config=SimpleNamespace(
get_total_num_hidden_layers=lambda: 1,
get_num_experts=lambda: 16,
get_num_experts_per_tok=lambda: 2,
hf_text_config=SimpleNamespace(model_type="test_moe"),
),
scheduler_config=SimpleNamespace(max_num_batched_tokens=16, max_num_seqs=4),
parallel_config=SimpleNamespace(data_parallel_rank=0, tensor_parallel_size=1, decode_context_parallel_size=1),
cache_config=SimpleNamespace(block_size=4, prefix_match_unit=None),
max_concurrent_batches=2,
)
runner.input_batch = SimpleNamespace(req_ids=["r"], num_computed_tokens_cpu=np.array([0]))
runner.query_start_loc = SimpleNamespace(np=np.array([0, 4]))
runner.synchronized = 0
runner._init_omni_aux_output(KVCacheConfig(8, [], []))
try:
yield runner
finally:
runner._close_omni_aux_output()
def _step(runner, *, emit_start=0, hashes=None, finished=()):
scheduler_output = SimpleNamespace(
aux_output_connector_metadata=AuxOutputConnectorMetadata(
generation=0,
requests={} if finished else {"r": emit_start},
block_hashes=hashes or {},
finished_requests=finished,
),
scheduled_spec_decode_tokens={},
)
runner._begin_omni_aux_output_step(scheduler_output)
return scheduler_output
def _capture(runner, ids):
logical_ids = torch.tensor(ids, dtype=torch.int32)
weights, returned = runner.model[0].router._select_experts(logical_ids)
assert torch.equal(returned, logical_ids)
assert weights.shape == logical_ids.shape
return logical_ids.numpy()[:, None, :].astype(np.uint8)
def test_routing_reaches_scheduler_and_owns_legacy_batch_snapshot(runner):
step = _step(runner)
expected = _capture(runner, [[0, 1], [1, 2], [2, 3], [3, 4]])
pending = runner._prepare_omni_aux_output()
# Simulate bookkeeping and the next batch reusing the runner's buffers.
runner.input_batch.num_computed_tokens_cpu[:] = 99
runner.query_start_loc.np[:] = 99
_capture(runner, [[7, 8]] * 4)
output = runner._finish_omni_aux_output(pending, step, torch.tensor([[5]]))
request = SimpleNamespace(request_id="r", num_tokens=5)
rows = AuxOutputSchedulerConnector().take_output(request, output)
np.testing.assert_array_equal(rows, expected)
assert output["r"].token_start == 0
assert runner.synchronized == 1
def test_speculative_rejection_drops_unaccepted_routing_suffix(runner):
step = _step(runner)
step.scheduled_spec_decode_tokens = {"r": [7, 8, 9]}
expected = _capture(runner, [[0, 1], [1, 2], [2, 3], [3, 4]])
pending = runner._prepare_omni_aux_output()
output = runner._finish_omni_aux_output(pending, step, torch.tensor([[5, 6, -1, -1]]))
np.testing.assert_array_equal(output["r"].rows, expected[:2])
def test_partial_prefill_accumulates_rows_and_no_forward_step_tears_down(runner):
step = _step(runner)
prefix = _capture(runner, [[0, 1], [1, 2], [2, 3], [3, 4]])
pending = runner._prepare_omni_aux_output()
assert runner._finish_omni_aux_output(pending, step, torch.tensor([[5]]), [0]) == {}
# Both completed four-token blocks need their scheduler-provided hashes.
step = _step(runner, hashes={"r": PackedBlockHashes(b"h0h1", 2)})
runner.input_batch.num_computed_tokens_cpu[:] = 4
suffix = _capture(runner, [[4, 5], [5, 6], [6, 7], [7, 8]])
pending = runner._prepare_omni_aux_output()
output = runner._finish_omni_aux_output(pending, step, torch.tensor([[5]]))
np.testing.assert_array_equal(output["r"].rows, np.concatenate((prefix, suffix)))
_step(runner, finished=("r",))
assert not runner.aux_output_connector._requests
store = runner.aux_output_connector._store
runner._close_omni_aux_output()
assert not store._thread.is_alive()
assert runner.model[0].capture_fn is None
runner._close_omni_aux_output() # shutdown is idempotent
def test_reinitialization_does_not_wrap_router_twice(runner):
selection = runner.model[0].router._select_experts
runner._init_omni_aux_output(KVCacheConfig(8, [], []))
assert runner.model[0].router._select_experts is selection
step = _step(runner)
expected = _capture(runner, [[0, 1]] * 4)
output = runner._finish_omni_aux_output(runner._prepare_omni_aux_output(), step, torch.tensor([[5]]))
np.testing.assert_array_equal(output["r"].rows, expected)
def test_disabled_capture_preserves_normal_runner_path(runner):
runner._close_omni_aux_output()
runner.vllm_config.aux_output_config.enable_return_routed_experts = False
runner._init_omni_aux_output(KVCacheConfig(8, [], []))
runner._begin_omni_aux_output_step(SimpleNamespace())
assert runner._prepare_omni_aux_output() is None
assert runner._finish_omni_aux_output(None, SimpleNamespace()) is None
@@ -3,6 +3,7 @@
import contextlib import contextlib
from types import SimpleNamespace from types import SimpleNamespace
from unittest.mock import Mock
import pytest import pytest
import torch import torch
@@ -22,6 +23,23 @@ class _StopExecutionError(Exception):
"""Halts execute_model at the model boundary.""" """Halts execute_model at the model boundary."""
@pytest.mark.parametrize("randomize_inputs", [False, True])
def test_generation_profile_accepts_upstream_randomize_inputs(randomize_inputs):
"""Release workers can pass the profiling flag to codec-stage runners."""
runner = object.__new__(GPUGenerationModelRunner)
runner.supports_mm_inputs = False
runner.max_num_tokens = 128
runner.encoder_cache = {"temporary": object()}
runner._dummy_run = Mock(return_value=(torch.zeros(1), None))
runner._sync_device = Mock()
runner.profile_run(randomize_inputs=randomize_inputs)
runner._dummy_run.assert_called_once_with(128, is_profile=True, randomize_inputs=randomize_inputs)
runner._sync_device.assert_called_once_with()
assert runner.encoder_cache == {}
class _DummyInputBatch: class _DummyInputBatch:
def __init__(self, num_reqs: int = 1): def __init__(self, num_reqs: int = 1):
self.req_ids = [f"req-{i + 1}" for i in range(num_reqs)] self.req_ids = [f"req-{i + 1}" for i in range(num_reqs)]
@@ -15,8 +15,8 @@ pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
def test_dispatch_eager_and_manager_paths(monkeypatch, dp_size): def test_dispatch_eager_and_manager_paths(monkeypatch, dp_size):
runner = object.__new__(OmniGPUModelRunner) runner = object.__new__(OmniGPUModelRunner)
runner.cudagraph_manager = None runner.cudagraph_manager = None
runner.dp_size = dp_size runner.parallel_config = SimpleNamespace(data_parallel_size=dp_size, data_parallel_rank=1)
runner.dp_rank = 1 runner.dp_size, runner.dp_rank = dp_size, 1
import vllm.v1.worker.gpu.dp_utils as dp_utils import vllm.v1.worker.gpu.dp_utils as dp_utils
@@ -52,6 +52,7 @@ def test_dispatch_eager_and_manager_paths(monkeypatch, dp_size):
return expected return expected
runner.cudagraph_manager = SimpleNamespace(dispatch=dispatch) runner.cudagraph_manager = SimpleNamespace(dispatch=dispatch)
runner.parallel_config.data_parallel_size = 1
runner.dp_size = 1 runner.dp_size = 1
batch_desc, _ = runner._dispatch_batch_descriptor( batch_desc, _ = runner._dispatch_batch_descriptor(
num_reqs=1, num_toks=8, uniform_tok_count=8, num_active_loras=0, use_eager=False, max_query_len=8 num_reqs=1, num_toks=8, uniform_tok_count=8, num_active_loras=0, use_eager=False, max_query_len=8
@@ -44,8 +44,8 @@ def _check_prefill():
spec = FullAttentionSpec(block_size=16, num_kv_heads=8, head_size=128, dtype=torch.bfloat16) spec = FullAttentionSpec(block_size=16, num_kv_heads=8, head_size=128, dtype=torch.bfloat16)
kvconfig = KVCacheConfig(num_blocks=1024, kv_cache_tensors=[], kv_cache_groups=[KVCacheGroupSpec(["attn"], spec)]) kvconfig = KVCacheConfig(num_blocks=1024, kv_cache_tensors=[], kv_cache_groups=[KVCacheGroupSpec(["attn"], spec)])
state = OmniModelState.__new__(OmniModelState) state = OmniModelState.__new__(OmniModelState)
state.model_config = SimpleNamespace(max_model_len=256)
state.vllm_config = c state.vllm_config = c
state.max_model_len = 256
state.supports_mm_inputs = False state.supports_mm_inputs = False
with set_current_vllm_config(c): with set_current_vllm_config(c):
group = AttentionGroup(FlashAttentionBackend, ["attn"], spec, 0) group = AttentionGroup(FlashAttentionBackend, ["attn"], spec, 0)
@@ -54,7 +54,7 @@ def test_full_decode_graph_matches_fa3_aot_schedule(graph_mode, tmp_path):
kvconfig = KVCacheConfig(num_blocks=1024, kv_cache_tensors=[], kv_cache_groups=[KVCacheGroupSpec(["attn"], spec)]) kvconfig = KVCacheConfig(num_blocks=1024, kv_cache_tensors=[], kv_cache_groups=[KVCacheGroupSpec(["attn"], spec)])
state = OmniModelState.__new__(OmniModelState) state = OmniModelState.__new__(OmniModelState)
state.vllm_config = c state.vllm_config = c
state.max_model_len = 2048 state.model_config = c.model_config
state.supports_mm_inputs = False state.supports_mm_inputs = False
with set_current_vllm_config(c): with set_current_vllm_config(c):
group = AttentionGroup(FlashAttentionBackend, ["attn"], spec, 0) group = AttentionGroup(FlashAttentionBackend, ["attn"], spec, 0)
+112 -16
View File
@@ -11,10 +11,13 @@ from unittest.mock import MagicMock
import numpy as np import numpy as np
import pytest import pytest
import torch import torch
from vllm.v1.outputs import RoutedExpertsTensors from vllm.distributed.aux_output_connector.connector import AuxRequestOutput
from vllm.sampling_params import SamplingParams
from vllm.v1.worker.gpu.sample.output import SamplerOutput, SamplingMaskTensors from vllm.v1.worker.gpu.sample.output import SamplerOutput, SamplingMaskTensors
from vllm.v1.worker.gpu.sample.prompt_logprob import PromptLogprobsWorker
import vllm_omni.worker_v2.omni_ar_model_runner as omni_ar_model_runner import vllm_omni.worker_v2.omni_ar_model_runner as omni_ar_model_runner
from tests.helpers.mark import hardware_test
from vllm_omni.model_executor.output_snapshot import PackedOutputSnapshot from vllm_omni.model_executor.output_snapshot import PackedOutputSnapshot
from vllm_omni.worker_v2.omni_ar_model_runner import OmniARModelRunner, OmniAsyncOutput from vllm_omni.worker_v2.omni_ar_model_runner import OmniARModelRunner, OmniAsyncOutput
from vllm_omni.worker_v2.output_snapshot import pack_output_snapshot from vllm_omni.worker_v2.output_snapshot import pack_output_snapshot
@@ -41,7 +44,7 @@ class _FakeEvent:
def _async_output(req_ids=("req-0",), **overrides) -> OmniAsyncOutput: def _async_output(req_ids=("req-0",), **overrides) -> OmniAsyncOutput:
rid2idx = {rid: i for i, rid in enumerate(req_ids)} rid2idx = {rid: i for i, rid in enumerate(req_ids)}
mro = omni_ar_model_runner.OmniModelRunnerOutput(list(req_ids), rid2idx, None, prompt_logprobs_dict={}) mro = omni_ar_model_runner.OmniModelRunnerOutput(list(req_ids), rid2idx, None, prompt_logprobs_dict={})
sampler_output = SamplerOutput(torch.tensor([[123]]), None, None, None) sampler_output = SamplerOutput(torch.tensor([[123]]), None, None, torch.tensor([1]), torch.tensor([0]))
kwargs = dict(model_runner_output=mro, sampler_output=sampler_output) kwargs = dict(model_runner_output=mro, sampler_output=sampler_output)
kwargs.update(num_sampled_tokens=torch.tensor([1] * len(req_ids)), copy_event=_FakeEvent()) kwargs.update(num_sampled_tokens=torch.tensor([1] * len(req_ids)), copy_event=_FakeEvent())
kwargs.update(main_stream=_FakeStream(), copy_stream=_FakeStream()) kwargs.update(main_stream=_FakeStream(), copy_stream=_FakeStream())
@@ -64,23 +67,38 @@ def test_async_output_blocking_event_and_routing_masks(monkeypatch, compact_widt
counts=torch.tensor([2, 0]), counts=torch.tensor([2, 0]),
vocab_size=4, vocab_size=4,
) )
sampler_output = SamplerOutput(torch.tensor([[2], [0]]), None, None, torch.tensor([1, 0]), None, masks) sampler_output = SamplerOutput(
routed = RoutedExpertsTensors(torch.tensor([[[2, 3]], [[4, 5]]]), torch.tensor([7, 9])) torch.tensor([[2], [0]]), None, None, torch.tensor([1, 0]), torch.tensor([0, 0]), masks
)
copied: list[tuple[np.ndarray, np.ndarray]] = []
class _FakePendingAuxOutput:
def enqueue_cpu_copy(self, *, num_sampled, num_rejected) -> None:
copied.append((num_sampled, num_rejected))
def process_output(self) -> dict[str, AuxRequestOutput]:
return {
"decode": AuxRequestOutput(token_start=0, rows=np.array([[2, 3]], dtype=np.uint8)),
"prefill": AuxRequestOutput(token_start=0, rows=np.array([[4, 5]], dtype=np.uint8)),
}
pending = _FakePendingAuxOutput()
output = _async_output( output = _async_output(
req_ids=["decode", "prefill"], req_ids=["decode", "prefill"],
sampler_output=sampler_output, sampler_output=sampler_output,
num_sampled_tokens=torch.tensor([1, 0]), num_sampled_tokens=torch.tensor([1, 0]),
copy_event=None, # None → constructor builds the default blocking event copy_event=None, # None → constructor builds the default blocking event
routed_experts=routed, pending_aux_output=pending,
).get_output() ).get_output()
assert event_kwargs == [{"blocking": True}] # blocking event by default assert event_kwargs == [{"blocking": True}] # blocking event by default
assert output.sampled_token_ids == [[2], []] assert output.sampled_token_ids == [[2], []]
assert output.sampling_masks.to_nested_list() == [[0, 2], []] assert output.sampling_masks.to_nested_list() == [[0, 2], []]
np.testing.assert_array_equal(output.sampling_masks.offsets, [0, 2, 2]) np.testing.assert_array_equal(output.sampling_masks.offsets, [0, 2, 2])
np.testing.assert_array_equal(output.routed_experts.routing_data, [[[2, 3]], [[4, 5]]]) np.testing.assert_array_equal(output.aux_output_connector_output["decode"].rows, [[2, 3]])
np.testing.assert_array_equal(output.routed_experts.slot_mapping, [7, 9]) np.testing.assert_array_equal(output.aux_output_connector_output["prefill"].rows, [[4, 5]])
assert output.aux_output_connector_output["decode"].token_start == 0
np.testing.assert_array_equal(output.sampling_masks.token_ids, [0, 2]) np.testing.assert_array_equal(output.sampling_masks.token_ids, [0, 2])
assert len(copied) == 1 and copied[0][0].tolist() == [1, 0]
@pytest.mark.parametrize("needs_history", [False, True]) @pytest.mark.parametrize("needs_history", [False, True])
@@ -88,19 +106,26 @@ def test_last_pp_rank_orchestration_and_kv_resolver(monkeypatch, needs_history)
runner = OmniARModelRunner.__new__(OmniARModelRunner) runner = OmniARModelRunner.__new__(OmniARModelRunner)
input_batch = SimpleNamespace(req_ids=["req"], num_reqs=1, seq_lens=torch.tensor([3])) input_batch = SimpleNamespace(req_ids=["req"], num_reqs=1, seq_lens=torch.tensor([3]))
input_batch.idx_mapping, input_batch.query_start_loc = torch.tensor([0]), torch.tensor([0, 1]) input_batch.idx_mapping, input_batch.query_start_loc = torch.tensor([0]), torch.tensor([0, 1])
input_batch.idx_mapping_np = np.array([0])
input_batch.num_computed_prefill_tokens_np = np.array([0])
input_batch.num_scheduled_tokens = np.array([3])
input_batch.prefill_len_np = np.array([3])
input_batch.query_start_loc_np = np.array([0, 3])
state = SimpleNamespace(input_batch=input_batch, hidden_states=torch.zeros(1, 2)) state = SimpleNamespace(input_batch=input_batch, hidden_states=torch.zeros(1, 2))
state.finished_req_ids, state.ec_connector_output, state.routed_experts = {"finished"}, None, None state.finished_req_ids, state.ec_connector_output = {"finished"}, None
runner.execute_model_state = state runner.execute_model_state = state
runner._kv_extracted_req_ids = runner._last_aux_output = runner._last_multimodal_outputs = None runner._kv_extracted_req_ids = runner._last_aux_output = runner._last_multimodal_outputs = None
runner.is_last_pp_rank, runner.pp_handler, runner.check_ep_fault = True, None, False runner.is_last_pp_rank, runner.pp_handler, runner.check_ep_fault = True, None, False
runner.aux_output_connector = None
runner.model_config = SimpleNamespace(async_chunk=False) runner.model_config = SimpleNamespace(async_chunk=False)
runner.vllm_config = SimpleNamespace(model_config=SimpleNamespace(engine_output_type="text")) runner.vllm_config = SimpleNamespace(model_config=SimpleNamespace(engine_output_type="text"))
runner.model_state = SimpleNamespace(postprocess_model_output=MagicMock(return_value=(torch.zeros(1, 2), None))) text_hidden = torch.tensor([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]])
runner.model_state = SimpleNamespace(postprocess_model_output=MagicMock(return_value=(text_hidden, None)))
runner.model_state.intermediate_buffer = SimpleNamespace(buffers={0: {"global_request_id": "global-req"}}) runner.model_state.intermediate_buffer = SimpleNamespace(buffers={0: {"global_request_id": "global-req"}})
runner.req_states = SimpleNamespace(req_id_to_index={"req": 0}) runner.req_states = SimpleNamespace(req_id_to_index={"req": 0})
runner.req_states.all_token_ids = SimpleNamespace(gpu=torch.tensor([[1]])) runner.req_states.all_token_ids = SimpleNamespace(gpu=torch.tensor([[1]]))
runner.req_states.num_computed_tokens = SimpleNamespace(gpu=torch.tensor([0])) runner.req_states.num_computed_tokens = SimpleNamespace(gpu=torch.tensor([0]))
runner.req_states.prompt_len = SimpleNamespace(np=np.array([1]), gpu=torch.tensor([1])) runner.req_states.prompt_len = SimpleNamespace(np=np.array([3]), gpu=torch.tensor([3]))
runner.main_stream = runner.output_copy_stream = MagicMock() runner.main_stream = runner.output_copy_stream = MagicMock()
runner.eplb = runner._finalize_native_data_plane_output = runner._reserve_native_data_plane_outputs = MagicMock() runner.eplb = runner._finalize_native_data_plane_output = runner._reserve_native_data_plane_outputs = MagicMock()
sampler_out = (SimpleNamespace(sampled_token_ids=torch.tensor([[2]])), MagicMock(), MagicMock()) sampler_out = (SimpleNamespace(sampled_token_ids=torch.tensor([[2]])), MagicMock(), MagicMock())
@@ -114,13 +139,17 @@ def test_last_pp_rank_orchestration_and_kv_resolver(monkeypatch, needs_history)
yield yield
sampling_active = False sampling_active = False
runner.model = SimpleNamespace(compute_logits=None, logitsprocs_need_output_token_ids=needs_history) runner.model = SimpleNamespace(
compute_logits=lambda hidden: hidden, logitsprocs_need_output_token_ids=needs_history
)
runner.model.mrv2_sampling_context = sampling_context runner.model.mrv2_sampling_context = sampling_context
runner.sampler = None runner.sampler = None
runner.sample = MagicMock(return_value=sampler_out) runner.sample = MagicMock(return_value=sampler_out)
runner.sample.side_effect = lambda *_: sampler_out if sampling_active is needs_history else pytest.fail() runner.sample.side_effect = lambda *_: sampler_out if sampling_active is needs_history else pytest.fail()
logprobs_mock = MagicMock(side_effect=lambda *_: pytest.fail("inside ctx") if sampling_active else {}) logprobs_mock = MagicMock(side_effect=lambda *_: pytest.fail("inside ctx") if sampling_active else {})
runner.prompt_logprobs_worker = SimpleNamespace(compute_prompt_logprobs=logprobs_mock) runner.prompt_logprobs_worker = PromptLogprobsWorker(1, torch.device("cpu"), logprobs_mode="raw_logits")
runner.prompt_logprobs_worker.add_request("req", 0, SamplingParams(prompt_logprob_token_ids=[1, 0]))
runner.prompt_logprobs_worker.compute_prompt_logprobs = logprobs_mock
runner.postprocess_sampled, connector_output = MagicMock(), object() runner.postprocess_sampled, connector_output = MagicMock(), object()
def post_forward(finished_req_ids): def post_forward(finished_req_ids):
@@ -135,10 +164,67 @@ def test_last_pp_rank_orchestration_and_kv_resolver(monkeypatch, needs_history)
assert runner.sample_tokens(None) is mock_out assert runner.sample_tokens(None) is mock_out
built = omni_ar_model_runner.OmniAsyncOutput.call_args.kwargs["model_runner_output"] built = omni_ar_model_runner.OmniAsyncOutput.call_args.kwargs["model_runner_output"]
assert built.kv_connector_output is connector_output assert built.kv_connector_output is connector_output
torch.testing.assert_close(built.prompt_token_id_logprobs_dict["req"], torch.tensor([[2.0, 1.0], [4.0, 3.0]]))
assert runner._resolve_global_request_id("req") == "global-req" # from the intermediate buffer assert runner._resolve_global_request_id("req") == "global-req" # from the intermediate buffer
assert runner._resolve_global_request_id("unknown") == "unknown" # fallback to the local id assert runner._resolve_global_request_id("unknown") == "unknown" # fallback to the local id
def test_async_output_preserves_fixed_token_scores_on_host(monkeypatch) -> None:
monkeypatch.setattr(torch.cuda, "set_stream", lambda _stream: None)
scores = torch.tensor([[-0.5, -1.5], [-0.2, -2.2]])
mro = omni_ar_model_runner.OmniModelRunnerOutput(
req_ids=["req-0"],
req_id_to_index={"req-0": 0},
sampled_token_ids=None,
prompt_logprobs_dict={},
prompt_token_id_logprobs_dict={"req-0": scores},
)
output = _async_output(model_runner_output=mro).get_output()
assert output.prompt_token_id_logprobs_dict["req-0"].device.type == "cpu"
torch.testing.assert_close(output.prompt_token_id_logprobs_dict["req-0"], scores)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_chunked_fixed_token_scores_are_copied_from_cuda() -> None:
device = torch.device("cuda")
hidden = torch.tensor([[1.0, 2.0, 3.0], [3.0, 1.0, 2.0], [2.0, 3.0, 1.0], [4.0, 5.0, 6.0]], device=device)
worker = PromptLogprobsWorker(1, device)
worker.add_request("req", 0, SamplingParams(prompt_logprob_token_ids=[2, 0], prompt_logprob_start=1))
batch = SimpleNamespace(
req_ids=["req"],
idx_mapping_np=np.array([0]),
num_computed_prefill_tokens_np=np.array([0]),
num_scheduled_tokens=np.array([2]),
prefill_len_np=np.array([4]),
query_start_loc_np=np.array([0, 2]),
)
assert worker.compute_prompt_token_id_logprobs(lambda hidden: hidden, hidden[:2], batch, np.array([4])) == {}
batch.num_computed_prefill_tokens_np = np.array([2])
scores = worker.compute_prompt_token_id_logprobs(lambda hidden: hidden, hidden[2:], batch, np.array([4]))
mro = omni_ar_model_runner.OmniModelRunnerOutput(
req_ids=["req"],
req_id_to_index={"req": 0},
sampled_token_ids=None,
prompt_logprobs_dict={},
prompt_token_id_logprobs_dict=scores,
)
num_sampled = torch.tensor([1], device=device, dtype=torch.int32)
sampler_output = SamplerOutput(
torch.tensor([[1]], device=device), None, None, num_sampled, torch.zeros_like(num_sampled)
)
output = OmniAsyncOutput(
model_runner_output=mro,
sampler_output=sampler_output,
num_sampled_tokens=num_sampled,
main_stream=torch.cuda.current_stream(),
copy_stream=torch.cuda.Stream(),
).get_output()
expected = hidden[1:3].log_softmax(dim=-1)[:, [2, 0]].cpu()
assert output.prompt_token_id_logprobs_dict["req"].device.type == "cpu"
torch.testing.assert_close(output.prompt_token_id_logprobs_dict["req"], expected)
def test_async_mm_snapshot_owns_output_until_copy_finishes() -> None: def test_async_mm_snapshot_owns_output_until_copy_finishes() -> None:
runner = OmniARModelRunner.__new__(OmniARModelRunner) runner = OmniARModelRunner.__new__(OmniARModelRunner)
runner.model_config = SimpleNamespace(async_chunk=True) runner.model_config = SimpleNamespace(async_chunk=True)
@@ -284,7 +370,13 @@ def test_async_output_slices_request_payloads_with_graph_padding(
batch.num_tokens_after_padding = padded_total batch.num_tokens_after_padding = padded_total
output = _async_output( output = _async_output(
req_ids=[f"req-{i}" for i in range(len(lengths))], req_ids=[f"req-{i}" for i in range(len(lengths))],
sampler_output=SamplerOutput(torch.ones(len(lengths), 1, dtype=torch.long), None, None, None), sampler_output=SamplerOutput(
torch.ones(len(lengths), 1, dtype=torch.long),
None,
None,
torch.ones(len(lengths), dtype=torch.long),
torch.zeros(len(lengths), dtype=torch.long),
),
text_hidden=hidden, text_hidden=hidden,
multimodal_outputs={"codes": {"audio": codes, "ref": refs}}, multimodal_outputs={"codes": {"audio": codes, "ref": refs}},
input_batch=batch, input_batch=batch,
@@ -585,7 +677,9 @@ def test_model_owned_audio_finalizer_runs_after_copy_without_hidden(monkeypatch,
result = _async_output( result = _async_output(
req_ids=["audio", "partial"], req_ids=["audio", "partial"],
sampler_output=SamplerOutput(torch.tensor([[99], [100]]), None, None, None), sampler_output=SamplerOutput(
torch.tensor([[99], [100]]), None, None, torch.tensor([1, 1]), torch.tensor([0, 0])
),
num_sampled_tokens=torch.tensor([1, 0]), num_sampled_tokens=torch.tensor([1, 0]),
multimodal_outputs={"snapshot": snapshot}, multimodal_outputs={"snapshot": snapshot},
input_batch=batch, input_batch=batch,
@@ -710,7 +804,7 @@ def test_stream_audio_snapshot_skips_discarded_code_partition(monkeypatch, mocke
extra = ({"extra": torch.tensor([7])}, _FakeEvent()) if consumer == "extra" else None extra = ({"extra": torch.tensor([7])}, _FakeEvent()) if consumer == "extra" else None
result = _async_output( result = _async_output(
req_ids=["frame", "eos", "prefill"], req_ids=["frame", "eos", "prefill"],
sampler_output=SamplerOutput(torch.tensor([[1], [2], [3]]), None, None, None), sampler_output=SamplerOutput(torch.tensor([[1], [2], [3]]), None, None, None, None),
num_sampled_tokens=torch.tensor([1, 1, 1]), num_sampled_tokens=torch.tensor([1, 1, 1]),
multimodal_outputs={"codes": {"audio": torch.ones(4, 2)}}, multimodal_outputs={"codes": {"audio": torch.ones(4, 2)}},
input_batch=batch, input_batch=batch,
@@ -753,7 +847,9 @@ def test_request_owned_snapshot_skips_generic_partition(monkeypatch, streaming):
result = _async_output( result = _async_output(
req_ids=["audio", "partial"], req_ids=["audio", "partial"],
sampler_output=SamplerOutput(torch.tensor([[99], [100]]), None, None, None), sampler_output=SamplerOutput(
torch.tensor([[99], [100]]), None, None, torch.tensor([1, 1]), torch.tensor([0, 0])
),
num_sampled_tokens=torch.tensor([1, 0]), num_sampled_tokens=torch.tensor([1, 0]),
multimodal_outputs={"snapshot": snapshot}, multimodal_outputs={"snapshot": snapshot},
input_batch=batch, input_batch=batch,
@@ -4,12 +4,14 @@
"""OmniGenerationModelRunner contracts: empty-step lifecycle, output partition, """OmniGenerationModelRunner contracts: empty-step lifecycle, output partition,
CPU-sync vs CUDA-async dispatch, and async-chunk slot recycling.""" CPU-sync vs CUDA-async dispatch, and async-chunk slot recycling."""
from contextlib import nullcontext
from types import SimpleNamespace from types import SimpleNamespace
from unittest.mock import MagicMock from unittest.mock import MagicMock
import numpy as np import numpy as np
import pytest import pytest
import torch import torch
from vllm.distributed.aux_output_connector.connector import AuxRequestOutput
from vllm_omni.model_executor.models.output_templates import OmniOutput from vllm_omni.model_executor.models.output_templates import OmniOutput
from vllm_omni.outputs import OmniModelRunnerOutput from vllm_omni.outputs import OmniModelRunnerOutput
@@ -18,6 +20,17 @@ from vllm_omni.worker_v2.omni_generation_model_runner import OmniGenerationModel
pytestmark = [pytest.mark.core_model, pytest.mark.cpu] pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
@pytest.mark.parametrize("randomize_inputs", [False, True])
def test_profile_run_accepts_release_flag_without_running_codec(monkeypatch, randomize_inputs):
runner = object.__new__(OmniGenerationModelRunner)
runner.model = MagicMock(side_effect=AssertionError("profiling ran codec model"))
synchronize = MagicMock()
monkeypatch.setattr(torch.accelerator, "synchronize", synchronize)
runner.profile_run(randomize_inputs=randomize_inputs)
synchronize.assert_called_once_with()
runner.model.assert_not_called()
class _FakeStagedField: class _FakeStagedField:
def __init__(self, data): def __init__(self, data):
self.np = data self.np = data
@@ -45,6 +58,7 @@ def _make_runner(model_output, num_reqs=1, prompt_len=10):
runner.execute_model_state = SimpleNamespace(finished_req_ids={"finished"}, ec_connector_output=None) runner.execute_model_state = SimpleNamespace(finished_req_ids={"finished"}, ec_connector_output=None)
runner.kv_connector = SimpleNamespace(post_forward=MagicMock(return_value=None)) runner.kv_connector = SimpleNamespace(post_forward=MagicMock(return_value=None))
runner.check_ep_fault = False runner.check_ep_fault = False
runner.aux_output_connector = None
runner.req_states = MagicMock( runner.req_states = MagicMock(
prompt_len=SimpleNamespace(np=np.full(num_reqs, prompt_len, dtype=np.int32)), prompt_len=SimpleNamespace(np=np.full(num_reqs, prompt_len, dtype=np.int32)),
num_computed_tokens=_FakeStagedField(np.zeros(num_reqs, dtype=np.int32)), num_computed_tokens=_FakeStagedField(np.zeros(num_reqs, dtype=np.int32)),
@@ -52,7 +66,8 @@ def _make_runner(model_output, num_reqs=1, prompt_len=10):
return runner return runner
def test_control_only_step_keeps_lifecycle_and_skips_input_construction(): @pytest.mark.parametrize("aux_enabled", [False, True])
def test_control_only_step_keeps_lifecycle_and_skips_input_construction(aux_enabled):
runner = object.__new__(OmniGenerationModelRunner) runner = object.__new__(OmniGenerationModelRunner)
order = [] order = []
for name in ( for name in (
@@ -68,8 +83,13 @@ def test_control_only_step_keeps_lifecycle_and_skips_input_construction():
runner.kv_connector = SimpleNamespace(no_forward=lambda _s: output) runner.kv_connector = SimpleNamespace(no_forward=lambda _s: output)
runner._merge_ec_connector_no_forward = lambda _s, value: value runner._merge_ec_connector_no_forward = lambda _s, value: value
runner._attach_native_data_plane_signals = lambda value: value runner._attach_native_data_plane_signals = lambda value: value
metadata = object()
runner.aux_output_connector = SimpleNamespace(begin_step=MagicMock()) if aux_enabled else None
scheduler_output = SimpleNamespace( scheduler_output = SimpleNamespace(
total_num_scheduled_tokens=0, scheduled_new_reqs=[], scheduled_cached_reqs=SimpleNamespace(req_ids=[]) total_num_scheduled_tokens=0,
scheduled_new_reqs=[],
scheduled_cached_reqs=SimpleNamespace(req_ids=[]),
aux_output_connector_metadata=metadata,
) )
assert runner.execute_model(scheduler_output) is output assert runner.execute_model(scheduler_output) is output
assert order == [ assert order == [
@@ -78,6 +98,9 @@ def test_control_only_step_keeps_lifecycle_and_skips_input_construction():
"free_states", "free_states",
"_apply_block_table_staged_writes_if_available", "_apply_block_table_staged_writes_if_available",
] ]
if aux_enabled:
assert runner.aux_output_connector is not None
runner.aux_output_connector.begin_step.assert_called_once_with(metadata)
def test_released_chunk_reuses_scheduler_output_and_slot_recycle_clears_state(): def test_released_chunk_reuses_scheduler_output_and_slot_recycle_clears_state():
@@ -184,6 +207,72 @@ def test_sample_tokens_uses_async_output_for_cuda_and_snapshots_req_ids(monkeypa
assert output_req_ids == ["req-0"] assert output_req_ids == ["req-0"]
def test_generation_aux_output_copies_and_materializes_without_sampling(monkeypatch):
from vllm_omni.worker_v2 import omni_generation_model_runner as module
events = []
class PendingOutput:
def enqueue_cpu_copy(self, *, num_sampled, num_rejected):
np.testing.assert_array_equal(num_sampled, [0])
np.testing.assert_array_equal(num_rejected, [0])
events.append("copy")
def process_output(self):
assert events == ["copy", "record", "synchronize"]
return {"req-0": AuxRequestOutput(0, np.array([[2, 3]], dtype=np.uint8))}
class CopyEvent:
def record(self, stream):
events.append("record")
def synchronize(self):
events.append("synchronize")
monkeypatch.setattr(torch.cuda, "Event", lambda **kw: CopyEvent())
monkeypatch.setattr(torch.cuda, "stream", lambda stream: nullcontext())
monkeypatch.setattr(module, "_async_copy_mm", lambda *a, **kw: {})
output = OmniModelRunnerOutput(["req-0"], {"req-0": 0}, [[]])
result = module.OmniGenerationAsyncOutput(
model_runner_output=output,
multimodal_outputs={},
num_reqs=1,
main_stream=object(),
copy_stream=SimpleNamespace(wait_stream=lambda stream: None),
pending_aux_output=PendingOutput(),
).get_output()
assert result.sampled_token_ids == [[]]
np.testing.assert_array_equal(result.aux_output_connector_output["req-0"].rows, [[2, 3]])
def test_pending_aux_output_forces_async_delivery_without_cuda_payload(monkeypatch):
from vllm_omni.worker_v2 import omni_generation_model_runner as module
runner = _make_runner(OmniOutput(text_hidden_states=torch.empty(0), multimodal_outputs={}))
runner.device = SimpleNamespace(type="cuda")
runner.main_stream = object()
runner.output_copy_stream = object()
pending = object()
runner.aux_output_connector = SimpleNamespace(prepare_output=MagicMock(return_value=pending))
runner._reserve_native_data_plane_outputs = MagicMock()
runner._release_generation_slots = MagicMock()
runner._finalize_native_data_plane_output = MagicMock()
captured = {}
class AsyncOutput:
def __init__(self, **kwargs):
captured.update(kwargs)
monkeypatch.setattr(module, "OmniGenerationAsyncOutput", AsyncOutput)
input_batch = runner._gen_input_batch
result = runner.sample_tokens()
assert isinstance(result, AsyncOutput)
runner.aux_output_connector.prepare_output.assert_called_once_with(input_batch)
assert captured["pending_aux_output"] is pending
@pytest.mark.parametrize("exact", [True, False]) @pytest.mark.parametrize("exact", [True, False])
def test_exact_input_shape_models_reject_padded_batches(exact): def test_exact_input_shape_models_reject_padded_batches(exact):
# Qwen3-Omni Code2Wav splits input_ids by seq_token_counts (#6712). # Qwen3-Omni Code2Wav splits input_ids by seq_token_counts (#6712).
+61 -8
View File
@@ -13,6 +13,7 @@ from vllm import SamplingParams
from vllm.config.compilation import CUDAGraphMode from vllm.config.compilation import CUDAGraphMode
from vllm.v1.core.sched.output import SchedulerOutput from vllm.v1.core.sched.output import SchedulerOutput
from vllm.v1.worker.gpu.model_runner import GPUModelRunner from vllm.v1.worker.gpu.model_runner import GPUModelRunner
from vllm.v1.worker.gpu.model_states.default import DefaultModelState
from vllm.v1.worker.gpu.sample.sampler import Sampler from vllm.v1.worker.gpu.sample.sampler import Sampler
from vllm_omni.config.model import OmniModelConfig from vllm_omni.config.model import OmniModelConfig
@@ -95,7 +96,8 @@ def test_full_payload_receive_is_polled_without_scheduled_tokens(mocker):
@pytest.mark.parametrize("output_form", ["tuple", "omni"]) @pytest.mark.parametrize("output_form", ["tuple", "omni"])
def test_capture_model_unwraps_exclude_full_and_capture_mtp(output_form): @pytest.mark.parametrize("profile_only", [False, True])
def test_capture_model_unwraps_exclude_full_and_capture_mtp(output_form, profile_only):
runner = object.__new__(OmniGPUModelRunner) runner = object.__new__(OmniGPUModelRunner)
hidden = torch.ones(1, 2) hidden = torch.ones(1, 2)
@@ -117,16 +119,21 @@ def test_capture_model_unwraps_exclude_full_and_capture_mtp(output_form):
runner.model_state = SimpleNamespace(capture_mtp_graphs=MagicMock()) runner.model_state = SimpleNamespace(capture_mtp_graphs=MagicMock())
runner._dispatch_mtp_batch_descriptor = MagicMock(return_value="desc") runner._dispatch_mtp_batch_descriptor = MagicMock(return_value="desc")
def assert_unwrapped(_self): def assert_unwrapped(_self, *, profile_only: bool = False):
assert torch.equal(runner.model.forward(), hidden) # forward unwrapped during capture assert torch.equal(runner.model.forward(), hidden) # forward unwrapped during capture
assert profile_only is expected_profile_only
return 3 return 3
expected_profile_only = profile_only
with patch.object(GPUModelRunner, "capture_model", assert_unwrapped): with patch.object(GPUModelRunner, "capture_model", assert_unwrapped):
assert runner.capture_model() == 3 assert runner.capture_model(profile_only=profile_only) == 3
assert runner.model.forward is original_forward # restored after capture assert runner.model.forward is original_forward # restored after capture
assert runner.cudagraph_manager._capture_descs == {CUDAGraphMode.PIECEWISE: [piecewise]} assert runner.cudagraph_manager._capture_descs == {CUDAGraphMode.PIECEWISE: [piecewise]}
runner.model_state.capture_mtp_graphs.assert_called_once_with(runner._dispatch_mtp_batch_descriptor) if profile_only:
runner.model_state.capture_mtp_graphs.assert_not_called()
else:
runner.model_state.capture_mtp_graphs.assert_called_once_with(runner._dispatch_mtp_batch_descriptor)
@pytest.mark.parametrize( @pytest.mark.parametrize(
@@ -163,6 +170,28 @@ def test_init_model_state_factory_dispatches_omni_only(monkeypatch, flag, expect
assert state is upstream.return_value assert state is upstream.return_value
@pytest.mark.parametrize("omni_stage", [False, True])
@pytest.mark.parametrize("specialized", [False, True])
def test_auxiliary_default_state_has_omni_lifecycle_but_preserves_upstream_states(monkeypatch, omni_stage, specialized):
class SpecializedState(DefaultModelState):
pass
state_cls = SpecializedState if specialized else DefaultModelState
upstream_state = object.__new__(state_cls)
monkeypatch.setattr("vllm_omni.worker_v2.model_states._upstream_init_model_state", lambda *_args: upstream_state)
monkeypatch.setattr(OmniModelState, "__init__", lambda *_args: None)
model_config = object.__new__(OmniModelConfig) if omni_stage else SimpleNamespace()
state = init_omni_model_state(
SimpleNamespace(model_config=model_config), SimpleNamespace(), None, torch.device("cpu")
)
if omni_stage and not specialized:
assert isinstance(state, OmniModelState)
assert callable(state.run_preprocess)
assert callable(state.postprocess_model_output)
else:
assert state is upstream_state
def test_finish_requests_notifies_model_and_cleans_only_known_slots(monkeypatch): def test_finish_requests_notifies_model_and_cleans_only_known_slots(monkeypatch):
runner = _make_runner() runner = _make_runner()
calls = [] calls = []
@@ -209,7 +238,7 @@ def test_full_graph_aux_contract_keeps_full_and_round_trips_leaves():
runner.model_state = SimpleNamespace() runner.model_state = SimpleNamespace()
captured = {} captured = {}
def capture(_self): def capture(_self, *, profile_only=False):
# The graph manager stores (hidden, leaves) in its aux buffers. # The graph manager stores (hidden, leaves) in its aux buffers.
assert runner.use_aux_hidden_state_outputs is True assert runner.use_aux_hidden_state_outputs is True
hidden, leaves = runner.model.forward(4) hidden, leaves = runner.model.forward(4)
@@ -262,7 +291,9 @@ def test_full_graph_output_without_contract_is_hidden_only():
@pytest.mark.parametrize("runner_kind", ["gpu", "ar", "generation"]) @pytest.mark.parametrize("runner_kind", ["gpu", "ar", "generation"])
def test_dummy_forward_uses_upstream_execution_state(runner_kind, monkeypatch): @pytest.mark.parametrize("randomize_inputs", [False, True])
@pytest.mark.parametrize("is_profile", [False, True])
def test_dummy_forward_uses_upstream_execution_state(runner_kind, randomize_inputs, is_profile, monkeypatch):
from vllm.v1.worker.gpu.input_batch import InputBatch from vllm.v1.worker.gpu.input_batch import InputBatch
from vllm.v1.worker.gpu.model_runner import ExecuteModelState from vllm.v1.worker.gpu.model_runner import ExecuteModelState
@@ -288,6 +319,7 @@ def test_dummy_forward_uses_upstream_execution_state(runner_kind, monkeypatch):
runner.model_state.prepare_inputs.return_value = {} runner.model_state.prepare_inputs.return_value = {}
runner._omni_data_plane = object() runner._omni_data_plane = object()
runner.supports_mm_inputs = False runner.supports_mm_inputs = False
runner.vocab_size = 32
runner.lora_config = None runner.lora_config = None
runner.is_encoder_decoder = False runner.is_encoder_decoder = False
runner.eplb = MagicMock() runner.eplb = MagicMock()
@@ -308,7 +340,15 @@ def test_dummy_forward_uses_upstream_execution_state(runner_kind, monkeypatch):
cg_mode=CUDAGraphMode.NONE, num_reqs=1, num_tokens=1, num_active_loras=0, max_query_len=1 cg_mode=CUDAGraphMode.NONE, num_reqs=1, num_tokens=1, num_active_loras=0, max_query_len=1
) )
runner._dispatch_batch_descriptor = MagicMock(return_value=(batch_desc, None)) runner._dispatch_batch_descriptor = MagicMock(return_value=(batch_desc, None))
monkeypatch.setattr(InputBatch, "make_dummy", lambda *args, **kwargs: input_batch) make_dummy = MagicMock(return_value=input_batch)
monkeypatch.setattr(InputBatch, "make_dummy", make_dummy)
randomized = []
def randomize(tensor, low, high):
randomized.append((low, high))
return tensor.fill_(7)
monkeypatch.setattr(torch.Tensor, "random_", randomize)
monkeypatch.setattr("vllm_omni.worker_v2.omni_model_runner.build_slot_mappings_by_layer", lambda *args: {}) monkeypatch.setattr("vllm_omni.worker_v2.omni_model_runner.build_slot_mappings_by_layer", lambda *args: {})
for module in ("omni_model_runner", "omni_generation_model_runner"): for module in ("omni_model_runner", "omni_generation_model_runner"):
monkeypatch.setattr(f"vllm_omni.worker_v2.{module}.set_forward_context", lambda *args, **kwargs: nullcontext()) monkeypatch.setattr(f"vllm_omni.worker_v2.{module}.set_forward_context", lambda *args, **kwargs: nullcontext())
@@ -318,7 +358,20 @@ def test_dummy_forward_uses_upstream_execution_state(runner_kind, monkeypatch):
# Upstream _dummy_run always supplies valid_dummy_state_slots, and the # Upstream _dummy_run always supplies valid_dummy_state_slots, and the
# result must use the real upstream constructor rather than a mocked state. # result must use the real upstream constructor rather than a mocked state.
assert runner.execute_model(scheduled, dummy_run=True, valid_dummy_state_slots=True) is None assert (
runner.execute_model(
scheduled,
dummy_run=True,
valid_dummy_state_slots=True,
randomize_inputs=randomize_inputs,
is_profile=is_profile,
)
is None
)
assert make_dummy.call_args.kwargs["is_padding"] is not is_profile
should_randomize = randomize_inputs and runner_kind != "generation"
assert randomized == ([(0, 32)] if should_randomize else [])
assert input_batch.input_ids.tolist() == ([7] if should_randomize else [1])
assert isinstance(runner.execute_model_state, ExecuteModelState) assert isinstance(runner.execute_model_state, ExecuteModelState)
assert runner.execute_model_state.input_batch is input_batch assert runner.execute_model_state.input_batch is input_batch
assert runner.execute_model_state.cudagraph_stats is None assert runner.execute_model_state.cudagraph_stats is None
+10
View File
@@ -22,6 +22,16 @@ from vllm_omni.worker_v2.model_states.omni_model_state import OmniModelState, _m
pytestmark = [pytest.mark.core_model, pytest.mark.cpu] pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
def test_prepare_attn_forwards_release_model_specific_metadata():
state = object.__new__(OmniModelState)
metadata = object()
output = object()
batch = SimpleNamespace()
with patch.object(DefaultModelState, "prepare_attn", return_value=output) as prepare:
assert state.prepare_attn(batch, None, (), None, [], None, model_specific_attn_metadata=metadata) is output
assert prepare.call_args.kwargs["model_specific_attn_metadata"] is metadata
class _DummyInputBatch: class _DummyInputBatch:
is_prefilling_np: np.ndarray is_prefilling_np: np.ndarray
req_ids: list[str] req_ids: list[str]
+26 -18
View File
@@ -25,7 +25,6 @@ from vllm.v1.spec_decode.metrics import SpecDecodingStats
from vllm_omni.core.sched.omni_scheduler_mixin import OmniSchedulerMixin from vllm_omni.core.sched.omni_scheduler_mixin import OmniSchedulerMixin
from vllm_omni.core.sched.utils import ( from vllm_omni.core.sched.utils import (
free_kv_blocks_in_physical_order, free_kv_blocks_in_physical_order,
omni_routed_experts_for_request,
) )
from vllm_omni.engine import OmniEngineCoreOutput from vllm_omni.engine import OmniEngineCoreOutput
from vllm_omni.engine.serialization import deserialize_additional_information from vllm_omni.engine.serialization import deserialize_additional_information
@@ -99,7 +98,7 @@ class OmniARScheduler(OmniSchedulerMixin, VLLMScheduler):
core scheduling logic. core scheduling logic.
""" """
max_num_running_reqs: int max_num_active_reqs: int
def reset_prefix_cache(self, reset_running_requests: bool = False, reset_connector: bool = False) -> bool: def reset_prefix_cache(self, reset_running_requests: bool = False, reset_connector: bool = False) -> bool:
model_config = self.vllm_config.model_config model_config = self.vllm_config.model_config
@@ -380,32 +379,32 @@ class OmniARScheduler(OmniSchedulerMixin, VLLMScheduler):
# them. Upstream vllm raises RuntimeError on this status; omni allows # them. Upstream vllm raises RuntimeError on this status; omni allows
# async abort (e.g. client disconnect during TTS streaming) to leave # async abort (e.g. client disconnect during TTS streaming) to leave
# requests in the waiting/running queues temporarily. # requests in the waiting/running queues temporarily.
waiting = getattr(self, "waiting")
self._drop_aborted_queued_requests() self._drop_aborted_queued_requests()
self._process_pending_omni_inputs(model_mode="ar") self._process_pending_omni_inputs(model_mode="ar")
self._drop_aborted_queued_requests() self._drop_aborted_queued_requests()
self._resync_streaming_input_counter() self._resync_streaming_input_counter()
original_waiting = None original_wait_queues = None
if self._should_defer_waiting_admission(): if self._should_defer_waiting_admission():
original_waiting = waiting original_wait_queues = self.waiting, self.kv_holding_waiting
self.waiting = create_request_queue(self.policy) self.waiting = create_request_queue(self.policy)
self.kv_holding_waiting = create_request_queue(self.policy)
original_max_num_running_reqs = self.max_num_running_reqs original_max_num_active_reqs = self.max_num_active_reqs
async_chunk_transport = self._async_chunk_transport_enabled() async_chunk_transport = self._async_chunk_transport_enabled()
reserved_running_slots = ( reserved_running_slots = (
self._get_async_chunk_reserved_running_slots() if async_chunk_transport and self.use_v2_model_runner else 0 self._get_async_chunk_reserved_running_slots() if async_chunk_transport and self.use_v2_model_runner else 0
) )
if reserved_running_slots: if reserved_running_slots:
self.max_num_running_reqs = max(0, original_max_num_running_reqs - reserved_running_slots) self.max_num_active_reqs = max(0, original_max_num_active_reqs - reserved_running_slots)
try: try:
scheduler_output = super().schedule(throttle_prefills) scheduler_output = super().schedule(throttle_prefills)
finally: finally:
self.max_num_running_reqs = original_max_num_running_reqs self.max_num_active_reqs = original_max_num_active_reqs
if original_waiting is not None: if original_wait_queues is not None:
deferred_waiting = list(self.waiting) original_waiting, original_kv_holding = original_wait_queues
if deferred_waiting: original_waiting.prepend_requests(self.waiting)
original_waiting.prepend_requests(deferred_waiting) original_kv_holding.prepend_requests(self.kv_holding_waiting)
self.waiting = original_waiting self.waiting, self.kv_holding_waiting = original_wait_queues
self._restore_omni_wait_queues() self._restore_omni_wait_queues()
self._postprocess_omni_schedule_output( self._postprocess_omni_schedule_output(
@@ -428,6 +427,7 @@ class OmniARScheduler(OmniSchedulerMixin, VLLMScheduler):
sampled_token_ids = model_runner_output.sampled_token_ids sampled_token_ids = model_runner_output.sampled_token_ids
logprobs = model_runner_output.logprobs logprobs = model_runner_output.logprobs
prompt_logprobs_dict = model_runner_output.prompt_logprobs_dict prompt_logprobs_dict = model_runner_output.prompt_logprobs_dict
prompt_token_id_logprobs_dict = getattr(model_runner_output, "prompt_token_id_logprobs_dict", {})
num_scheduled_tokens = scheduler_output.num_scheduled_tokens num_scheduled_tokens = scheduler_output.num_scheduled_tokens
pooler_outputs = model_runner_output.pooler_output pooler_outputs = model_runner_output.pooler_output
mm_outputs = getattr(model_runner_output, "multimodal_outputs", None) mm_outputs = getattr(model_runner_output, "multimodal_outputs", None)
@@ -704,8 +704,11 @@ class OmniARScheduler(OmniSchedulerMixin, VLLMScheduler):
if stopped: if stopped:
if self.chunk_transfer_adapter is not None: if self.chunk_transfer_adapter is not None:
confirmed_num_computed_tokens = self.chunk_transfer_adapter._confirmed_num_computed_tokens(request) confirmed_num_computed_tokens = self.chunk_transfer_adapter._confirmed_num_computed_tokens(request)
if model_runner_output.routed_experts is not None: if (
routed_experts = omni_routed_experts_for_request(model_runner_output.routed_experts, request) self.aux_output_connector is not None
and (aux_output := model_runner_output.aux_output_connector_output) is not None
):
routed_experts = self.aux_output_connector.take_output(request, aux_output)
# Capture finish_reason BEFORE _handle_stopped_request, which may # Capture finish_reason BEFORE _handle_stopped_request, which may
# reset the status to WAITING for streaming requests that continue. # reset the status to WAITING for streaming requests that continue.
@@ -778,6 +781,7 @@ class OmniARScheduler(OmniSchedulerMixin, VLLMScheduler):
# Get prompt logprobs for this request. # Get prompt logprobs for this request.
prompt_logprobs_tensors = prompt_logprobs_dict.get(req_id) prompt_logprobs_tensors = prompt_logprobs_dict.get(req_id)
prompt_token_id_logprobs = prompt_token_id_logprobs_dict.get(req_id)
has_stage_output = ( has_stage_output = (
bool(new_token_ids) bool(new_token_ids)
or mm_output is not None or mm_output is not None
@@ -798,6 +802,7 @@ class OmniARScheduler(OmniSchedulerMixin, VLLMScheduler):
finish_reason=finish_reason, finish_reason=finish_reason,
new_logprobs=new_logprobs, new_logprobs=new_logprobs,
new_prompt_logprobs_tensors=prompt_logprobs_tensors, new_prompt_logprobs_tensors=prompt_logprobs_tensors,
prompt_token_id_logprobs=prompt_token_id_logprobs,
pooling_output=pooling_output_payload, pooling_output=pooling_output_payload,
multimodal_output=mm_output, multimodal_output=mm_output,
stop_reason=request.stop_reason, stop_reason=request.stop_reason,
@@ -813,6 +818,7 @@ class OmniARScheduler(OmniSchedulerMixin, VLLMScheduler):
else: else:
# Invariant: EngineCore returns no partial prefill outputs. # Invariant: EngineCore returns no partial prefill outputs.
assert not prompt_logprobs_tensors assert not prompt_logprobs_tensors
assert prompt_token_id_logprobs is None
if omits_chunk_transfer and inter_stage_output is not None: if omits_chunk_transfer and inter_stage_output is not None:
logger.warning( logger.warning(
@@ -976,8 +982,9 @@ class OmniARScheduler(OmniSchedulerMixin, VLLMScheduler):
if session.status == RequestStatus.WAITING_FOR_STREAMING_REQ: if session.status == RequestStatus.WAITING_FOR_STREAMING_REQ:
self.num_waiting_for_streaming_input -= 1 self.num_waiting_for_streaming_input -= 1
session.status = RequestStatus.WAITING session.status = RequestStatus.WAITING
if session in self.skipped_waiting: self.deferred_waiting.discard(session)
self.skipped_waiting.remove_requests((session,)) if session in self.kv_holding_waiting:
self.kv_holding_waiting.remove_requests((session,))
self._enqueue_waiting_request(session) self._enqueue_waiting_request(session)
if self.log_stats: if self.log_stats:
@@ -1401,7 +1408,8 @@ class OmniARScheduler(OmniSchedulerMixin, VLLMScheduler):
``_free_blocks``) and skips the caller's input-coordinator cleanup. ``_free_blocks``) and skips the caller's input-coordinator cleanup.
""" """
self.waiting.remove_requests((request,)) self.waiting.remove_requests((request,))
self.skipped_waiting.remove_requests((request,)) self.kv_holding_waiting.remove_requests((request,))
self.deferred_waiting.discard(request)
if request.status == RequestStatus.WAITING_FOR_STREAMING_REQ: if request.status == RequestStatus.WAITING_FOR_STREAMING_REQ:
self.num_waiting_for_streaming_input -= 1 self.num_waiting_for_streaming_input -= 1
request.status = RequestStatus.FINISHED_ERROR request.status = RequestStatus.FINISHED_ERROR
@@ -32,7 +32,7 @@ class OmniCFGARScheduler(OmniARScheduler):
raise ValueError("Atomic CFG reservations require one full-attention KV group") raise ValueError("Atomic CFG reservations require one full-attention KV group")
self._capacity = self.kv_cache_manager.block_pool.num_gpu_blocks - 1 - self.kv_cache_manager.watermark_blocks self._capacity = self.kv_cache_manager.block_pool.num_gpu_blocks - 1 - self.kv_cache_manager.watermark_blocks
pair_pages = 2 * -(-self.max_model_len // self.block_size) pair_pages = 2 * -(-self.max_model_len // self.block_size)
if self._capacity < pair_pages or self.max_num_running_reqs < 2: if self._capacity < pair_pages or min(self.max_num_running_reqs, self.max_num_active_reqs) < 2:
raise ValueError("Atomic CFG requires KV capacity and max_num_seqs for two maximum-length branches") raise ValueError("Atomic CFG requires KV capacity and max_num_seqs for two maximum-length branches")
if self.max_num_scheduled_tokens < 2 * self.max_model_len: if self.max_num_scheduled_tokens < 2 * self.max_model_len:
raise ValueError("Atomic CFG requires max_num_batched_tokens >= 2 * max_model_len") raise ValueError("Atomic CFG requires max_num_batched_tokens >= 2 * max_model_len")
@@ -81,7 +81,7 @@ class OmniCFGARScheduler(OmniARScheduler):
admitted = create_request_queue(self.policy) admitted = create_request_queue(self.policy)
pending = {request.request_id: request for request in original_waiting} pending = {request.request_id: request for request in original_waiting}
pages = self._capacity - sum(self._reserved_pages(request) for request in self.running) pages = self._capacity - sum(self._reserved_pages(request) for request in self.running)
slots = self.max_num_running_reqs - len(self.running) slots = self.max_num_active_reqs - len(self.running)
tokens = self.max_num_scheduled_tokens - len(self.running) tokens = self.max_num_scheduled_tokens - len(self.running)
chosen: set[str] = set() chosen: set[str] = set()
for request in original_waiting: for request in original_waiting:
@@ -26,7 +26,6 @@ from vllm.v1.spec_decode.metrics import SpecDecodingStats
from vllm_omni.core.sched.omni_scheduler_mixin import OmniSchedulerMixin from vllm_omni.core.sched.omni_scheduler_mixin import OmniSchedulerMixin
from vllm_omni.core.sched.output import OmniCachedRequestData, OmniNewRequestData from vllm_omni.core.sched.output import OmniCachedRequestData, OmniNewRequestData
from vllm_omni.core.sched.utils import omni_routed_experts_for_request
from vllm_omni.engine.serialization import deserialize_additional_information from vllm_omni.engine.serialization import deserialize_additional_information
from vllm_omni.outputs import OmniModelRunnerOutput from vllm_omni.outputs import OmniModelRunnerOutput
@@ -275,6 +274,10 @@ class OmniGenerationScheduler(OmniSchedulerMixin, VLLMScheduler):
if self.connector is not None: if self.connector is not None:
meta = self._build_kv_connector_meta(self.connector, scheduler_output) meta = self._build_kv_connector_meta(self.connector, scheduler_output)
scheduler_output.kv_connector_metadata = meta scheduler_output.kv_connector_metadata = meta
if self.aux_output_connector is not None:
scheduler_output.aux_output_connector_metadata = self.aux_output_connector.build_connector_meta(
scheduler_output, self.requests
)
# EC Connector: package metadata # EC Connector: package metadata
if self.ec_connector is not None: if self.ec_connector is not None:
ec_meta = self.ec_connector.build_connector_meta(scheduler_output) ec_meta = self.ec_connector.build_connector_meta(scheduler_output)
@@ -330,7 +333,7 @@ class OmniGenerationScheduler(OmniSchedulerMixin, VLLMScheduler):
): ):
# Downstream async-chunk stages receive the next segment from the # Downstream async-chunk stages receive the next segment from the
# connector, not from an API StreamingUpdate. Enqueue them as # connector, not from an API StreamingUpdate. Enqueue them as
# schedulable before the base class can park them in skipped_waiting. # schedulable before the base class can defer a streaming-input wait.
request.status = RequestStatus.WAITING request.status = RequestStatus.WAITING
self._enqueue_waiting_request(request) self._enqueue_waiting_request(request)
return False return False
@@ -342,7 +345,7 @@ class OmniGenerationScheduler(OmniSchedulerMixin, VLLMScheduler):
A generation batch slot belongs to one chunk, not the entire stream. A generation batch slot belongs to one chunk, not the entire stream.
Keep requests with outstanding output in ``running`` so their state Keep requests with outstanding output in ``running`` so their state
cannot be reused before completion. Other live requests join the tail cannot be reused before completion. Other live requests join the tail
of ``waiting`` without freeing their decoder state or KV blocks. of the appropriate waiting queue without freeing decoder state or KV blocks.
Preserve WAITING_FOR_CHUNK until the coordinator observes fresh input. Preserve WAITING_FOR_CHUNK until the coordinator observes fresh input.
Stateful codecs retain lifetime admission until their decoder state is Stateful codecs retain lifetime admission until their decoder state is
released, even when no chunk is currently executing. released, even when no chunk is currently executing.
@@ -360,7 +363,7 @@ class OmniGenerationScheduler(OmniSchedulerMixin, VLLMScheduler):
continue continue
if request.status == RequestStatus.RUNNING: if request.status == RequestStatus.RUNNING:
request.status = RequestStatus.WAITING request.status = RequestStatus.WAITING
self.waiting.add_request(request) self._enqueue_waiting_request(request)
self.running = in_flight self.running = in_flight
def schedule(self, throttle_prefills: bool = False) -> SchedulerOutput: def schedule(self, throttle_prefills: bool = False) -> SchedulerOutput:
@@ -391,6 +394,7 @@ class OmniGenerationScheduler(OmniSchedulerMixin, VLLMScheduler):
# Temporary queue: preserve waiting order while requests await input. # Temporary queue: preserve waiting order while requests await input.
skipped_waiting_requests = create_request_queue(self.policy) skipped_waiting_requests = create_request_queue(self.policy)
skipped_kv_holding_requests = create_request_queue(self.policy)
req_index = 0 req_index = 0
self._drop_aborted_queued_requests() self._drop_aborted_queued_requests()
self._requeue_completed_native_chunks() self._requeue_completed_native_chunks()
@@ -502,7 +506,7 @@ class OmniGenerationScheduler(OmniSchedulerMixin, VLLMScheduler):
# Fast path selection and scheduling (treat all as diffusion requests, # Fast path selection and scheduling (treat all as diffusion requests,
# independent of pooling_params) # independent of pooling_params)
while ( while (
self.waiting (self.kv_holding_waiting or self.waiting)
and token_budget > 0 and token_budget > 0
and len(num_scheduled_tokens) < execution_batch_size and len(num_scheduled_tokens) < execution_batch_size
and ( and (
@@ -510,38 +514,47 @@ class OmniGenerationScheduler(OmniSchedulerMixin, VLLMScheduler):
if native_chunks and not self._retains_state_across_chunks if native_chunks and not self._retains_state_across_chunks
else len(self.running) + reserved_running_slots else len(self.running) + reserved_running_slots
) )
< self.max_num_running_reqs < self.max_num_active_reqs
and self._pause_state == PauseState.UNPAUSED and self._pause_state == PauseState.UNPAUSED
): ):
request = self.waiting.peek_request() request_queue = self.kv_holding_waiting or self.waiting
skipped_requests = (
skipped_kv_holding_requests if request_queue is self.kv_holding_waiting else skipped_waiting_requests
)
request = request_queue.peek_request()
if express and request.request_id in self._chunk_started: if express and request.request_id in self._chunk_started:
self.waiting.pop_request() request_queue.pop_request()
skipped_waiting_requests.add_request(request) self.deferred_waiting.add(request)
skipped_requests.add_request(request)
continue continue
if native_chunks and request.num_in_flight_tokens > 0: if native_chunks and request.num_in_flight_tokens > 0:
# A restored waiting entry can still own an outstanding batch. # A restored waiting entry can still own an outstanding batch.
# Do not let it block other ready streams or execute it twice. # Do not let it block other ready streams or execute it twice.
self.waiting.pop_request() request_queue.pop_request()
skipped_waiting_requests.add_request(request) self.deferred_waiting.add(request)
skipped_requests.add_request(request)
continue continue
# OMNI: Skip requests that are not in self.requests # OMNI: Skip requests that are not in self.requests
if request.request_id not in self.requests or ( if request.request_id not in self.requests or (
self.chunk_transfer_adapter is None and request.status == RequestStatus.FINISHED_STOPPED self.chunk_transfer_adapter is None and request.status == RequestStatus.FINISHED_STOPPED
): ):
# Pop the finished request from waiting queue and don't schedule it # Pop the finished request from waiting queue and don't schedule it
self.waiting.pop_request() request_queue.pop_request()
self.deferred_waiting.discard(request)
continue continue
# async_chunk: wait for the first upstream chunk (don't start with placeholders). # async_chunk: wait for the first upstream chunk (don't start with placeholders).
if async_chunk_transport and len(request.prompt_token_ids) == 0: if async_chunk_transport and len(request.prompt_token_ids) == 0:
if self._is_done_receiving_chunks(request.request_id): if self._is_done_receiving_chunks(request.request_id):
if not _has_async_chunk_payload_to_run(request): if not _has_async_chunk_payload_to_run(request):
self.waiting.pop_request() request_queue.pop_request()
self.deferred_waiting.discard(request)
self._pending_finish_reqs.append(request) self._pending_finish_reqs.append(request)
continue continue
else: else:
self.waiting.pop_request() request_queue.pop_request()
skipped_waiting_requests.prepend_request(request) self.deferred_waiting.add(request)
skipped_requests.prepend_request(request)
continue continue
# Allocate all input tokens for the request in one shot # Allocate all input tokens for the request in one shot
@@ -557,11 +570,13 @@ class OmniGenerationScheduler(OmniSchedulerMixin, VLLMScheduler):
): ):
required_tokens = 1 required_tokens = 1
else: else:
self.waiting.pop_request() request_queue.pop_request()
if self._is_done_receiving_chunks(request.request_id): if self._is_done_receiving_chunks(request.request_id):
self.deferred_waiting.discard(request)
self._pending_finish_reqs.append(request) self._pending_finish_reqs.append(request)
else: else:
skipped_waiting_requests.add_request(request) self.deferred_waiting.add(request)
skipped_requests.add_request(request)
continue continue
num_new_tokens = min(required_tokens, token_budget) num_new_tokens = min(required_tokens, token_budget)
new_blocks = self.kv_cache_manager.allocate_slots( new_blocks = self.kv_cache_manager.allocate_slots(
@@ -577,7 +592,8 @@ class OmniGenerationScheduler(OmniSchedulerMixin, VLLMScheduler):
break break
# Officially schedule this request # Officially schedule this request
request = self.waiting.pop_request() request = request_queue.pop_request()
self.deferred_waiting.discard(request)
self.running.append(request) self.running.append(request)
if self.log_stats: if self.log_stats:
request.record_event(EngineCoreEventType.SCHEDULED, scheduled_timestamp) request.record_event(EngineCoreEventType.SCHEDULED, scheduled_timestamp)
@@ -592,7 +608,9 @@ class OmniGenerationScheduler(OmniSchedulerMixin, VLLMScheduler):
if first_chunk_express: if first_chunk_express:
self._chunk_started.update(num_scheduled_tokens) self._chunk_started.update(num_scheduled_tokens)
# Return skipped waiting requests # Return skipped requests to their original queue.
if skipped_kv_holding_requests:
self.kv_holding_waiting.prepend_requests(skipped_kv_holding_requests)
if skipped_waiting_requests: if skipped_waiting_requests:
self.waiting.prepend_requests(skipped_waiting_requests) self.waiting.prepend_requests(skipped_waiting_requests)
@@ -936,8 +954,11 @@ class OmniGenerationScheduler(OmniSchedulerMixin, VLLMScheduler):
prefill_stats.finalize(self.kv_cache_manager.estimate_cached_tokens(request)) prefill_stats.finalize(self.kv_cache_manager.estimate_cached_tokens(request))
if stopped: if stopped:
if model_runner_output.routed_experts is not None: if (
routed_experts = omni_routed_experts_for_request(model_runner_output.routed_experts, request) self.aux_output_connector is not None
and (aux_output := model_runner_output.aux_output_connector_output) is not None
):
routed_experts = self.aux_output_connector.take_output(request, aux_output)
finish_reason = request.get_finished_reason() finish_reason = request.get_finished_reason()
finished = self._handle_stopped_request(request) finished = self._handle_stopped_request(request)
is_segment_finished = not finished is_segment_finished = not finished
+75 -17
View File
@@ -111,6 +111,56 @@ else:
_SchedulerMixinBase = object _SchedulerMixinBase = object
class _OmniWaitingQueue:
"""Let one input-gating pass visit both upstream waiting queues.
Requests holding KV blocks retain upstream admission priority when input
gates park, restore or requeue them. Connector state is consumed once per
scheduler tick, including when both physical queues contain requests.
"""
def __init__(self, scheduler: Any) -> None:
self.scheduler = scheduler
def __iter__(self) -> Iterator[Request]:
for request_queue in (self.scheduler.kv_holding_waiting, self.scheduler.waiting):
for request in request_queue:
# Upstream owns grammar, remote-KV and streaming-update waits.
# Omni input readiness must not overwrite those statuses.
if not self.scheduler._is_blocked_waiting_status(request.status):
yield request
def add_request(self, request: Request) -> None:
self.scheduler._enqueue_waiting_request(request)
def prepend_requests(self, requests: Iterable[Request]) -> None:
holding: list[Request] = []
fresh: list[Request] = []
for request in requests:
if self.scheduler._is_blocked_waiting_status(request.status):
self.scheduler.deferred_waiting.add(request)
(holding if self.scheduler._holds_kv_blocks(request) else fresh).append(request)
self.scheduler.kv_holding_waiting.prepend_requests(holding)
self.scheduler.waiting.prepend_requests(fresh)
def remove_request(self, request: Request) -> None:
# Input metadata can change the computed frontier before removal, so
# choose the queue by membership rather than the request's new state.
if request in self.scheduler.kv_holding_waiting:
self.scheduler.kv_holding_waiting.remove_request(request)
else:
self.scheduler.waiting.remove_request(request)
self.scheduler.deferred_waiting.discard(request)
remove = remove_request
def remove_requests(self, requests: Iterable[Request]) -> None:
requests = set(requests)
self.scheduler.kv_holding_waiting.remove_requests(requests)
self.scheduler.waiting.remove_requests(requests)
self.scheduler.deferred_waiting.difference_update(requests)
class OmniSchedulerMixin(_SchedulerMixinBase): class OmniSchedulerMixin(_SchedulerMixinBase):
"""Shared scheduler helpers for omni-specific request handling.""" """Shared scheduler helpers for omni-specific request handling."""
@@ -118,6 +168,8 @@ class OmniSchedulerMixin(_SchedulerMixinBase):
vllm_config: VllmConfig vllm_config: VllmConfig
requests: dict[str, Request] requests: dict[str, Request]
waiting: RequestQueue waiting: RequestQueue
kv_holding_waiting: RequestQueue
deferred_waiting: set[Request]
running: list[Request] running: list[Request]
_pending_input_timeout_outputs: dict[str, tuple[int, OmniEngineCoreOutput]] _pending_input_timeout_outputs: dict[str, tuple[int, OmniEngineCoreOutput]]
@@ -243,8 +295,9 @@ class OmniSchedulerMixin(_SchedulerMixinBase):
if session.status == RequestStatus.WAITING_FOR_STREAMING_REQ: if session.status == RequestStatus.WAITING_FOR_STREAMING_REQ:
self.num_waiting_for_streaming_input -= 1 self.num_waiting_for_streaming_input -= 1
session.status = RequestStatus.WAITING session.status = RequestStatus.WAITING
if session in self.skipped_waiting: self.deferred_waiting.discard(session)
self.skipped_waiting.remove_requests((session,)) if session in self.kv_holding_waiting:
self.kv_holding_waiting.remove_requests((session,))
self._enqueue_waiting_request(session) self._enqueue_waiting_request(session)
if self.log_stats: if self.log_stats:
session.record_event(EngineCoreEventType.QUEUED) session.record_event(EngineCoreEventType.QUEUED)
@@ -377,14 +430,14 @@ class OmniSchedulerMixin(_SchedulerMixinBase):
) )
if input_coordinator._async_chunk: if input_coordinator._async_chunk:
input_coordinator.process_pending_chunks( input_coordinator.process_pending_chunks(
self.waiting, _OmniWaitingQueue(self),
self.running, self.running,
chunk_ready_req_ids, chunk_ready_req_ids,
chunk_finished_req_ids, chunk_finished_req_ids,
) )
else: else:
input_coordinator.process_pending_full_payload_inputs( input_coordinator.process_pending_full_payload_inputs(
self.waiting, _OmniWaitingQueue(self),
stage_recv_req_ids, stage_recv_req_ids,
) )
@@ -394,7 +447,7 @@ class OmniSchedulerMixin(_SchedulerMixinBase):
self._process_pending_input_timeouts() self._process_pending_input_timeouts()
if self.chunk_transfer_adapter: if self.chunk_transfer_adapter:
self.chunk_transfer_adapter.process_pending_chunks( self.chunk_transfer_adapter.process_pending_chunks(
self.waiting, _OmniWaitingQueue(self),
self.running, self.running,
scheduler_requests=self.requests, scheduler_requests=self.requests,
) )
@@ -426,12 +479,12 @@ class OmniSchedulerMixin(_SchedulerMixinBase):
"""Restore requests temporarily parked by Omni input gates.""" """Restore requests temporarily parked by Omni input gates."""
if self.chunk_transfer_adapter: if self.chunk_transfer_adapter:
self.chunk_transfer_adapter.restore_queues( self.chunk_transfer_adapter.restore_queues(
self.waiting, _OmniWaitingQueue(self),
self.running, self.running,
scheduler_requests=self.requests, scheduler_requests=self.requests,
) )
if self.input_coordinator: if self.input_coordinator:
self.input_coordinator.restore_queues(self.waiting, self.running) self.input_coordinator.restore_queues(_OmniWaitingQueue(self), self.running)
def _process_pending_input_timeouts(self) -> None: def _process_pending_input_timeouts(self) -> None:
"""Force-fail requests waiting on the full-payload coordinator too long. """Force-fail requests waiting on the full-payload coordinator too long.
@@ -669,6 +722,7 @@ class OmniSchedulerMixin(_SchedulerMixinBase):
finish_reason: Any = None, finish_reason: Any = None,
new_logprobs: Any = None, new_logprobs: Any = None,
new_prompt_logprobs_tensors: Any = None, new_prompt_logprobs_tensors: Any = None,
prompt_token_id_logprobs: Any = None,
pooling_output: Any = None, pooling_output: Any = None,
multimodal_output: Any = None, multimodal_output: Any = None,
stop_reason: Any = None, stop_reason: Any = None,
@@ -693,6 +747,7 @@ class OmniSchedulerMixin(_SchedulerMixinBase):
finish_reason=finish_reason, finish_reason=finish_reason,
new_logprobs=new_logprobs, new_logprobs=new_logprobs,
new_prompt_logprobs_tensors=new_prompt_logprobs_tensors, new_prompt_logprobs_tensors=new_prompt_logprobs_tensors,
prompt_token_id_logprobs=prompt_token_id_logprobs,
pooling_output=pooling_output, pooling_output=pooling_output,
pooling_output_payload=pooling_output_payload, pooling_output_payload=pooling_output_payload,
multimodal_output=multimodal_output, multimodal_output=multimodal_output,
@@ -834,7 +889,8 @@ class OmniSchedulerMixin(_SchedulerMixinBase):
) )
} }
self.waiting.remove_requests(removable) self.waiting.remove_requests(removable)
self.skipped_waiting.remove_requests(removable) self.kv_holding_waiting.remove_requests(removable)
self.deferred_waiting.difference_update(removable)
def _resume_downstream_chunk_receiver(self, request: Request) -> None: def _resume_downstream_chunk_receiver(self, request: Request) -> None:
"""Resume duplex connector polling without an external update.""" """Resume duplex connector polling without an external update."""
@@ -850,7 +906,8 @@ class OmniSchedulerMixin(_SchedulerMixinBase):
return return
self.num_waiting_for_streaming_input -= 1 self.num_waiting_for_streaming_input -= 1
request.status = RequestStatus.WAITING request.status = RequestStatus.WAITING
self.skipped_waiting.remove_requests((request,)) self.deferred_waiting.discard(request)
self.kv_holding_waiting.remove_requests((request,))
self._enqueue_waiting_request(request) self._enqueue_waiting_request(request)
def _aggregate_kv_connector_stats( def _aggregate_kv_connector_stats(
@@ -973,10 +1030,10 @@ class OmniSchedulerMixin(_SchedulerMixinBase):
request_ids = tuple(request_ids) request_ids = tuple(request_ids)
target_request_ids = set(request_ids) target_request_ids = set(request_ids)
skipped_waiting_ids = {r.request_id for r in getattr(self, "skipped_waiting", ())} deferred_waiting_ids = {r.request_id for r in self.deferred_waiting}
pre_adapter_streaming_wait_ids = { pre_adapter_streaming_wait_ids = {
rid rid
for rid in target_request_ids & skipped_waiting_ids for rid in target_request_ids & deferred_waiting_ids
if (req := self.requests.get(rid)) is not None if (req := self.requests.get(rid)) is not None
and ( and (
req.status == RequestStatus.WAITING_FOR_STREAMING_REQ req.status == RequestStatus.WAITING_FOR_STREAMING_REQ
@@ -1038,7 +1095,7 @@ class OmniSchedulerMixin(_SchedulerMixinBase):
status is not ``RUNNING``, set it to ``RUNNING``; symmetrically status is not ``RUNNING``, set it to ``RUNNING``; symmetrically
flip ``RUNNING → WAITING`` when the request is actually in flip ``RUNNING → WAITING`` when the request is actually in
``self.waiting``. A resumable segment stop still held in ``self.waiting``. A resumable segment stop still held in
``self.skipped_waiting`` is restored to ``self.kv_holding_waiting`` is restored to
``WAITING_FOR_STREAMING_REQ`` so upstream also balances its ``WAITING_FOR_STREAMING_REQ`` so upstream also balances its
paused-session counter. Because adapter cleanup may restore a paused-session counter. Because adapter cleanup may restore a
connector-owned request's prior status first, ``finish_requests`` connector-owned request's prior status first, ``finish_requests``
@@ -1075,8 +1132,8 @@ class OmniSchedulerMixin(_SchedulerMixinBase):
return return
running_ids = {r.request_id for r in self.running} running_ids = {r.request_id for r in self.running}
waiting_ids = {r.request_id for r in self.waiting} waiting_ids = {r.request_id for queue in (self.waiting, self.kv_holding_waiting) for r in queue}
skipped_waiting_ids = {r.request_id for r in getattr(self, "skipped_waiting", ())} deferred_waiting_ids = {r.request_id for r in self.deferred_waiting}
for rid in ids_to_align: for rid in ids_to_align:
req = self.requests.get(rid) req = self.requests.get(rid)
@@ -1098,7 +1155,7 @@ class OmniSchedulerMixin(_SchedulerMixinBase):
) )
if req.is_finished() and not (resumable_segment_stop or streaming_wait): if req.is_finished() and not (resumable_segment_stop or streaming_wait):
continue continue
if rid in skipped_waiting_ids and streaming_wait: if rid in deferred_waiting_ids and streaming_wait:
req.status = RequestStatus.WAITING_FOR_STREAMING_REQ req.status = RequestStatus.WAITING_FOR_STREAMING_REQ
elif rid in running_ids and req.status != RequestStatus.RUNNING: elif rid in running_ids and req.status != RequestStatus.RUNNING:
req.status = RequestStatus.RUNNING req.status = RequestStatus.RUNNING
@@ -1161,10 +1218,11 @@ class OmniSchedulerMixin(_SchedulerMixinBase):
self.running[:] = [req for req in self.running if keep_running(req)] self.running[:] = [req for req in self.running if keep_running(req)]
def _drop_aborted_queued_requests(self) -> None: def _drop_aborted_queued_requests(self) -> None:
for request_queue in (self.waiting, self.skipped_waiting): for request_queue in (self.waiting, self.kv_holding_waiting):
aborted = [req for req in request_queue if req.status == RequestStatus.FINISHED_ABORTED] aborted = [req for req in request_queue if req.status == RequestStatus.FINISHED_ABORTED]
if aborted: if aborted:
request_queue.remove_requests(aborted) request_queue.remove_requests(aborted)
self.deferred_waiting.difference_update(aborted)
self.running[:] = [req for req in self.running if req.status != RequestStatus.FINISHED_ABORTED] self.running[:] = [req for req in self.running if req.status != RequestStatus.FINISHED_ABORTED]
def _resync_streaming_input_counter(self) -> None: def _resync_streaming_input_counter(self) -> None:
@@ -1173,7 +1231,7 @@ class OmniSchedulerMixin(_SchedulerMixinBase):
return return
parked = sum( parked = sum(
1 1
for queue in (getattr(self, "waiting", None), getattr(self, "skipped_waiting", None)) for queue in (getattr(self, "waiting", None), getattr(self, "kv_holding_waiting", None))
if queue if queue
for request in queue for request in queue
if request.status == RequestStatus.WAITING_FOR_STREAMING_REQ if request.status == RequestStatus.WAITING_FOR_STREAMING_REQ
-19
View File
@@ -2,29 +2,10 @@
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Shared utilities for omni schedulers.""" """Shared utilities for omni schedulers."""
import numpy as np
from vllm.v1.core.kv_cache_manager import KVCacheManager from vllm.v1.core.kv_cache_manager import KVCacheManager
from vllm.v1.outputs import RoutedExpertsLists
from vllm.v1.request import Request from vllm.v1.request import Request
def omni_routed_experts_for_request(routed_experts: RoutedExpertsLists, request) -> np.ndarray | None:
"""Extract per-request routed experts from RoutedExpertsLists using slot_mapping.
Matches upstream RoutedExpertsManager.get() pattern — filters routing_data
rows whose slot_mapping entries belong to this request's block_table.
"""
if routed_experts is None:
return None
slots = getattr(request, "block_table", None)
if slots is None:
return None
slot_set = set(slots)
mask = np.isin(routed_experts.slot_mapping, list(slot_set))
data = routed_experts.routing_data[mask]
return data if data.size > 0 else None
def free_kv_blocks_in_physical_order(manager: KVCacheManager, request: Request) -> None: def free_kv_blocks_in_physical_order(manager: KVCacheManager, request: Request) -> None:
"""Return non-cached request blocks in increasing physical order. """Return non-cached request blocks in increasing physical order.
@@ -64,6 +64,7 @@ from vllm_omni.diffusion.worker.request_batch import DiffusionRequestBatch, spli
from vllm_omni.model_executor.model_loader.weight_utils import download_weights_from_hf_specific from vllm_omni.model_executor.model_loader.weight_utils import download_weights_from_hf_specific
from vllm_omni.platforms import current_omni_platform from vllm_omni.platforms import current_omni_platform
from vllm_omni.quantization.component_config import resolve_component_quant_config from vllm_omni.quantization.component_config import resolve_component_quant_config
from vllm_omni.quantization.fp8_config import DiffusionFp8Config
logger = init_logger(__name__) logger = init_logger(__name__)
@@ -322,7 +323,7 @@ class BooguImagePipeline(CFGParallelMixin, nn.Module, ProgressBarMixin, Supports
"""Load MLLM through HF, or prepare it for deferred online FP8 loading.""" """Load MLLM through HF, or prepare it for deferred online FP8 loading."""
config = None config = None
if quant_config is not None: if quant_config is not None:
if not isinstance(quant_config, Fp8Config): if not isinstance(quant_config, (Fp8Config, DiffusionFp8Config)):
raise ValueError( raise ValueError(
"Boogu MLLM only supports FP8 quantization. Set mllm to null to disable online quantization." "Boogu MLLM only supports FP8 quantization. Set mllm to null to disable online quantization."
) )
+2
View File
@@ -123,6 +123,8 @@ class OmniEngineCoreRequest(EngineCoreRequest):
trace_headers=request.trace_headers, trace_headers=request.trace_headers,
resumable=request.resumable, resumable=request.resumable,
external_req_id=request.external_req_id, external_req_id=request.external_req_id,
session_id=request.session_id,
kv_hints=request.kv_hints,
reasoning_ended=request.reasoning_ended, reasoning_ended=request.reasoning_ended,
reasoning_parser_kwargs=request.reasoning_parser_kwargs, reasoning_parser_kwargs=request.reasoning_parser_kwargs,
abort_immediately=request.abort_immediately, abort_immediately=request.abort_immediately,
+4
View File
@@ -270,6 +270,10 @@ class OmniEngineArgs(EngineArgs):
) )
validate_worker_omni_connector(self.worker_cls, needs_connector) validate_worker_omni_connector(self.worker_cls, needs_connector)
super().__post_init__() super().__post_init__()
# The NPU runner implements the auxiliary connector on its legacy
# execution path; CUDA/ROCm Omni stages use the V2 implementation.
if self.aux_output_config.enabled and not self.use_v2_model_runner and not current_omni_platform.is_npu():
raise ValueError("Auxiliary outputs require use_v2_model_runner=True for this Omni stage.")
def _ensure_omni_models_registered(self): def _ensure_omni_models_registered(self):
if hasattr(self, "_omni_models_registered"): if hasattr(self, "_omni_models_registered"):
+9
View File
@@ -14,6 +14,7 @@ from typing import Any, Literal, cast
from vllm.inputs import PromptType from vllm.inputs import PromptType
from vllm.logger import init_logger from vllm.logger import init_logger
from vllm.v1.engine import EngineCoreRequest from vllm.v1.engine import EngineCoreRequest
from vllm.v1.kv_hints import KvHintsEnvelope
from vllm_omni.data_entry_keys import REQUEST_ARTIFACT_DIRS_KEY, TRANSFORM_OWNED_META_KEYS from vllm_omni.data_entry_keys import REQUEST_ARTIFACT_DIRS_KEY, TRANSFORM_OWNED_META_KEYS
from vllm_omni.engine import OmniEngineCoreRequest from vllm_omni.engine import OmniEngineCoreRequest
@@ -259,6 +260,7 @@ class AsyncOmniEngine(OmniEngineBase):
data_parallel_rank: int | None = None, data_parallel_rank: int | None = None,
reasoning_ended: bool | None = None, reasoning_ended: bool | None = None,
*, *,
kv_hints: KvHintsEnvelope | None = None,
resumable: bool = False, resumable: bool = False,
message_type: Literal["add_request", "streaming_update"] = "add_request", message_type: Literal["add_request", "streaming_update"] = "add_request",
) -> StageSubmissionMessage: ) -> StageSubmissionMessage:
@@ -289,6 +291,8 @@ class AsyncOmniEngine(OmniEngineBase):
request_artifact_dirs: list[str] = [] request_artifact_dirs: list[str] = []
stage_type = self.stage_metadata[0].stage_type stage_type = self.stage_metadata[0].stage_type
if stage_type == "diffusion" and kv_hints is not None:
raise ValueError("kv_hints require an AR/LLM stage with a KV cache")
output_prompt_text: Any = None output_prompt_text: Any = None
_preprocess_ms = 0.0 _preprocess_ms = 0.0
if stage_type != "diffusion" and not isinstance(prompt, EngineCoreRequest): if stage_type != "diffusion" and not isinstance(prompt, EngineCoreRequest):
@@ -343,6 +347,7 @@ class AsyncOmniEngine(OmniEngineBase):
priority=priority, priority=priority,
data_parallel_rank=data_parallel_rank, data_parallel_rank=data_parallel_rank,
resumable=resumable, resumable=resumable,
**({"kv_hints": kv_hints} if kv_hints is not None else {}),
) )
except Exception: except Exception:
if preselected_stage0_replica is not None and self.stage_pools: if preselected_stage0_replica is not None and self.stage_pools:
@@ -518,6 +523,7 @@ class AsyncOmniEngine(OmniEngineBase):
data_parallel_rank: int | None = None, data_parallel_rank: int | None = None,
reasoning_ended: bool | None = None, reasoning_ended: bool | None = None,
*, *,
kv_hints: KvHintsEnvelope | None = None,
resumable: bool = False, resumable: bool = False,
) -> None: ) -> None:
"""Process stage-0 input locally, then send to the Orchestrator. """Process stage-0 input locally, then send to the Orchestrator.
@@ -543,6 +549,7 @@ class AsyncOmniEngine(OmniEngineBase):
data_parallel_rank=data_parallel_rank, data_parallel_rank=data_parallel_rank,
reasoning_ended=reasoning_ended, reasoning_ended=reasoning_ended,
resumable=resumable, resumable=resumable,
**({"kv_hints": kv_hints} if kv_hints is not None else {}),
) )
except BaseException: except BaseException:
if isinstance(prompt, dict): if isinstance(prompt, dict):
@@ -599,6 +606,7 @@ class AsyncOmniEngine(OmniEngineBase):
data_parallel_rank: int | None = None, data_parallel_rank: int | None = None,
reasoning_ended: bool | None = None, reasoning_ended: bool | None = None,
*, *,
kv_hints: KvHintsEnvelope | None = None,
resumable: bool = False, resumable: bool = False,
) -> None: ) -> None:
"""Async add_request API.""" """Async add_request API."""
@@ -617,6 +625,7 @@ class AsyncOmniEngine(OmniEngineBase):
data_parallel_rank=data_parallel_rank, data_parallel_rank=data_parallel_rank,
reasoning_ended=reasoning_ended, reasoning_ended=reasoning_ended,
resumable=resumable, resumable=resumable,
**({"kv_hints": kv_hints} if kv_hints is not None else {}),
) )
def add_streaming_update( def add_streaming_update(
Loaded 100 of 141 files, more files were not shown because too many files have changed in this diff. Show more