Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ae9bdd8c2 | ||
|
|
80a0d8d20a | ||
|
|
121d0b9aee | ||
|
|
c4b37471c4 | ||
|
|
f270ff7c3d | ||
|
|
fb94b87d4b | ||
|
|
a58825ed6c | ||
|
|
2f454823a6 | ||
|
|
ec3880df34 | ||
|
|
1439847f33 | ||
|
|
188d021023 | ||
|
|
efda9369a8 | ||
|
|
eef8e2bb94 | ||
|
|
26a37abfaf | ||
|
|
b751b6e62c | ||
|
|
b79911841f | ||
|
|
4653b49a0c | ||
|
|
51c5a284a6 | ||
|
|
90b7b6c5c9 | ||
|
|
9af0331b21 | ||
|
|
e4c47a2569 | ||
|
|
94914a1270 | ||
|
|
b5146fa981 | ||
|
|
85d1f92ee5 | ||
|
|
c1e32fc01d | ||
|
|
9a399adba8 | ||
|
|
08884c4cfb | ||
|
|
c306445ff2 | ||
|
|
f9879239ba | ||
|
|
87a1fd8da2 | ||
|
|
6a63f5cb99 |
No files matched your search
@@ -28,6 +28,9 @@
|
||||
# packed onto g6.12xlarge L4 nodes (``vllm.ci/gpu-pool=l4x4``). GPU / CPU /
|
||||
# memory / shm requests scale with card count (1 GPU = 10 CPU / 40Gi / 24Gi shm).
|
||||
# Retry covers lost agents / killed pods, not pytest or OOM (exit 1 / 137).
|
||||
# The Kubernetes HF secret deliberately uses VLLM_CI_HF_TOKEN. Scheduled
|
||||
# build-level HF_TOKEN values can override pod env entries with the same name;
|
||||
# upload_pipeline.py restores the secret-backed value inside the job shell.
|
||||
|
||||
x-npu-k8s-plugins: &npu_k8s_plugins
|
||||
- kubernetes:
|
||||
@@ -86,7 +89,7 @@ mirror_hardwares:
|
||||
env:
|
||||
- name: HF_HOME
|
||||
value: /fsx/hf_cache
|
||||
- name: HF_TOKEN
|
||||
- name: VLLM_CI_HF_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: hf-token-secret
|
||||
@@ -137,7 +140,7 @@ mirror_hardwares:
|
||||
env:
|
||||
- name: HF_HOME
|
||||
value: /fsx/hf_cache
|
||||
- name: HF_TOKEN
|
||||
- name: VLLM_CI_HF_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: hf-token-secret
|
||||
@@ -188,7 +191,7 @@ mirror_hardwares:
|
||||
env:
|
||||
- name: HF_HOME
|
||||
value: /fsx/hf_cache
|
||||
- name: HF_TOKEN
|
||||
- name: VLLM_CI_HF_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: hf-token-secret
|
||||
@@ -239,7 +242,7 @@ mirror_hardwares:
|
||||
env:
|
||||
- name: HF_HOME
|
||||
value: /fsx/hf_cache
|
||||
- name: HF_TOKEN
|
||||
- name: VLLM_CI_HF_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: hf-token-secret
|
||||
@@ -273,7 +276,7 @@ mirror_hardwares:
|
||||
env:
|
||||
- name: HF_HOME
|
||||
value: /root/.cache/huggingface
|
||||
- name: HF_TOKEN
|
||||
- name: VLLM_CI_HF_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: hf-token-secret
|
||||
@@ -308,7 +311,7 @@ mirror_hardwares:
|
||||
env:
|
||||
- name: HF_HOME
|
||||
value: /root/.cache/huggingface
|
||||
- name: HF_TOKEN
|
||||
- name: VLLM_CI_HF_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: hf-token-secret
|
||||
@@ -343,7 +346,7 @@ mirror_hardwares:
|
||||
env:
|
||||
- name: HF_HOME
|
||||
value: /root/.cache/huggingface
|
||||
- name: HF_TOKEN
|
||||
- name: VLLM_CI_HF_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: hf-token-secret
|
||||
@@ -378,7 +381,7 @@ mirror_hardwares:
|
||||
env:
|
||||
- name: HF_HOME
|
||||
value: /root/.cache/huggingface
|
||||
- name: HF_TOKEN
|
||||
- name: VLLM_CI_HF_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: hf-token-secret
|
||||
@@ -428,7 +431,7 @@ mirror_hardwares:
|
||||
value: "0"
|
||||
- name: HF_HOME
|
||||
value: /raid/hf_cache
|
||||
- name: HF_TOKEN
|
||||
- name: VLLM_CI_HF_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: hf-token-secret
|
||||
@@ -479,7 +482,7 @@ mirror_hardwares:
|
||||
value: "0"
|
||||
- name: HF_HOME
|
||||
value: /raid/hf_cache
|
||||
- name: HF_TOKEN
|
||||
- name: VLLM_CI_HF_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: hf-token-secret
|
||||
@@ -530,7 +533,7 @@ mirror_hardwares:
|
||||
value: "0"
|
||||
- name: HF_HOME
|
||||
value: /raid/hf_cache
|
||||
- name: HF_TOKEN
|
||||
- name: VLLM_CI_HF_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: hf-token-secret
|
||||
@@ -581,7 +584,7 @@ mirror_hardwares:
|
||||
value: "0"
|
||||
- name: HF_HOME
|
||||
value: /raid/hf_cache
|
||||
- name: HF_TOKEN
|
||||
- name: VLLM_CI_HF_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: hf-token-secret
|
||||
|
||||
@@ -87,6 +87,8 @@ BOOTSTRAP_UPLOAD_IF_KEYS = {
|
||||
}
|
||||
E2E_GROUP_MARKER = "E2E Test"
|
||||
CI_MIRROR_HARDWARES_PATH = ROOT / ".buildkite/common/ci_mirror_hardwares.yml"
|
||||
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'
|
||||
|
||||
# Bootstrap Buildkite ``if`` expressions.
|
||||
# ``*_MAIN_IF``: main + env schedule. ``*_LABEL_IF``: PR label (and/or composed with MAIN).
|
||||
@@ -509,6 +511,17 @@ def _expand_mirror_hardwares(step: dict[str, Any]) -> dict[str, Any] | None:
|
||||
|
||||
expanded = copy.deepcopy(preset)
|
||||
merged = {key: value for key, value in step.items() if key != "mirror_hardwares"} | expanded
|
||||
# A scheduled build's HF_TOKEN may override a pod env entry with the same
|
||||
# name. The preset injects the Kubernetes secret under a collision-proof
|
||||
# alias; restore the conventional name after Buildkite has merged env.
|
||||
if any(preset_name == chip or preset_name.startswith(f"{chip}_") for chip in _cuda_mirror_chips()):
|
||||
commands = merged.get("commands")
|
||||
if isinstance(commands, list):
|
||||
merged["commands"] = [CUDA_HF_TOKEN_EXPORT, *commands]
|
||||
elif commands is None:
|
||||
merged["commands"] = [CUDA_HF_TOKEN_EXPORT]
|
||||
else:
|
||||
merged["commands"] = [CUDA_HF_TOKEN_EXPORT, commands]
|
||||
# Preset retry (K8S_RETRY on l4_*) must not clobber a step that opted out.
|
||||
if "retry" in step:
|
||||
merged["retry"] = step["retry"]
|
||||
|
||||
+6
-23
@@ -1,27 +1,10 @@
|
||||
# Target vllm version: v0.28.0 (tag v0.28.0 @ 2cf0a6915ce5).
|
||||
# Keep VLLM_BASE_TAG aligned with the target vLLM release on every rebase.
|
||||
#
|
||||
# Release-boundary form: there is no precompiled-wheel pin here, because the
|
||||
# base image already ships exactly the target vLLM. Everything the runtime
|
||||
# needs — torch, flashinfer, nvJitLink, numpy — comes from that image as one
|
||||
# self-consistent set.
|
||||
#
|
||||
# This block reappears only when the rebase targets a vLLM commit that is NOT a
|
||||
# released tag. During the 0.27 rebase, targeting v0.27.0rc1 on top of the
|
||||
# v0.26.0 base image required reinstalling vLLM from wheels.vllm.ai and then
|
||||
# hand-repairing the fallout: pinning torch ahead of flashinfer (installing it
|
||||
# after bound flashinfer's TVM FFI runtime to the old torch and aborted engine
|
||||
# core init), registering libnvJitLink.so.13 with ldconfig, and re-pinning numpy
|
||||
# under numba's ceiling. All of it was compensation for a base image and a vLLM
|
||||
# that disagreed. At a real release boundary they agree, so none of it is
|
||||
# needed. The 0.28 rebase repeated the cycle: the v0.28.0rc / final-tag wheels
|
||||
# rode on the v0.27.0 base image with the full reinstall block until the
|
||||
# official v0.28.0 image shipped, at which point this file dropped back to the
|
||||
# form you are reading. If you pin an untagged commit again, restore the
|
||||
# reinstall block — agent/tasks/20_update_wheel_url.sh fails the run rather
|
||||
# than letting the Dockerfile silently ship the wrong vLLM.
|
||||
# Target vLLM version: v0.29.0 (tag @ 98dff2a81d74).
|
||||
# Use the matching release image and its coherent CUDA/PyTorch/FlashInfer
|
||||
# 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_TAG=v0.28.0
|
||||
ARG VLLM_BASE_TAG=v0.29.0
|
||||
|
||||
# We use registry-based layer caching for the dependencies,
|
||||
# where the tag is determined based on the hash of the contents
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
This section lists the most common options for running vLLM-Omni.
|
||||
|
||||
For options within a vLLM Engine, please refer to the [vLLM 0.28 configuration guide](https://docs.vllm.ai/en/v0.28.0/configuration/index.html).
|
||||
For options within a vLLM Engine, please refer to the [vLLM 0.29 configuration guide](https://docs.vllm.ai/en/v0.29.0/configuration/index.html).
|
||||
|
||||
Each model defines fixed topology in a registered `PipelineConfig` and runtime
|
||||
overrides in a deploy YAML.
|
||||
|
||||
@@ -151,7 +151,7 @@ their keys only.
|
||||
## Inherited vLLM variables
|
||||
|
||||
vLLM-Omni also reads variables through its aligned vLLM dependency. Refer to
|
||||
the [vLLM 0.28 environment-variable reference](https://docs.vllm.ai/en/v0.28.0/configuration/env_vars.html)
|
||||
the [vLLM 0.29 environment-variable reference](https://docs.vllm.ai/en/v0.29.0/configuration/env_vars.html)
|
||||
for their definitions. This includes vLLM launch, cache, logging, plugin, ROCm,
|
||||
XPU, ModelScope, and FlashInfer workspace settings.
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Installation
|
||||
|
||||
!!! important
|
||||
vLLM-Omni is released against the matching upstream vLLM major/minor version. vLLM-Omni 0.28.x requires vLLM 0.28.x; the stable v0.28.0 instructions pin vLLM 0.28.0.
|
||||
vLLM-Omni is released against the matching upstream vLLM major/minor version. The 0.29 development line requires vLLM 0.29.x; the source-install instructions pin vLLM 0.29.0. Published vLLM-Omni 0.28.0 wheels and images still require vLLM 0.28.x.
|
||||
|
||||
vLLM-Omni supports the following hardware platforms:
|
||||
|
||||
|
||||
@@ -34,6 +34,8 @@ vLLM-Omni is a Python library that supports the following GPU variants. The libr
|
||||
|
||||
### Pre-built wheels
|
||||
|
||||
The source-install instructions target vLLM 0.29.0. Until vLLM-Omni 0.29.0 artifacts are published, the CUDA/ROCm pre-built examples below remain on the matching 0.28.0 release pair.
|
||||
|
||||
Note: Pre-built wheels are currently available for vLLM-Omni 0.11.0rc1, 0.12.0rc1, 0.14.0rc1, 0.14.0, 0.16.0, 0.18.0, 0.20.0, 0.21.0, 0.22.0, 0.23.0, 0.24.0, 0.25.0, 0.26.0, and 0.28.0. If you need a newer unreleased revision, please [build from source](https://docs.vllm.ai/projects/vllm-omni/en/latest/getting_started/installation/gpu/#build-wheel-from-source).
|
||||
|
||||
=== "NVIDIA CUDA"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
# --8<-- [end:requirements]
|
||||
# --8<-- [start:set-up-using-python]
|
||||
|
||||
vLLM-Omni depends on the matching major/minor release of vLLM. The vLLM-Omni 0.28.x release line uses vLLM 0.28.x.
|
||||
vLLM-Omni depends on the matching major/minor release of vLLM. The 0.29 development line uses vLLM 0.29.x. Published 0.28.0 wheels and images use vLLM 0.28.x.
|
||||
|
||||
!!! 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.
|
||||
@@ -18,6 +18,8 @@ Therefore, it is recommended to install vLLM and vLLM-Omni with a **fresh new**
|
||||
|
||||
#### Installation of vLLM
|
||||
|
||||
These pre-built wheel instructions install the published vLLM-Omni 0.28.0 release. For the 0.29 development line, use the source-install instructions below.
|
||||
|
||||
vLLM-Omni is built based on vLLM. Please install it with command below.
|
||||
```bash
|
||||
uv pip install vllm==0.28.0 --torch-backend=auto
|
||||
@@ -26,12 +28,12 @@ uv pip install vllm==0.28.0 --torch-backend=auto
|
||||
#### Installation of vLLM-Omni
|
||||
|
||||
```bash
|
||||
uv pip install vllm-omni
|
||||
uv pip install vllm-omni==0.28.0
|
||||
```
|
||||
|
||||
To run Gradio demos, also install the optional extras:
|
||||
```bash
|
||||
uv pip install 'vllm-omni[demo]'
|
||||
uv pip install 'vllm-omni[demo]==0.28.0'
|
||||
```
|
||||
|
||||
# --8<-- [end:pre-built-wheels]
|
||||
@@ -39,13 +41,13 @@ uv pip install 'vllm-omni[demo]'
|
||||
# --8<-- [start:build-wheel-from-source]
|
||||
|
||||
#### Installation of vLLM
|
||||
If you do not need to modify source code of vLLM, you can directly install the stable 0.28.0 release version of the library
|
||||
If you do not need to modify source code of vLLM, you can directly install the stable 0.29.0 release version of the library
|
||||
|
||||
```bash
|
||||
uv pip install vllm==0.28.0 --torch-backend=auto
|
||||
uv pip install vllm==0.29.0 --torch-backend=auto
|
||||
```
|
||||
|
||||
The 0.28.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 0.29.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.
|
||||
|
||||
#### Installation of vLLM-Omni
|
||||
Since vllm-omni is rapidly evolving, it's recommended to install it from source
|
||||
@@ -66,12 +68,12 @@ If you want to check, modify or debug with source code of vLLM, install the libr
|
||||
```bash
|
||||
git clone https://github.com/vllm-project/vllm.git
|
||||
cd vllm
|
||||
git checkout v0.28.0
|
||||
git checkout v0.29.0
|
||||
```
|
||||
Set up environment variables to get pre-built wheels. If there are internet problems, just download the whl file manually. And set `VLLM_PRECOMPILED_WHEEL_LOCATION` as your local absolute path of whl file.
|
||||
```bash
|
||||
#For CUDA 13.0 (the default for v0.28.0; the wheel filename has no `+cu130` suffix)
|
||||
export VLLM_PRECOMPILED_WHEEL_LOCATION=https://github.com/vllm-project/vllm/releases/download/v0.28.0/vllm-0.28.0-cp38-abi3-manylinux_2_28_x86_64.whl
|
||||
#For CUDA 13.0 (the default for v0.29.0; the wheel filename has no `+cu130` suffix)
|
||||
export VLLM_PRECOMPILED_WHEEL_LOCATION=https://github.com/vllm-project/vllm/releases/download/v0.29.0/vllm-0.29.0-cp38-abi3-manylinux_2_28_x86_64.whl
|
||||
```
|
||||
Install vllm with command below (If you have no existing PyTorch).
|
||||
```bash
|
||||
@@ -131,7 +133,7 @@ If you want to specify the base vLLM version:
|
||||
```bash
|
||||
DOCKER_BUILDKIT=1 docker build \
|
||||
-f docker/Dockerfile.cuda \
|
||||
--build-arg BASE_IMAGE=vllm/vllm-openai:v0.28.0 \
|
||||
--build-arg BASE_IMAGE=vllm/vllm-openai:v0.29.0 \
|
||||
-t vllm-omni-cuda .
|
||||
```
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ vLLM-Omni current recommends the steps in under setup through Docker Images.
|
||||
|
||||
#### Installation of vLLM
|
||||
|
||||
These pre-built wheel instructions install the published vLLM-Omni 0.28.0 release. For the 0.29 development line, use the source-install instructions below.
|
||||
|
||||
vLLM-Omni is built based on vLLM. Please install it with command below.
|
||||
```bash
|
||||
uv pip install vllm==0.28.0+rocm723 --extra-index-url https://wheels.vllm.ai/rocm/0.28.0/rocm723
|
||||
@@ -22,7 +24,7 @@ uv pip install vllm==0.28.0+rocm723 --extra-index-url https://wheels.vllm.ai/roc
|
||||
# we need to add --no-build-isolation as the torch
|
||||
# is not obtained from pypi, we have to install using the
|
||||
# torch installed in our environment
|
||||
uv pip install vllm-omni
|
||||
uv pip install vllm-omni==0.28.0
|
||||
|
||||
# Optional if want to run Qwen3 TTS
|
||||
uv pip uninstall onnxruntime # should be removed before we can install onnxruntime-rocm
|
||||
@@ -34,13 +36,13 @@ uv pip install onnxruntime-rocm
|
||||
# --8<-- [start:build-wheel-from-source]
|
||||
|
||||
#### Installation of vLLM
|
||||
If you do not need to modify source code of vLLM, you can directly install the stable 0.28.0 release version of the library
|
||||
If you do not need to modify source code of vLLM, you can directly install the stable 0.29.0 release version of the library
|
||||
|
||||
```bash
|
||||
uv pip install vllm==0.28.0+rocm723 --extra-index-url https://wheels.vllm.ai/rocm/0.28.0/rocm723
|
||||
uv pip install vllm==0.29.0+rocm723 --extra-index-url https://wheels.vllm.ai/rocm/0.29.0/rocm723
|
||||
```
|
||||
|
||||
The pre-built 0.28.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.29.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
|
||||
Since vllm-omni is rapidly evolving, it's recommended to install it from source
|
||||
@@ -58,7 +60,7 @@ If you want to check, modify or debug with source code of vLLM, install the libr
|
||||
```bash
|
||||
git clone https://github.com/vllm-project/vllm.git
|
||||
cd vllm
|
||||
git checkout v0.28.0
|
||||
git checkout v0.29.0
|
||||
python3 -m pip install -r requirements/rocm.txt
|
||||
python3 setup.py develop
|
||||
```
|
||||
|
||||
@@ -19,10 +19,10 @@ uv venv --python 3.12 --seed
|
||||
source .venv/bin/activate
|
||||
|
||||
# On CUDA
|
||||
uv pip install vllm==0.28.0 --torch-backend=auto
|
||||
uv pip install vllm==0.29.0 --torch-backend=auto
|
||||
|
||||
# On ROCm
|
||||
uv pip install vllm==0.28.0+rocm723 --extra-index-url https://wheels.vllm.ai/rocm/0.28.0/rocm723
|
||||
uv pip install vllm==0.29.0+rocm723 --extra-index-url https://wheels.vllm.ai/rocm/0.29.0/rocm723
|
||||
|
||||
git clone https://github.com/vllm-project/vllm-omni.git
|
||||
cd vllm-omni
|
||||
@@ -34,7 +34,7 @@ For additional installation methods — please see the [installation guide](inst
|
||||
!!! 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.
|
||||
|
||||
If you are seeing strange behavior with the `vllm` command not handling the `--omni` flag correctly, you most likely have a version mismatch with vLLM < `0.28.0` and vLLM Omni `0.28.0`, as vLLM Omni no longer hijacks the vLLM entrypoint. Updating vLLM should resolve this issue.
|
||||
If you are seeing strange behavior with the `vllm` command not handling the `--omni` flag correctly, you most likely have a version mismatch with vLLM < `0.29.0` and vLLM Omni `0.29.0`, as vLLM Omni no longer hijacks the vLLM entrypoint. Updating vLLM should resolve this issue.
|
||||
|
||||
## Offline Inference
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
@@ -214,11 +214,20 @@ def test_fish_kvcache_backend_wraps_only_model_instance(monkeypatch):
|
||||
assert fish_kvcache_backend.get_fish_kvcache_attn_stats()["small_hit_count"] == 1
|
||||
|
||||
|
||||
def _vllm_kv_cache_shape(num_blocks, block_size, num_kv_heads, head_size):
|
||||
def _vllm_kv_cache_shape(num_blocks, block_size, num_kv_heads, head_size, dtype=torch.float16):
|
||||
# Source of truth for the paged KV cache layout the backend must unpack.
|
||||
from vllm.v1.attention.backends.flash_attn import FlashAttentionBackend
|
||||
# vLLM 0.29 removed AttentionBackend.get_kv_cache_shape; the per-layer shape
|
||||
# now comes from the spec, with the content dim expressed in bytes.
|
||||
from vllm.v1.kv_cache_interface import FullAttentionSpec, compute_layer_kv_cache_shape_bytes
|
||||
|
||||
return FlashAttentionBackend.get_kv_cache_shape(num_blocks, block_size, num_kv_heads, head_size)
|
||||
spec = FullAttentionSpec(
|
||||
block_size=block_size,
|
||||
num_kv_heads=num_kv_heads,
|
||||
head_size=head_size,
|
||||
dtype=dtype,
|
||||
)
|
||||
num_pages, num_heads, num_states, content_bytes = compute_layer_kv_cache_shape_bytes(spec, num_blocks)
|
||||
return (num_pages, num_heads, num_states, content_bytes // dtype.itemsize)
|
||||
|
||||
|
||||
def test_fish_kvcache_backend_unbinds_kv_on_vllm_cache_layout(monkeypatch):
|
||||
|
||||
@@ -12,6 +12,7 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[2] / ".buildkite" / "com
|
||||
|
||||
from skip_ci import resolve_ci_decision # noqa: E402
|
||||
from upload_pipeline import ( # noqa: E402
|
||||
CUDA_HF_TOKEN_EXPORT,
|
||||
_expand_mirror_hardwares,
|
||||
_get_mirror_hw_selector,
|
||||
_load_bootstrap_steps,
|
||||
@@ -143,6 +144,10 @@ def test_mirror_hardwares_l4_1_expands_to_agents_and_plugins(monkeypatch: pytest
|
||||
container = step["plugins"][0]["kubernetes"]["podSpec"]["containers"][0]
|
||||
assert container["image"].endswith("$BUILDKITE_COMMIT")
|
||||
assert container["resources"]["limits"]["nvidia.com/gpu"] == 1
|
||||
env_names = {item["name"] for item in container["env"]}
|
||||
assert "VLLM_CI_HF_TOKEN" in env_names
|
||||
assert "HF_TOKEN" not in env_names
|
||||
assert step["commands"] == [CUDA_HF_TOKEN_EXPORT, "pytest -sv tests/example"]
|
||||
|
||||
|
||||
def test_mirror_hardwares_l4_preserves_explicit_retry() -> None:
|
||||
@@ -182,6 +187,33 @@ def test_mirror_hardwares_a2b3_npu_4_expands_agents_image_and_plugins() -> None:
|
||||
assert step["plugins"][0]["kubernetes"]["podSpecPatch"]["imagePullSecrets"] == [
|
||||
{"name": "swr-secret"},
|
||||
]
|
||||
assert step["commands"] == ["pytest -sv tests/example"]
|
||||
|
||||
|
||||
def test_all_cuda_mirror_hardwares_restore_hf_token_at_runtime() -> None:
|
||||
for name in (
|
||||
"l4_1",
|
||||
"l4_2",
|
||||
"l4_3",
|
||||
"l4_4",
|
||||
"h100_1",
|
||||
"h100_2",
|
||||
"h100_3",
|
||||
"h100_4",
|
||||
"b200_1",
|
||||
"b200_2",
|
||||
"b200_3",
|
||||
"b200_4",
|
||||
):
|
||||
step = _expand_mirror_hardwares(
|
||||
{"label": name, "mirror_hardwares": name, "commands": ["pytest -sv tests/example"]},
|
||||
)
|
||||
assert step is not None
|
||||
container = step["plugins"][0]["kubernetes"]["podSpec"]["containers"][0]
|
||||
env_names = {item["name"] for item in container["env"]}
|
||||
assert "VLLM_CI_HF_TOKEN" in env_names
|
||||
assert "HF_TOKEN" not in env_names
|
||||
assert step["commands"][0] == CUDA_HF_TOKEN_EXPORT
|
||||
|
||||
|
||||
def _gpu_limit(step: dict) -> int:
|
||||
|
||||
@@ -215,6 +215,16 @@ def test_from_pipeline_config_normalizes_stage_engine_extras_without_expanding_s
|
||||
assert stage.diffusion_config.model_config["default_robot_embodiment"] == "roboarena"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("disabled", [True, False])
|
||||
def test_frontend_log_stats_flag_is_not_an_unowned_stage_argument(disabled):
|
||||
from vllm_omni.engine.stage_init_utils import build_engine_args_dict_from_omni_stage_config
|
||||
|
||||
config = _from_pipeline_key("dots_tts", cli_overrides={"disable_log_stats": disabled})
|
||||
assert config.stage_configs
|
||||
engine_args = build_engine_args_dict_from_omni_stage_config(config.stage_by_id(0), model="test-model")
|
||||
assert "disable_log_stats" not in engine_args
|
||||
|
||||
|
||||
def test_from_pipeline_config_applies_cli_overrides_without_stage_config_runtime_bridge():
|
||||
omni_config = _from_pipeline_key(
|
||||
"qwen3_tts",
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""The omni scheduler must stay on the v1 model runner, like every omni worker.
|
||||
|
||||
vLLM 0.28 resolved this config to v1; 0.29 ends its fallback chain with v2. When
|
||||
only the workers forced v1, the scheduler took its v2 fast path -- which omits
|
||||
resumed-request token ids -- while the v1 runner still read them, so resuming a
|
||||
request raised ``KeyError: <req_id>`` in ``_update_states`` and killed the engine
|
||||
core (Multi-Replica Startup Test).
|
||||
|
||||
The default is upstream's to change, so this pins Omni's side of the contract
|
||||
rather than the default itself.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from vllm_omni.core.sched.omni_scheduler_mixin import OmniSchedulerMixin
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||
|
||||
|
||||
class _Scheduler(OmniSchedulerMixin):
|
||||
"""Minimal carrier for the shared init hook both omni schedulers call."""
|
||||
|
||||
def __init__(self, *, use_v2_model_runner: bool) -> None:
|
||||
self.use_v2_model_runner = use_v2_model_runner
|
||||
self.vllm_config = SimpleNamespace(
|
||||
model_config=SimpleNamespace(
|
||||
async_chunk=False,
|
||||
stage_id=0,
|
||||
pooling_output_decoder=None,
|
||||
)
|
||||
)
|
||||
self._init_omni_io_scheduling_state()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("initial", [True, False])
|
||||
def test_scheduler_ends_on_v1_model_runner(initial: bool) -> None:
|
||||
"""Whatever upstream resolves, the omni scheduler runs v1."""
|
||||
assert _Scheduler(use_v2_model_runner=initial).use_v2_model_runner is False
|
||||
|
||||
|
||||
def test_v2_default_does_not_reach_the_resumed_request_fast_path() -> None:
|
||||
"""The v2 fast path is what dropped resumed-request token ids.
|
||||
|
||||
``OmniGenerationScheduler`` branches on ``use_v2_model_runner`` when it
|
||||
assembles SchedulerOutput; forcing it false keeps the v1 assembly that
|
||||
carries ``all_token_ids`` for resumed requests, which the v1 runner reads.
|
||||
"""
|
||||
scheduler = _Scheduler(use_v2_model_runner=True)
|
||||
|
||||
assert not scheduler.use_v2_model_runner, (
|
||||
"scheduler would take the v2 fast path while omni workers run v1; "
|
||||
"resumed requests then KeyError in _update_states"
|
||||
)
|
||||
@@ -279,17 +279,17 @@ def _install_native_modules(monkeypatch: pytest.MonkeyPatch, events: list[tuple]
|
||||
group = SimpleNamespace(get_metadata_builder=lambda _index: SimpleNamespace(reorder_batch_threshold=None))
|
||||
return [[group], [group]], "cg-support", [4, 8]
|
||||
|
||||
# vLLM 0.29 dropped attn_groups and cache_dtype from init_kv_cache: the
|
||||
# layout now rides on the resolved CacheConfig.kv_cache_layout instead.
|
||||
def init_cache(
|
||||
runner_kv_caches,
|
||||
forward_context,
|
||||
kv_cache_config,
|
||||
attn_groups,
|
||||
device,
|
||||
cache_dtype,
|
||||
kernel_block_sizes,
|
||||
vllm_config,
|
||||
):
|
||||
events.append(("cache", kv_cache_config, attn_groups, kernel_block_sizes))
|
||||
events.append(("cache", kv_cache_config, kernel_block_sizes))
|
||||
runner_kv_caches.extend(["cache-0", "cache-1"])
|
||||
return {"layer-0": "tensor-0", "layer-1": "tensor-1"}
|
||||
|
||||
|
||||
@@ -299,8 +299,10 @@ def test_minus_one_max_model_len_is_auto_fitted_by_native_cache_sizing() -> None
|
||||
)
|
||||
|
||||
assert vllm_config.model_config.original_max_model_len == -1
|
||||
# Sixteen physical pages provide ``16 * block_size`` tokens. The native
|
||||
# backend chooses the block geometry, so keep this assertion backend
|
||||
# Sixteen physical pages provide ``(16 - 1) * block_size`` tokens: vLLM 0.29
|
||||
# subtracts the null block that BlockPool permanently holds back before
|
||||
# sizing (check_enough_kv_cache_memory), so only 15 pages are usable. The
|
||||
# native backend chooses the block geometry, so keep this assertion backend
|
||||
# independent instead of assuming the generic 16-token block size.
|
||||
expected_max_model_len = min(model_limit, spec.block_size * 16)
|
||||
expected_max_model_len = min(model_limit, spec.block_size * (16 - 1))
|
||||
assert vllm_config.model_config.max_model_len == expected_max_model_len
|
||||
@@ -0,0 +1,147 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Physical KV cache layout selection and propagation for the Diffusion cache."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
from vllm.config import CacheConfig
|
||||
from vllm.v1.kv_cache_interface import FullAttentionSpec
|
||||
from vllm.v1.kv_cache_layout import KVCacheLayout
|
||||
|
||||
from vllm_omni.diffusion.diffusion_kv.layout import (
|
||||
DEFAULT_DIFFUSION_KV_CACHE_LAYOUT,
|
||||
adopt_kv_cache_layout,
|
||||
assert_backend_layout_supported,
|
||||
build_kv_cache_tensor,
|
||||
resolve_diffusion_kv_cache_layout,
|
||||
)
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu, pytest.mark.diffusion]
|
||||
|
||||
|
||||
def _config():
|
||||
return SimpleNamespace(cache_config=CacheConfig())
|
||||
|
||||
|
||||
class _DenseBackend:
|
||||
@classmethod
|
||||
def indexes_kv_by_block_stride(cls) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
class _BlockStrideBackend:
|
||||
@classmethod
|
||||
def indexes_kv_by_block_stride(cls) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def test_default_layout_is_not_block_outermost() -> None:
|
||||
"""The default reproduces the pre-0.29 indexes_kv_by_block_stride=False."""
|
||||
vllm_config = _config()
|
||||
layout = resolve_diffusion_kv_cache_layout(vllm_config)
|
||||
|
||||
assert layout is DEFAULT_DIFFUSION_KV_CACHE_LAYOUT
|
||||
assert layout.is_block_outermost is False
|
||||
assert vllm_config.cache_config.get_resolved_kv_cache_layout() is layout
|
||||
|
||||
|
||||
def test_block_stride_backend_selects_block_outermost_layout() -> None:
|
||||
layout = resolve_diffusion_kv_cache_layout(_config(), indexes_kv_by_block_stride=True)
|
||||
|
||||
assert layout.is_block_outermost is True
|
||||
|
||||
|
||||
def test_resolution_is_idempotent() -> None:
|
||||
vllm_config = _config()
|
||||
|
||||
assert resolve_diffusion_kv_cache_layout(vllm_config) is resolve_diffusion_kv_cache_layout(vllm_config)
|
||||
|
||||
|
||||
def test_contradicting_requirement_raises_instead_of_misaddressing() -> None:
|
||||
vllm_config = _config()
|
||||
resolve_diffusion_kv_cache_layout(vllm_config)
|
||||
|
||||
with pytest.raises(ValueError, match="contradicts the attention"):
|
||||
resolve_diffusion_kv_cache_layout(vllm_config, indexes_kv_by_block_stride=True)
|
||||
|
||||
|
||||
def test_preset_layout_is_honoured() -> None:
|
||||
vllm_config = _config()
|
||||
vllm_config.cache_config.kv_cache_layout = KVCacheLayout.BLNHC.name
|
||||
|
||||
layout = resolve_diffusion_kv_cache_layout(vllm_config, indexes_kv_by_block_stride=True)
|
||||
|
||||
assert layout is KVCacheLayout.BLNHC
|
||||
|
||||
|
||||
def test_backend_assertion_is_quiet_until_the_layout_is_resolved() -> None:
|
||||
"""Workers collect specs before the control plane hands them the layout."""
|
||||
vllm_config = _config()
|
||||
|
||||
assert_backend_layout_supported(vllm_config, _BlockStrideBackend) # unresolved: nothing to check
|
||||
|
||||
resolve_diffusion_kv_cache_layout(vllm_config)
|
||||
assert_backend_layout_supported(vllm_config, _DenseBackend)
|
||||
assert_backend_layout_supported(vllm_config, None)
|
||||
with pytest.raises(ValueError, match="contradicts the attention"):
|
||||
assert_backend_layout_supported(vllm_config, _BlockStrideBackend)
|
||||
|
||||
|
||||
def test_worker_adopts_the_layout_carried_on_the_kv_cache_config() -> None:
|
||||
"""Diffusion workers receive only a KVCacheConfig, so the name rides on it."""
|
||||
worker = _config()
|
||||
shipped = SimpleNamespace(kv_cache_layout=KVCacheLayout.BLNHC.name)
|
||||
|
||||
assert adopt_kv_cache_layout(worker, shipped) is KVCacheLayout.BLNHC
|
||||
assert worker.cache_config.get_resolved_kv_cache_layout() is KVCacheLayout.BLNHC
|
||||
|
||||
|
||||
def test_worker_without_a_carried_layout_falls_back_to_the_same_default() -> None:
|
||||
worker = _config()
|
||||
|
||||
assert adopt_kv_cache_layout(worker, SimpleNamespace(kv_cache_layout=None)) is DEFAULT_DIFFUSION_KV_CACHE_LAYOUT
|
||||
|
||||
|
||||
def test_worker_disagreeing_with_the_control_plane_raises() -> None:
|
||||
worker = _config()
|
||||
resolve_diffusion_kv_cache_layout(worker)
|
||||
|
||||
with pytest.raises(ValueError, match="disagrees with the layout"):
|
||||
adopt_kv_cache_layout(worker, SimpleNamespace(kv_cache_layout=KVCacheLayout.BLNHC.name))
|
||||
|
||||
|
||||
def test_block_stride_requirement_propagates_control_plane_to_worker() -> None:
|
||||
"""A block-stride backend must reach the worker as a block-outermost layout.
|
||||
|
||||
Covers the whole seam: the control plane resolves with the requirement, the
|
||||
name rides on each rank-local KVCacheConfig, the worker adopts it, and the
|
||||
backend contract check then passes instead of raising.
|
||||
"""
|
||||
control_plane = _config()
|
||||
layout = resolve_diffusion_kv_cache_layout(control_plane, indexes_kv_by_block_stride=True)
|
||||
assert layout.is_block_outermost is True
|
||||
|
||||
shipped = SimpleNamespace(kv_cache_layout=layout.name)
|
||||
|
||||
worker = _config()
|
||||
assert adopt_kv_cache_layout(worker, shipped) is layout
|
||||
# The worker's own backend requirement now agrees with what it was handed.
|
||||
assert_backend_layout_supported(worker, _BlockStrideBackend)
|
||||
with pytest.raises(ValueError, match="contradicts the attention"):
|
||||
assert_backend_layout_supported(worker, _DenseBackend)
|
||||
|
||||
|
||||
def test_tensor_strides_come_from_the_upstream_derivation() -> None:
|
||||
spec = FullAttentionSpec(block_size=16, num_kv_heads=2, head_size=8, dtype=torch.bfloat16)
|
||||
|
||||
tensor = build_kv_cache_tensor(spec, 4, ["layer0", "layer1"])
|
||||
|
||||
assert tensor.layers == ["layer0", "layer1"]
|
||||
assert tensor.size == spec.page_size_bytes * 2 * 4
|
||||
# Layer-outermost: each layer owns a contiguous run of its own blocks.
|
||||
assert tensor.layer_stride == spec.page_size_bytes * 4
|
||||
assert tensor.block_stride == spec.page_size_bytes
|
||||
@@ -1,11 +1,12 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
import vllm.v1.core.single_type_kv_cache_manager as native_kv_managers
|
||||
from vllm.v1.kv_cache_interface import FullAttentionSpec, KVCacheConfig, KVCacheGroupSpec, KVCacheTensor
|
||||
from vllm.v1.kv_cache_interface import FullAttentionSpec, KVCacheConfig, KVCacheGroupSpec
|
||||
|
||||
from tests.helpers.kv_layout import build_kv_cache_tensor
|
||||
from vllm_omni.diffusion.diffusion_kv.manager import DiffusionKVAdmissionError, DiffusionKVCacheManager
|
||||
from vllm_omni.diffusion.diffusion_kv.request import DiffusionKVContext, DiffusionKVRequest
|
||||
|
||||
@@ -24,7 +25,7 @@ def _config(num_blocks: int) -> KVCacheConfig:
|
||||
)
|
||||
return KVCacheConfig(
|
||||
num_blocks=num_blocks,
|
||||
kv_cache_tensors=[KVCacheTensor(size=spec.page_size_bytes * num_blocks, shared_by=["layer0"])],
|
||||
kv_cache_tensors=[build_kv_cache_tensor(spec, num_blocks, ["layer0"])],
|
||||
kv_cache_groups=[KVCacheGroupSpec(layer_names=["layer0"], kv_cache_spec=spec)],
|
||||
)
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import torch
|
||||
from torch import nn
|
||||
from vllm.v1.kv_cache_interface import FullAttentionSpec
|
||||
|
||||
from tests.helpers.kv_layout import layout_for_backend
|
||||
from vllm_omni.diffusion.attention.backends.flash_attn import FlashAttentionImpl
|
||||
from vllm_omni.diffusion.attention.layer import Attention
|
||||
from vllm_omni.diffusion.diffusion_kv import paged_attention_adapter as adapter_module
|
||||
@@ -1239,7 +1240,8 @@ def test_layer_adapter_accepts_platform_native_backend_and_uses_rank_local_heads
|
||||
assert native_layer.num_heads == 4
|
||||
assert native_layer.num_kv_heads == 2
|
||||
assert native_layer.spec.num_kv_heads == 2
|
||||
assert native_layer.spec.indexes_kv_by_block_stride is True
|
||||
# 0.29: the flag no longer rides on the spec, it selects the layout.
|
||||
assert layout_for_backend(native_layer.attn_backend).is_block_outermost is True
|
||||
assert native_layer._q_scale_float == 1.0
|
||||
assert native_layer._k_scale_float == 1.0
|
||||
assert native_layer._v_scale_float == 1.0
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -10,12 +10,14 @@ import torch
|
||||
from torch import nn
|
||||
from torch.nn import functional as F
|
||||
from vllm.config import AttentionConfig, CacheConfig, VllmConfig, set_current_vllm_config
|
||||
from vllm.v1.kv_cache_interface import FullAttentionSpec, KVCacheConfig, KVCacheGroupSpec, KVCacheTensor
|
||||
from vllm.v1.kv_cache_interface import FullAttentionSpec, KVCacheConfig, KVCacheGroupSpec
|
||||
from vllm.v1.worker.gpu.attn_utils import init_attn_backend, init_kv_cache
|
||||
from vllm.v1.worker.gpu.block_table import BlockTables
|
||||
|
||||
from tests.helpers.kv_layout import build_kv_cache_tensor
|
||||
from vllm_omni.diffusion.attention.backends.abstract import AttentionMetadata
|
||||
from vllm_omni.diffusion.attention.backends.flash_attn import FlashAttentionImpl
|
||||
from vllm_omni.diffusion.diffusion_kv.layout import resolve_diffusion_kv_cache_layout
|
||||
from vllm_omni.diffusion.diffusion_kv.paged_attention_adapter import (
|
||||
DiffusionPagedAttentionAdapter,
|
||||
DiffusionPagedAttentionLayerAdapter,
|
||||
@@ -88,12 +90,7 @@ def test_adapter_executes_native_paged_attention_on_non_contiguous_blocks() -> N
|
||||
canonical_spec = native_layer.spec
|
||||
kv_cache_config = KVCacheConfig(
|
||||
num_blocks=4,
|
||||
kv_cache_tensors=[
|
||||
KVCacheTensor(
|
||||
size=canonical_spec.page_size_bytes * 4,
|
||||
shared_by=[_LAYER_NAME],
|
||||
)
|
||||
],
|
||||
kv_cache_tensors=[build_kv_cache_tensor(canonical_spec, 4, [_LAYER_NAME])],
|
||||
kv_cache_groups=[
|
||||
KVCacheGroupSpec(
|
||||
layer_names=[_LAYER_NAME],
|
||||
@@ -101,6 +98,9 @@ def test_adapter_executes_native_paged_attention_on_non_contiguous_blocks() -> N
|
||||
)
|
||||
],
|
||||
)
|
||||
# 0.29 reads the resolved physical layout when allocating; the diffusion
|
||||
# control plane pins it, so this standalone harness must too.
|
||||
resolve_diffusion_kv_cache_layout(vllm_config)
|
||||
with set_current_vllm_config(vllm_config):
|
||||
attn_groups, _, kernel_block_sizes = init_attn_backend(
|
||||
kv_cache_config,
|
||||
@@ -108,13 +108,13 @@ def test_adapter_executes_native_paged_attention_on_non_contiguous_blocks() -> N
|
||||
device,
|
||||
)
|
||||
runner_kv_caches: list[torch.Tensor | list[torch.Tensor]] = []
|
||||
# vLLM 0.29 dropped attn_groups and cache_dtype here: the layout now
|
||||
# comes from the resolved CacheConfig.kv_cache_layout inside the call.
|
||||
init_kv_cache(
|
||||
runner_kv_caches,
|
||||
vllm_config.compilation_config.static_forward_context,
|
||||
kv_cache_config,
|
||||
attn_groups,
|
||||
device,
|
||||
vllm_config.cache_config.cache_dtype,
|
||||
kernel_block_sizes,
|
||||
vllm_config,
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
@@ -10,10 +10,10 @@ from vllm.v1.kv_cache_interface import (
|
||||
FullAttentionSpec,
|
||||
KVCacheConfig,
|
||||
KVCacheGroupSpec,
|
||||
KVCacheTensor,
|
||||
)
|
||||
from vllm.v1.request import RequestStatus
|
||||
|
||||
from tests.helpers.kv_layout import build_kv_cache_tensor
|
||||
from vllm_omni.diffusion.diffusion_kv.request import DiffusionKVContext, DiffusionKVRequest
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu, pytest.mark.diffusion]
|
||||
@@ -119,7 +119,7 @@ def _manager(*, enable_caching: bool) -> KVCacheManager:
|
||||
group = KVCacheGroupSpec(layer_names=["layer0"], kv_cache_spec=spec)
|
||||
config = KVCacheConfig(
|
||||
num_blocks=8,
|
||||
kv_cache_tensors=[KVCacheTensor(size=spec.page_size_bytes * 8, shared_by=["layer0"])],
|
||||
kv_cache_tensors=[build_kv_cache_tensor(spec, 8, ["layer0"])],
|
||||
kv_cache_groups=[group],
|
||||
)
|
||||
return KVCacheManager(
|
||||
|
||||
@@ -8,9 +8,10 @@ from unittest.mock import Mock
|
||||
import pytest
|
||||
import torch
|
||||
from torch import nn
|
||||
from vllm.v1.kv_cache_interface import FullAttentionSpec, KVCacheConfig, KVCacheGroupSpec, KVCacheTensor
|
||||
from vllm.v1.kv_cache_interface import FullAttentionSpec, KVCacheConfig, KVCacheGroupSpec
|
||||
|
||||
import vllm_omni.diffusion.worker.diffusion_worker as diffusion_worker_module
|
||||
from tests.helpers.kv_layout import build_kv_cache_tensor, layout_for_backend
|
||||
from vllm_omni.diffusion.attention.layer import Attention
|
||||
from vllm_omni.diffusion.diffusion_kv.config import DiffusionKVCacheMode
|
||||
from vllm_omni.diffusion.worker.diffusion_model_runner import DiffusionModelRunner
|
||||
@@ -79,7 +80,9 @@ def test_runner_discovers_native_spec_from_loaded_attention() -> None:
|
||||
assert spec.num_kv_heads == 2
|
||||
assert spec.head_size == 8
|
||||
assert spec.dtype is torch.bfloat16
|
||||
assert spec.indexes_kv_by_block_stride is True
|
||||
# vLLM 0.29 moved the block-stride decision off the spec onto the resolved
|
||||
# physical layout; the backend's declaration must still select one.
|
||||
assert layout_for_backend(_Backend).is_block_outermost is True
|
||||
assert spec.non_causal is True
|
||||
|
||||
|
||||
@@ -112,7 +115,7 @@ def test_runner_retains_matching_rank_local_config() -> None:
|
||||
spec = runner.get_kv_cache_spec()["image_attention"]
|
||||
config = KVCacheConfig(
|
||||
num_blocks=8,
|
||||
kv_cache_tensors=[KVCacheTensor(size=spec.page_size_bytes * 8, shared_by=["image_attention"])],
|
||||
kv_cache_tensors=[build_kv_cache_tensor(spec, 8, ["image_attention"])],
|
||||
kv_cache_groups=[KVCacheGroupSpec(layer_names=["image_attention"], kv_cache_spec=spec)],
|
||||
)
|
||||
|
||||
@@ -126,7 +129,7 @@ def test_runner_rejects_rank_local_config_for_different_layers() -> None:
|
||||
spec = runner.get_kv_cache_spec()["image_attention"]
|
||||
config = KVCacheConfig(
|
||||
num_blocks=8,
|
||||
kv_cache_tensors=[KVCacheTensor(size=spec.page_size_bytes * 8, shared_by=["other_attention"])],
|
||||
kv_cache_tensors=[build_kv_cache_tensor(spec, 8, ["other_attention"])],
|
||||
kv_cache_groups=[KVCacheGroupSpec(layer_names=["other_attention"], kv_cache_spec=spec)],
|
||||
)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Unit tests for shared AdaLayerNorm layers used by FLUX and other models."""
|
||||
|
||||
import os
|
||||
@@ -46,7 +46,7 @@ def _force_default_gemm(monkeypatch):
|
||||
|
||||
monkeypatch.setattr(
|
||||
"vllm.model_executor.layers.linear.dispatch_unquantized_gemm",
|
||||
lambda: default_unquantized_gemm,
|
||||
lambda *_args, **_kwargs: default_unquantized_gemm,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""
|
||||
Shape broadcasting regression tests for RotaryEmbedding.forward_native.
|
||||
|
||||
@@ -16,7 +16,11 @@ import types
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from vllm_omni.diffusion.layers.rope import RotaryEmbedding, apply_rotary_emb_mindiesd
|
||||
from vllm_omni.diffusion.layers.rope import (
|
||||
RotaryEmbedding,
|
||||
RotaryEmbeddingWan,
|
||||
apply_rotary_emb_mindiesd,
|
||||
)
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||
|
||||
@@ -239,6 +243,25 @@ class TestRotaryEmbeddingNativeShapeRegression:
|
||||
assert not torch.isnan(output).any()
|
||||
|
||||
|
||||
def test_cuda_paths_fall_back_without_vendored_rotary_kernel() -> None:
|
||||
"""Source/CPU installs need not contain the wheel-vendored CUDA kernel."""
|
||||
x = torch.randn(1, 5, 2, 8)
|
||||
|
||||
rope = RotaryEmbedding(is_neox_style=True)
|
||||
rope.apply_rotary_emb_vllm_flash_attn = None
|
||||
output = rope.forward_cuda(x, torch.randn(5, 4), torch.randn(5, 4))
|
||||
assert output.shape == x.shape
|
||||
|
||||
wan_rope = RotaryEmbeddingWan(is_neox_style=False, half_head_dim=True)
|
||||
wan_rope.apply_rotary_emb_vllm_flash_attn = None
|
||||
output = wan_rope.forward_cuda(
|
||||
x,
|
||||
torch.randn(1, 5, 1, 4),
|
||||
torch.randn(1, 5, 1, 4),
|
||||
)
|
||||
assert output.shape == x.shape
|
||||
|
||||
|
||||
def test_mindie_full_dim_rope_keeps_h3_cos_sin_layout(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""H3 passes complete rotary dims to MindIE instead of the CUDA half-dim layout."""
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
@@ -59,7 +59,7 @@ def _force_default_gemm(monkeypatch):
|
||||
|
||||
monkeypatch.setattr(
|
||||
"vllm.model_executor.layers.linear.dispatch_unquantized_gemm",
|
||||
lambda: default_unquantized_gemm,
|
||||
lambda *_args, **_kwargs: default_unquantized_gemm,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
"""Equivalence test for DreamZero self-attn QKV fusion.
|
||||
|
||||
@@ -52,7 +52,7 @@ def _force_default_gemm(monkeypatch):
|
||||
|
||||
monkeypatch.setattr(
|
||||
"vllm.model_executor.layers.linear.dispatch_unquantized_gemm",
|
||||
lambda: default_unquantized_gemm,
|
||||
lambda *_args, **_kwargs: default_unquantized_gemm,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Unit tests for GLM-Image quantization support (W4A16/AutoRound).
|
||||
|
||||
These tests verify that the GLM-Image DiT transformer correctly accepts and uses
|
||||
@@ -69,7 +69,7 @@ def _force_default_gemm(monkeypatch):
|
||||
|
||||
monkeypatch.setattr(
|
||||
"vllm.model_executor.layers.linear.dispatch_unquantized_gemm",
|
||||
lambda: default_unquantized_gemm,
|
||||
lambda *_args, **_kwargs: default_unquantized_gemm,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
@@ -64,6 +64,13 @@ def setup_tp_group(monkeypatch, mocker):
|
||||
lambda: 0,
|
||||
)
|
||||
|
||||
# vLLM 0.29 added a CUDA-only fused embedding kernel that engages whenever
|
||||
# tp_size > 1; these tests drive CPU tensors, so keep the portable masked path.
|
||||
monkeypatch.setattr(
|
||||
"vllm.model_executor.layers.vocab_parallel_embedding.current_platform.is_cuda",
|
||||
lambda: False,
|
||||
)
|
||||
|
||||
mock_tp_group = mocker.MagicMock()
|
||||
mock_tp_group.world_size = 2
|
||||
mocker.patch("vllm.distributed.parallel_state.get_tp_group", return_value=mock_tp_group)
|
||||
@@ -94,7 +101,7 @@ def setup_tp_group(monkeypatch, mocker):
|
||||
|
||||
monkeypatch.setattr(
|
||||
"vllm.model_executor.layers.linear.dispatch_unquantized_gemm",
|
||||
lambda: default_unquantized_gemm,
|
||||
lambda *_args, **_kwargs: default_unquantized_gemm,
|
||||
)
|
||||
|
||||
with set_current_vllm_config(VllmConfig(device_config=device_config)):
|
||||
|
||||
@@ -1,12 +1,39 @@
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from vllm_omni.diffusion.models.qwen_image.qwen_image_transformer import QwenEmbedRope
|
||||
from vllm_omni.diffusion.models.qwen_image.qwen_image_transformer import (
|
||||
QwenEmbedRope,
|
||||
_apply_qwen_image_rotary_emb,
|
||||
)
|
||||
from vllm_omni.diffusion.models.qwen_image.rope_utils import txt_seq_lens_from_embeds
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [torch.float32, torch.float16, torch.bfloat16])
|
||||
@pytest.mark.parametrize("noncontiguous", [False, True])
|
||||
def test_qwen_rotary_preserves_frequency_precision(dtype, noncontiguous):
|
||||
from diffusers.models.transformers.transformer_qwenimage import apply_rotary_emb_qwen
|
||||
|
||||
generator = torch.Generator().manual_seed(42)
|
||||
x = torch.randn(2, 17, 3, 128, generator=generator).to(dtype)
|
||||
if noncontiguous:
|
||||
x = x.transpose(1, 2).contiguous().transpose(1, 2)
|
||||
angles = torch.randn(17, 64, generator=generator)
|
||||
freqs = torch.polar(torch.ones_like(angles), angles)
|
||||
expected = apply_rotary_emb_qwen(x, freqs, use_real=False)
|
||||
actual = _apply_qwen_image_rotary_emb(x, freqs)
|
||||
assert actual.dtype == dtype
|
||||
assert actual.shape == x.shape
|
||||
torch.testing.assert_close(actual, expected, rtol=0, atol=0)
|
||||
|
||||
if dtype == torch.bfloat16:
|
||||
# A regression to early BF16 frequency rounding must be observable.
|
||||
rounded_freqs = torch.complex(freqs.real.to(dtype).float(), freqs.imag.to(dtype).float())
|
||||
rounded = _apply_qwen_image_rotary_emb(x, rounded_freqs)
|
||||
assert not torch.equal(actual, rounded)
|
||||
|
||||
|
||||
def test_txt_seq_lens_from_embeds_uses_padded_width_not_valid_token_count():
|
||||
prompt_embeds = torch.zeros(2, 64, 8)
|
||||
prompt_embeds_mask = torch.zeros(2, 64, dtype=torch.bool)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
import os
|
||||
|
||||
@@ -22,7 +22,7 @@ def _init_distributed(monkeypatch):
|
||||
|
||||
monkeypatch.setattr(
|
||||
"vllm.model_executor.layers.linear.dispatch_unquantized_gemm",
|
||||
lambda: default_unquantized_gemm,
|
||||
lambda *_args, **_kwargs: default_unquantized_gemm,
|
||||
)
|
||||
os.environ.setdefault("MASTER_ADDR", "localhost")
|
||||
os.environ.setdefault("MASTER_PORT", "29501")
|
||||
|
||||
@@ -41,8 +41,8 @@ def tp1_group():
|
||||
|
||||
os.environ.setdefault("MASTER_ADDR", "localhost")
|
||||
os.environ.setdefault("MASTER_PORT", "29501")
|
||||
init_distributed_environment(world_size=1, rank=0, local_rank=0, distributed_init_method="env://")
|
||||
initialize_model_parallel()
|
||||
init_distributed_environment(world_size=1, rank=0, local_rank=0, distributed_init_method="env://", backend="gloo")
|
||||
initialize_model_parallel(backend="gloo")
|
||||
yield
|
||||
cleanup_dist_env_and_memory()
|
||||
|
||||
@@ -54,7 +54,7 @@ def force_default_gemm(monkeypatch):
|
||||
|
||||
monkeypatch.setattr(
|
||||
"vllm.model_executor.layers.linear.dispatch_unquantized_gemm",
|
||||
lambda: default_unquantized_gemm,
|
||||
lambda *_args, **_kwargs: default_unquantized_gemm,
|
||||
)
|
||||
|
||||
|
||||
@@ -482,8 +482,12 @@ class _LockstepP2PDist:
|
||||
def test_tiny_transformer_sp2_lockstep_matches_dense(tp1_group, force_default_gemm, mocker, task) -> None:
|
||||
"""Two lockstep rank threads over uneven frame shards must reproduce the
|
||||
dense tiny-transformer output, and must actually have sharded the work."""
|
||||
from vllm_omni.diffusion.attention.backends.sdpa import SDPABackend
|
||||
from vllm_omni.diffusion.models.sana_video import SanaVideoTransformer3DModel
|
||||
|
||||
# These CPU tensor tests must not probe a CUDA device during construction.
|
||||
mocker.patch("vllm_omni.diffusion.attention.selector._cached_get_backend_cls", return_value=SDPABackend)
|
||||
|
||||
torch.manual_seed(3)
|
||||
model = SanaVideoTransformer3DModel(**_TINY_SP_CONFIG).eval()
|
||||
for _, param in sorted(model.named_parameters()):
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
"""TP unit tests for SANA-Video: distributed RMSNorm, sharded attention and
|
||||
sharded GLUMB temporal conv.
|
||||
@@ -51,7 +51,7 @@ def force_default_gemm(monkeypatch):
|
||||
|
||||
monkeypatch.setattr(
|
||||
"vllm.model_executor.layers.linear.dispatch_unquantized_gemm",
|
||||
lambda: default_unquantized_gemm,
|
||||
lambda *_args, **_kwargs: default_unquantized_gemm,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
import pytest
|
||||
import torch
|
||||
from transformers import T5Config
|
||||
@@ -54,6 +54,13 @@ def setup_tp_group(monkeypatch, mocker):
|
||||
lambda: 0,
|
||||
)
|
||||
|
||||
# vLLM 0.29 added a CUDA-only fused embedding kernel that engages whenever
|
||||
# tp_size > 1; these tests drive CPU tensors, so keep the portable masked path.
|
||||
monkeypatch.setattr(
|
||||
"vllm.model_executor.layers.vocab_parallel_embedding.current_platform.is_cuda",
|
||||
lambda: False,
|
||||
)
|
||||
|
||||
# TP group
|
||||
mock_tp_group = mocker.MagicMock()
|
||||
mock_tp_group.world_size = 2
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Tests for component routing for quantization."""
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
@@ -372,7 +372,8 @@ class TestComponentApplyVllmMapper:
|
||||
"thinker.model": "thinker.language_model.model",
|
||||
"talker.model": "talker.language_model.model",
|
||||
}
|
||||
).get_unstacked_mapper()
|
||||
# vLLM 0.29 renamed get_unstacked_mapper to get_rename_mapper.
|
||||
).get_rename_mapper()
|
||||
|
||||
config.apply_vllm_mapper(mapper)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
import asyncio
|
||||
import queue
|
||||
@@ -10,8 +10,9 @@ import pytest
|
||||
import torch
|
||||
import vllm.v1.core.single_type_kv_cache_manager as native_kv_managers
|
||||
from pytest_mock import MockerFixture
|
||||
from vllm.v1.kv_cache_interface import FullAttentionSpec, KVCacheConfig, KVCacheGroupSpec, KVCacheTensor
|
||||
from vllm.v1.kv_cache_interface import FullAttentionSpec, KVCacheConfig, KVCacheGroupSpec
|
||||
|
||||
from tests.helpers.kv_layout import build_kv_cache_tensor
|
||||
from vllm_omni.diffusion.data import DiffusionOutput, DiffusionRequestAbortedError
|
||||
from vllm_omni.diffusion.diffusion_engine import DiffusionEngine, DiffusionExecutionMode
|
||||
from vllm_omni.diffusion.diffusion_kv.config import DiffusionKVCacheMode
|
||||
@@ -104,7 +105,7 @@ def _initialize_paged_scheduler(
|
||||
)
|
||||
config = KVCacheConfig(
|
||||
num_blocks=num_blocks,
|
||||
kv_cache_tensors=[KVCacheTensor(size=spec.page_size_bytes * num_blocks, shared_by=["layer0"])],
|
||||
kv_cache_tensors=[build_kv_cache_tensor(spec, num_blocks, ["layer0"])],
|
||||
kv_cache_groups=[KVCacheGroupSpec(layer_names=["layer0"], kv_cache_spec=spec)],
|
||||
)
|
||||
scheduler.initialize(
|
||||
|
||||
@@ -164,6 +164,7 @@ def _run_diffusers_wan22_i2v(*, model: str, output_path: Path, conditioning_imag
|
||||
)
|
||||
pipe.scheduler = UniPCMultistepScheduler.from_config(pipe.scheduler.config, flow_shift=FLOW_SHIFT)
|
||||
pipe.to("cuda")
|
||||
_set_matched_attention_backend(pipe)
|
||||
|
||||
_diffusers_dummy_run(pipe)
|
||||
|
||||
|
||||
@@ -85,5 +85,7 @@ def test_text_only_nonstreaming_pcm_001(omni_server, openai_client) -> None:
|
||||
"voice": "default",
|
||||
"min_audio_bytes": _MIN_AUDIO_BYTES,
|
||||
"min_hnr_db": -2.0,
|
||||
# dots.tts-soar emits native 48 kHz PCM, which has no rate header.
|
||||
"expected_sample_rate": 48_000,
|
||||
}
|
||||
openai_client.send_audio_speech_request(request_config)
|
||||
@@ -1593,73 +1593,58 @@ def test_model_path_resolver_is_generic_and_model_owned(tmp_path):
|
||||
assert "model_path_resolver" not in engine_args
|
||||
|
||||
|
||||
def test_build_stage0_input_processor_uses_omni_input_preprocessor(monkeypatch):
|
||||
def test_build_stage0_input_processor_uses_omni_renderer_subclass(monkeypatch):
|
||||
from vllm.renderers import BaseRenderer
|
||||
|
||||
import vllm_omni.engine.stage_init_utils as init_mod
|
||||
from vllm_omni.inputs.preprocess import OmniRenderer, omni_renderer_cls
|
||||
|
||||
class DummyInputProcessor:
|
||||
def __init__(self, vllm_config, renderer=None):
|
||||
self.vllm_config = vllm_config
|
||||
self.renderer = renderer or object()
|
||||
self.input_preprocessor = None
|
||||
class _Base(BaseRenderer):
|
||||
def __init__(self, config, tokenizer):
|
||||
self.config, self.tokenizer = config, tokenizer
|
||||
|
||||
class DummyOmniInputPreprocessor:
|
||||
def __init__(self, vllm_config, renderer=None):
|
||||
self.vllm_config = vllm_config
|
||||
self.renderer = renderer
|
||||
def render_messages(self, messages, params): # pragma: no cover - abstract stub
|
||||
raise NotImplementedError
|
||||
|
||||
monkeypatch.setattr(init_mod, "InputProcessor", DummyInputProcessor)
|
||||
monkeypatch.setattr(init_mod, "OmniInputPreprocessor", DummyOmniInputPreprocessor)
|
||||
|
||||
input_processor = build_stage0_input_processor(
|
||||
types.SimpleNamespace(model_config=types.SimpleNamespace(try_get_generation_config=lambda: {}))
|
||||
)
|
||||
|
||||
assert isinstance(input_processor.input_preprocessor, DummyOmniInputPreprocessor)
|
||||
assert input_processor.input_preprocessor.renderer is input_processor.renderer
|
||||
|
||||
|
||||
def test_build_stage0_input_processor_does_not_resolve_tokenizer_when_skipped(
|
||||
monkeypatch,
|
||||
):
|
||||
import vllm_omni.engine.stage_init_utils as init_mod
|
||||
|
||||
token_only_renderer = object()
|
||||
config = types.SimpleNamespace(model_config=types.SimpleNamespace(try_get_generation_config=lambda: {}))
|
||||
built = omni_renderer_cls(_Base)(config, "tok")
|
||||
seen = {}
|
||||
|
||||
class DummyInputProcessor:
|
||||
def __init__(self, vllm_config, renderer=None):
|
||||
if renderer is None:
|
||||
raise AssertionError("skip_tokenizer_init must supply a renderer")
|
||||
seen["renderer"] = renderer
|
||||
self.renderer = renderer
|
||||
self.input_preprocessor = None
|
||||
|
||||
class DummyOmniInputPreprocessor:
|
||||
def __init__(self, vllm_config, renderer=None):
|
||||
seen["preprocessor_renderer"] = renderer
|
||||
|
||||
monkeypatch.setattr(init_mod, "InputProcessor", DummyInputProcessor)
|
||||
monkeypatch.setattr(
|
||||
init_mod,
|
||||
"_build_token_only_renderer",
|
||||
lambda _config: token_only_renderer,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
init_mod,
|
||||
"OmniInputPreprocessor",
|
||||
DummyOmniInputPreprocessor,
|
||||
)
|
||||
monkeypatch.setattr(init_mod, "build_omni_renderer", lambda cfg: built if cfg is config else None)
|
||||
processor = build_stage0_input_processor(config)
|
||||
assert seen["renderer"] is built
|
||||
assert isinstance(processor.renderer, OmniRenderer)
|
||||
assert isinstance(processor.renderer, _Base)
|
||||
assert not hasattr(processor, "input_preprocessor")
|
||||
|
||||
|
||||
def test_build_stage0_input_processor_does_not_resolve_tokenizer_when_skipped(monkeypatch):
|
||||
import vllm_omni.engine.stage_init_utils as init_mod
|
||||
|
||||
original = object()
|
||||
|
||||
class DummyInputProcessor:
|
||||
def __init__(self, vllm_config, renderer=None):
|
||||
assert renderer is original
|
||||
self.renderer = renderer
|
||||
|
||||
def _must_not_resolve(_cfg):
|
||||
raise AssertionError("tokenizer must not be resolved when skip_tokenizer_init=True")
|
||||
|
||||
monkeypatch.setattr(init_mod, "InputProcessor", DummyInputProcessor)
|
||||
monkeypatch.setattr(init_mod, "_build_token_only_renderer", lambda _: original)
|
||||
monkeypatch.setattr(init_mod, "build_omni_renderer", _must_not_resolve)
|
||||
config = types.SimpleNamespace(
|
||||
model_config=types.SimpleNamespace(
|
||||
skip_tokenizer_init=True,
|
||||
try_get_generation_config=lambda: {},
|
||||
)
|
||||
model_config=types.SimpleNamespace(skip_tokenizer_init=True, try_get_generation_config=lambda: {})
|
||||
)
|
||||
build_stage0_input_processor(config)
|
||||
|
||||
assert seen["renderer"] is token_only_renderer
|
||||
assert seen["preprocessor_renderer"] is token_only_renderer
|
||||
processor = build_stage0_input_processor(config)
|
||||
assert processor.renderer is original
|
||||
|
||||
|
||||
def test_inject_kv_stage_info_infers_sender_tp_topology():
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
import asyncio
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
@@ -12,7 +15,7 @@ from vllm.entrypoints.openai.chat_completion.protocol import (
|
||||
ChatCompletionResponseChoice,
|
||||
ChatMessage,
|
||||
)
|
||||
from vllm.entrypoints.openai.engine.protocol import ErrorResponse, UsageInfo
|
||||
from vllm.entrypoints.serve.engine.protocol import ErrorResponse, UsageInfo
|
||||
|
||||
from vllm_omni.entrypoints.openai.batch_serving import OmniOpenAIServingChatBatch
|
||||
|
||||
|
||||
@@ -248,7 +248,7 @@ class TestResolveAudioFormat:
|
||||
assert result == "pcm"
|
||||
|
||||
def test_invalid_format_returns_error(self, serving_chat):
|
||||
from vllm.entrypoints.openai.engine.protocol import ErrorResponse
|
||||
from vllm.entrypoints.serve.engine.protocol import ErrorResponse
|
||||
|
||||
request = self._make_request({"format": "aac", "voice": "alloy"})
|
||||
result = serving_chat._resolve_audio_format(request)
|
||||
@@ -256,7 +256,7 @@ class TestResolveAudioFormat:
|
||||
assert "aac" in result.error.message
|
||||
|
||||
def test_all_supported_formats_accepted(self, serving_chat):
|
||||
from vllm.entrypoints.openai.engine.protocol import ErrorResponse
|
||||
from vllm.entrypoints.serve.engine.protocol import ErrorResponse
|
||||
|
||||
for fmt in SUPPORTED_CHAT_AUDIO_FORMATS:
|
||||
request = self._make_request({"format": fmt, "voice": "alloy"})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Regression tests for output modality validation in chat completions.
|
||||
|
||||
Covers:
|
||||
@@ -13,7 +14,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionRequest
|
||||
from vllm.entrypoints.openai.engine.protocol import ErrorResponse
|
||||
from vllm.entrypoints.serve.engine.protocol import ErrorResponse
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Unit tests for OmniChatCompletionResponse/StreamResponse metrics field."""
|
||||
|
||||
from types import SimpleNamespace
|
||||
@@ -19,7 +20,7 @@ pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||
|
||||
def test_omni_chat_completion_response_metrics():
|
||||
"""Test OmniChatCompletionResponse metrics field works correctly."""
|
||||
from vllm.entrypoints.openai.engine.protocol import UsageInfo
|
||||
from vllm.entrypoints.serve.engine.protocol import UsageInfo
|
||||
|
||||
from vllm_omni.entrypoints.openai.protocol.chat_completion import (
|
||||
OmniChatCompletionResponse,
|
||||
|
||||
@@ -25,7 +25,7 @@ from fastapi.responses import JSONResponse
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import ValidationError
|
||||
from pytest_mock import MockerFixture
|
||||
from vllm.entrypoints.openai.engine.protocol import ErrorInfo, ErrorResponse
|
||||
from vllm.entrypoints.serve.engine.protocol import ErrorInfo, ErrorResponse
|
||||
|
||||
from vllm_omni.diffusion.request import OmniDiffusionRequest
|
||||
from vllm_omni.diffusion.sched.request_scheduler import RequestScheduler
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Runtime behavior tests for VideoStreamHandler.
|
||||
|
||||
Every test creates a mock WebSocket, drives handle_session through a
|
||||
@@ -281,7 +281,7 @@ class TestHandleQueryErrors:
|
||||
@pytest.mark.asyncio
|
||||
async def test_query_returns_error_response(self):
|
||||
"""create_chat_completion returns ErrorResponse → error forwarded."""
|
||||
from vllm.entrypoints.openai.engine.protocol import ErrorInfo, ErrorResponse
|
||||
from vllm.entrypoints.serve.engine.protocol import ErrorInfo, ErrorResponse
|
||||
|
||||
err = ErrorResponse(
|
||||
error=ErrorInfo(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Tests for multi-modal streaming finish_reason behavior (commit 44c799bc).
|
||||
|
||||
Verifies that the /v1/chat/completions streaming endpoint emits exactly one
|
||||
@@ -411,7 +412,7 @@ async def test_audio_chunk_without_waveform_keeps_stream_alive():
|
||||
@pytest.mark.asyncio
|
||||
async def test_audio_choice_error_response_is_not_iterated_as_choices():
|
||||
"""Any ErrorResponse from the audio path is skipped, not iterated."""
|
||||
from vllm.entrypoints.openai.engine.protocol import ErrorResponse
|
||||
from vllm.entrypoints.serve.engine.protocol import ErrorResponse
|
||||
|
||||
serving_chat = build_serving_chat()
|
||||
request = make_request(modalities=["text", "audio"])
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Shared access to the production ``KVCacheTensor`` layout derivation.
|
||||
|
||||
Tests deliberately go through the same code production uses
|
||||
(``vllm_omni.diffusion.diffusion_kv.layout``) rather than recomputing strides,
|
||||
so a regression in that derivation fails the suite instead of being masked by a
|
||||
parallel test-only copy.
|
||||
"""
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
from vllm.config import CacheConfig
|
||||
from vllm.v1.kv_cache_layout import KVCacheLayout
|
||||
|
||||
from vllm_omni.diffusion.diffusion_kv.layout import (
|
||||
build_kv_cache_tensor,
|
||||
resolve_diffusion_kv_cache_layout,
|
||||
)
|
||||
|
||||
__all__ = ["build_kv_cache_tensor", "layout_for_backend"]
|
||||
|
||||
|
||||
def layout_for_backend(attn_backend) -> KVCacheLayout:
|
||||
"""The physical layout ``attn_backend``'s block-stride declaration selects.
|
||||
|
||||
Before 0.29 a backend's ``indexes_kv_by_block_stride()`` was copied onto
|
||||
``AttentionSpec``; asserting on the resolved layout is the surviving way to
|
||||
check that declaration still reaches the cache.
|
||||
"""
|
||||
|
||||
vllm_config = SimpleNamespace(cache_config=CacheConfig())
|
||||
return resolve_diffusion_kv_cache_layout(
|
||||
vllm_config,
|
||||
indexes_kv_by_block_stride=attn_backend.indexes_kv_by_block_stride(),
|
||||
)
|
||||
@@ -1,4 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Shared scaffolding for OmniOpenAIServingChat unit tests.
|
||||
|
||||
Provides factories for a minimal serving-chat instance, chat requests, and
|
||||
@@ -8,11 +9,11 @@ OmniRequestOutput objects, plus SSE stream collection/parsing utilities.
|
||||
import json
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from vllm.entrypoints.generate.base.protocol import DeltaMessage
|
||||
from vllm.entrypoints.openai.chat_completion.protocol import (
|
||||
ChatCompletionRequest,
|
||||
ChatCompletionResponseStreamChoice,
|
||||
)
|
||||
from vllm.entrypoints.openai.engine.protocol import DeltaMessage
|
||||
from vllm.entrypoints.openai.models.serving import OpenAIServingModels
|
||||
from vllm.outputs import CompletionOutput, RequestOutput
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
from transformers import BatchFeature
|
||||
|
||||
from vllm_omni.inputs.mm_processor import OmniMultiModalProcessor
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("counts", [{}, {"audio": 1, "image": 0}])
|
||||
@pytest.mark.parametrize("original_text", [None, "", "voice cloning text"])
|
||||
def test_custom_processor_bridge_preserves_prompt_kwargs_and_passthrough(counts, original_text):
|
||||
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(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"}))
|
||||
items = Mock()
|
||||
items.get_all_counts.return_value = counts
|
||||
kwargs = {"sampling_rate": 16000}
|
||||
if original_text is not None:
|
||||
kwargs[processor._OMNI_PROMPT_TEXT_KEY] = original_text
|
||||
before = dict(kwargs)
|
||||
result = processor._apply_hf_processor_main(items, kwargs)
|
||||
items.select.assert_called_once_with({k for k, v in counts.items() if v > 0})
|
||||
processor._call_hf_processor.assert_called_once_with(
|
||||
"dummy" if original_text is None else original_text,
|
||||
{"audios": ["wave"]},
|
||||
{"sampling_rate": 16000},
|
||||
{},
|
||||
)
|
||||
assert dict(result) == {"features": "processed", "embedding": "preserve"}
|
||||
assert kwargs == before
|
||||
+146
-24
@@ -1,35 +1,157 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Unit tests for OmniInputPreprocessor._process_text routing."""
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Exercise Omni routing through the active upstream rendering pipeline."""
|
||||
|
||||
import asyncio
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, Mock
|
||||
|
||||
import pytest
|
||||
from pytest_mock import MockerFixture
|
||||
import torch
|
||||
from vllm.inputs import tokens_input
|
||||
from vllm.pooling_params import PoolingParams
|
||||
from vllm.renderers import BaseRenderer
|
||||
from vllm.renderers.params import TokenizeParams
|
||||
from vllm.v1.engine.input_processor import InputProcessor
|
||||
|
||||
from vllm_omni.inputs import preprocess as preprocess_mod
|
||||
from vllm_omni.inputs.preprocess import OmniRenderer, build_omni_renderer, omni_renderer_cls
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||
|
||||
|
||||
class TestProcessTextMmProcessorKwargsRouting:
|
||||
"""Presence-based routing: an explicit empty ``mm_processor_kwargs`` dict
|
||||
on the prompt still routes through ``_process_multimodal``. Required for
|
||||
AR-based image generation (e.g. GLM-Image t2i) where the HF processor
|
||||
supplies the image-generation scaffold from its own defaults and no extra
|
||||
kwargs are needed from the caller.
|
||||
"""
|
||||
class _Renderer(BaseRenderer):
|
||||
def __init__(self):
|
||||
self.model_config = SimpleNamespace(is_encoder_decoder=False, enable_prompt_embeds=True)
|
||||
self.tokenizer = None
|
||||
self.default_cmpl_tok_params = TokenizeParams(max_total_tokens=128)
|
||||
self._tokenize_prompt = lambda prompt, params: {**prompt, "prompt_token_ids": [1, 2, 3]}
|
||||
self._tokenize_prompt_async = AsyncMock(side_effect=self._tokenize_prompt)
|
||||
self._process_multimodal = Mock(return_value=tokens_input([1, 2, 3, 99]))
|
||||
self._process_multimodal_async = AsyncMock(side_effect=self._process_multimodal)
|
||||
|
||||
@pytest.fixture
|
||||
def preprocessor(self, mocker: MockerFixture):
|
||||
from vllm_omni.inputs.preprocess import OmniInputPreprocessor
|
||||
def render_messages(self, messages, params):
|
||||
return messages, {"prompt": "hello"}
|
||||
|
||||
instance = object.__new__(OmniInputPreprocessor)
|
||||
instance._process_multimodal = mocker.MagicMock(return_value={})
|
||||
instance._tokenize_prompt = mocker.MagicMock(return_value=[1, 2, 3])
|
||||
return instance
|
||||
|
||||
def test_empty_mm_processor_kwargs_routes_to_multimodal(self, preprocessor):
|
||||
preprocessor._process_text({"prompt": "hello", "mm_processor_kwargs": {}})
|
||||
assert preprocessor._process_multimodal.called
|
||||
assert not preprocessor._tokenize_prompt.called
|
||||
@pytest.fixture
|
||||
def renderer():
|
||||
return omni_renderer_cls(_Renderer)()
|
||||
|
||||
def test_missing_mm_processor_kwargs_routes_to_tokenize(self, preprocessor):
|
||||
preprocessor._process_text({"prompt": "hello"})
|
||||
assert preprocessor._tokenize_prompt.called
|
||||
assert not preprocessor._process_multimodal.called
|
||||
|
||||
@pytest.mark.parametrize("kwargs", [{}, {"target_h": 512, "target_w": 768}])
|
||||
@pytest.mark.parametrize("tokenized", [False, True])
|
||||
def test_process_inputs_routes_no_media_processor_kwargs(renderer, kwargs, tokenized):
|
||||
# Keep real process_inputs/render_cmpl; stub unrelated config validation.
|
||||
processor = object.__new__(InputProcessor)
|
||||
processor.renderer = renderer
|
||||
processor.model_config = renderer.model_config
|
||||
processor.vllm_config = SimpleNamespace(
|
||||
parallel_config=SimpleNamespace(
|
||||
data_parallel_size=1,
|
||||
data_parallel_size_local=1,
|
||||
local_engines_only=False,
|
||||
)
|
||||
)
|
||||
processor._validate_params = Mock()
|
||||
processor._validate_lora = Mock()
|
||||
processor._validate_model_inputs = Mock()
|
||||
prompt: dict[str, object] = {"prompt_token_ids": [1, 2, 3]} if tokenized else {"prompt": "hello"}
|
||||
prompt.update(mm_processor_kwargs=kwargs, cache_salt="salt")
|
||||
request = processor.process_inputs("image-request", prompt, PoolingParams(), ("embed",))
|
||||
assert request.prompt_token_ids == [1, 2, 3, 99]
|
||||
assert request.cache_salt == "salt"
|
||||
renderer._process_multimodal.assert_called_once_with(
|
||||
[1, 2, 3],
|
||||
{},
|
||||
mm_processor_kwargs=kwargs,
|
||||
mm_uuids=None,
|
||||
skip_mm_cache=False,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("async_mode", [False, True])
|
||||
@pytest.mark.parametrize("kind", ["text", "tokens", "media", "kwargs", "embeds"])
|
||||
def test_renderer_preserves_routes_extras_and_cache_policy(renderer, async_mode, kind):
|
||||
prompt: dict[str, object] = {
|
||||
"prompt": "hello",
|
||||
"additional_information": {"speaker": 1},
|
||||
"model_intermediate_buffer": {"x": 2},
|
||||
}
|
||||
if kind == "tokens":
|
||||
prompt["prompt_token_ids"] = [1, 2, 3]
|
||||
elif kind == "media":
|
||||
prompt["multi_modal_data"] = {"image": "image"}
|
||||
elif kind == "kwargs":
|
||||
prompt["mm_processor_kwargs"] = {}
|
||||
elif kind == "embeds":
|
||||
prompt["prompt_embeds"] = torch.ones(3, 4)
|
||||
if async_mode:
|
||||
(result,) = asyncio.run(renderer.render_cmpl_async([prompt], skip_mm_cache=True))
|
||||
else:
|
||||
(result,) = renderer.render_cmpl([prompt], skip_mm_cache=True)
|
||||
assert result["additional_information"] == {"speaker": 1}
|
||||
assert result["model_intermediate_buffer"] == {"x": 2}
|
||||
assert result["prompt"] == "hello"
|
||||
if kind in ("media", "kwargs"):
|
||||
assert result["prompt_token_ids"] == [1, 2, 3, 99]
|
||||
assert renderer._process_multimodal.call_args.kwargs["skip_mm_cache"] is True
|
||||
else:
|
||||
renderer._process_multimodal.assert_not_called()
|
||||
if kind == "embeds":
|
||||
torch.testing.assert_close(result["prompt_embeds"], prompt["prompt_embeds"])
|
||||
else:
|
||||
assert result["prompt_token_ids"] == [1, 2, 3]
|
||||
|
||||
|
||||
def test_omni_renderer_is_a_real_subclass_created_once():
|
||||
cls = omni_renderer_cls(_Renderer)
|
||||
assert issubclass(cls, OmniRenderer) and issubclass(cls, _Renderer)
|
||||
assert omni_renderer_cls(_Renderer) is cls
|
||||
assert omni_renderer_cls(cls) is cls
|
||||
assert cls.__name__ == "Omni_Renderer"
|
||||
instance = cls()
|
||||
assert isinstance(instance, _Renderer)
|
||||
assert not hasattr(instance, "_renderer")
|
||||
|
||||
|
||||
def test_concrete_renderer_overrides_are_not_suppressed():
|
||||
"""The old proxy bound BaseRenderer.render_cmpl itself and hid subclass overrides."""
|
||||
|
||||
class _Custom(_Renderer):
|
||||
def render_cmpl(self, prompts, *args, **kwargs):
|
||||
return ["custom"]
|
||||
|
||||
assert omni_renderer_cls(_Custom)().render_cmpl([{"prompt": "x"}]) == ["custom"]
|
||||
|
||||
|
||||
class _CtorRenderer(BaseRenderer):
|
||||
def __init__(self, config, tokenizer):
|
||||
self.config = config
|
||||
self.tokenizer = tokenizer
|
||||
|
||||
def render_messages(self, messages, params):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def test_build_omni_renderer_with_explicit_class_skips_registry(monkeypatch):
|
||||
monkeypatch.setattr(preprocess_mod, "cached_tokenizer_from_config", Mock(side_effect=AssertionError("resolved")))
|
||||
config = SimpleNamespace(model_config=SimpleNamespace())
|
||||
built = build_omni_renderer(config, renderer_cls=_CtorRenderer, tokenizer=None)
|
||||
assert isinstance(built, OmniRenderer) and isinstance(built, _CtorRenderer)
|
||||
assert built.config is config and built.tokenizer is None
|
||||
|
||||
|
||||
def test_build_omni_renderer_resolves_like_upstream(monkeypatch):
|
||||
config = SimpleNamespace(model_config=SimpleNamespace())
|
||||
tokenizer = object()
|
||||
monkeypatch.setattr(
|
||||
preprocess_mod, "cached_tokenizer_from_config", lambda mc: tokenizer if mc is config.model_config else None
|
||||
)
|
||||
monkeypatch.setattr(preprocess_mod, "tokenizer_args_from_config", lambda mc: ("fake-mode", None))
|
||||
load_cls = Mock(return_value=_CtorRenderer)
|
||||
monkeypatch.setattr(preprocess_mod.RENDERER_REGISTRY, "load_renderer_cls", load_cls)
|
||||
built = build_omni_renderer(config)
|
||||
load_cls.assert_called_once_with("fake-mode")
|
||||
assert isinstance(built, OmniRenderer) and isinstance(built, _CtorRenderer)
|
||||
assert built.tokenizer is tokenizer
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Unit tests for GLM-Image AR model: DataParser, processor, and M-RoPE."""
|
||||
|
||||
import importlib.util
|
||||
@@ -91,6 +91,41 @@ GlmImageRotaryEmbedding = _ar_mod.GlmImageRotaryEmbedding
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("num_images", [0, 1, 2])
|
||||
@pytest.mark.parametrize("preformatted", [False, True])
|
||||
def test_generation_prompt_ids_preserve_hf_target_scaffold(num_images, preformatted):
|
||||
from types import SimpleNamespace
|
||||
|
||||
from vllm.multimodal.parse import MultiModalDataItems
|
||||
from vllm.multimodal.processing import BaseMultiModalProcessor, ProcessorInputs
|
||||
|
||||
processor = object.__new__(GlmImageMultiModalProcessor)
|
||||
# Only the public token attributes of GlmImageProcessor are needed; the
|
||||
# expected suffix below is what HF's own formatter produces for 512x768.
|
||||
hf_processor = SimpleNamespace(grid_bos_token="<sop>", grid_eos_token="<eop>", bos_token="<image_start>")
|
||||
tokenizer = SimpleNamespace(encode=lambda text, **kwargs: list(text.encode()))
|
||||
processor.info = SimpleNamespace(
|
||||
get_tokenizer=lambda: tokenizer,
|
||||
get_hf_processor=lambda: hf_processor,
|
||||
get_hf_config=lambda: _make_hf_config(),
|
||||
)
|
||||
items = MagicMock(spec=MultiModalDataItems)
|
||||
items.get_all_counts.return_value = {"image": num_images}
|
||||
suffix = "<sop>16 24<eop>" + ("<sop>13 19<eop>" if not num_images else "") + "<image_start>"
|
||||
text_ids = list(b"a red bird")
|
||||
expected = [167855] * num_images + text_ids + list(suffix.encode())
|
||||
inputs = ProcessorInputs(
|
||||
prompt=expected if preformatted else text_ids,
|
||||
mm_data_items=items,
|
||||
hf_processor_mm_kwargs={"target_h": 512, "target_w": 768},
|
||||
)
|
||||
with patch.object(BaseMultiModalProcessor, "apply", side_effect=lambda inputs, timing: inputs) as upstream:
|
||||
result = processor.apply(inputs, MagicMock())
|
||||
assert result.prompt == expected
|
||||
assert inputs.prompt == (expected if preformatted else text_ids)
|
||||
upstream.assert_called_once()
|
||||
|
||||
|
||||
def test_glm_image_ar_omits_hidden_pooler_payload():
|
||||
assert GlmImageForConditionalGeneration.omni_pooler_payload_include_hidden is False
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Regression tests for the MiniCPM-o 4.5 ``audio_embeds`` input path.
|
||||
|
||||
Two defects made the OpenAI ``audio_embeds`` content part unusable with this model:
|
||||
@@ -71,6 +71,12 @@ class _IdentityTokenizer:
|
||||
def decode(self, tokens: str) -> str:
|
||||
return tokens
|
||||
|
||||
def convert_tokens_to_ids(self, token: str) -> int:
|
||||
# vLLM 0.29 prompt updates select placeholder positions by token id,
|
||||
# so the processor resolves ``<unk>`` once; the value is irrelevant here.
|
||||
assert token == "<unk>"
|
||||
return 0
|
||||
|
||||
|
||||
def _processing_info() -> MiniCPMO45OmniLLMProcessingInfo:
|
||||
processor = _FakeProcessor()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Regression tests for MiniCPM-o 4.5 >30s audio preprocessing.
|
||||
|
||||
Covers:
|
||||
@@ -43,6 +43,34 @@ _FEAT_DIM = 128
|
||||
_CHUNK_FRAMES = 3000
|
||||
|
||||
|
||||
@pytest.mark.parametrize("version", [(2, 5), (4, 5)])
|
||||
def test_prompt_batching_delegates_upstream_without_tts_kwargs(version):
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
info = SimpleNamespace(
|
||||
get_model_version=lambda: version,
|
||||
get_hf_processor=MagicMock(return_value=object()),
|
||||
ctx=SimpleNamespace(call_hf_processor=MagicMock()),
|
||||
)
|
||||
info.ctx.call_hf_processor.side_effect = (
|
||||
[{"features": [1]}, {"features": [2]}] if version == (2, 5) else [{"features": [1, 2]}]
|
||||
)
|
||||
kwargs = {"use_tts": True, "sampling_rate": 16000}
|
||||
result = MiniCPMO45OmniLLMMultiModalProcessor._call_hf_processor_on_prompts(
|
||||
SimpleNamespace(info=info), ["one", "two"], {"audios": [10, 20]}, kwargs, out_keys={"features"}
|
||||
)
|
||||
assert result == {"features": [1, 2]}
|
||||
assert kwargs == {"use_tts": True, "sampling_rate": 16000}
|
||||
for call in info.ctx.call_hf_processor.call_args_list:
|
||||
assert call.args[2] == {"sampling_rate": 16000}
|
||||
payloads = [call.args[1] for call in info.ctx.call_hf_processor.call_args_list]
|
||||
assert payloads == (
|
||||
[{"text": "one", "audios": 10}, {"text": "two", "audios": 20}]
|
||||
if version == (2, 5)
|
||||
else [{"text": ["one", "two"], "audios": [10, 20]}]
|
||||
)
|
||||
|
||||
|
||||
class TestMiniCPMOFieldConfig:
|
||||
def test_short_audios_stay_batched(self) -> None:
|
||||
# two <=30s audios: one feature entry + one 1-element lens per audio
|
||||
@@ -99,10 +127,10 @@ def _make_processor(
|
||||
processor.info = SimpleNamespace(audio_pattern="(<audio>./</audio>)")
|
||||
processor.data_parser = MiniCPMOMultiModalDataParser(target_sr=_SAMPLE_RATE)
|
||||
|
||||
def fake_base_call_hf_processor(prompts, mm_data, mm_kwargs, tok_kwargs, *, out_keys):
|
||||
def fake_call_hf_processor_on_prompts(prompts, mm_data, mm_kwargs, *, out_keys):
|
||||
return {key: fake_hf_outputs[key] for key in out_keys}
|
||||
|
||||
processor._base_call_hf_processor = fake_base_call_hf_processor
|
||||
processor._call_hf_processor_on_prompts = fake_call_hf_processor_on_prompts
|
||||
return processor
|
||||
|
||||
|
||||
@@ -120,7 +148,7 @@ class TestProcessAudiosUnpadding:
|
||||
}
|
||||
processor = _make_processor(fake_outputs)
|
||||
|
||||
result = processor.process_audios({"audios": [_audio_seconds(45)]}, {}, {})
|
||||
result = processor.process_audios({"audios": [_audio_seconds(45)]}, {})
|
||||
|
||||
features = result["audio_features"]
|
||||
assert [feat.shape[-1] for feat in features] == chunk_lens
|
||||
@@ -138,7 +166,7 @@ class TestProcessAudiosUnpadding:
|
||||
}
|
||||
processor = _make_processor(fake_outputs)
|
||||
|
||||
result = processor.process_audios({"audios": [_audio_seconds(42), _audio_seconds(38)]}, {}, {})
|
||||
result = processor.process_audios({"audios": [_audio_seconds(42), _audio_seconds(38)]}, {})
|
||||
|
||||
widths = [feat.shape[-1] for feat in result["audio_features"]]
|
||||
assert widths == [_CHUNK_FRAMES, 1200, _CHUNK_FRAMES, 800]
|
||||
@@ -151,7 +179,7 @@ class TestProcessAudiosUnpadding:
|
||||
}
|
||||
processor = _make_processor(fake_outputs)
|
||||
|
||||
result = processor.process_audios({"audios": [_audio_seconds(9.8)]}, {}, {})
|
||||
result = processor.process_audios({"audios": [_audio_seconds(9.8)]}, {})
|
||||
|
||||
features = result["audio_features"]
|
||||
assert len(features) == 1
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""OmniVoice processor: the prompt is tokenized exactly once per request.
|
||||
|
||||
Upstream ``_cached_apply_hf_processor`` calls ``_apply_hf_processor_main`` for
|
||||
every request, including text-only requests and requests whose reference audio
|
||||
is already cached (empty ``mm_items``). These tests pin that only ``apply``
|
||||
touches the text tokenizer and that the media path only encodes the audio.
|
||||
"""
|
||||
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
import torch
|
||||
from vllm.multimodal.parse import MultiModalDataItems
|
||||
from vllm.multimodal.processing import ProcessorInputs
|
||||
|
||||
from vllm_omni.inputs.mm_processor import OmniMultiModalProcessor
|
||||
from vllm_omni.model_executor.models.omnivoice.omnivoice import OmniVoiceMultiModalProcessor
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||
|
||||
_PROMPT_IDS = [5, 6, 7]
|
||||
|
||||
|
||||
def _make_processor(monkeypatch: pytest.MonkeyPatch) -> OmniVoiceMultiModalProcessor:
|
||||
processor = object.__new__(OmniVoiceMultiModalProcessor)
|
||||
tokenizer = SimpleNamespace(decode=Mock(return_value="hello world"))
|
||||
processor.info = SimpleNamespace(
|
||||
ctx=SimpleNamespace(
|
||||
get_hf_config=lambda *_args, **_kwargs: SimpleNamespace(sample_rate=24000),
|
||||
model_config=SimpleNamespace(model="fake-model"),
|
||||
),
|
||||
get_tokenizer=lambda: tokenizer,
|
||||
)
|
||||
processor.text_tokenizer = Mock(return_value=SimpleNamespace(input_ids=torch.tensor([_PROMPT_IDS])))
|
||||
processor.audio_tokenizer = SimpleNamespace(encode=Mock(return_value=torch.zeros(8, 4, dtype=torch.long)))
|
||||
processor.feature_extractor = None
|
||||
monkeypatch.setattr(processor, "_ensure_cached_runtime_components", lambda *_args, **_kwargs: None)
|
||||
return processor
|
||||
|
||||
|
||||
def _items(counts: dict[str, int], mm_data: dict[str, object]) -> SimpleNamespace:
|
||||
return SimpleNamespace(
|
||||
get_all_counts=lambda: counts,
|
||||
select=lambda _keys: mm_data,
|
||||
)
|
||||
|
||||
|
||||
def test_apply_tokenizes_the_prompt_once(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
processor = _make_processor(monkeypatch)
|
||||
captured: dict[str, ProcessorInputs] = {}
|
||||
|
||||
def fake_parent_apply(self, inputs, timing_ctx):
|
||||
captured["inputs"] = inputs
|
||||
return "sentinel"
|
||||
|
||||
monkeypatch.setattr(OmniMultiModalProcessor, "apply", fake_parent_apply)
|
||||
inputs = ProcessorInputs(
|
||||
prompt=[1, 2],
|
||||
mm_data_items=MultiModalDataItems({}),
|
||||
hf_processor_mm_kwargs={"ref_text": "reference"},
|
||||
)
|
||||
|
||||
assert processor.apply(inputs, timing_ctx=None) == "sentinel"
|
||||
assert captured["inputs"].prompt == _PROMPT_IDS
|
||||
assert processor.text_tokenizer.call_count == 1
|
||||
(text_prompt,), _ = processor.text_tokenizer.call_args
|
||||
assert text_prompt.endswith("<|text_start|>reference hello world<|text_end|>")
|
||||
assert captured["inputs"].hf_processor_mm_kwargs["ref_text"] == "reference"
|
||||
|
||||
|
||||
def test_media_path_encodes_audio_without_retokenizing(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
processor = _make_processor(monkeypatch)
|
||||
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"})
|
||||
|
||||
assert tuple(out["ref_audio_tokens"].shape) == (8, 4)
|
||||
assert list(out["ref_audio_len"]) == [4]
|
||||
assert "input_ids" not in out
|
||||
assert processor.audio_tokenizer.encode.call_count == 1
|
||||
assert processor.text_tokenizer.call_count == 0
|
||||
|
||||
|
||||
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."""
|
||||
processor = _make_processor(monkeypatch)
|
||||
monkeypatch.setattr(processor, "_get_hf_mm_data", lambda _items: ({}, {}))
|
||||
|
||||
out = processor._apply_hf_processor_main(_items({}, {}), {"ref_text": "reference"})
|
||||
|
||||
assert len(out.keys()) == 0
|
||||
assert processor.text_tokenizer.call_count == 0
|
||||
assert processor.audio_tokenizer.encode.call_count == 0
|
||||
|
||||
|
||||
def test_legacy_call_hf_processor_is_gone() -> None:
|
||||
assert not hasattr(OmniVoiceMultiModalProcessor, "_call_hf_processor")
|
||||
@@ -266,8 +266,6 @@ def test_cached_apply_hf_processor_expands_pair_miss_for_audio_hit_video_miss():
|
||||
inputs = MagicMock()
|
||||
inputs.mm_data_items = mm_data_items
|
||||
inputs.hf_processor_mm_kwargs = {"use_audio_in_video": True}
|
||||
inputs.tokenization_kwargs = {}
|
||||
inputs.prompt = [1, 2, 3]
|
||||
inputs.get_mm_hashes = MagicMock(return_value={"video": ["v0"], "audio": ["a0"]})
|
||||
|
||||
timing_ctx = MagicMock()
|
||||
@@ -307,7 +305,7 @@ def test_cached_apply_hf_processor_expands_pair_miss_for_audio_hit_video_miss():
|
||||
)
|
||||
fake_self._paired_cache_keys = Qwen2_5OmniThinkerMultiModalProcessor._paired_cache_keys
|
||||
fake_self.info.parse_mm_data = MagicMock(return_value=SimpleNamespace(name="missing"))
|
||||
fake_self._apply_hf_processor_main = MagicMock(return_value=([9], {"video_grid_thw": object()}, False))
|
||||
fake_self._apply_hf_processor_main = MagicMock(return_value={"video_grid_thw": object()})
|
||||
fake_self._get_mm_fields_config = MagicMock(return_value={})
|
||||
fake_self._get_mm_prompt_updates = MagicMock(return_value={"video": [[object()]], "audio": [[object()]]})
|
||||
fake_self._merge_mm_kwargs = capture_merge
|
||||
@@ -316,13 +314,12 @@ def test_cached_apply_hf_processor_expands_pair_miss_for_audio_hit_video_miss():
|
||||
"vllm_omni.model_executor.models.qwen2_5_omni.qwen2_5_omni_thinker.MultiModalKwargsItems.from_hf_inputs",
|
||||
return_value={"video": [object()], "audio": [object()]},
|
||||
):
|
||||
prompt_ids, mm_info, _ = Qwen2_5OmniThinkerMultiModalProcessor._cached_apply_hf_processor(
|
||||
mm_info = Qwen2_5OmniThinkerMultiModalProcessor._cached_apply_hf_processor(
|
||||
fake_self,
|
||||
inputs,
|
||||
timing_ctx,
|
||||
)
|
||||
|
||||
assert prompt_ids == [9]
|
||||
# Both modalities were reprocessed as one unit after pair-miss expansion.
|
||||
assert fake_self.info.parse_mm_data.call_args.args[0] == {
|
||||
"video": ["video-0"],
|
||||
@@ -623,10 +620,17 @@ def test_derive_audio_from_video_placeholders_only_pairs_true_videos():
|
||||
|
||||
|
||||
def test_qwen3_processor_inherits_vllm_omni_per_video_helpers():
|
||||
from vllm.model_executor.models.qwen3_omni_moe_thinker import (
|
||||
Qwen3OmniMoeThinkerMultiModalProcessor as UpstreamQwen3Processor,
|
||||
)
|
||||
|
||||
assert issubclass(
|
||||
Qwen3OmniMoeThinkerMultiModalProcessor,
|
||||
Qwen2_5OmniThinkerMultiModalProcessor,
|
||||
)
|
||||
assert Qwen3OmniMoeThinkerMultiModalProcessor._get_hf_mm_data is UpstreamQwen3Processor._get_hf_mm_data
|
||||
mro = Qwen3OmniMoeThinkerMultiModalProcessor.__mro__
|
||||
assert mro.index(Qwen2_5OmniThinkerMultiModalProcessor) < mro.index(UpstreamQwen3Processor)
|
||||
|
||||
|
||||
def test_qwen3_prompt_updates_prefer_hf_second_per_grid_ts_from_outputs():
|
||||
|
||||
@@ -113,7 +113,9 @@ def _save_peft_adapter(tmp_path: Path) -> Path:
|
||||
return adapter_dir
|
||||
|
||||
|
||||
def test_peft_expert_lora_loads_into_thinker(tmp_path: Path):
|
||||
def test_peft_expert_lora_loads_into_thinker(tmp_path: Path, monkeypatch):
|
||||
# This CPU harness tests weight mapping, not CUDA pinned-memory allocation.
|
||||
monkeypatch.setattr("vllm.lora.lora_model.PIN_MEMORY", False)
|
||||
adapter_dir = _save_peft_adapter(tmp_path)
|
||||
peft_helper = PEFTHelper.from_local_dir(str(adapter_dir), max_position_embeddings=128)
|
||||
lora_model = LoRAModel.from_local_checkpoint(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
@@ -157,7 +157,10 @@ def _load_weights_noop(model: Qwen3TTSCode2Wav) -> set[str]:
|
||||
def __init__(self, *_: object, **__: object):
|
||||
pass
|
||||
|
||||
def load_weights(self, _weights: object) -> set[str]:
|
||||
def load_weights(self, _weights: object, *, mapper: object | None = None) -> set[str]:
|
||||
# vLLM 0.29 dropped skip_prefixes from AutoWeightsLoader; the model
|
||||
# now passes the stage exclusions as a WeightsMapper instead.
|
||||
del mapper
|
||||
return {"decoder.fake_weight"}
|
||||
|
||||
with (
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
from torch import nn
|
||||
from vllm.model_executor.models.utils import AutoWeightsLoader, WeightsMapper
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||
|
||||
|
||||
class _TinyModel(nn.Module):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.kept = nn.Linear(1, 1, bias=False)
|
||||
self.skipped = nn.Linear(1, 1, bias=False)
|
||||
self.rotary_embed = nn.Module()
|
||||
self.rotary_embed.register_parameter("inv_freq", nn.Parameter(torch.zeros(1)))
|
||||
|
||||
|
||||
def test_upstream_mapper_preserves_stage_exclusions_after_renaming() -> None:
|
||||
model = _TinyModel()
|
||||
model.skipped.weight.data.zero_()
|
||||
|
||||
loaded = AutoWeightsLoader(model).load_weights(
|
||||
[
|
||||
("checkpoint.kept.weight", torch.ones(1, 1)),
|
||||
("checkpoint.skipped.weight", torch.full((1, 1), 2.0)),
|
||||
("rotary_embed.inv_freq", torch.ones(1)),
|
||||
],
|
||||
mapper=WeightsMapper(orig_to_new_prefix={"checkpoint.": ""})
|
||||
| WeightsMapper(orig_to_new_prefix={"skipped.": None}, orig_to_new_substr={"rotary_embed.inv_freq": None}),
|
||||
)
|
||||
|
||||
assert loaded == {"kept.weight"}
|
||||
torch.testing.assert_close(model.kept.weight, torch.ones(1, 1))
|
||||
torch.testing.assert_close(model.skipped.weight, torch.zeros(1, 1))
|
||||
torch.testing.assert_close(model.rotary_embed.inv_freq, torch.zeros(1))
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Tests for the NVFP4 W4A4 weight_scale NaN clamp installed by
|
||||
``vllm_omni.patch``.
|
||||
|
||||
@@ -198,7 +198,7 @@ def test_reload_does_not_nest_wrapper():
|
||||
print("NESTED" if nested else "FLAT")
|
||||
"""
|
||||
)
|
||||
result = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True, timeout=120)
|
||||
result = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True, timeout=600)
|
||||
if result.stdout.strip().startswith("SKIP:"):
|
||||
pytest.skip(result.stdout.strip())
|
||||
assert "FLAT" in result.stdout, (
|
||||
@@ -265,9 +265,12 @@ def test_env_var_escape_hatch_disables_install(env_value, acceptable):
|
||||
)
|
||||
env = os.environ.copy()
|
||||
env["VLLM_OMNI_SKIP_NVFP4_NAN_CLAMP"] = env_value
|
||||
# 120s rather than 60s: cold CI containers can take 30-50s just to
|
||||
# import vllm_omni → vllm → torch before the subprocess does any work.
|
||||
result = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True, timeout=120, env=env)
|
||||
# Generous timeout: cold CI containers can take 30-50s just to import
|
||||
# vllm_omni → vllm → torch, and heavily loaded shared machines have
|
||||
# been observed taking >2min for the same import under disk I/O
|
||||
# contention. The subprocess does a single import + flag print, so 600s
|
||||
# only bounds pathological hangs, not normal runtime.
|
||||
result = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True, timeout=600, env=env)
|
||||
if result.stdout.strip().startswith("SKIP:"):
|
||||
pytest.skip("modelopt not available in subprocess")
|
||||
# Exact token match on the final stdout line — not a substring test, since
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Guard that Omni only calls prompt-update helpers upstream still provides.
|
||||
|
||||
vLLM 0.29 removed ``PromptUpdateDetails.select_text`` in favour of the
|
||||
token-oriented ``select_token_id`` / ``select_token_ids``. Three MiniCPM-o call
|
||||
sites survived the rebase and only failed at engine-core startup, where the
|
||||
traceback surfaced as an opaque "Engine core initialization failed" across eight
|
||||
CI jobs.
|
||||
|
||||
These helpers are referenced from replacement closures that no unit test drives,
|
||||
so a static check is what actually catches the next removal. It resolves the
|
||||
attribute against the installed class rather than a hard-coded list, so it
|
||||
follows upstream automatically.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from vllm.multimodal.processing.processor import PromptUpdateDetails
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||
|
||||
_OMNI_ROOT = Path(__file__).resolve().parents[2] / "vllm_omni"
|
||||
_GUARDED = "PromptUpdateDetails"
|
||||
|
||||
|
||||
def _referenced_attributes() -> dict[str, set[str]]:
|
||||
"""Map ``PromptUpdateDetails.<attr>`` -> source files referencing it."""
|
||||
found: dict[str, set[str]] = {}
|
||||
for path in _OMNI_ROOT.rglob("*.py"):
|
||||
try:
|
||||
tree = ast.parse(path.read_text(encoding="utf-8"))
|
||||
except (SyntaxError, UnicodeDecodeError): # pragma: no cover - defensive
|
||||
continue
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.Attribute) and isinstance(node.value, ast.Name) and node.value.id == _GUARDED:
|
||||
found.setdefault(node.attr, set()).add(str(path.relative_to(_OMNI_ROOT.parent)))
|
||||
return found
|
||||
|
||||
|
||||
def test_omni_only_uses_prompt_update_helpers_that_exist_upstream() -> None:
|
||||
referenced = _referenced_attributes()
|
||||
assert referenced, "expected Omni to reference PromptUpdateDetails somewhere"
|
||||
|
||||
missing = {attr: sorted(files) for attr, files in referenced.items() if not hasattr(PromptUpdateDetails, attr)}
|
||||
|
||||
assert not missing, (
|
||||
"Omni calls PromptUpdateDetails helpers that the installed vLLM does not "
|
||||
f"provide: {missing}. Upstream moved prompt updates to token ids; use "
|
||||
"select_token_id/select_token_ids with the encoded text."
|
||||
)
|
||||
|
||||
|
||||
def test_token_oriented_helpers_are_the_ones_upstream_exposes() -> None:
|
||||
"""Pin the replacements, so their removal fails here and not at startup."""
|
||||
assert hasattr(PromptUpdateDetails, "select_token_id")
|
||||
assert hasattr(PromptUpdateDetails, "select_token_ids")
|
||||
assert not hasattr(PromptUpdateDetails, "select_text"), (
|
||||
"select_text is back upstream; re-check the MiniCPM-o replacements."
|
||||
)
|
||||
@@ -0,0 +1,38 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
|
||||
import vllm_omni.platforms as platforms
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||
|
||||
|
||||
def test_failing_oot_plugin_is_logged_and_falls_back(monkeypatch, caplog):
|
||||
plugin_name = "raising_oot_plugin"
|
||||
|
||||
def raising_plugin():
|
||||
raise RuntimeError("injected platform detection failure")
|
||||
|
||||
monkeypatch.setattr(platforms, "builtin_omni_platform_plugins", {})
|
||||
monkeypatch.setattr(
|
||||
platforms,
|
||||
"load_omni_plugins_by_group",
|
||||
lambda _group: {plugin_name: raising_plugin},
|
||||
)
|
||||
|
||||
target_logger = logging.getLogger(platforms.__name__)
|
||||
target_logger.addHandler(caplog.handler)
|
||||
previous_level = target_logger.level
|
||||
target_logger.setLevel(logging.DEBUG)
|
||||
try:
|
||||
platform_cls = platforms.resolve_current_omni_platform_cls_qualname()
|
||||
finally:
|
||||
target_logger.removeHandler(caplog.handler)
|
||||
target_logger.setLevel(previous_level)
|
||||
|
||||
assert platform_cls == "vllm_omni.platforms.interface.UnspecifiedOmniPlatform"
|
||||
assert plugin_name in caplog.text
|
||||
assert "RuntimeError: injected platform detection failure" in caplog.text
|
||||
@@ -2975,7 +2975,9 @@ async def benchmark(
|
||||
"duration": benchmark_duration,
|
||||
"completed": metrics.completed,
|
||||
"total_input_tokens": metrics.total_input,
|
||||
"total_input_sequences": metrics.total_input_sequences,
|
||||
"request_throughput": metrics.request_throughput,
|
||||
"input_sequence_throughput": metrics.input_sequence_throughput,
|
||||
"total_token_throughput": metrics.total_token_throughput,
|
||||
"input_lens": [output.prompt_len for output in outputs],
|
||||
"errors": [output.error for output in outputs],
|
||||
|
||||
@@ -107,6 +107,17 @@ class OmniSchedulerMixin:
|
||||
|
||||
def _init_omni_io_scheduling_state(self) -> None:
|
||||
"""Initialize scheduler state shared by AR and generation stages."""
|
||||
# Every omni worker forces the v1 model runner (the v2 runner carries no
|
||||
# omni hooks), so the scheduler has to agree or the two disagree about
|
||||
# what the SchedulerOutput carries. vLLM 0.29 defaults this to v2 where
|
||||
# 0.28 did not, so the scheduler took its v2 fast path -- the one that
|
||||
# deliberately omits resumed-request token ids -- while the v1 runner
|
||||
# still read them, and resuming a request raised ``KeyError: <req_id>``
|
||||
# in ``_update_states``.
|
||||
if getattr(self, "use_v2_model_runner", False):
|
||||
logger.warning("OMNI scheduler forces v1 model runner for omni hooks.")
|
||||
self.use_v2_model_runner = False
|
||||
|
||||
model_config = self.vllm_config.model_config
|
||||
self.chunk_transfer_adapter = (
|
||||
OmniChunkTransferAdapter(self.vllm_config) if getattr(model_config, "async_chunk", False) else None
|
||||
|
||||
@@ -121,6 +121,13 @@ class AttentionBackend(ABC):
|
||||
when it unifies cache layouts across layers. Dense diffusion backends
|
||||
conservatively keep the default ``False``; a paged backend should
|
||||
override this only when its kernel actually follows that layout.
|
||||
|
||||
Since vLLM 0.29 this no longer rides on ``AttentionSpec``. It selects
|
||||
the block-outermost physical layout pinned on
|
||||
``CacheConfig.kv_cache_layout`` -- see
|
||||
``vllm_omni.diffusion.diffusion_kv.layout`` -- whose
|
||||
``is_block_outermost`` is what upstream reads when it sizes
|
||||
``KVCacheTensor`` regions.
|
||||
"""
|
||||
|
||||
return False
|
||||
|
||||
@@ -24,6 +24,7 @@ from vllm_omni.diffusion.attention.parallel.ring import RingParallelAttention
|
||||
from vllm_omni.diffusion.attention.selector import get_attn_backend_for_role
|
||||
from vllm_omni.diffusion.config import get_current_diffusion_config_or_none
|
||||
from vllm_omni.diffusion.diffusion_kv.config import DiffusionKVCacheMode
|
||||
from vllm_omni.diffusion.diffusion_kv.layout import assert_backend_layout_supported
|
||||
from vllm_omni.diffusion.distributed.parallel_state import get_sp_group
|
||||
from vllm_omni.diffusion.forward_context import (
|
||||
get_forward_context,
|
||||
@@ -261,15 +262,19 @@ class Attention(nn.Module):
|
||||
return None
|
||||
dtype = self.paged_kv_cache_dtype or vllm_config.model_config.dtype
|
||||
# Keep backend layout discovery under the same config context used by
|
||||
# upstream vLLM's attention-spec collector.
|
||||
# upstream vLLM's attention-spec collector. vLLM 0.29 moved the
|
||||
# block-stride decision off the spec and onto the single physical
|
||||
# ``CacheConfig.kv_cache_layout`` resolved before the cache is built,
|
||||
# so the backend's preference is enforced there (see
|
||||
# ``vllm_omni.diffusion.diffusion_kv.initialization``) rather than
|
||||
# carried per layer.
|
||||
with set_current_vllm_config(vllm_config):
|
||||
indexes_kv_by_block_stride = self.attn_backend.indexes_kv_by_block_stride()
|
||||
assert_backend_layout_supported(vllm_config, self.attn_backend)
|
||||
return FullAttentionSpec(
|
||||
block_size=vllm_config.cache_config.block_size,
|
||||
num_kv_heads=self.num_kv_heads,
|
||||
head_size=self.head_size,
|
||||
dtype=dtype,
|
||||
indexes_kv_by_block_stride=indexes_kv_by_block_stride,
|
||||
non_causal=not self.causal,
|
||||
)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -16,6 +16,7 @@ from vllm.v1.kv_cache_interface import KVCacheConfig, KVCacheSpec
|
||||
|
||||
from vllm_omni.diffusion.data import OmniDiffusionConfig
|
||||
from vllm_omni.diffusion.diffusion_kv.config import DiffusionKVCacheMode
|
||||
from vllm_omni.diffusion.diffusion_kv.layout import resolve_diffusion_kv_cache_layout
|
||||
from vllm_omni.diffusion.vllm_config import create_diffusion_vllm_config
|
||||
from vllm_omni.platforms import current_omni_platform
|
||||
|
||||
@@ -28,14 +29,35 @@ def build_native_kv_cache_configs(
|
||||
vllm_config: VllmConfig,
|
||||
worker_specs: list[dict[str, KVCacheSpec]],
|
||||
available_memory: list[int],
|
||||
*,
|
||||
indexes_kv_by_block_stride: bool = False,
|
||||
) -> tuple[list[KVCacheConfig], KVCacheConfig]:
|
||||
"""Build rank-local and Scheduler-native configs using vLLM utilities."""
|
||||
"""Build rank-local and Scheduler-native configs using vLLM utilities.
|
||||
|
||||
``indexes_kv_by_block_stride`` carries the native backend requirement that
|
||||
0.29 removed from ``AttentionSpec``. Every diffusion backend in tree
|
||||
declares ``False``, so the default reproduces current behaviour; a caller
|
||||
that learns otherwise from its workers passes ``True`` here and the
|
||||
block-outermost layout propagates to every rank-local config.
|
||||
"""
|
||||
|
||||
from vllm.v1.core import single_type_kv_cache_manager
|
||||
|
||||
# 0.29 sizes every KVCacheTensor region from the resolved physical layout,
|
||||
# so it must be pinned before get_kv_cache_configs. Idempotent, so callers
|
||||
# that already resolved it (the control plane) are unaffected.
|
||||
layout = resolve_diffusion_kv_cache_layout(
|
||||
vllm_config,
|
||||
indexes_kv_by_block_stride=indexes_kv_by_block_stride,
|
||||
)
|
||||
single_type_kv_cache_manager.register_all_kvcache_specs(vllm_config)
|
||||
worker_configs = get_kv_cache_configs(vllm_config, worker_specs, available_memory)
|
||||
return worker_configs, generate_scheduler_kv_cache_config(worker_configs)
|
||||
scheduler_config = generate_scheduler_kv_cache_config(worker_configs)
|
||||
# Workers only receive a KVCacheConfig, so the resolved layout travels on it
|
||||
# and is adopted into each worker's own CacheConfig before allocation.
|
||||
for config in (*worker_configs, scheduler_config):
|
||||
config.kv_cache_layout = layout.name
|
||||
return worker_configs, scheduler_config
|
||||
|
||||
|
||||
def initialize_diffusion_kv_control_plane(
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Physical KV cache layout selection for the native Diffusion paged cache.
|
||||
|
||||
vLLM 0.29 removed ``AttentionSpec.indexes_kv_by_block_stride`` and replaced it
|
||||
with a single physical layout per model, resolved once and recorded on
|
||||
``CacheConfig.kv_cache_layout``. ``KVCacheLayout.is_block_outermost`` is the
|
||||
direct successor of the old per-spec flag: upstream derives
|
||||
``interleaved_block_stride`` from it when it lays out ``KVCacheTensor`` regions
|
||||
(``vllm/v1/core/kv_cache_utils.py``).
|
||||
|
||||
Diffusion never reaches vLLM's engine-core resolution. Its backends subclass
|
||||
Omni's own ``AttentionBackend`` ABC, which upstream's
|
||||
``get_supported_kv_cache_layouts`` never sees, so the layout is pinned here
|
||||
before ``get_kv_cache_configs`` runs.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from vllm.v1.kv_cache_interface import KVCacheSpec, KVCacheTensor, compute_layout_strides
|
||||
from vllm.v1.kv_cache_layout import KVCacheLayout
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from vllm.config import VllmConfig
|
||||
|
||||
# Layer-outermost, matching the head of upstream's ``_DEFAULT_LAYOUT_PREFERENCE``
|
||||
# and reproducing the ``indexes_kv_by_block_stride=False`` that every dense
|
||||
# diffusion backend declared before 0.29.
|
||||
DEFAULT_DIFFUSION_KV_CACHE_LAYOUT = KVCacheLayout.LBNHC
|
||||
# Block-outermost counterpart, for a paged backend whose kernel really does read
|
||||
# K/V pages by the runtime block stride.
|
||||
BLOCK_STRIDE_DIFFUSION_KV_CACHE_LAYOUT = KVCacheLayout.BLNHC
|
||||
|
||||
|
||||
def resolve_diffusion_kv_cache_layout(
|
||||
vllm_config: VllmConfig,
|
||||
*,
|
||||
indexes_kv_by_block_stride: bool = False,
|
||||
) -> KVCacheLayout:
|
||||
"""Pin the physical KV layout for the Diffusion cache and return it.
|
||||
|
||||
A layout already present on the config wins, mirroring upstream's
|
||||
``resolve_kv_cache_layout``; it is validated against the requirement rather
|
||||
than silently overwritten, because an inconsistent layout reads K/V
|
||||
from the wrong offsets instead of raising.
|
||||
"""
|
||||
|
||||
required_block_outermost = bool(indexes_kv_by_block_stride)
|
||||
cache_config = getattr(vllm_config, "cache_config", None)
|
||||
if cache_config is None:
|
||||
# A config double with nowhere to record the choice; report what the
|
||||
# requirement implies without pretending it was pinned.
|
||||
return BLOCK_STRIDE_DIFFUSION_KV_CACHE_LAYOUT if required_block_outermost else DEFAULT_DIFFUSION_KV_CACHE_LAYOUT
|
||||
|
||||
current = getattr(cache_config, "kv_cache_layout", None)
|
||||
if current is not None:
|
||||
layout = KVCacheLayout[current]
|
||||
if layout.is_block_outermost != required_block_outermost:
|
||||
raise ValueError(
|
||||
"Diffusion KV cache layout "
|
||||
f"{layout.name} (is_block_outermost="
|
||||
f"{layout.is_block_outermost}) contradicts the attention "
|
||||
"backend, which requires is_block_outermost="
|
||||
f"{required_block_outermost}."
|
||||
)
|
||||
return layout
|
||||
|
||||
layout = BLOCK_STRIDE_DIFFUSION_KV_CACHE_LAYOUT if required_block_outermost else DEFAULT_DIFFUSION_KV_CACHE_LAYOUT
|
||||
cache_config.kv_cache_layout = layout.name
|
||||
return layout
|
||||
|
||||
|
||||
def adopt_kv_cache_layout(vllm_config: VllmConfig, kv_cache_config) -> KVCacheLayout:
|
||||
"""Adopt the control plane's resolved layout inside a worker.
|
||||
|
||||
The engine core resolves the layout once and every worker adopts it before
|
||||
allocating (``CacheConfig.kv_cache_layout``). Diffusion workers receive only
|
||||
a ``KVCacheConfig``, so the name rides along on ``kv_cache_config`` and is
|
||||
copied onto the worker's own config here. Without this, anything calling
|
||||
``get_resolved_kv_cache_layout`` in a worker -- ``init_kv_cache`` does --
|
||||
raises.
|
||||
"""
|
||||
|
||||
name = getattr(kv_cache_config, "kv_cache_layout", None)
|
||||
if name is None:
|
||||
# Older configs, or a standalone harness: fall back to the same default
|
||||
# the control plane pins so both sides still agree.
|
||||
return resolve_diffusion_kv_cache_layout(vllm_config)
|
||||
|
||||
cache_config = getattr(vllm_config, "cache_config", None)
|
||||
if cache_config is None:
|
||||
return KVCacheLayout[name]
|
||||
|
||||
current = getattr(cache_config, "kv_cache_layout", None)
|
||||
if current is None:
|
||||
cache_config.kv_cache_layout = name
|
||||
elif current != name:
|
||||
raise ValueError(
|
||||
f"Worker KV cache layout {current!r} disagrees with the layout resolved by the control plane ({name!r})."
|
||||
)
|
||||
return KVCacheLayout[name]
|
||||
|
||||
|
||||
def build_kv_cache_tensor(
|
||||
spec: KVCacheSpec,
|
||||
num_blocks: int,
|
||||
layers: Sequence[str],
|
||||
*,
|
||||
layout: KVCacheLayout | None = None,
|
||||
offset: int = 0,
|
||||
) -> KVCacheTensor:
|
||||
"""A ``KVCacheTensor`` covering ``layers`` for ``num_blocks`` blocks.
|
||||
|
||||
Before 0.29 a tensor named the layers sharing it
|
||||
(``KVCacheTensor(size=..., shared_by=[...])``); 0.29 replaced that with an
|
||||
explicit layout, so layer ``l``'s block ``b`` starts at
|
||||
``offset + l * layer_stride + b * block_stride``. Note this lays the layers
|
||||
out at distinct offsets rather than aliasing them, which is what upstream
|
||||
now does for a single cache group.
|
||||
|
||||
The strides come from upstream's own ``compute_layout_strides``, called the
|
||||
way ``vllm.v1.core.kv_cache_utils`` calls it, because hand-computed strides
|
||||
read K/V from the wrong offsets silently instead of raising.
|
||||
"""
|
||||
|
||||
layout = layout or DEFAULT_DIFFUSION_KV_CACHE_LAYOUT
|
||||
layer_names = list(layers)
|
||||
# Mirrors kv_cache_utils: the block stride is pinned up front only for
|
||||
# block-outermost layouts, where every block repeats the same packing.
|
||||
bytes_per_block = spec.page_size_bytes * len(layer_names)
|
||||
interleaved_block_stride = bytes_per_block if layout.is_block_outermost else None
|
||||
layer_stride, block_stride, _, _, _ = compute_layout_strides(
|
||||
spec,
|
||||
num_blocks,
|
||||
len(layer_names),
|
||||
layout,
|
||||
fixed_strides=(None, interleaved_block_stride, None, None, None),
|
||||
)
|
||||
return KVCacheTensor(
|
||||
size=bytes_per_block * num_blocks,
|
||||
layers=layer_names,
|
||||
layer_stride=layer_stride,
|
||||
block_stride=block_stride,
|
||||
offset=offset,
|
||||
)
|
||||
|
||||
|
||||
def assert_backend_layout_supported(vllm_config: VllmConfig, attn_backend: type | None) -> None:
|
||||
"""Fail loudly when a backend's block-stride need contradicts the layout.
|
||||
|
||||
Skipped while the layout is still unresolved -- worker processes collect
|
||||
specs before the control plane hands them the resolved name -- so this
|
||||
enforces the contract wherever it is knowable and never crashes a rank that
|
||||
has simply not been told yet.
|
||||
"""
|
||||
|
||||
if attn_backend is None:
|
||||
return
|
||||
# Spec discovery runs against lightweight config doubles in tests and against
|
||||
# worker configs that have not been handed the resolved name yet; in both
|
||||
# cases there is simply nothing to check.
|
||||
cache_config = getattr(vllm_config, "cache_config", None)
|
||||
if cache_config is None or getattr(cache_config, "kv_cache_layout", None) is None:
|
||||
return
|
||||
indexes_by_block_stride = getattr(attn_backend, "indexes_kv_by_block_stride", None)
|
||||
if indexes_by_block_stride is None:
|
||||
return
|
||||
resolve_diffusion_kv_cache_layout(
|
||||
vllm_config,
|
||||
indexes_kv_by_block_stride=indexes_by_block_stride(),
|
||||
)
|
||||
@@ -18,6 +18,10 @@ from vllm.v1.worker.gpu.block_table import BlockTables
|
||||
|
||||
from vllm_omni.diffusion.data import OmniDiffusionConfig
|
||||
from vllm_omni.diffusion.diffusion_kv.config import DiffusionKVCacheMode, is_scheduler_paged_kv_mode
|
||||
from vllm_omni.diffusion.diffusion_kv.layout import (
|
||||
adopt_kv_cache_layout,
|
||||
assert_backend_layout_supported,
|
||||
)
|
||||
from vllm_omni.diffusion.diffusion_kv.metadata import DiffusionKVMetadata
|
||||
from vllm_omni.diffusion.diffusion_kv.paged_attention_adapter import (
|
||||
DiffusionPagedAttentionAdapter,
|
||||
@@ -285,13 +289,16 @@ class DiffusionKVModelRunnerBackend:
|
||||
layers=self._kv_cache_layer_adapters,
|
||||
resolve_row=self._resolve_paged_attention_row,
|
||||
)
|
||||
# vLLM 0.29 reads the resolved physical layout when allocating,
|
||||
# and dropped attn_groups/cache_dtype from the signature.
|
||||
adopt_kv_cache_layout(self.vllm_config, kv_cache_config)
|
||||
for layer_adapter in self._kv_cache_layer_adapters.values():
|
||||
assert_backend_layout_supported(self.vllm_config, getattr(layer_adapter, "attn_backend", None))
|
||||
init_kv_cache(
|
||||
kv_caches,
|
||||
self.vllm_config.compilation_config.static_forward_context,
|
||||
kv_cache_config,
|
||||
attn_groups,
|
||||
self.device,
|
||||
self.vllm_config.cache_config.cache_dtype,
|
||||
kernel_block_sizes,
|
||||
self.vllm_config,
|
||||
)
|
||||
|
||||
@@ -105,10 +105,12 @@ class DiffusionPagedAttentionLayerAdapter(AttentionLayerBase):
|
||||
attn_backend,
|
||||
ulysses_degree=ulysses_degree,
|
||||
)
|
||||
# vLLM 0.29 carries the block-stride decision on the resolved physical
|
||||
# ``CacheConfig.kv_cache_layout`` instead of the spec; the backend's
|
||||
# requirement is enforced against that layout rather than copied here.
|
||||
canonical_spec = replace(
|
||||
spec,
|
||||
num_kv_heads=num_kv_heads,
|
||||
indexes_kv_by_block_stride=attn_backend.indexes_kv_by_block_stride(),
|
||||
)
|
||||
self.layer_name = layer_name
|
||||
self.spec = canonical_spec
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
from importlib import import_module
|
||||
from importlib.util import find_spec
|
||||
|
||||
import torch
|
||||
@@ -98,6 +102,7 @@ class RotaryEmbedding(CustomOp):
|
||||
self.interleaved = not is_neox_style
|
||||
self.half_head_dim = half_head_dim
|
||||
self.apply_rotary_emb_flash_attn = None
|
||||
self.apply_rotary_emb_vllm_flash_attn = None
|
||||
self.has_mindie = False
|
||||
# ``find_spec("flash_attn")`` is True as long as *any* package publishes
|
||||
# the ``flash_attn`` namespace — including ``flash-attn-4``, which ships
|
||||
@@ -111,6 +116,17 @@ class RotaryEmbedding(CustomOp):
|
||||
self.apply_rotary_emb_flash_attn = apply_rotary
|
||||
except ImportError:
|
||||
pass
|
||||
# CUDA wheels vendor this kernel under vllm_flash_attn, while source
|
||||
# and CPU-only installations may not contain the generated package.
|
||||
# Resolve it lazily and retain the native implementation as a portable
|
||||
# fallback for those environments.
|
||||
if current_omni_platform.is_cuda():
|
||||
try:
|
||||
self.apply_rotary_emb_vllm_flash_attn = import_module(
|
||||
"vllm.vllm_flash_attn.layers.rotary"
|
||||
).apply_rotary_emb
|
||||
except ImportError:
|
||||
pass
|
||||
if find_spec("mindiesd") is not None:
|
||||
self.has_mindie = True
|
||||
|
||||
@@ -135,12 +151,18 @@ class RotaryEmbedding(CustomOp):
|
||||
cos: torch.Tensor,
|
||||
sin: torch.Tensor,
|
||||
) -> torch.Tensor:
|
||||
from vllm.vllm_flash_attn.layers.rotary import apply_rotary_emb
|
||||
|
||||
cos, sin = self._prepare_half_head_dim_cos_sin(cos, sin)
|
||||
|
||||
if self.apply_rotary_emb_vllm_flash_attn is None:
|
||||
return apply_rotary_emb_torch(
|
||||
x,
|
||||
cos,
|
||||
sin,
|
||||
interleaved=self.interleaved,
|
||||
)
|
||||
|
||||
x, squeezed = _ensure_batch_dim(x)
|
||||
output = apply_rotary_emb(
|
||||
output = self.apply_rotary_emb_vllm_flash_attn(
|
||||
x,
|
||||
cos,
|
||||
sin,
|
||||
@@ -231,13 +253,14 @@ class RotaryEmbeddingWan(RotaryEmbedding):
|
||||
cos: torch.Tensor,
|
||||
sin: torch.Tensor,
|
||||
) -> torch.Tensor:
|
||||
from vllm.vllm_flash_attn.layers.rotary import apply_rotary_emb
|
||||
if self.apply_rotary_emb_vllm_flash_attn is None:
|
||||
return self.forward_native(x, cos, sin)
|
||||
|
||||
if cos.dim() > 2:
|
||||
cos = cos.reshape(-1, cos.shape[-1])
|
||||
sin = sin.reshape(-1, sin.shape[-1])
|
||||
|
||||
return apply_rotary_emb(
|
||||
return self.apply_rotary_emb_vllm_flash_attn(
|
||||
x,
|
||||
cos,
|
||||
sin,
|
||||
|
||||
@@ -454,11 +454,10 @@ class HunyuanImage3Pipeline(
|
||||
|
||||
# Note: guidance_emb and timestep_r_emb are no longer skipped
|
||||
# to support HunyuanImage-3.0-Distil and MeanFlow distilled models
|
||||
loader = AutoWeightsLoader(
|
||||
self,
|
||||
skip_prefixes=skip_prefixes,
|
||||
loader = AutoWeightsLoader(self)
|
||||
return loader.load_weights(
|
||||
weights, mapper=WeightsMapper(orig_to_new_prefix={name: None for name in (skip_prefixes or ())})
|
||||
)
|
||||
return loader.load_weights(weights)
|
||||
|
||||
def prepare_seed(self, seed=None, batch_size=1):
|
||||
# random seed
|
||||
|
||||
@@ -24,7 +24,7 @@ from vllm.distributed import (
|
||||
tensor_model_parallel_all_reduce,
|
||||
)
|
||||
from vllm.logger import init_logger
|
||||
from vllm.model_executor.layers.batch_invariant import linear_batch_invariant
|
||||
from vllm.model_executor.determinism.batch_invariant import linear_batch_invariant
|
||||
from vllm.model_executor.layers.linear import (
|
||||
ColumnParallelLinear,
|
||||
QKVParallelLinear,
|
||||
|
||||
@@ -50,6 +50,17 @@ from vllm_omni.diffusion.layers.rope import RotaryEmbedding
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
def _apply_qwen_image_rotary_emb(x: torch.Tensor, freqs: torch.Tensor) -> torch.Tensor:
|
||||
"""Rotate interleaved pairs before rounding back to the activation dtype.
|
||||
|
||||
Qwen-Image's reference uses complex FP32 multiplication. Rounding the
|
||||
frequencies to BF16 before rotation loses positional precision; those
|
||||
errors accumulate across the denoising steps.
|
||||
"""
|
||||
paired = torch.view_as_complex(x.float().reshape(*x.shape[:-1], -1, 2))
|
||||
return torch.view_as_real(paired * freqs.unsqueeze(1)).flatten(3).to(x.dtype)
|
||||
|
||||
|
||||
def _normalize_qwen_image_weight_name(name: str) -> str:
|
||||
name = name.removeprefix("transformer.")
|
||||
if ".to_out.0." in name:
|
||||
@@ -642,15 +653,23 @@ class QwenImageCrossAttention(nn.Module):
|
||||
txt_query = self.norm_added_q(txt_query)
|
||||
txt_key = self.norm_added_k(txt_key)
|
||||
|
||||
img_cos = torch.real(vid_freqs).to(img_query.dtype)
|
||||
img_sin = torch.imag(vid_freqs).to(img_query.dtype)
|
||||
txt_cos = torch.real(txt_freqs).to(txt_query.dtype)
|
||||
txt_sin = torch.imag(txt_freqs).to(txt_query.dtype)
|
||||
if img_query.device.type == "cuda":
|
||||
img_query = _apply_qwen_image_rotary_emb(img_query, vid_freqs)
|
||||
img_key = _apply_qwen_image_rotary_emb(img_key, vid_freqs)
|
||||
txt_query = _apply_qwen_image_rotary_emb(txt_query, txt_freqs)
|
||||
txt_key = _apply_qwen_image_rotary_emb(txt_key, txt_freqs)
|
||||
else:
|
||||
# Retain the platform-specific kernels on other accelerators,
|
||||
# which may not support complex tensors.
|
||||
img_cos = torch.real(vid_freqs).to(img_query.dtype)
|
||||
img_sin = torch.imag(vid_freqs).to(img_query.dtype)
|
||||
txt_cos = torch.real(txt_freqs).to(txt_query.dtype)
|
||||
txt_sin = torch.imag(txt_freqs).to(txt_query.dtype)
|
||||
|
||||
img_query = self.rope(img_query, img_cos, img_sin)
|
||||
img_key = self.rope(img_key, img_cos, img_sin)
|
||||
txt_query = self.rope(txt_query, txt_cos, txt_sin)
|
||||
txt_key = self.rope(txt_key, txt_cos, txt_sin)
|
||||
img_query = self.rope(img_query, img_cos, img_sin)
|
||||
img_key = self.rope(img_key, img_cos, img_sin)
|
||||
txt_query = self.rope(txt_query, txt_cos, txt_sin)
|
||||
txt_key = self.rope(txt_key, txt_cos, txt_sin)
|
||||
|
||||
seq_len_txt = encoder_hidden_states.shape[1]
|
||||
joint_query = torch.cat([txt_query, img_query], dim=1)
|
||||
|
||||
@@ -443,8 +443,8 @@ class OmniEngineArgs(EngineArgs):
|
||||
@dataclass
|
||||
class OmniAsyncEngineArgs(AsyncEngineArgs, OmniEngineArgs):
|
||||
@classmethod
|
||||
def add_cli_args(cls, parser: argparse.ArgumentParser) -> argparse.ArgumentParser:
|
||||
parser = AsyncEngineArgs.add_cli_args(parser)
|
||||
def add_cli_args(cls, parser: argparse.ArgumentParser, async_args_only: bool = False) -> argparse.ArgumentParser:
|
||||
parser = AsyncEngineArgs.add_cli_args(parser, async_args_only=async_args_only)
|
||||
parser = OmniEngineArgs._add_omni_specific_args(parser)
|
||||
return parser
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
"""Process manager for omni stage engine subprocesses.
|
||||
|
||||
@@ -83,6 +83,12 @@ class StageEngineCoreProcManager(CoreEngineProcManager):
|
||||
if local_engine_count <= 0:
|
||||
raise ValueError(f"local_engine_count must be > 0, got {local_engine_count}")
|
||||
|
||||
# Mirrors the vLLM 0.29 parent __init__: the inherited shutdown() reads
|
||||
# this to bound how long in-flight requests may drain. Omitting it makes
|
||||
# shutdown raise AttributeError, which leaves the engine core
|
||||
# subprocesses alive and hangs interpreter exit until the job timeout.
|
||||
self._request_shutdown_timeout = vllm_config.shutdown_timeout
|
||||
|
||||
context = get_mp_context()
|
||||
common_kwargs: dict[str, object] = {
|
||||
"vllm_config": vllm_config,
|
||||
|
||||
@@ -54,7 +54,7 @@ from vllm_omni.engine.arg_utils import OmniEngineArgs
|
||||
from vllm_omni.entrypoints.stage_utils import _to_dict, set_stage_devices
|
||||
from vllm_omni.entrypoints.utils import filter_dataclass_kwargs
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams, OmniSamplingParams
|
||||
from vllm_omni.inputs.preprocess import OmniInputPreprocessor
|
||||
from vllm_omni.inputs.preprocess import build_omni_renderer, omni_renderer_cls
|
||||
from vllm_omni.outputs.output_processor import MultimodalOutputProcessor
|
||||
from vllm_omni.platforms import current_omni_platform
|
||||
from vllm_omni.quantization.inc_config import OmniINCConfig
|
||||
@@ -1549,25 +1549,24 @@ class _TokenOnlyRenderer(BaseRenderer):
|
||||
|
||||
|
||||
def _build_token_only_renderer(stage_vllm_config: Any) -> BaseRenderer:
|
||||
return _TokenOnlyRenderer(stage_vllm_config, tokenizer=None)
|
||||
return omni_renderer_cls(_TokenOnlyRenderer)(stage_vllm_config, tokenizer=None)
|
||||
|
||||
|
||||
def build_stage0_input_processor(stage_vllm_config: Any) -> InputProcessor:
|
||||
"""Build the shared stage-0 input processor."""
|
||||
"""Build the shared stage-0 input processor.
|
||||
|
||||
The renderer is the Omni subclass of the upstream renderer class that
|
||||
``renderer_from_config`` would have picked (or of the token-only renderer
|
||||
when the stage skips tokenizer initialization), so upstream's
|
||||
``InputProcessor`` runs unmodified on top of it.
|
||||
"""
|
||||
|
||||
patch_generation_config_if_needed(stage_vllm_config.model_config)
|
||||
if bool(getattr(stage_vllm_config.model_config, "skip_tokenizer_init", False)):
|
||||
input_processor = InputProcessor(
|
||||
vllm_config=stage_vllm_config,
|
||||
renderer=_build_token_only_renderer(stage_vllm_config),
|
||||
)
|
||||
renderer = _build_token_only_renderer(stage_vllm_config)
|
||||
else:
|
||||
input_processor = InputProcessor(vllm_config=stage_vllm_config)
|
||||
input_processor.input_preprocessor = OmniInputPreprocessor(
|
||||
vllm_config=stage_vllm_config,
|
||||
renderer=input_processor.renderer,
|
||||
)
|
||||
return input_processor
|
||||
renderer = build_omni_renderer(stage_vllm_config)
|
||||
return InputProcessor(vllm_config=stage_vllm_config, renderer=renderer)
|
||||
|
||||
|
||||
def device_init_lock_path(device_id: int, lock_dir: str = "/tmp") -> str:
|
||||
|
||||
@@ -51,9 +51,9 @@ from vllm_omni.metrics.stats import OrchestratorAggregator as OrchestratorMetric
|
||||
from vllm_omni.outputs import OmniRequestOutput
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from vllm.inputs.preprocess import InputPreprocessor
|
||||
from vllm.tokenizers import TokenizerLike
|
||||
from vllm.v1.engine import PauseMode
|
||||
from vllm.v1.engine.input_processor import InputProcessor
|
||||
|
||||
from vllm_omni.inputs.data import OmniInteractionPrompt, OmniPromptType
|
||||
|
||||
@@ -1714,7 +1714,7 @@ class AsyncOmni(EngineClient, OmniBase):
|
||||
|
||||
# ==================== EngineClient Interface ====================
|
||||
|
||||
async def get_input_preprocessor(self) -> InputPreprocessor:
|
||||
async def get_input_preprocessor(self) -> InputProcessor:
|
||||
"""Get input preprocessor."""
|
||||
return self.input_processor
|
||||
|
||||
@@ -1755,10 +1755,12 @@ class AsyncOmni(EngineClient, OmniBase):
|
||||
"""
|
||||
logger.debug("Weight update start requested (no-op in omni)")
|
||||
|
||||
async def finish_weight_update(self) -> None:
|
||||
async def finish_weight_update(self, weight_version: str | None = None) -> None:
|
||||
"""Finish the current weight update.
|
||||
|
||||
Omni does not currently support weight transfer, so this is a no-op.
|
||||
``weight_version`` is accepted for upstream ``EngineClient`` protocol
|
||||
compatibility (RLHF weight-transfer routers pass it positionally).
|
||||
"""
|
||||
logger.debug("Weight update finish requested (no-op in omni)")
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ from typing import Any, cast
|
||||
|
||||
import uvloop
|
||||
from vllm.entrypoints.cli.types import CLISubcommand
|
||||
from vllm.entrypoints.openai.cli_args import make_arg_parser, validate_parsed_serve_args
|
||||
from vllm.entrypoints.launchers.cli_args import make_arg_parser, validate_parsed_serve_args
|
||||
from vllm.entrypoints.serve.utils.api_utils import VLLM_SUBCMD_PARSER_EPILOG
|
||||
from vllm.logger import init_logger
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ from collections.abc import AsyncGenerator
|
||||
from typing import Any
|
||||
|
||||
from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionRequest
|
||||
from vllm.entrypoints.openai.engine.protocol import ErrorResponse
|
||||
from vllm.entrypoints.serve.engine.protocol import ErrorResponse
|
||||
from vllm.logger import init_logger
|
||||
|
||||
from vllm_omni.entrypoints.duplex.protocol import DuplexSession
|
||||
|
||||
@@ -34,7 +34,10 @@ from starlette.types import ASGIApp, Receive, Scope, Send
|
||||
from vllm.engine.protocol import EngineClient
|
||||
from vllm.entrypoints.anthropic.serving import AnthropicServingMessages
|
||||
from vllm.entrypoints.chat_utils import ChatTemplateConfig, load_chat_template
|
||||
from vllm.entrypoints.launcher import serve_http, terminate_if_errored
|
||||
from vllm.entrypoints.generate.base.protocol import RequestResponseMetadata
|
||||
from vllm.entrypoints.launchers.cli_args import make_arg_parser
|
||||
from vllm.entrypoints.launchers.launcher import serve_http, terminate_if_errored
|
||||
from vllm.entrypoints.launchers.utils.server_utils import get_uvicorn_log_config
|
||||
from vllm.entrypoints.mcp.tool_server import DemoToolServer, MCPToolServer, ToolServer
|
||||
from vllm.entrypoints.openai.api_server import build_app as build_openai_app
|
||||
from vllm.entrypoints.openai.api_server import setup_server as setup_openai_server
|
||||
@@ -43,19 +46,11 @@ from vllm.entrypoints.openai.chat_completion.protocol import (
|
||||
ChatCompletionRequest,
|
||||
ChatCompletionResponse,
|
||||
)
|
||||
from vllm.entrypoints.openai.cli_args import make_arg_parser
|
||||
|
||||
# yapf conflicts with isort for this block
|
||||
# yapf: disable
|
||||
# yapf: enable
|
||||
from vllm.entrypoints.openai.completion.serving import OpenAIServingCompletion
|
||||
from vllm.entrypoints.openai.engine.protocol import (
|
||||
ErrorResponse,
|
||||
ModelCard,
|
||||
ModelList,
|
||||
ModelPermission,
|
||||
RequestResponseMetadata,
|
||||
)
|
||||
from vllm.entrypoints.openai.models.protocol import BaseModelPath
|
||||
from vllm.entrypoints.openai.models.serving import OpenAIServingModels
|
||||
from vllm.entrypoints.openai.responses.serving import OpenAIServingResponses
|
||||
@@ -64,14 +59,13 @@ from vllm.entrypoints.pooling.embed.serving import ServingEmbedding as OpenAISer
|
||||
from vllm.entrypoints.pooling.pooling.serving import ServingPooling
|
||||
from vllm.entrypoints.pooling.scoring.serving import ServingScores
|
||||
from vllm.entrypoints.scale_out.token_in_token_out.serving import ServingTokens
|
||||
|
||||
# vLLM < 0.28 keeps create_error_response under serve.utils (it is not
|
||||
# re-exported from vllm.entrypoints.serve); 0.28+ moved it under
|
||||
# serve.exception_handling and re-exports it from the package root.
|
||||
try:
|
||||
from vllm.entrypoints.serve import create_error_response
|
||||
except ImportError:
|
||||
from vllm.entrypoints.serve.utils.error_response import create_error_response
|
||||
from vllm.entrypoints.serve import create_error_response
|
||||
from vllm.entrypoints.serve.engine.protocol import (
|
||||
ErrorResponse,
|
||||
ModelCard,
|
||||
ModelList,
|
||||
ModelPermission,
|
||||
)
|
||||
|
||||
# vLLM moved `base` from openai.basic.api_router to serve.instrumentator.basic.
|
||||
# Keep a fallback for older/newer upstream layouts during rebase windows.
|
||||
@@ -85,7 +79,6 @@ from vllm.entrypoints.serve.utils.api_utils import (
|
||||
)
|
||||
from vllm.entrypoints.serve.utils.orca_metrics import metrics_header
|
||||
from vllm.entrypoints.serve.utils.request_logger import RequestLogger
|
||||
from vllm.entrypoints.serve.utils.server_utils import get_uvicorn_log_config
|
||||
from vllm.entrypoints.speech_to_text.realtime.serving import OpenAIServingRealtime
|
||||
from vllm.entrypoints.speech_to_text.transcription.serving import (
|
||||
OpenAIServingTranscription,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
@@ -11,7 +11,7 @@ from vllm.entrypoints.openai.chat_completion.protocol import (
|
||||
ChatCompletionResponse,
|
||||
ChatCompletionResponseChoice,
|
||||
)
|
||||
from vllm.entrypoints.openai.engine.protocol import (
|
||||
from vllm.entrypoints.serve.engine.protocol import (
|
||||
ErrorResponse,
|
||||
UsageInfo,
|
||||
)
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
@@ -8,7 +11,7 @@ from typing import cast
|
||||
from uuid import uuid4
|
||||
|
||||
import numpy as np
|
||||
from vllm.entrypoints.openai.engine.protocol import UsageInfo
|
||||
from vllm.entrypoints.serve.engine.protocol import UsageInfo
|
||||
from vllm.entrypoints.speech_to_text.realtime.connection import RealtimeConnection as VllmRealtimeConnection
|
||||
from vllm.entrypoints.speech_to_text.realtime.protocol import TranscriptionDelta, TranscriptionDone
|
||||
from vllm.logger import init_logger
|
||||
|
||||
@@ -54,8 +54,17 @@ except ImportError:
|
||||
soundfile = None
|
||||
|
||||
|
||||
from vllm.entrypoints.generate.base.protocol import (
|
||||
DeltaFunctionCall,
|
||||
DeltaMessage,
|
||||
DeltaToolCall,
|
||||
FunctionCall,
|
||||
FunctionDefinition,
|
||||
RequestResponseMetadata,
|
||||
ToolCall,
|
||||
)
|
||||
from vllm.entrypoints.generate.base.serving import clamp_prompt_logprobs
|
||||
from vllm.entrypoints.launcher import terminate_if_errored
|
||||
from vllm.entrypoints.launchers.launcher import terminate_if_errored
|
||||
from vllm.entrypoints.openai.chat_completion.protocol import (
|
||||
ChatCompletionNamedToolChoiceParam,
|
||||
ChatCompletionRequest,
|
||||
@@ -69,22 +78,15 @@ from vllm.entrypoints.openai.chat_completion.serving import (
|
||||
_get_mm_token_counts,
|
||||
_make_prompt_tokens_details,
|
||||
)
|
||||
from vllm.entrypoints.openai.engine.protocol import (
|
||||
DeltaFunctionCall,
|
||||
DeltaMessage,
|
||||
DeltaToolCall,
|
||||
ErrorInfo,
|
||||
ErrorResponse,
|
||||
FunctionCall,
|
||||
FunctionDefinition,
|
||||
RequestResponseMetadata,
|
||||
ToolCall,
|
||||
UsageInfo,
|
||||
)
|
||||
from vllm.entrypoints.openai.parser.harmony_utils import (
|
||||
get_streamable_parser_for_assistant,
|
||||
)
|
||||
from vllm.entrypoints.openai.responses.protocol import ResponsesRequest
|
||||
from vllm.entrypoints.serve.engine.protocol import (
|
||||
ErrorInfo,
|
||||
ErrorResponse,
|
||||
UsageInfo,
|
||||
)
|
||||
from vllm.entrypoints.serve.engine.typing import ChatLikeRequest
|
||||
from vllm.entrypoints.serve.utils.api_utils import should_include_usage
|
||||
from vllm.entrypoints.serve.utils.tool_calls_utils import maybe_filter_parallel_tool_calls
|
||||
@@ -700,7 +702,7 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin):
|
||||
if negative_prompt is not None:
|
||||
tprompt["negative_prompt"] = negative_prompt
|
||||
# Always attach mm_processor_kwargs (possibly empty) so
|
||||
# OmniInputPreprocessor._process_text routes through the
|
||||
# OmniRenderer routes through the
|
||||
# multimodal processor path. Without it, the preprocessor
|
||||
# falls back to plain _tokenize_prompt and AR-based image-gen
|
||||
# models like GLM-Image never see their image-generation
|
||||
|
||||
@@ -24,12 +24,10 @@ import soundfile as sf
|
||||
import torch
|
||||
from fastapi import HTTPException, Request, UploadFile
|
||||
from fastapi.responses import Response, StreamingResponse
|
||||
from vllm.entrypoints.generate.base.protocol import RequestResponseMetadata
|
||||
from vllm.entrypoints.generate.base.serving import GenerateBaseServing as OpenAIServing
|
||||
from vllm.entrypoints.launcher import terminate_if_errored
|
||||
from vllm.entrypoints.openai.engine.protocol import (
|
||||
ErrorResponse,
|
||||
RequestResponseMetadata,
|
||||
)
|
||||
from vllm.entrypoints.launchers.launcher import terminate_if_errored
|
||||
from vllm.entrypoints.serve.engine.protocol import ErrorResponse
|
||||
from vllm.logger import init_logger
|
||||
from vllm.multimodal.media import MediaConnector
|
||||
from vllm.utils import random_uuid
|
||||
|
||||
@@ -156,7 +156,9 @@ def _decode_video_bytes(
|
||||
frames_array, metadata = loader.load_bytes(
|
||||
video_bytes,
|
||||
num_frames=num_frames,
|
||||
backend="pyav",
|
||||
# vLLM 0.29 removed the "pyav" decoder backend; "opencv" is
|
||||
# upstream's default and the remaining CPU-only option.
|
||||
backend="opencv",
|
||||
keep=keep,
|
||||
)
|
||||
except Exception as exc:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Video streaming session manager and WebSocket handler.
|
||||
|
||||
Provides ``VideoStreamConfig``, ``VideoStreamSession`` (frame/audio buffer
|
||||
@@ -20,7 +20,7 @@ from fastapi import WebSocket, WebSocketDisconnect
|
||||
from vllm.entrypoints.openai.chat_completion.protocol import (
|
||||
ChatCompletionRequest,
|
||||
)
|
||||
from vllm.entrypoints.openai.engine.protocol import ErrorResponse
|
||||
from vllm.entrypoints.serve.engine.protocol import ErrorResponse
|
||||
from vllm.logger import init_logger
|
||||
|
||||
from vllm_omni.entrypoints.openai.video_frame_filter import FrameSimilarityFilter
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Engine-level KV cache orchestration for AR-Diffusion models.
|
||||
|
||||
This is the *body* of AR-Diffusion's KV management: it owns a vLLM ``KVCacheManager`` (a
|
||||
@@ -22,10 +23,10 @@ from vllm.v1.kv_cache_interface import (
|
||||
KVCacheConfig,
|
||||
KVCacheGroupSpec,
|
||||
KVCacheSpec,
|
||||
KVCacheTensor,
|
||||
)
|
||||
from vllm.v1.request import RequestStatus
|
||||
|
||||
from vllm_omni.diffusion.diffusion_kv.layout import build_kv_cache_tensor
|
||||
from vllm_omni.experimental.ar_diffusion.capability import ARDiffusionKVBranchSpec
|
||||
from vllm_omni.experimental.ar_diffusion.kv_cache.config import ARDiffusionKVConfig
|
||||
from vllm_omni.experimental.ar_diffusion.kv_cache.paged import (
|
||||
@@ -134,7 +135,7 @@ def build_kv_manager(
|
||||
"""
|
||||
layer_names = list(layer_names)
|
||||
group = KVCacheGroupSpec(layer_names=layer_names, kv_cache_spec=spec)
|
||||
tensors = [KVCacheTensor(size=spec.page_size_bytes * num_blocks, shared_by=layer_names)]
|
||||
tensors = [build_kv_cache_tensor(spec, num_blocks, layer_names)]
|
||||
config = KVCacheConfig(
|
||||
num_blocks=num_blocks,
|
||||
kv_cache_tensors=tensors,
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
"""Multimodal data plumbing for Omni's custom, non-HF processors."""
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import TypeVar
|
||||
|
||||
from transformers import BatchFeature
|
||||
from vllm.multimodal.parse import MultiModalDataItems
|
||||
from vllm.multimodal.processing import BaseMultiModalProcessor, BaseProcessingInfo
|
||||
|
||||
_I = TypeVar("_I", bound=BaseProcessingInfo)
|
||||
|
||||
|
||||
class OmniMultiModalProcessor(BaseMultiModalProcessor[_I]):
|
||||
"""Share data/passthrough handling, leaving model processing in its owner.
|
||||
|
||||
These processors implement ``_call_hf_processor`` locally, sometimes
|
||||
without any HF processor object. Prompt tokens are prepared separately
|
||||
in ``apply``; the upstream base still owns caching and prompt updates.
|
||||
"""
|
||||
|
||||
_OMNI_PROMPT_TEXT_KEY = "_vllm_omni_original_prompt_text"
|
||||
|
||||
def _apply_hf_processor_main(
|
||||
self,
|
||||
mm_items: MultiModalDataItems,
|
||||
hf_processor_mm_kwargs: Mapping[str, object],
|
||||
) -> BatchFeature:
|
||||
kwargs = dict(hf_processor_mm_kwargs)
|
||||
prompt = kwargs.pop(self._OMNI_PROMPT_TEXT_KEY, None)
|
||||
if prompt is None:
|
||||
prompt = self.dummy_inputs.get_dummy_text(mm_items.get_all_counts())
|
||||
valid_items = mm_items.select({key for key, count in mm_items.get_all_counts().items() if count > 0})
|
||||
mm_data, passthrough = self._get_hf_mm_data(valid_items)
|
||||
result = self._call_hf_processor(str(prompt), dict(mm_data), kwargs, {})
|
||||
result.update(passthrough)
|
||||
return result
|
||||
+96
-178
@@ -1,200 +1,118 @@
|
||||
from typing import Any
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Omni renderer: the upstream renderer class with Omni engine-input routing.
|
||||
|
||||
from typing_extensions import assert_never
|
||||
from vllm.inputs import EmbedsInput, MultiModalInput, SingletonInput
|
||||
from vllm.inputs.preprocess import InputPreprocessor
|
||||
from vllm.logger import init_logger
|
||||
from vllm.renderers.inputs import SingletonDictPrompt
|
||||
``OmniRenderer`` is a mixin. ``omni_renderer_cls`` puts it in front of a
|
||||
concrete ``BaseRenderer`` subclass, so the Omni instance *is* an upstream
|
||||
renderer (tokenizer, executors, multimodal cache and every public entry point
|
||||
come from upstream unchanged) and only ``_process_singleton`` /
|
||||
``_process_singleton_async`` are extended. ``build_omni_renderer`` resolves
|
||||
the concrete class the same way upstream ``renderer_from_config`` does.
|
||||
"""
|
||||
|
||||
from vllm_omni.inputs.data import (
|
||||
OmniEmbedsPrompt,
|
||||
OmniTextPrompt,
|
||||
OmniTokenInputs,
|
||||
OmniTokensPrompt,
|
||||
token_inputs_omni,
|
||||
)
|
||||
from __future__ import annotations
|
||||
|
||||
logger = init_logger(__name__)
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
from vllm.renderers import BaseRenderer
|
||||
from vllm.renderers.registry import RENDERER_REGISTRY
|
||||
from vllm.tokenizers.registry import cached_tokenizer_from_config, tokenizer_args_from_config
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from vllm.config import VllmConfig
|
||||
from vllm.tokenizers import TokenizerLike
|
||||
|
||||
|
||||
class OmniInputPreprocessor(InputPreprocessor):
|
||||
"""Input preprocessor for omni models.
|
||||
class OmniRenderer:
|
||||
"""Mixin that routes Omni prompts through the upstream renderer.
|
||||
|
||||
Extends the base InputPreprocessor to handle omni-specific input
|
||||
types including prompt embeddings and additional information payloads.
|
||||
Supports processing tokens, embeddings, text, and multimodal inputs.
|
||||
It has no base class of its own; ``omni_renderer_cls`` prepends it to a
|
||||
concrete ``BaseRenderer`` subclass, so ``self`` and ``super()`` are that
|
||||
renderer at runtime (hence the casts below).
|
||||
|
||||
Two extensions over upstream ``_process_singleton``:
|
||||
|
||||
* a token prompt that carries ``mm_processor_kwargs`` but no
|
||||
``multi_modal_data`` still goes through the multimodal processor (AR
|
||||
image-generation models such as GLM-Image read their target size from
|
||||
those kwargs);
|
||||
* Omni-only prompt keys (``prompt``, ``cache_salt``,
|
||||
``additional_information``, ``model_intermediate_buffer``) are copied
|
||||
onto the engine input.
|
||||
"""
|
||||
|
||||
def _process_text(
|
||||
self,
|
||||
parsed_content: OmniTextPrompt,
|
||||
tokenization_kwargs: dict[str, Any] | None = None,
|
||||
*,
|
||||
mm_uuids: Any | None = None,
|
||||
) -> OmniTokenInputs | MultiModalInput:
|
||||
"""Process text prompts with support for mm_processor_kwargs.
|
||||
@staticmethod
|
||||
def _with_omni_extras(inputs, prompt):
|
||||
for key in ("prompt", "cache_salt", "additional_information", "model_intermediate_buffer"):
|
||||
if key in prompt:
|
||||
inputs[key] = prompt[key]
|
||||
return inputs
|
||||
|
||||
Extends base class to support mm_processor_kwargs without multi_modal_data.
|
||||
This is needed for models like GLM-Image where text-to-image generation
|
||||
requires processor kwargs (target_h, target_w) to format the prompt.
|
||||
"""
|
||||
prompt_text = parsed_content["prompt"]
|
||||
mm_processor_kwargs = parsed_content.get("mm_processor_kwargs") or {}
|
||||
# When the deprecated raw-prompt path is used, process_inputs does
|
||||
# not pass mm_uuids to preprocess(). Fall back to reading it from
|
||||
# the prompt dict so the Renderer's _validate_mm_uuids can see it.
|
||||
effective_mm_uuids = mm_uuids or parsed_content.get("multi_modal_uuids")
|
||||
@staticmethod
|
||||
def _routes_no_media_kwargs(prompt) -> bool:
|
||||
return "prompt_embeds" not in prompt and "mm_processor_kwargs" in prompt and not prompt.get("multi_modal_data")
|
||||
|
||||
inputs: OmniTokenInputs | MultiModalInput
|
||||
if multi_modal_data := parsed_content.get("multi_modal_data"):
|
||||
inputs = self._process_multimodal(
|
||||
prompt_text,
|
||||
multi_modal_data,
|
||||
mm_processor_kwargs,
|
||||
tokenization_kwargs=tokenization_kwargs,
|
||||
mm_uuids=effective_mm_uuids,
|
||||
)
|
||||
prompt_embeds = parsed_content.get("prompt_embeds")
|
||||
if prompt_embeds is not None:
|
||||
inputs["prompt_embeds"] = prompt_embeds
|
||||
additional_information = parsed_content.get("additional_information")
|
||||
if additional_information is not None:
|
||||
inputs["additional_information"] = additional_information
|
||||
model_intermediate_buffer = parsed_content.get("model_intermediate_buffer")
|
||||
if model_intermediate_buffer is not None:
|
||||
inputs["model_intermediate_buffer"] = model_intermediate_buffer
|
||||
elif "mm_processor_kwargs" in parsed_content:
|
||||
# Presence — not truthiness. An explicitly-set empty dict still
|
||||
# signals "route through the multimodal processor" (needed for
|
||||
# AR-based image-gen where the HF processor supplies its own
|
||||
# defaults and scaffold).
|
||||
inputs = self._process_multimodal(
|
||||
prompt_text,
|
||||
def _process_singleton(self, prompt, *, skip_mm_cache: bool = False):
|
||||
renderer = cast(BaseRenderer, self)
|
||||
if self._routes_no_media_kwargs(prompt):
|
||||
inputs = renderer._process_multimodal(
|
||||
prompt["prompt_token_ids"],
|
||||
{},
|
||||
mm_processor_kwargs,
|
||||
tokenization_kwargs=tokenization_kwargs,
|
||||
mm_uuids=effective_mm_uuids,
|
||||
mm_processor_kwargs=prompt["mm_processor_kwargs"],
|
||||
mm_uuids=prompt.get("multi_modal_uuids"),
|
||||
skip_mm_cache=skip_mm_cache,
|
||||
)
|
||||
else:
|
||||
prompt_token_ids = self._tokenize_prompt(
|
||||
prompt_text,
|
||||
tokenization_kwargs=tokenization_kwargs,
|
||||
inputs = cast(BaseRenderer, super())._process_singleton(prompt, skip_mm_cache=skip_mm_cache)
|
||||
return self._with_omni_extras(inputs, prompt)
|
||||
|
||||
async def _process_singleton_async(self, prompt, *, skip_mm_cache: bool = False):
|
||||
renderer = cast(BaseRenderer, self)
|
||||
if self._routes_no_media_kwargs(prompt):
|
||||
inputs = await renderer._process_multimodal_async(
|
||||
prompt["prompt_token_ids"],
|
||||
{},
|
||||
mm_processor_kwargs=prompt["mm_processor_kwargs"],
|
||||
mm_uuids=prompt.get("multi_modal_uuids"),
|
||||
skip_mm_cache=skip_mm_cache,
|
||||
)
|
||||
inputs = token_inputs_omni(
|
||||
prompt_token_ids,
|
||||
prompt_embeds=parsed_content.get("prompt_embeds"),
|
||||
additional_information=parsed_content.get("additional_information"),
|
||||
model_intermediate_buffer=parsed_content.get("model_intermediate_buffer"),
|
||||
)
|
||||
prompt_embeds = parsed_content.get("prompt_embeds")
|
||||
if prompt_embeds is not None:
|
||||
inputs["prompt_embeds"] = prompt_embeds
|
||||
additional_information = parsed_content.get("additional_information")
|
||||
if additional_information is not None:
|
||||
inputs["additional_information"] = additional_information
|
||||
model_intermediate_buffer = parsed_content.get("model_intermediate_buffer")
|
||||
if model_intermediate_buffer is not None:
|
||||
inputs["model_intermediate_buffer"] = model_intermediate_buffer
|
||||
if cache_salt := parsed_content.get("cache_salt"):
|
||||
inputs["cache_salt"] = cache_salt
|
||||
|
||||
return inputs
|
||||
|
||||
def _process_tokens(
|
||||
self,
|
||||
parsed_content: OmniTokensPrompt,
|
||||
tokenization_kwargs: dict[str, Any] | None = None,
|
||||
) -> OmniTokenInputs | MultiModalInput:
|
||||
prompt_token_ids = self._truncate_inputs(parsed_content["prompt_token_ids"], tokenization_kwargs)
|
||||
prompt_embeds = parsed_content.get("prompt_embeds")
|
||||
additional_information = parsed_content.get("additional_information")
|
||||
model_intermediate_buffer = parsed_content.get("model_intermediate_buffer")
|
||||
|
||||
multi_modal_data = parsed_content.get("multi_modal_data")
|
||||
|
||||
inputs: OmniTokenInputs | MultiModalInput
|
||||
if multi_modal_data:
|
||||
inputs = self._process_multimodal(
|
||||
prompt_token_ids,
|
||||
multi_modal_data,
|
||||
parsed_content.get("mm_processor_kwargs"),
|
||||
tokenization_kwargs=tokenization_kwargs,
|
||||
mm_uuids=parsed_content.get("multi_modal_uuids"),
|
||||
)
|
||||
|
||||
else:
|
||||
inputs = token_inputs_omni(
|
||||
prompt_token_ids=prompt_token_ids,
|
||||
prompt_embeds=prompt_embeds,
|
||||
additional_information=additional_information,
|
||||
model_intermediate_buffer=model_intermediate_buffer,
|
||||
)
|
||||
if prompt_embeds is not None:
|
||||
inputs["prompt_embeds"] = prompt_embeds
|
||||
if additional_information is not None:
|
||||
inputs["additional_information"] = additional_information
|
||||
if model_intermediate_buffer is not None:
|
||||
inputs["model_intermediate_buffer"] = model_intermediate_buffer
|
||||
if prompt_text := parsed_content.get("prompt"):
|
||||
inputs["prompt"] = prompt_text
|
||||
if cache_salt := parsed_content.get("cache_salt"):
|
||||
inputs["cache_salt"] = cache_salt
|
||||
inputs = await cast(BaseRenderer, super())._process_singleton_async(prompt, skip_mm_cache=skip_mm_cache)
|
||||
return self._with_omni_extras(inputs, prompt)
|
||||
|
||||
return inputs
|
||||
|
||||
def _process_embeds(
|
||||
self,
|
||||
parsed_content: OmniEmbedsPrompt,
|
||||
) -> EmbedsInput:
|
||||
"""Process embeddings prompt with omni-specific extensions.
|
||||
_OMNI_RENDERER_CLASSES: dict[type[BaseRenderer], type[BaseRenderer]] = {}
|
||||
|
||||
Extends base _process_embeds to handle additional_information payload
|
||||
for direct transfer between pipeline stages.
|
||||
"""
|
||||
# Call parent implementation for base embeds processing
|
||||
inputs = super()._process_embeds(parsed_content)
|
||||
|
||||
# Add omni-specific additional_information if present
|
||||
additional_information = parsed_content.get("additional_information")
|
||||
if additional_information is not None:
|
||||
inputs["additional_information"] = additional_information # type: ignore[typeddict-unknown-key]
|
||||
model_intermediate_buffer = parsed_content.get("model_intermediate_buffer")
|
||||
if model_intermediate_buffer is not None:
|
||||
inputs["model_intermediate_buffer"] = model_intermediate_buffer # type: ignore[typeddict-unknown-key]
|
||||
def omni_renderer_cls(base_cls: type[BaseRenderer]) -> type[BaseRenderer]:
|
||||
"""Return the Omni subclass of ``base_cls`` (created once per base class)."""
|
||||
if issubclass(base_cls, OmniRenderer):
|
||||
return base_cls
|
||||
cls = _OMNI_RENDERER_CLASSES.get(base_cls)
|
||||
if cls is None:
|
||||
name = f"Omni{base_cls.__name__}"
|
||||
cls = type(name, (OmniRenderer, base_cls), {"__module__": __name__, "__qualname__": name})
|
||||
_OMNI_RENDERER_CLASSES[base_cls] = cls
|
||||
return cls
|
||||
|
||||
return inputs
|
||||
|
||||
def _prompt_to_llm_inputs(
|
||||
self,
|
||||
prompt: SingletonDictPrompt,
|
||||
tokenization_kwargs: dict[str, Any] | None = None,
|
||||
*,
|
||||
mm_uuids: Any | None = None,
|
||||
) -> SingletonInput:
|
||||
"""
|
||||
Extract the singleton inputs from a prompt.
|
||||
def build_omni_renderer(
|
||||
vllm_config: VllmConfig,
|
||||
*,
|
||||
renderer_cls: type[BaseRenderer] | None = None,
|
||||
tokenizer: TokenizerLike | None = None,
|
||||
) -> BaseRenderer:
|
||||
"""Build the Omni renderer for ``vllm_config``.
|
||||
|
||||
Arguments:
|
||||
|
||||
* prompt: single encoder or decoder input prompt
|
||||
|
||||
Returns:
|
||||
|
||||
* [`SingletonInput`][vllm.inputs.engine.SingletonInput] instance
|
||||
"""
|
||||
if "prompt_embeds" in prompt:
|
||||
return self._process_embeds(prompt) # type: ignore[arg-type]
|
||||
|
||||
if "prompt_token_ids" in prompt:
|
||||
return self._process_tokens(
|
||||
prompt, # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
if "prompt" in prompt:
|
||||
return self._process_text(
|
||||
prompt, # type: ignore[arg-type]
|
||||
tokenization_kwargs=tokenization_kwargs,
|
||||
mm_uuids=mm_uuids,
|
||||
)
|
||||
|
||||
assert_never(prompt) # type: ignore[arg-type]
|
||||
With ``renderer_cls`` unset this mirrors upstream ``renderer_from_config``:
|
||||
the tokenizer and renderer mode come from the model config and the class
|
||||
from ``RENDERER_REGISTRY``. Pass ``renderer_cls`` (and optionally
|
||||
``tokenizer``) to skip that resolution, e.g. for tokenizer-less stages.
|
||||
"""
|
||||
model_config = vllm_config.model_config
|
||||
if renderer_cls is None:
|
||||
if tokenizer is None:
|
||||
tokenizer = cached_tokenizer_from_config(model_config)
|
||||
renderer_mode, *_ = tokenizer_args_from_config(model_config)
|
||||
renderer_cls = RENDERER_REGISTRY.load_renderer_cls(renderer_mode)
|
||||
return omni_renderer_cls(renderer_cls)(vllm_config, tokenizer)
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
#
|
||||
# Adapted from nvidia/Nemotron-Labs-Audex-2B (Apache-2.0):
|
||||
# inference_scripts_vllm/audioqa_scripts/audex_2b_vllm/audio_features.py
|
||||
@@ -33,13 +33,14 @@ from vllm.multimodal.inputs import MultiModalFieldConfig, MultiModalKwargsItems
|
||||
from vllm.multimodal.parse import MultiModalDataItems, MultiModalDataParser
|
||||
from vllm.multimodal.processing import (
|
||||
BaseDummyInputsBuilder,
|
||||
BaseMultiModalProcessor,
|
||||
BaseProcessingInfo,
|
||||
PromptReplacement,
|
||||
PromptUpdate,
|
||||
PromptUpdateDetails,
|
||||
)
|
||||
|
||||
from vllm_omni.inputs.mm_processor import OmniMultiModalProcessor
|
||||
|
||||
SOUND_TOKEN = "<so_embedding>"
|
||||
SOUND_START_TOKEN = "<so_start>"
|
||||
SOUND_END_TOKEN = "<so_end>"
|
||||
@@ -176,7 +177,7 @@ class AudexDummyInputsBuilder(BaseDummyInputsBuilder[AudexProcessingInfo]):
|
||||
return {"audio": self._get_dummy_audios(length=audio_len, num_audios=num_audios, overrides=overrides)}
|
||||
|
||||
|
||||
class AudexMultiModalProcessor(BaseMultiModalProcessor[AudexProcessingInfo]):
|
||||
class AudexMultiModalProcessor(OmniMultiModalProcessor[AudexProcessingInfo]):
|
||||
def _call_hf_processor(
|
||||
self,
|
||||
prompt: str,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
from collections.abc import Iterable, Mapping, Sequence
|
||||
from dataclasses import replace
|
||||
from math import isqrt
|
||||
from typing import Any
|
||||
|
||||
@@ -20,7 +21,7 @@ from vllm.model_executor.model_loader.weight_utils import default_weight_loader
|
||||
from vllm.model_executor.models.bagel import BagelForConditionalGeneration
|
||||
from vllm.model_executor.models.interfaces import MultiModalEmbeddings
|
||||
from vllm.model_executor.models.qwen2 import Qwen2DecoderLayer, Qwen2MLP
|
||||
from vllm.model_executor.models.utils import AutoWeightsLoader
|
||||
from vllm.model_executor.models.utils import AutoWeightsLoader, WeightsMapper
|
||||
from vllm.multimodal import MULTIMODAL_REGISTRY
|
||||
from vllm.multimodal.inputs import (
|
||||
MultiModalFieldConfig,
|
||||
@@ -221,6 +222,20 @@ class OmniBagelDataParser(MultiModalDataParser):
|
||||
class OmniBagelMultiModalProcessor(BaseMultiModalProcessor[OmniBagelProcessingInfo]):
|
||||
IMG2IMG_PLACEHOLDER = "<|fim_middle|>"
|
||||
|
||||
def apply(self, inputs, timing_ctx):
|
||||
num_img2img = inputs.mm_data_items.get_all_counts().get("img2img", 0)
|
||||
if num_img2img:
|
||||
token_id = self.info.get_tokenizer().get_vocab().get(self.IMG2IMG_PLACEHOLDER)
|
||||
if token_id is not None:
|
||||
prompt_ids = list(inputs.prompt)
|
||||
missing = num_img2img - prompt_ids.count(token_id)
|
||||
if missing > 0:
|
||||
inputs = replace(
|
||||
inputs,
|
||||
prompt=[token_id] * missing + prompt_ids,
|
||||
)
|
||||
return super().apply(inputs, timing_ctx)
|
||||
|
||||
@staticmethod
|
||||
def _mm_kwargs_for_bagel_img2img_hf(mm_kwargs: Mapping[str, object]) -> dict[str, object]:
|
||||
# OpenAI / GLM-style serving may pass target_h/target_w for output grid sizing.
|
||||
@@ -241,69 +256,51 @@ class OmniBagelMultiModalProcessor(BaseMultiModalProcessor[OmniBagelProcessingIn
|
||||
"pixel_values_img2img": MultiModalFieldConfig.batched("img2img"),
|
||||
}
|
||||
|
||||
def _call_hf_processor(
|
||||
def _apply_hf_processor_main(
|
||||
self,
|
||||
prompt: str,
|
||||
mm_data: Mapping[str, object],
|
||||
mm_kwargs: Mapping[str, object],
|
||||
tok_kwargs: Mapping[str, object],
|
||||
) -> "BatchFeature":
|
||||
has_image = "images" in mm_data
|
||||
has_img2img = "pixel_values_img2img" in mm_data
|
||||
|
||||
if has_img2img and self.IMG2IMG_PLACEHOLDER not in prompt:
|
||||
prompt = f"{self.IMG2IMG_PLACEHOLDER}{prompt}"
|
||||
|
||||
if has_image and has_img2img:
|
||||
outputs = BatchFeature()
|
||||
|
||||
img_data = dict(mm_data)
|
||||
if "pixel_values_img2img" in img_data:
|
||||
del img_data["pixel_values_img2img"]
|
||||
kwargs_img = dict(mm_kwargs)
|
||||
kwargs_img["is_img2img"] = False
|
||||
out_img = super()._call_hf_processor(prompt, img_data, kwargs_img, tok_kwargs)
|
||||
if "pixel_values" in out_img:
|
||||
outputs["pixel_values"] = out_img["pixel_values"]
|
||||
for k, v in out_img.items():
|
||||
if k != "pixel_values":
|
||||
outputs[k] = v
|
||||
|
||||
img2img_data = dict(mm_data)
|
||||
if "images" in img2img_data:
|
||||
del img2img_data["images"]
|
||||
img2img_data["images"] = img2img_data.pop("pixel_values_img2img")
|
||||
kwargs_img2img = self._mm_kwargs_for_bagel_img2img_hf(mm_kwargs)
|
||||
kwargs_img2img["is_img2img"] = True
|
||||
out_img2img = super()._call_hf_processor(prompt, img2img_data, kwargs_img2img, tok_kwargs)
|
||||
if "pixel_values" in out_img2img:
|
||||
outputs["pixel_values_img2img"] = out_img2img["pixel_values"]
|
||||
for k, v in out_img2img.items():
|
||||
if k not in outputs:
|
||||
outputs[k] = v
|
||||
|
||||
return outputs
|
||||
|
||||
elif has_img2img:
|
||||
mm_data = dict(mm_data)
|
||||
mm_data["images"] = mm_data.pop("pixel_values_img2img")
|
||||
mm_kwargs = self._mm_kwargs_for_bagel_img2img_hf(mm_kwargs)
|
||||
mm_kwargs["is_img2img"] = True
|
||||
outputs = super()._call_hf_processor(prompt, mm_data, mm_kwargs, tok_kwargs)
|
||||
if "pixel_values" in outputs:
|
||||
outputs["pixel_values_img2img"] = outputs.pop("pixel_values")
|
||||
return outputs
|
||||
|
||||
return super()._call_hf_processor(prompt, mm_data, mm_kwargs, tok_kwargs)
|
||||
|
||||
def _hf_processor_applies_updates(
|
||||
self,
|
||||
prompt_text: str,
|
||||
mm_items: MultiModalDataItems,
|
||||
hf_processor_mm_kwargs: Mapping[str, object],
|
||||
tokenization_kwargs: Mapping[str, object],
|
||||
) -> bool:
|
||||
return False
|
||||
) -> BatchFeature:
|
||||
valid_mm_items = mm_items.select({key for key, count in mm_items.get_all_counts().items() if count > 0})
|
||||
mm_data, passthrough_data = self._get_hf_mm_data(valid_mm_items)
|
||||
prompt_text = self.dummy_inputs.get_dummy_text(mm_items.get_all_counts())
|
||||
processor = self.info.get_hf_processor(**hf_processor_mm_kwargs)
|
||||
|
||||
has_image = "images" in mm_data
|
||||
has_img2img = "pixel_values_img2img" in mm_data
|
||||
processed_data = BatchFeature()
|
||||
|
||||
if has_image:
|
||||
image_kwargs = {**hf_processor_mm_kwargs, "is_img2img": False}
|
||||
image_outputs = self.info.ctx.call_hf_processor(
|
||||
processor,
|
||||
{"text": prompt_text, "images": mm_data["images"]},
|
||||
image_kwargs,
|
||||
)
|
||||
processed_data.update(image_outputs)
|
||||
|
||||
if has_img2img:
|
||||
img2img_kwargs = self._mm_kwargs_for_bagel_img2img_hf(hf_processor_mm_kwargs)
|
||||
img2img_kwargs["is_img2img"] = True
|
||||
img2img_outputs = self.info.ctx.call_hf_processor(
|
||||
processor,
|
||||
{
|
||||
"text": prompt_text,
|
||||
"images": mm_data["pixel_values_img2img"],
|
||||
},
|
||||
img2img_kwargs,
|
||||
)
|
||||
pixel_values = img2img_outputs.pop("pixel_values", None)
|
||||
if pixel_values is not None:
|
||||
processed_data["pixel_values_img2img"] = pixel_values
|
||||
for key, value in img2img_outputs.items():
|
||||
processed_data.setdefault(key, value)
|
||||
|
||||
if not has_image and not has_img2img:
|
||||
processed_data = BatchFeature(dict(passthrough_data))
|
||||
else:
|
||||
processed_data.update(passthrough_data)
|
||||
return processed_data
|
||||
|
||||
def _get_prompt_updates(
|
||||
self,
|
||||
@@ -372,7 +369,8 @@ class OmniBagelMultiModalProcessor(BaseMultiModalProcessor[OmniBagelProcessingIn
|
||||
embed_mask = [True] * num_vae_total + [False] + [True] * num_vit_total
|
||||
return PromptUpdateDetails(
|
||||
full=tokens,
|
||||
is_embed=lambda _tok, _seq, _m=embed_mask: torch.tensor(_m, dtype=torch.bool),
|
||||
# vLLM 0.29 calls is_embed with just the full token list.
|
||||
is_embed=lambda _full, _m=embed_mask: torch.tensor(_m, dtype=torch.bool),
|
||||
)
|
||||
|
||||
replacements.append(
|
||||
@@ -1102,11 +1100,12 @@ class OmniBagelForConditionalGeneration(BagelForConditionalGeneration):
|
||||
filtered_weights.append((mapped_name, tensor))
|
||||
|
||||
loader = AutoWeightsLoader(
|
||||
self,
|
||||
skip_prefixes=["vit_pos_embed.pos_embed"],
|
||||
ignore_unexpected_prefixes=["vae.", "latent_pos_embed.", "time_embedder.", "vae2llm."],
|
||||
self, ignore_unexpected_prefixes=["vae.", "latent_pos_embed.", "time_embedder.", "vae2llm."]
|
||||
)
|
||||
loaded = loader.load_weights(
|
||||
filtered_weights,
|
||||
mapper=(self.hf_to_vllm_mapper) | WeightsMapper(orig_to_new_prefix={"vit_pos_embed.pos_embed": None}),
|
||||
)
|
||||
loaded = loader.load_weights(filtered_weights, mapper=self.hf_to_vllm_mapper)
|
||||
|
||||
loaded |= self._load_moe_gen_weights(moe_gen_weights)
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
import os
|
||||
from collections.abc import Iterable, Mapping, Sequence
|
||||
from dataclasses import replace
|
||||
from functools import partial
|
||||
from math import gcd
|
||||
from threading import Lock
|
||||
@@ -24,7 +25,6 @@ from vllm.multimodal.inputs import MultiModalFieldConfig, MultiModalKwargsItems
|
||||
from vllm.multimodal.parse import MultiModalDataItems, MultiModalDataParser
|
||||
from vllm.multimodal.processing import (
|
||||
BaseDummyInputsBuilder,
|
||||
BaseMultiModalProcessor,
|
||||
BaseProcessingInfo,
|
||||
ProcessorInputs,
|
||||
PromptIndexTargets,
|
||||
@@ -38,6 +38,7 @@ from vllm.v1.sample.ops.topk_topp_sampler import random_sample
|
||||
from vllm.v1.sample.sampler import Sampler
|
||||
|
||||
from vllm_omni.data_entry_keys import EmbeddingsStruct, OmniPayloadStruct, to_dict, to_struct
|
||||
from vllm_omni.inputs.mm_processor import OmniMultiModalProcessor
|
||||
from vllm_omni.model_executor.models.cosyvoice3.tokenizer import get_qwen_tokenizer
|
||||
from vllm_omni.model_executor.models.cosyvoice3.utils import (
|
||||
concat_text_with_prompt_ids,
|
||||
@@ -108,7 +109,45 @@ class CosyVoice3MultiModalProcessingInfo(BaseProcessingInfo):
|
||||
)
|
||||
|
||||
|
||||
class CosyVoice3MultiModalProcessor(BaseMultiModalProcessor[CosyVoice3MultiModalProcessingInfo]):
|
||||
class CosyVoice3MultiModalProcessor(OmniMultiModalProcessor[CosyVoice3MultiModalProcessingInfo]):
|
||||
def apply(self, inputs: ProcessorInputs, timing_ctx):
|
||||
tokenizer = self.info.get_tokenizer()
|
||||
prompt_text = tokenizer.decode(inputs.prompt, skip_special_tokens=False)
|
||||
config = self.info.ctx.get_hf_config()
|
||||
model_dir = self.info.ctx.model_config.model
|
||||
self._ensure_cached_runtime_components(model_dir, config)
|
||||
|
||||
text_token, text_token_len = extract_text_token(
|
||||
prompt_text,
|
||||
self.tokenizer,
|
||||
config.allowed_special,
|
||||
)
|
||||
if inputs.mm_data_items.get_all_counts().get("audio", 0):
|
||||
reference_text = inputs.hf_processor_mm_kwargs.get("prompt_text")
|
||||
if not isinstance(reference_text, str):
|
||||
raise ValueError(f"prompt text is None : {reference_text}")
|
||||
prompt_text_token, prompt_text_token_len = extract_text_token(
|
||||
reference_text,
|
||||
self.tokenizer,
|
||||
config.allowed_special,
|
||||
)
|
||||
text_token, _ = concat_text_with_prompt_ids(
|
||||
text_token,
|
||||
text_token_len,
|
||||
prompt_text_token,
|
||||
prompt_text_token_len,
|
||||
)
|
||||
|
||||
inputs = replace(
|
||||
inputs,
|
||||
prompt=text_token.reshape(-1).tolist(),
|
||||
hf_processor_mm_kwargs={
|
||||
**inputs.hf_processor_mm_kwargs,
|
||||
self._OMNI_PROMPT_TEXT_KEY: prompt_text,
|
||||
},
|
||||
)
|
||||
return super().apply(inputs, timing_ctx)
|
||||
|
||||
def _ensure_cached_runtime_components(self, model_dir: str, config: CosyVoice3Config) -> None:
|
||||
cached_model_dir = getattr(self, "_cached_model_dir", None)
|
||||
if cached_model_dir == model_dir:
|
||||
@@ -376,15 +415,6 @@ class CosyVoice3MultiModalProcessor(BaseMultiModalProcessor[CosyVoice3MultiModal
|
||||
"embedding": MultiModalFieldConfig.batched("audio"),
|
||||
}
|
||||
|
||||
def _hf_processor_applies_updates(
|
||||
self,
|
||||
prompt_text: str,
|
||||
mm_items: MultiModalDataItems,
|
||||
hf_processor_mm_kwargs: Mapping[str, object],
|
||||
tokenization_kwargs: Mapping[str, object],
|
||||
) -> bool:
|
||||
return False
|
||||
|
||||
def _get_prompt_updates(
|
||||
self,
|
||||
mm_items: MultiModalDataItems,
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
# Copyright 2026 Tencent.
|
||||
from collections.abc import Iterable, Mapping, Sequence
|
||||
from typing import Any
|
||||
@@ -17,7 +20,6 @@ from vllm.multimodal.inputs import MultiModalFieldConfig
|
||||
from vllm.multimodal.parse import MultiModalDataItems, MultiModalDataParser
|
||||
from vllm.multimodal.processing import (
|
||||
BaseDummyInputsBuilder,
|
||||
BaseMultiModalProcessor,
|
||||
BaseProcessingInfo,
|
||||
ProcessorInputs,
|
||||
PromptReplacement,
|
||||
@@ -26,6 +28,7 @@ from vllm.multimodal.processing import (
|
||||
from vllm.sequence import IntermediateTensors
|
||||
from vllm.v1.sample.metadata import SamplingMetadata
|
||||
|
||||
from vllm_omni.inputs.mm_processor import OmniMultiModalProcessor
|
||||
from vllm_omni.model_executor.custom_process_mixin import CustomProcessMixin
|
||||
from vllm_omni.model_executor.models.output_templates import OmniOutput
|
||||
from vllm_omni.model_executor.models.utils import add_prefix_to_loaded_weights
|
||||
@@ -106,10 +109,7 @@ class CovoAudioDummyInputsBuilder(BaseDummyInputsBuilder[CovoAudioProcessingInfo
|
||||
)
|
||||
|
||||
|
||||
class CovoAudioMultiModalProcessor(BaseMultiModalProcessor[CovoAudioProcessingInfo]):
|
||||
def _hf_processor_applies_updates(self, prompt_text, mm_items, hf_processor_mm_kwargs, tokenization_kwargs) -> bool:
|
||||
return False
|
||||
|
||||
class CovoAudioMultiModalProcessor(OmniMultiModalProcessor[CovoAudioProcessingInfo]):
|
||||
def _call_hf_processor(
|
||||
self, prompt: str, mm_data: Mapping[str, object], mm_kwargs: Mapping[str, Any], tok_kwargs: Mapping[str, object]
|
||||
) -> BatchFeature:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Gepard-1.0 native-AR talker.
|
||||
|
||||
Single-stage AR TTS on a vLLM-native Qwen3.5 backbone. Each step samples one
|
||||
@@ -22,7 +22,7 @@ import torch.nn as nn
|
||||
from vllm.config import VllmConfig
|
||||
from vllm.logger import init_logger
|
||||
from vllm.model_executor.models.qwen3_5 import Qwen3_5ForCausalLM
|
||||
from vllm.model_executor.models.utils import AutoWeightsLoader, maybe_prefix
|
||||
from vllm.model_executor.models.utils import AutoWeightsLoader, WeightsMapper, maybe_prefix
|
||||
from vllm.sequence import IntermediateTensors
|
||||
|
||||
from vllm_omni.model_executor.models.gepard.configuration_gepard import GepardConfig
|
||||
@@ -199,8 +199,10 @@ class GepardTalkerForConditionalGeneration(nn.Module):
|
||||
|
||||
# NanoCodec loads lazily on first decode, so load_format=dummy and
|
||||
# NeMo-less environments never pay for it.
|
||||
loader = AutoWeightsLoader(self, skip_prefixes=["mtp.", "ref_compressor."])
|
||||
return loader.load_weights(iter(rest))
|
||||
loader = AutoWeightsLoader(self)
|
||||
return loader.load_weights(
|
||||
iter(rest), mapper=WeightsMapper(orig_to_new_prefix={"mtp.": None, "ref_compressor.": None})
|
||||
)
|
||||
|
||||
def _get_or_create_state(self, request_id: str) -> _GepardState:
|
||||
st = self._active_states.get(request_id)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
# Adapted from
|
||||
# https://github.com/huggingface/transformers/blob/main/src/transformers/models/glm_image/modeling_glm_image.py
|
||||
@@ -24,6 +24,7 @@
|
||||
import math
|
||||
import os
|
||||
from collections.abc import Iterable, Mapping, Sequence
|
||||
from dataclasses import replace
|
||||
from functools import lru_cache
|
||||
from typing import Annotated, Literal
|
||||
|
||||
@@ -81,7 +82,6 @@ from vllm.multimodal.inputs import (
|
||||
MultiModalKwargsItems,
|
||||
)
|
||||
from vllm.multimodal.parse import (
|
||||
ImageProcessorItems,
|
||||
MultiModalDataItems,
|
||||
MultiModalDataParser,
|
||||
)
|
||||
@@ -341,6 +341,30 @@ class GlmImageDummyInputsBuilder(BaseDummyInputsBuilder[GlmImageProcessingInfo])
|
||||
}
|
||||
|
||||
|
||||
_GLM_IMAGE_GRID_FACTOR = 32
|
||||
|
||||
|
||||
def _build_target_shape_scaffold(processor, *, height: int, width: int, is_text_to_image: bool) -> str:
|
||||
"""Return the grid scaffold HF appends to a GLM-Image generation prompt.
|
||||
|
||||
Mirrors ``GlmImageProcessor._build_prompt_with_target_shape`` (identical in
|
||||
transformers 5.13 and 5.14) using only the processor's public token
|
||||
attributes, so this does not depend on a private HF method that a stub or
|
||||
a future release may not provide.
|
||||
"""
|
||||
factor = _GLM_IMAGE_GRID_FACTOR
|
||||
token_h = (height // factor * factor) // factor
|
||||
token_w = (width // factor * factor) // factor
|
||||
grid_bos, grid_eos, bos = processor.grid_bos_token, processor.grid_eos_token, processor.bos_token
|
||||
scaffold = f"{grid_bos}{token_h} {token_w}{grid_eos}"
|
||||
if is_text_to_image:
|
||||
ratio = token_h / token_w
|
||||
prev_token_h = int(math.sqrt(ratio) * (factor // 2))
|
||||
prev_token_w = int(math.sqrt(1 / ratio) * (factor // 2))
|
||||
scaffold += f"{grid_bos}{prev_token_h} {prev_token_w}{grid_eos}"
|
||||
return scaffold + bos
|
||||
|
||||
|
||||
class GlmImageMultiModalProcessor(BaseMultiModalProcessor[GlmImageProcessingInfo]):
|
||||
"""
|
||||
Multimodal processor for GLM-Image.
|
||||
@@ -358,265 +382,38 @@ class GlmImageMultiModalProcessor(BaseMultiModalProcessor[GlmImageProcessingInfo
|
||||
return self._apply_hf_processor(inputs, timing_ctx)
|
||||
return super()._cached_apply_hf_processor(inputs, timing_ctx)
|
||||
|
||||
def _call_hf_processor(
|
||||
self,
|
||||
prompt: str,
|
||||
mm_data: Mapping[str, object],
|
||||
mm_kwargs: Mapping[str, object],
|
||||
tok_kwargs: Mapping[str, object],
|
||||
) -> BatchFeature:
|
||||
"""
|
||||
Call the HuggingFace processor.
|
||||
|
||||
For text-to-image mode (no images), we need to:
|
||||
1. Build the prompt with target grid dimensions
|
||||
2. Build the image_grid_thw tensor for M-RoPE position encoding
|
||||
|
||||
For image-to-image mode:
|
||||
1. Process source images through the image processor
|
||||
2. Build prompt with image placeholders expanded
|
||||
3. Build image_grid_thw including source and target grids
|
||||
"""
|
||||
processor = self.info.get_hf_processor()
|
||||
|
||||
# Get target dimensions from mm_kwargs or use defaults
|
||||
target_h = mm_kwargs.get("target_h", 1024) if mm_kwargs else 1024
|
||||
target_w = mm_kwargs.get("target_w", 1024) if mm_kwargs else 1024
|
||||
|
||||
logger.debug(
|
||||
f"_call_hf_processor: target dimensions for generation: {target_h}x{target_w}, mm_kwargs={mm_kwargs}"
|
||||
)
|
||||
|
||||
if not mm_data or not mm_data.get("images"):
|
||||
# Text-to-image mode
|
||||
if processor is not None:
|
||||
# Build messages format expected by processor
|
||||
messages = [{"role": "user", "content": [{"type": "text", "text": prompt}]}]
|
||||
|
||||
# Use apply_chat_template which handles target dimensions
|
||||
hf_inputs = processor.apply_chat_template(
|
||||
messages,
|
||||
tokenize=True,
|
||||
target_h=target_h,
|
||||
target_w=target_w,
|
||||
return_dict=True,
|
||||
return_tensors="pt",
|
||||
)
|
||||
|
||||
return hf_inputs
|
||||
else:
|
||||
# Fallback: just tokenize (this won't work properly for generation)
|
||||
tokenizer = self.info.get_tokenizer()
|
||||
prompt_ids = tokenizer.encode(prompt)
|
||||
return BatchFeature(dict(input_ids=[prompt_ids]), tensor_type="pt")
|
||||
|
||||
# Image-to-image mode
|
||||
# NOTE: Use "images" (plural) - this is what vLLM's ImageProcessorItems.get_processor_data() returns
|
||||
images = mm_data.get("images")
|
||||
if not isinstance(images, list):
|
||||
images = [images]
|
||||
|
||||
logger.debug(
|
||||
f"_call_hf_processor i2i: num_images={len(images)}, image_types={[type(img).__name__ for img in images]}"
|
||||
)
|
||||
|
||||
if processor is not None:
|
||||
# Build messages with image objects directly in content
|
||||
# This is how GlmImageProcessor expects images - embedded in the content dict
|
||||
# NOT as a separate images= parameter
|
||||
#
|
||||
# IMPORTANT: Remove <|image|> placeholders from prompt since apply_chat_template
|
||||
# will automatically insert them for each image in content. Having both leads to
|
||||
# index out of bounds when processing image_grid_thw.
|
||||
clean_prompt = prompt.replace("<|image|>", "")
|
||||
content = []
|
||||
for img in images:
|
||||
content.append({"type": "image", "image": img})
|
||||
content.append({"type": "text", "text": clean_prompt})
|
||||
messages = [{"role": "user", "content": content}]
|
||||
|
||||
logger.debug(f"_call_hf_processor: calling apply_chat_template with {len(images)} images in content")
|
||||
|
||||
# Use apply_chat_template - processor will process images when they're in content
|
||||
hf_inputs = processor.apply_chat_template(
|
||||
messages,
|
||||
tokenize=True,
|
||||
target_h=target_h,
|
||||
target_w=target_w,
|
||||
return_dict=True,
|
||||
return_tensors="pt",
|
||||
def apply(self, inputs, timing_ctx):
|
||||
"""Add the generation scaffold before upstream expands source images."""
|
||||
num_images = inputs.mm_data_items.get_all_counts().get("image", 0)
|
||||
prompt_ids = list(inputs.prompt)
|
||||
if num_images:
|
||||
image_token_id = getattr(
|
||||
self.info.get_hf_config(),
|
||||
"image_token_id",
|
||||
167855,
|
||||
)
|
||||
missing = num_images - prompt_ids.count(image_token_id)
|
||||
if missing > 0:
|
||||
prompt_ids = [image_token_id] * missing + prompt_ids
|
||||
|
||||
logger.debug(f"_call_hf_processor: apply_chat_template returned keys: {list(hf_inputs.keys())}")
|
||||
|
||||
# IMPORTANT (i2i): vLLM multimodal encoder must see source-only grids
|
||||
# (matching pixel_values and number of images), but M-RoPE needs full
|
||||
# grids (source + target) to compute correct decode positions.
|
||||
image_grid_thw = hf_inputs.get("image_grid_thw")
|
||||
if image_grid_thw is not None:
|
||||
# Preserve full grids for M-RoPE.
|
||||
hf_inputs["mrope_image_grid_thw"] = image_grid_thw
|
||||
|
||||
# Expose source-only grids for MM.
|
||||
# In most i2i requests, we process one prompt at a time here,
|
||||
# so `len(images)` is the number of source images.
|
||||
num_source_images = len(images)
|
||||
if image_grid_thw.shape[0] != num_source_images:
|
||||
source_grids = image_grid_thw[:num_source_images]
|
||||
hf_inputs["image_grid_thw"] = source_grids
|
||||
logger.debug(
|
||||
"_call_hf_processor: adjusted image_grid_thw for MM from %s to %s (num_source_images=%d)",
|
||||
tuple(image_grid_thw.shape),
|
||||
tuple(source_grids.shape),
|
||||
num_source_images,
|
||||
)
|
||||
|
||||
# Debug: Analyze input_ids for image tokens
|
||||
input_ids = hf_inputs.get("input_ids")
|
||||
if input_ids is not None:
|
||||
if hasattr(input_ids, "tolist"):
|
||||
ids_list = input_ids.tolist()
|
||||
if isinstance(ids_list[0], list):
|
||||
ids_list = ids_list[0] # Unbatch
|
||||
else:
|
||||
ids_list = list(input_ids)
|
||||
|
||||
# Get image token ID from config
|
||||
hf_config = self.info.get_hf_config()
|
||||
image_token_id = getattr(hf_config, "image_token_id", 167855)
|
||||
|
||||
# Count image tokens
|
||||
image_token_count = ids_list.count(image_token_id)
|
||||
logger.debug(
|
||||
f"_call_hf_processor: input_ids length={len(ids_list)}, "
|
||||
f"image_token_id={image_token_id}, "
|
||||
f"image_token_count={image_token_count}"
|
||||
)
|
||||
|
||||
# Log first/last few tokens to understand structure
|
||||
logger.debug(f"_call_hf_processor: first 20 tokens: {ids_list[:20]}")
|
||||
logger.debug(f"_call_hf_processor: last 20 tokens: {ids_list[-20:]}")
|
||||
|
||||
# Find positions of image tokens
|
||||
image_positions = [i for i, t in enumerate(ids_list) if t == image_token_id]
|
||||
if image_positions:
|
||||
logger.debug(f"_call_hf_processor: image token positions (first 10): {image_positions[:10]}")
|
||||
|
||||
return hf_inputs
|
||||
else:
|
||||
# Fallback without processor - this is not ideal but prevents crashes
|
||||
logger.warning("GlmImageProcessor not available, using fallback for i2i")
|
||||
tokenizer = self.info.get_tokenizer()
|
||||
hf_config = self.info.get_hf_config()
|
||||
|
||||
# Get image token
|
||||
image_token_id = getattr(hf_config, "image_token_id", 167855)
|
||||
try:
|
||||
image_token = tokenizer.convert_ids_to_tokens(image_token_id)
|
||||
except Exception:
|
||||
image_token = "<|image|>"
|
||||
|
||||
# Build prompt with image placeholders
|
||||
image_placeholders = image_token * len(images)
|
||||
full_prompt = f"{image_placeholders}{prompt}"
|
||||
prompt_ids = tokenizer.encode(full_prompt)
|
||||
return BatchFeature(dict(input_ids=[prompt_ids]), tensor_type="pt")
|
||||
|
||||
def _apply_hf_processor_mm_only(
|
||||
self,
|
||||
mm_items: MultiModalDataItems,
|
||||
hf_processor_mm_kwargs: Mapping[str, object],
|
||||
tokenization_kwargs: Mapping[str, object],
|
||||
) -> BatchFeature:
|
||||
"""
|
||||
Apply the HF processor on the multi-modal data only.
|
||||
|
||||
GLM-Image requires special handling because apply_chat_template always
|
||||
adds a target <|image|> placeholder in addition to source image placeholders.
|
||||
This causes an IndexError when the HF processor tries to find grid info
|
||||
for the target placeholder (which doesn't exist for source-only processing).
|
||||
|
||||
Solution: Call the image processor directly to get pixel_values and
|
||||
image_grid_thw, bypassing apply_chat_template's target handling.
|
||||
"""
|
||||
mm_counts = mm_items.get_all_counts()
|
||||
num_images = mm_counts.get("image", 0)
|
||||
|
||||
if num_images == 0:
|
||||
# No images - call parent implementation
|
||||
return super()._apply_hf_processor_mm_only(
|
||||
mm_items=mm_items,
|
||||
hf_processor_mm_kwargs=hf_processor_mm_kwargs,
|
||||
tokenization_kwargs=tokenization_kwargs,
|
||||
)
|
||||
|
||||
# For i2i mode, we need to process images directly with the image processor
|
||||
# to avoid the apply_chat_template target placeholder issue
|
||||
processor = self.info.get_hf_processor()
|
||||
image_processor = processor.image_processor
|
||||
|
||||
# Get images from mm_items
|
||||
images = mm_items.get_items("image", ImageProcessorItems)
|
||||
image_list = [images.get(i) for i in range(images.get_count())]
|
||||
|
||||
logger.debug(f"_apply_hf_processor_mm_only: processing {len(image_list)} images directly")
|
||||
|
||||
# Process images directly with image processor
|
||||
image_inputs = image_processor(
|
||||
images=image_list,
|
||||
return_tensors="pt",
|
||||
)
|
||||
|
||||
# Get grid info for source images only (no target)
|
||||
pixel_values = image_inputs.get("pixel_values")
|
||||
image_grid_thw = image_inputs.get("image_grid_thw")
|
||||
if image_grid_thw is not None and image_grid_thw.shape[0] != num_images:
|
||||
# Be defensive: some processors may include extra target grids.
|
||||
image_grid_thw = image_grid_thw[:num_images]
|
||||
image_inputs["image_grid_thw"] = image_grid_thw
|
||||
|
||||
logger.debug(
|
||||
f"_apply_hf_processor_mm_only: pixel_values shape=\
|
||||
{pixel_values.shape if pixel_values is not None else None}, "
|
||||
f"image_grid_thw shape={image_grid_thw.shape if image_grid_thw is not None else None}"
|
||||
)
|
||||
|
||||
# Build input_ids with image token placeholders
|
||||
# The _get_prompt_updates returns PromptReplacement(target=[image_token_id], ...)
|
||||
# which needs to find image tokens in input_ids to replace them.
|
||||
# We need to include one image_token_id per image so the replacement can work.
|
||||
tokenizer = self.info.get_tokenizer()
|
||||
image_token_id = tokenizer.convert_tokens_to_ids("<|image|>")
|
||||
|
||||
# Build input_ids: [image_token] * num_images + tokenized text
|
||||
# This way _apply_prompt_updates can find the image tokens and replace them
|
||||
dummy_text = self.dummy_inputs.get_dummy_text(mm_counts)
|
||||
text_ids = tokenizer.encode(dummy_text, add_special_tokens=False)
|
||||
input_ids = [image_token_id] * num_images + text_ids
|
||||
|
||||
logger.debug(
|
||||
f"_apply_hf_processor_mm_only: built input_ids with {num_images} image tokens + {len(text_ids)} text tokens"
|
||||
# Append HF's target-shape scaffold without decoding/re-tokenizing the
|
||||
# user's tokens. Text-to-image needs both target and preview grids; i2i
|
||||
# needs only the target. Grid metadata alone does not supply these AR
|
||||
# tokens.
|
||||
target_grid = self._build_generation_grids(inputs.hf_processor_mm_kwargs)[0]
|
||||
suffix = _build_target_shape_scaffold(
|
||||
processor,
|
||||
height=int(target_grid[1]) * 32,
|
||||
width=int(target_grid[2]) * 32,
|
||||
is_text_to_image=num_images == 0,
|
||||
)
|
||||
|
||||
return BatchFeature(
|
||||
dict(
|
||||
input_ids=[input_ids],
|
||||
pixel_values=pixel_values,
|
||||
image_grid_thw=image_grid_thw,
|
||||
),
|
||||
tensor_type="pt",
|
||||
)
|
||||
|
||||
def _apply_hf_processor_text_only(
|
||||
self, prompt_text: str, hf_processor_mm_kwargs: Mapping[str, object], tokenization_kwargs: Mapping[str, object]
|
||||
) -> list[int]:
|
||||
prompt_ids, _, _ = super()._apply_hf_processor_text_mm(
|
||||
prompt_text=prompt_text,
|
||||
mm_items=MultiModalDataItems({}),
|
||||
hf_processor_mm_kwargs=hf_processor_mm_kwargs,
|
||||
tokenization_kwargs=tokenization_kwargs,
|
||||
)
|
||||
return prompt_ids
|
||||
suffix_ids = tokenizer.encode(suffix, add_special_tokens=False)
|
||||
if prompt_ids[-len(suffix_ids) :] != suffix_ids:
|
||||
prompt_ids.extend(suffix_ids)
|
||||
inputs = replace(inputs, prompt=prompt_ids)
|
||||
return super().apply(inputs, timing_ctx)
|
||||
|
||||
def _build_generation_grids(self, hf_processor_mm_kwargs: Mapping[str, object]) -> torch.Tensor:
|
||||
"""Build generation grids for M-RoPE decode positions.
|
||||
@@ -661,171 +458,39 @@ class GlmImageMultiModalProcessor(BaseMultiModalProcessor[GlmImageProcessingInfo
|
||||
|
||||
def _apply_hf_processor_main(
|
||||
self,
|
||||
prompt: str | list[int],
|
||||
mm_items: MultiModalDataItems,
|
||||
hf_processor_mm_kwargs: Mapping[str, object],
|
||||
tokenization_kwargs: Mapping[str, object],
|
||||
*,
|
||||
enable_hf_prompt_update: bool,
|
||||
) -> tuple[list[int], BatchFeature, bool]:
|
||||
"""
|
||||
Override to handle GLM-Image i2i mode correctly.
|
||||
|
||||
Problem: When vLLM processes cached mm items (enable_hf_prompt_update=False),
|
||||
the base implementation:
|
||||
1. Gets prompt_ids from _apply_hf_processor_text_only (no image tokens)
|
||||
2. Gets mm_data from _apply_hf_processor_mm_only
|
||||
3. Returns is_update_applied=False
|
||||
|
||||
This causes _apply_prompt_updates to fail because prompt_ids has no image tokens.
|
||||
|
||||
Solution: For i2i mode, we build prompt_ids that include image placeholders,
|
||||
and return is_update_applied=False so _apply_prompt_updates can expand them.
|
||||
"""
|
||||
) -> BatchFeature:
|
||||
"""Process source images under vLLM's token-only prompt contract."""
|
||||
mm_counts = mm_items.get_all_counts()
|
||||
num_images = mm_counts.get("image", 0)
|
||||
if num_images == 0:
|
||||
return BatchFeature({"mrope_image_grid_thw": self._build_generation_grids(hf_processor_mm_kwargs)})
|
||||
|
||||
logger.debug(f"_apply_hf_processor_main: mm_counts={mm_counts}, num_images={num_images}")
|
||||
valid_mm_items = mm_items.select({key for key, count in mm_counts.items() if count > 0})
|
||||
processor_data, passthrough_data = self._get_hf_mm_data(valid_mm_items)
|
||||
images = processor_data.get("images")
|
||||
if not images:
|
||||
return BatchFeature(dict(passthrough_data))
|
||||
|
||||
if num_images == 0 and isinstance(prompt, str):
|
||||
# t2i mode or normal flow - use parent implementation
|
||||
prompt_ids = self._apply_hf_processor_text_only(
|
||||
prompt_text=prompt,
|
||||
hf_processor_mm_kwargs=hf_processor_mm_kwargs,
|
||||
tokenization_kwargs=tokenization_kwargs,
|
||||
)
|
||||
mm_processed_data = self._apply_hf_processor_mm_only(
|
||||
mm_items=mm_items,
|
||||
hf_processor_mm_kwargs=hf_processor_mm_kwargs,
|
||||
tokenization_kwargs=tokenization_kwargs,
|
||||
)
|
||||
|
||||
# t2i has no source images, so mm features cannot provide image_grid_thw.
|
||||
# Provide explicit generation grids for M-RoPE to avoid fallback token parsing
|
||||
# (which can degrade high-resolution spatial positions, e.g. 1920x1920).
|
||||
try:
|
||||
mrope_grid_thw = self._build_generation_grids(hf_processor_mm_kwargs)
|
||||
mm_processed_data["mrope_image_grid_thw"] = mrope_grid_thw
|
||||
logger.debug(
|
||||
"_apply_hf_processor_main t2i: mrope_image_grid_thw=%s",
|
||||
mrope_grid_thw.tolist(),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning("_apply_hf_processor_main t2i: failed to set mrope_image_grid_thw: %s", e)
|
||||
|
||||
return prompt_ids, mm_processed_data, False
|
||||
|
||||
# i2i mode: use unified HF processor path only.
|
||||
# This avoids drift between duplicated manual/HF i2i implementations.
|
||||
logger.debug(
|
||||
"_apply_hf_processor_main: i2i mode (enable_hf_prompt_update=%s), num_images=%s",
|
||||
enable_hf_prompt_update,
|
||||
num_images,
|
||||
processor = self.info.get_hf_processor()
|
||||
image_inputs = processor.image_processor(
|
||||
images=list(images),
|
||||
return_tensors="pt",
|
||||
)
|
||||
image_grid_thw = image_inputs.get("image_grid_thw")
|
||||
if image_grid_thw is not None:
|
||||
source_grid_thw = image_grid_thw[:num_images]
|
||||
image_inputs["image_grid_thw"] = source_grid_thw
|
||||
|
||||
if not isinstance(prompt, str):
|
||||
# Online OpenAI chat preprocessing can arrive here with tokenized
|
||||
# prompts (list[int]) before serving_chat replaces engine prompt
|
||||
# with the clean text prompt. Do not fail the whole request.
|
||||
logger.warning(
|
||||
"_apply_hf_processor_main i2i: got tokenized prompt type=%s; "
|
||||
"using compatibility path for preprocessing",
|
||||
type(prompt).__name__,
|
||||
target_grid = self._build_generation_grids(hf_processor_mm_kwargs)[:1].to(dtype=source_grid_thw.dtype)
|
||||
image_inputs["mrope_image_grid_thw"] = torch.cat(
|
||||
[source_grid_thw, target_grid],
|
||||
dim=0,
|
||||
)
|
||||
|
||||
prompt_ids = list(prompt)
|
||||
mm_processed_data = self._apply_hf_processor_mm_only(
|
||||
mm_items=mm_items,
|
||||
hf_processor_mm_kwargs=hf_processor_mm_kwargs,
|
||||
tokenization_kwargs=tokenization_kwargs,
|
||||
)
|
||||
|
||||
# Preserve full grids for M-RoPE decode (source + target), while
|
||||
# keeping image_grid_thw source-only for MM batching.
|
||||
try:
|
||||
source_grid_thw = mm_processed_data.get("image_grid_thw")
|
||||
if source_grid_thw is not None and isinstance(source_grid_thw, torch.Tensor):
|
||||
target_h = (
|
||||
hf_processor_mm_kwargs.get("target_h")
|
||||
if isinstance(hf_processor_mm_kwargs.get("target_h"), int)
|
||||
else None
|
||||
)
|
||||
target_w = (
|
||||
hf_processor_mm_kwargs.get("target_w")
|
||||
if isinstance(hf_processor_mm_kwargs.get("target_w"), int)
|
||||
else None
|
||||
)
|
||||
if target_h is None or target_w is None:
|
||||
target_h = (
|
||||
hf_processor_mm_kwargs.get("height")
|
||||
if isinstance(hf_processor_mm_kwargs.get("height"), int)
|
||||
else 1024
|
||||
)
|
||||
target_w = (
|
||||
hf_processor_mm_kwargs.get("width")
|
||||
if isinstance(hf_processor_mm_kwargs.get("width"), int)
|
||||
else 1024
|
||||
)
|
||||
|
||||
factor = 32
|
||||
token_h = max(1, target_h // factor)
|
||||
token_w = max(1, target_w // factor)
|
||||
target_grid = torch.tensor([[1, token_h, token_w]], dtype=source_grid_thw.dtype)
|
||||
mm_processed_data["mrope_image_grid_thw"] = torch.cat([source_grid_thw, target_grid], dim=0)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Prompt updates will expand image placeholders in this compatibility path.
|
||||
return prompt_ids, mm_processed_data, False
|
||||
|
||||
images = mm_items.get_items("image", ImageProcessorItems)
|
||||
image_list = [images.get(i) for i in range(images.get_count())]
|
||||
if not image_list:
|
||||
raise ValueError("GLM-Image i2i requires at least one source image in mm_items")
|
||||
|
||||
hf_inputs = self._call_hf_processor(
|
||||
prompt=prompt,
|
||||
mm_data={"images": image_list},
|
||||
mm_kwargs=hf_processor_mm_kwargs,
|
||||
tok_kwargs=tokenization_kwargs,
|
||||
)
|
||||
|
||||
input_ids = hf_inputs.get("input_ids")
|
||||
if input_ids is None:
|
||||
raise ValueError("HF i2i processor returned no input_ids")
|
||||
|
||||
if isinstance(input_ids, torch.Tensor):
|
||||
prompt_ids = input_ids[0].tolist() if input_ids.dim() > 1 else input_ids.tolist()
|
||||
else:
|
||||
prompt_ids = (
|
||||
input_ids[0]
|
||||
if isinstance(input_ids, list) and input_ids and isinstance(input_ids[0], list)
|
||||
else list(input_ids)
|
||||
)
|
||||
|
||||
mm_processed_data = BatchFeature(dict(), tensor_type="pt")
|
||||
for key in ("pixel_values", "image_grid_thw", "mrope_image_grid_thw"):
|
||||
value = hf_inputs.get(key)
|
||||
if value is not None:
|
||||
mm_processed_data[key] = value
|
||||
|
||||
image_grid_thw = mm_processed_data.get("image_grid_thw")
|
||||
mrope_grid_thw = mm_processed_data.get("mrope_image_grid_thw")
|
||||
hf_config = self.info.get_hf_config()
|
||||
image_token_id = getattr(hf_config, "image_token_id", 167855)
|
||||
image_token_count = prompt_ids.count(image_token_id)
|
||||
logger.debug(
|
||||
"_apply_hf_processor_main i2i(HF): num_images=%s, prompt_len=%s, image_token_count=%s, "
|
||||
"source_grid_shape=%s, mrope_grid_shape=%s",
|
||||
num_images,
|
||||
len(prompt_ids),
|
||||
image_token_count,
|
||||
tuple(image_grid_thw.shape) if image_grid_thw is not None else None,
|
||||
tuple(mrope_grid_thw.shape) if mrope_grid_thw is not None else None,
|
||||
)
|
||||
|
||||
# HF processor already expanded image placeholders in input_ids.
|
||||
return prompt_ids, mm_processed_data, True
|
||||
image_inputs.update(passthrough_data)
|
||||
return image_inputs
|
||||
|
||||
def _get_mm_fields_config(
|
||||
self,
|
||||
@@ -836,7 +501,7 @@ class GlmImageMultiModalProcessor(BaseMultiModalProcessor[GlmImageProcessingInfo
|
||||
Get the multimodal field configuration.
|
||||
|
||||
For GLM-Image i2i mode:
|
||||
- image_grid_thw has been sliced in _call_hf_processor to only include source images
|
||||
- image_grid_thw contains only source grids from _apply_hf_processor_main
|
||||
- pixel_values has shape [total_patches, C, H, W] - only for source images
|
||||
|
||||
For t2i mode:
|
||||
@@ -844,7 +509,7 @@ class GlmImageMultiModalProcessor(BaseMultiModalProcessor[GlmImageProcessingInfo
|
||||
"""
|
||||
result = {}
|
||||
|
||||
# Get image_grid_thw if present (already sliced in _call_hf_processor)
|
||||
# Get the source-only image_grid_thw if present.
|
||||
image_grid_thw = hf_inputs.get("image_grid_thw")
|
||||
|
||||
if "pixel_values" in hf_inputs and image_grid_thw is not None:
|
||||
@@ -861,45 +526,21 @@ class GlmImageMultiModalProcessor(BaseMultiModalProcessor[GlmImageProcessingInfo
|
||||
|
||||
result["pixel_values"] = MultiModalFieldConfig.flat_from_sizes("image", image_grid_sizes)
|
||||
|
||||
# Register image_grid_thw - it's been sliced in _call_hf_processor
|
||||
# Register image_grid_thw - it's been sliced in _apply_hf_processor_main
|
||||
# to only include source image grids, so batching will work correctly
|
||||
result["image_grid_thw"] = MultiModalFieldConfig.batched("image")
|
||||
|
||||
if "mrope_image_grid_thw" in hf_inputs:
|
||||
result["mrope_image_grid_thw"] = MultiModalFieldConfig.shared(
|
||||
"image",
|
||||
num_source_images,
|
||||
keep_on_cpu=True,
|
||||
)
|
||||
|
||||
logger.debug(f"_get_mm_fields_config: result keys: {list(result.keys())}")
|
||||
|
||||
return result
|
||||
|
||||
def _hf_processor_applies_updates(
|
||||
self,
|
||||
prompt_text: str,
|
||||
mm_items: MultiModalDataItems,
|
||||
hf_processor_mm_kwargs: Mapping[str, object],
|
||||
tokenization_kwargs: Mapping[str, object],
|
||||
) -> bool:
|
||||
"""
|
||||
Return whether the HF processor applies prompt updates.
|
||||
|
||||
For GLM-Image i2i mode, the HF processor's apply_chat_template already
|
||||
expands <|image|> to N tokens (e.g., 4096 for 64x64 grid).
|
||||
|
||||
By returning True, we tell vLLM that HF processor DID apply prompt updates,
|
||||
so vLLM will use _find_mm_placeholders to locate the expanded tokens
|
||||
instead of trying to apply replacements.
|
||||
|
||||
For t2i mode (no images), there are no image placeholders to expand.
|
||||
"""
|
||||
# Check if we have images (i2i mode)
|
||||
num_images = mm_items.get_all_counts().get("image", 0)
|
||||
if num_images > 0:
|
||||
logger.debug(
|
||||
f"_hf_processor_applies_updates: returning True for i2i mode "
|
||||
f"(num_images={num_images}) - HF processor already expanded tokens"
|
||||
)
|
||||
return True
|
||||
|
||||
# For t2i mode (no images), use default behavior
|
||||
return True
|
||||
|
||||
def _get_prompt_updates(
|
||||
self,
|
||||
mm_items: MultiModalDataItems,
|
||||
@@ -909,17 +550,8 @@ class GlmImageMultiModalProcessor(BaseMultiModalProcessor[GlmImageProcessingInfo
|
||||
"""
|
||||
Get prompt updates for image tokens.
|
||||
|
||||
For GLM-Image image-to-image mode, the HF processor's apply_chat_template
|
||||
already expands each <|image|> placeholder to the correct number of
|
||||
image tokens (grid_h * grid_w tokens per source image).
|
||||
|
||||
The HF processor does:
|
||||
1. Replace each <|image|> with num_image_tokens copies of <|placeholder|>
|
||||
2. Replace all <|placeholder|> back to <|image|>
|
||||
|
||||
So the tokenized input already has the expanded tokens. We use
|
||||
target=[image_token_id] to match each occurrence of the image token,
|
||||
similar to how Qwen2VL handles this pattern.
|
||||
Expand each source-image placeholder into grid_h * grid_w image tokens
|
||||
through vLLM's token-based prompt updates.
|
||||
|
||||
We use image_grid_thw from out_mm_kwargs to get the actual processed grid
|
||||
size, following the Qwen2VL pattern. This is critical because the HF processor
|
||||
@@ -930,8 +562,7 @@ class GlmImageMultiModalProcessor(BaseMultiModalProcessor[GlmImageProcessingInfo
|
||||
"""
|
||||
hf_config = self.info.get_hf_config()
|
||||
|
||||
# Get image token ID - this is the token that appears multiple times
|
||||
# in the tokenized input after HF processor expansion
|
||||
# Match the source-image placeholder in the tokenized input.
|
||||
image_token_id = getattr(hf_config, "image_token_id", 167855)
|
||||
|
||||
# Debug: log mm_items info
|
||||
|
||||
@@ -39,7 +39,6 @@ from vllm.multimodal.inputs import MultiModalFieldConfig, MultiModalKwargsItems
|
||||
from vllm.multimodal.parse import MultiModalDataItems, MultiModalDataParser
|
||||
from vllm.multimodal.processing import (
|
||||
BaseDummyInputsBuilder,
|
||||
BaseMultiModalProcessor,
|
||||
BaseProcessingInfo,
|
||||
ProcessorInputs,
|
||||
PromptIndexTargets,
|
||||
@@ -51,6 +50,7 @@ from vllm.v1.outputs import SamplerOutput
|
||||
from vllm.v1.sample.metadata import SamplingMetadata
|
||||
from vllm.v1.sample.sampler import Sampler
|
||||
|
||||
from vllm_omni.inputs.mm_processor import OmniMultiModalProcessor
|
||||
from vllm_omni.model_executor.models.common.nucleus_ras_sampling import ras_sample_one as _ras_sample_one
|
||||
from vllm_omni.model_executor.models.output_templates import OmniOutput
|
||||
from vllm_omni.platforms import current_omni_platform
|
||||
@@ -364,7 +364,7 @@ class GLMTTSMultiModalProcessingInfo(BaseProcessingInfo):
|
||||
)
|
||||
|
||||
|
||||
class GLMTTSMultiModalProcessor(BaseMultiModalProcessor[GLMTTSMultiModalProcessingInfo]):
|
||||
class GLMTTSMultiModalProcessor(OmniMultiModalProcessor[GLMTTSMultiModalProcessingInfo]):
|
||||
"""GLM-TTS voice-clone processor.
|
||||
|
||||
Unlike CosyVoice3, GLM-TTS prompt speech tokens are normal Llama vocab IDs
|
||||
@@ -373,6 +373,53 @@ class GLMTTSMultiModalProcessor(BaseMultiModalProcessor[GLMTTSMultiModalProcessi
|
||||
handoff.
|
||||
"""
|
||||
|
||||
def apply(self, inputs: ProcessorInputs, timing_ctx):
|
||||
source_tokenizer = self.info.get_tokenizer()
|
||||
prompt_text = source_tokenizer.decode(
|
||||
inputs.prompt,
|
||||
skip_special_tokens=False,
|
||||
)
|
||||
config = self.info.ctx.get_hf_config()
|
||||
model_dir = self.info.ctx.model_config.model
|
||||
self._ensure_cached_runtime_components(
|
||||
model_dir,
|
||||
config,
|
||||
load_voice_clone=False,
|
||||
)
|
||||
|
||||
normalized_text = _normalize_glm_tts_processor_text(
|
||||
self.text_frontend,
|
||||
prompt_text,
|
||||
)
|
||||
text_ids = self._encode_text(normalized_text)
|
||||
prompt_parts = []
|
||||
if inputs.mm_data_items.get_all_counts().get("audio", 0):
|
||||
reference_text = inputs.hf_processor_mm_kwargs.get("prompt_text")
|
||||
if not isinstance(reference_text, str) or not reference_text.strip():
|
||||
raise ValueError("GLM-TTS voice cloning requires mm_processor_kwargs['prompt_text'].")
|
||||
normalized_reference = _normalize_glm_tts_processor_text(
|
||||
self.text_frontend,
|
||||
reference_text,
|
||||
add_trailing_space=True,
|
||||
)
|
||||
prompt_parts.append(self._encode_text(normalized_reference))
|
||||
prompt_parts.extend(
|
||||
[
|
||||
text_ids,
|
||||
torch.tensor([[int(self.special_ids["boa"])]], dtype=torch.long),
|
||||
]
|
||||
)
|
||||
prompt_ids = torch.cat(prompt_parts, dim=1).reshape(-1).tolist()
|
||||
inputs = replace(
|
||||
inputs,
|
||||
prompt=prompt_ids,
|
||||
hf_processor_mm_kwargs={
|
||||
**inputs.hf_processor_mm_kwargs,
|
||||
self._OMNI_PROMPT_TEXT_KEY: prompt_text,
|
||||
},
|
||||
)
|
||||
return super().apply(inputs, timing_ctx)
|
||||
|
||||
def _ensure_cached_runtime_components(
|
||||
self,
|
||||
model_dir: str,
|
||||
@@ -599,15 +646,6 @@ class GLMTTSMultiModalProcessor(BaseMultiModalProcessor[GLMTTSMultiModalProcessi
|
||||
if key in hf_inputs
|
||||
}
|
||||
|
||||
def _hf_processor_applies_updates(
|
||||
self,
|
||||
prompt_text: str,
|
||||
mm_items: MultiModalDataItems,
|
||||
hf_processor_mm_kwargs: Mapping[str, object],
|
||||
tokenization_kwargs: Mapping[str, object],
|
||||
) -> bool:
|
||||
return False
|
||||
|
||||
def _cached_apply_hf_processor(self, inputs: ProcessorInputs, timing_ctx: Any):
|
||||
# GLM-TTS builds the actual AR prompt from both the request text and
|
||||
# mm_processor_kwargs["prompt_text"]. The base cache path separates text
|
||||
|
||||
@@ -1669,8 +1669,10 @@ class _BackboneWrapper(nn.Module):
|
||||
self.config = config
|
||||
|
||||
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
|
||||
from vllm.model_executor.models.utils import AutoWeightsLoader
|
||||
from vllm.model_executor.models.utils import AutoWeightsLoader, WeightsMapper
|
||||
|
||||
skip = ["lm_head."] if getattr(self.config, "tie_word_embeddings", False) else None
|
||||
loader = AutoWeightsLoader(self, skip_prefixes=skip)
|
||||
return loader.load_weights(weights)
|
||||
loader = AutoWeightsLoader(self)
|
||||
return loader.load_weights(
|
||||
weights, mapper=WeightsMapper(orig_to_new_prefix={name: None for name in (skip or ())})
|
||||
)
|
||||
@@ -0,0 +1,625 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
#
|
||||
# Vendored from vllm/model_executor/models/hunyuan_v1.py at vllm@2cf0a6915c
|
||||
# (the upstream base vllm-omni origin/main was rebased against). Upstream
|
||||
# removed the native HunYuan V1 implementation in #53272 / #53615 (migrated
|
||||
# to the Transformers modeling backend). vllm-omni's HunyuanImage3 model
|
||||
# subclasses the removed HunYuanModel / HunYuanSparseMoeBlock / HunYuanMLP,
|
||||
# so those internals are preserved here to keep the omni diffusion model
|
||||
# working against current upstream.
|
||||
# Weight loading, expert mapping, and QKV splitting are owned by Omni's
|
||||
# HunyuanModel subclass; their unused upstream implementations are omitted.
|
||||
"""Inference-only HunYuan V1 internals vendored for vllm-omni HunyuanImage3."""
|
||||
|
||||
from itertools import islice
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
from transformers import PretrainedConfig
|
||||
from vllm.compilation.decorators import support_torch_compile
|
||||
from vllm.config import CacheConfig, VllmConfig, get_current_vllm_config
|
||||
from vllm.distributed import (
|
||||
get_ep_group,
|
||||
get_pp_group,
|
||||
get_tensor_model_parallel_world_size,
|
||||
)
|
||||
from vllm.model_executor.layers.activation import SiluAndMul
|
||||
from vllm.model_executor.layers.attention import Attention
|
||||
from vllm.model_executor.layers.fused_moe import (
|
||||
FusedMoEFactory,
|
||||
)
|
||||
from vllm.model_executor.layers.layernorm import RMSNorm
|
||||
from vllm.model_executor.layers.linear import (
|
||||
ColumnParallelLinear,
|
||||
MergedColumnParallelLinear,
|
||||
QKVParallelLinear,
|
||||
ReplicatedLinear,
|
||||
RowParallelLinear,
|
||||
)
|
||||
from vllm.model_executor.layers.quantization import QuantizationConfig
|
||||
from vllm.model_executor.layers.rotary_embedding import get_rope
|
||||
from vllm.model_executor.layers.vocab_parallel_embedding import (
|
||||
VocabParallelEmbedding,
|
||||
)
|
||||
from vllm.model_executor.models.interfaces import EagleModelMixin
|
||||
from vllm.model_executor.models.utils import (
|
||||
PPMissingLayer,
|
||||
make_layers,
|
||||
)
|
||||
from vllm.sequence import IntermediateTensors
|
||||
from vllm.v1.attention.backend import AttentionType
|
||||
|
||||
|
||||
def _is_moe(config: PretrainedConfig) -> bool:
|
||||
num_experts = getattr(config, "num_experts", None)
|
||||
if isinstance(num_experts, int):
|
||||
return num_experts > 1
|
||||
if isinstance(num_experts, list) and num_experts:
|
||||
# Ensure all elements are integers before calling max.
|
||||
if all(isinstance(e, int) for e in num_experts):
|
||||
return max(num_experts) > 1
|
||||
else:
|
||||
return False
|
||||
return False
|
||||
|
||||
|
||||
def _get_cla_factor(config: PretrainedConfig) -> int:
|
||||
if not getattr(config, "use_cla", False):
|
||||
return 1
|
||||
return getattr(config, "cla_share_factor", 1)
|
||||
|
||||
|
||||
class HunYuanMLP(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
hidden_size: int,
|
||||
intermediate_size: int,
|
||||
hidden_act: str,
|
||||
quant_config: QuantizationConfig | None = None,
|
||||
bias: bool = False,
|
||||
prefix: str = "",
|
||||
reduce_results: bool = True,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.gate_up_proj = MergedColumnParallelLinear(
|
||||
input_size=hidden_size,
|
||||
output_sizes=[intermediate_size] * 2,
|
||||
bias=bias,
|
||||
quant_config=quant_config,
|
||||
prefix=f"{prefix}.gate_up_proj",
|
||||
)
|
||||
self.down_proj = RowParallelLinear(
|
||||
input_size=intermediate_size,
|
||||
output_size=hidden_size,
|
||||
bias=bias,
|
||||
quant_config=quant_config,
|
||||
prefix=f"{prefix}.down_proj",
|
||||
reduce_results=reduce_results,
|
||||
)
|
||||
if hidden_act != "silu":
|
||||
raise ValueError(f"Unsupported activation: {hidden_act}. Only silu is supported for now.")
|
||||
self.act_fn = SiluAndMul()
|
||||
|
||||
def forward(self, x):
|
||||
gate_up, _ = self.gate_up_proj(x)
|
||||
x = self.act_fn(gate_up)
|
||||
x, _ = self.down_proj(x)
|
||||
return x
|
||||
|
||||
|
||||
class HunYuanAttention(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
config: PretrainedConfig,
|
||||
hidden_size: int,
|
||||
num_heads: int,
|
||||
num_kv_heads: int,
|
||||
max_position_embeddings: int = 8192,
|
||||
quant_config: QuantizationConfig | None = None,
|
||||
bias: bool = False,
|
||||
cache_config: CacheConfig | None = None,
|
||||
prefix: str = "",
|
||||
layer_id: int = -1,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.hidden_size = hidden_size
|
||||
tp_size = get_tensor_model_parallel_world_size()
|
||||
self.total_num_heads = num_heads
|
||||
assert self.total_num_heads % tp_size == 0
|
||||
self.num_heads = self.total_num_heads // tp_size
|
||||
self.total_num_kv_heads = num_kv_heads
|
||||
if self.total_num_kv_heads >= tp_size:
|
||||
# Number of KV heads is greater than TP size, so we partition
|
||||
# the KV heads across multiple tensor parallel GPUs.
|
||||
assert self.total_num_kv_heads % tp_size == 0
|
||||
else:
|
||||
# Number of KV heads is less than TP size, so we replicate
|
||||
# the KV heads across multiple tensor parallel GPUs.
|
||||
assert tp_size % self.total_num_kv_heads == 0
|
||||
self.num_kv_heads = max(1, self.total_num_kv_heads // tp_size)
|
||||
|
||||
if hasattr(config, "head_dim") and config.head_dim:
|
||||
self.head_dim = config.head_dim
|
||||
elif hasattr(config, "attention_head_dim"):
|
||||
self.head_dim = config.attention_head_dim
|
||||
else:
|
||||
self.head_dim = self.hidden_size // self.total_num_heads
|
||||
self.q_size = self.num_heads * self.head_dim
|
||||
self.kv_size = self.num_kv_heads * self.head_dim
|
||||
self.scaling = self.head_dim**-0.5
|
||||
self.max_position_embeddings = max_position_embeddings
|
||||
self.use_qk_norm = getattr(config, "use_qk_norm", False)
|
||||
self.layer_id = layer_id
|
||||
|
||||
self.qkv_proj = QKVParallelLinear(
|
||||
hidden_size=hidden_size,
|
||||
head_size=self.head_dim,
|
||||
total_num_heads=self.total_num_heads,
|
||||
total_num_kv_heads=self.total_num_kv_heads,
|
||||
bias=bias,
|
||||
quant_config=quant_config,
|
||||
prefix=f"{prefix}.qkv_proj",
|
||||
)
|
||||
|
||||
self.o_proj = RowParallelLinear(
|
||||
input_size=self.total_num_heads * self.head_dim,
|
||||
output_size=hidden_size,
|
||||
bias=bias,
|
||||
quant_config=quant_config,
|
||||
prefix=f"{prefix}.o_proj",
|
||||
)
|
||||
|
||||
self.rotary_emb = get_rope(
|
||||
self.head_dim,
|
||||
max_position=max_position_embeddings,
|
||||
rope_parameters=config.rope_parameters,
|
||||
is_neox_style=True,
|
||||
)
|
||||
self.attn = Attention(
|
||||
self.num_heads,
|
||||
self.head_dim,
|
||||
self.scaling,
|
||||
num_kv_heads=self.num_kv_heads,
|
||||
cache_config=cache_config,
|
||||
quant_config=quant_config,
|
||||
prefix=f"{prefix}.attn",
|
||||
)
|
||||
|
||||
if self.use_qk_norm:
|
||||
self.query_layernorm = RMSNorm(self.head_dim, eps=config.rms_norm_eps)
|
||||
self.key_layernorm = RMSNorm(self.head_dim, eps=config.rms_norm_eps)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
positions: torch.Tensor,
|
||||
hidden_states: torch.Tensor,
|
||||
kv_states: tuple[torch.Tensor] | None = None,
|
||||
) -> torch.Tensor:
|
||||
qkv, _ = self.qkv_proj(hidden_states)
|
||||
q, k, v = qkv.split([self.q_size, self.kv_size, self.kv_size], dim=-1)
|
||||
q, k = self.rotary_emb(positions, q, k)
|
||||
ori_k = k
|
||||
if self.use_qk_norm:
|
||||
q = self.query_layernorm(
|
||||
q.view(-1, self.num_heads, self.head_dim),
|
||||
)
|
||||
k = self.key_layernorm(
|
||||
k.view(-1, self.num_kv_heads, self.head_dim),
|
||||
)
|
||||
|
||||
attn_output = self.attn(q, k, v)
|
||||
# For o_proj
|
||||
attn_output = attn_output.view(q.shape[0], -1)
|
||||
output, _ = self.o_proj(attn_output)
|
||||
return output, (ori_k, v)
|
||||
|
||||
|
||||
class HunYuanCrossAttention(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
config: PretrainedConfig,
|
||||
hidden_size: int,
|
||||
num_heads: int,
|
||||
num_kv_heads: int,
|
||||
max_position_embeddings: int = 8192,
|
||||
quant_config: QuantizationConfig | None = None,
|
||||
bias: bool = False,
|
||||
cache_config: CacheConfig | None = None,
|
||||
prefix: str = "",
|
||||
layer_id: int = -1,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.hidden_size = hidden_size
|
||||
tp_size = get_tensor_model_parallel_world_size()
|
||||
self.total_num_heads = num_heads
|
||||
assert self.total_num_heads % tp_size == 0
|
||||
self.num_heads = self.total_num_heads // tp_size
|
||||
self.total_num_kv_heads = num_kv_heads
|
||||
if self.total_num_kv_heads >= tp_size:
|
||||
# Number of KV heads is greater than TP size, so we partition
|
||||
# the KV heads across multiple tensor parallel GPUs.
|
||||
assert self.total_num_kv_heads % tp_size == 0
|
||||
else:
|
||||
# Number of KV heads is less than TP size, so we replicate
|
||||
# the KV heads across multiple tensor parallel GPUs.
|
||||
assert tp_size % self.total_num_kv_heads == 0
|
||||
self.num_kv_heads = max(1, self.total_num_kv_heads // tp_size)
|
||||
# MistralConfig has an optional head_dim introduced by Mistral-Nemo
|
||||
if hasattr(config, "head_dim"):
|
||||
self.head_dim = config.head_dim
|
||||
elif hasattr(config, "attention_head_dim"):
|
||||
self.head_dim = config.attention_head_dim
|
||||
else:
|
||||
self.head_dim = self.hidden_size // self.total_num_heads
|
||||
self.q_size = self.num_heads * self.head_dim
|
||||
self.kv_size = self.num_kv_heads * self.head_dim
|
||||
self.scaling = self.head_dim**-0.5
|
||||
self.max_position_embeddings = max_position_embeddings
|
||||
self.use_qk_norm = getattr(config, "use_qk_norm", False)
|
||||
self.layer_id = layer_id
|
||||
|
||||
self.q_proj = ColumnParallelLinear(
|
||||
hidden_size,
|
||||
hidden_size,
|
||||
bias=bias,
|
||||
quant_config=quant_config,
|
||||
prefix=f"{prefix}.q_proj",
|
||||
)
|
||||
|
||||
self.o_proj = RowParallelLinear(
|
||||
input_size=self.total_num_heads * self.head_dim,
|
||||
output_size=hidden_size,
|
||||
bias=bias,
|
||||
quant_config=quant_config,
|
||||
prefix=f"{prefix}.o_proj",
|
||||
)
|
||||
|
||||
self.rotary_emb = get_rope(
|
||||
self.head_dim,
|
||||
max_position=max_position_embeddings,
|
||||
rope_parameters=config.rope_parameters,
|
||||
is_neox_style=True,
|
||||
)
|
||||
self.attn = Attention(
|
||||
self.num_heads,
|
||||
self.head_dim,
|
||||
self.scaling,
|
||||
num_kv_heads=self.num_kv_heads,
|
||||
cache_config=cache_config,
|
||||
quant_config=quant_config,
|
||||
prefix=f"{prefix}.attn",
|
||||
attn_type=AttentionType.ENCODER_DECODER,
|
||||
)
|
||||
|
||||
if self.use_qk_norm:
|
||||
self.query_layernorm = RMSNorm(self.head_dim, eps=config.rms_norm_eps)
|
||||
self.key_layernorm = RMSNorm(self.head_dim, eps=config.rms_norm_eps)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
positions: torch.Tensor,
|
||||
hidden_states: torch.Tensor,
|
||||
kv_states: tuple[torch.Tensor] | None = None,
|
||||
) -> torch.Tensor:
|
||||
assert kv_states is not None
|
||||
ori_k, v = kv_states # use last layer kv,
|
||||
k = ori_k
|
||||
q, _ = self.q_proj(hidden_states)
|
||||
k_tmp = torch.empty_like(k) # Todo: reduant rotary embedding
|
||||
q, _ = self.rotary_emb(positions, q, k_tmp)
|
||||
if self.use_qk_norm:
|
||||
q = self.query_layernorm(
|
||||
q.view(-1, self.num_heads, self.head_dim),
|
||||
)
|
||||
k = self.key_layernorm(
|
||||
k.view(-1, self.num_kv_heads, self.head_dim),
|
||||
)
|
||||
|
||||
attn_output = self.attn(q, k, v)
|
||||
# For o_proj
|
||||
attn_output = attn_output.view(q.shape[0], -1)
|
||||
output, _ = self.o_proj(attn_output)
|
||||
return output, (ori_k, v)
|
||||
|
||||
|
||||
class HunYuanSparseMoeBlock(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
config: PretrainedConfig,
|
||||
quant_config: QuantizationConfig | None = None,
|
||||
layer_id: int = -1,
|
||||
prefix: str = "",
|
||||
enable_eplb: bool = False,
|
||||
):
|
||||
super().__init__()
|
||||
self.tp_size = get_tensor_model_parallel_world_size()
|
||||
|
||||
self.ep_group = get_ep_group().device_group
|
||||
self.ep_size = self.ep_group.size()
|
||||
self.n_routed_experts = config.num_experts
|
||||
|
||||
if self.tp_size > config.num_experts:
|
||||
raise ValueError(
|
||||
f"Tensor parallel size {self.tp_size} is greater than the number of experts {config.num_experts}."
|
||||
)
|
||||
|
||||
# Get layer_id topk if config.moe_topk is a list
|
||||
if isinstance(config.moe_topk, list):
|
||||
assert layer_id >= 0
|
||||
assert len(config.moe_topk) > layer_id
|
||||
top_k = config.moe_topk[layer_id]
|
||||
else:
|
||||
top_k = config.moe_topk
|
||||
|
||||
# If it is moe, moe_intermediate_size is preferred
|
||||
intermediate_size = config.intermediate_size
|
||||
if config.moe_intermediate_size is not None:
|
||||
intermediate_size = (
|
||||
config.moe_intermediate_size
|
||||
if isinstance(config.moe_intermediate_size, int)
|
||||
else config.moe_intermediate_size[layer_id]
|
||||
)
|
||||
|
||||
# Load balancing settings.
|
||||
vllm_config = get_current_vllm_config()
|
||||
eplb_config = vllm_config.parallel_config.eplb_config
|
||||
self.enable_eplb = enable_eplb
|
||||
|
||||
self.n_logical_experts = self.n_routed_experts
|
||||
self.n_redundant_experts = eplb_config.num_redundant_experts
|
||||
self.n_physical_experts = self.n_logical_experts + self.n_redundant_experts
|
||||
self.n_local_physical_experts = self.n_physical_experts // self.ep_size
|
||||
self.gate = ReplicatedLinear(
|
||||
config.hidden_size,
|
||||
config.num_experts,
|
||||
bias=False,
|
||||
quant_config=None,
|
||||
prefix=f"{prefix}.gate",
|
||||
)
|
||||
if config.use_mixed_mlp_moe > 0:
|
||||
# Get layer_id num_shared_expert if config.num_shared_expert is
|
||||
# a list.
|
||||
if isinstance(config.num_shared_expert, list):
|
||||
assert layer_id >= 0
|
||||
assert len(config.num_shared_expert) > layer_id
|
||||
num_shared_expert = config.num_shared_expert[layer_id]
|
||||
else:
|
||||
num_shared_expert = config.num_shared_expert
|
||||
|
||||
self.shared_mlp = HunYuanMLP(
|
||||
hidden_size=config.hidden_size,
|
||||
intermediate_size=config.intermediate_size * num_shared_expert,
|
||||
hidden_act=config.hidden_act,
|
||||
quant_config=quant_config,
|
||||
reduce_results=False,
|
||||
prefix=f"{prefix}.shared_mlp",
|
||||
)
|
||||
else:
|
||||
self.shared_mlp = None
|
||||
|
||||
self.experts = FusedMoEFactory(
|
||||
shared_experts=self.shared_mlp,
|
||||
num_experts=self.n_routed_experts,
|
||||
top_k=top_k,
|
||||
hidden_size=config.hidden_size,
|
||||
intermediate_size=intermediate_size,
|
||||
renormalize=top_k > 1,
|
||||
quant_config=quant_config,
|
||||
prefix=f"{prefix}.experts",
|
||||
enable_eplb=self.enable_eplb,
|
||||
num_redundant_experts=self.n_redundant_experts,
|
||||
)
|
||||
|
||||
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
|
||||
# NOTE: hidden_states can have either 1D or 2D shape.
|
||||
orig_shape = hidden_states.shape
|
||||
hidden_dim = hidden_states.shape[-1]
|
||||
hidden_states = hidden_states.view(-1, hidden_dim)
|
||||
|
||||
# router_logits: (num_tokens, n_experts)
|
||||
router_logits, _ = self.gate(hidden_states)
|
||||
final_hidden_states = self.experts(hidden_states=hidden_states, router_logits=router_logits)
|
||||
|
||||
return final_hidden_states.view(orig_shape)
|
||||
|
||||
|
||||
class HunYuanDecoderLayer(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
config: PretrainedConfig,
|
||||
cache_config: CacheConfig | None = None,
|
||||
quant_config: QuantizationConfig | None = None,
|
||||
prefix: str = "",
|
||||
layer_id: int = -1,
|
||||
enable_eplb: bool = False,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
assert layer_id >= 0
|
||||
self.layer_id = layer_id
|
||||
self.hidden_size = config.hidden_size
|
||||
self.intermediate_size = (
|
||||
config.intermediate_size
|
||||
if isinstance(config.intermediate_size, int)
|
||||
else config.intermediate_size[layer_id]
|
||||
)
|
||||
max_position_embeddings = getattr(config, "max_position_embeddings", 8192)
|
||||
attention_bias = getattr(config, "attention_bias", False) or getattr(config, "bias", False)
|
||||
cla_factor = _get_cla_factor(config)
|
||||
attention_type = (
|
||||
AttentionType.ENCODER_DECODER if layer_id >= 0 and layer_id % cla_factor != 0 else AttentionType.DECODER
|
||||
)
|
||||
if attention_type == AttentionType.DECODER:
|
||||
self.self_attn = HunYuanAttention(
|
||||
config=config,
|
||||
hidden_size=self.hidden_size,
|
||||
num_heads=config.num_attention_heads,
|
||||
num_kv_heads=getattr(config, "num_key_value_heads", config.num_attention_heads),
|
||||
max_position_embeddings=max_position_embeddings,
|
||||
quant_config=quant_config,
|
||||
bias=attention_bias,
|
||||
cache_config=cache_config,
|
||||
prefix=f"{prefix}.self_attn",
|
||||
layer_id=layer_id,
|
||||
)
|
||||
elif attention_type == AttentionType.ENCODER_DECODER:
|
||||
self.self_attn = HunYuanCrossAttention(
|
||||
config=config,
|
||||
hidden_size=self.hidden_size,
|
||||
num_heads=config.num_attention_heads,
|
||||
num_kv_heads=getattr(config, "num_key_value_heads", config.num_attention_heads),
|
||||
max_position_embeddings=max_position_embeddings,
|
||||
quant_config=quant_config,
|
||||
bias=attention_bias,
|
||||
cache_config=cache_config,
|
||||
prefix=f"{prefix}.self_attn",
|
||||
layer_id=layer_id,
|
||||
)
|
||||
else:
|
||||
raise RuntimeError(f"Unsupported attention type: {attention_type}")
|
||||
|
||||
if _is_moe(config):
|
||||
self.mlp = HunYuanSparseMoeBlock(
|
||||
config=config,
|
||||
quant_config=quant_config,
|
||||
layer_id=layer_id,
|
||||
prefix=f"{prefix}.mlp",
|
||||
enable_eplb=enable_eplb,
|
||||
)
|
||||
else:
|
||||
self.mlp = HunYuanMLP(
|
||||
hidden_size=self.hidden_size,
|
||||
intermediate_size=self.intermediate_size,
|
||||
hidden_act=config.hidden_act,
|
||||
quant_config=quant_config,
|
||||
bias=getattr(config, "mlp_bias", False),
|
||||
prefix=f"{prefix}.mlp",
|
||||
)
|
||||
|
||||
self.input_layernorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
|
||||
self.post_attention_layernorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
positions: torch.Tensor,
|
||||
hidden_states: torch.Tensor,
|
||||
residual: torch.Tensor | None,
|
||||
kv_states: tuple[torch.Tensor] | None = None,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
# Self Attention
|
||||
if residual is None:
|
||||
residual = hidden_states
|
||||
hidden_states = self.input_layernorm(hidden_states)
|
||||
else:
|
||||
hidden_states, residual = self.input_layernorm(hidden_states, residual)
|
||||
hidden_states, ori_kv_states = self.self_attn(
|
||||
positions=positions,
|
||||
hidden_states=hidden_states,
|
||||
kv_states=kv_states,
|
||||
)
|
||||
|
||||
# Fully Connected
|
||||
hidden_states, residual = self.post_attention_layernorm(hidden_states, residual)
|
||||
hidden_states = self.mlp(hidden_states)
|
||||
return hidden_states, residual, ori_kv_states
|
||||
|
||||
|
||||
@support_torch_compile(
|
||||
dynamic_arg_dims={
|
||||
"input_ids": 0,
|
||||
# positions is of shape (xd, seq_len) if xdrope is enabled for hunyuan-vl,
|
||||
# otherwise (seq_len, ).
|
||||
"positions": -1,
|
||||
"intermediate_tensors": 0,
|
||||
"inputs_embeds": 0,
|
||||
}
|
||||
)
|
||||
class HunYuanModel(nn.Module, EagleModelMixin):
|
||||
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
|
||||
super().__init__()
|
||||
|
||||
config = vllm_config.model_config.hf_config
|
||||
cache_config = vllm_config.cache_config
|
||||
quant_config = vllm_config.quant_config
|
||||
|
||||
eplb_config = vllm_config.parallel_config.eplb_config
|
||||
enable_eplb = vllm_config.parallel_config.enable_eplb
|
||||
self.num_redundant_experts = eplb_config.num_redundant_experts
|
||||
|
||||
self.config = config
|
||||
self.quant_config = quant_config
|
||||
|
||||
self.vocab_size = config.vocab_size
|
||||
|
||||
if get_pp_group().is_first_rank or (config.tie_word_embeddings and get_pp_group().is_last_rank):
|
||||
self.embed_tokens = VocabParallelEmbedding(
|
||||
self.vocab_size,
|
||||
config.hidden_size,
|
||||
quant_config=quant_config,
|
||||
)
|
||||
else:
|
||||
self.embed_tokens = PPMissingLayer()
|
||||
self.start_layer, self.end_layer, self.layers = make_layers(
|
||||
config.num_hidden_layers,
|
||||
lambda prefix: HunYuanDecoderLayer(
|
||||
config=config,
|
||||
layer_id=int(prefix.split(".")[-1]),
|
||||
cache_config=cache_config,
|
||||
quant_config=quant_config,
|
||||
prefix=prefix,
|
||||
enable_eplb=enable_eplb,
|
||||
),
|
||||
prefix=f"{prefix}.layers",
|
||||
)
|
||||
if get_pp_group().is_last_rank:
|
||||
self.norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
|
||||
else:
|
||||
self.norm = PPMissingLayer()
|
||||
|
||||
def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
|
||||
return self.embed_tokens(input_ids)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
input_ids: torch.Tensor | None,
|
||||
positions: torch.Tensor,
|
||||
intermediate_tensors: IntermediateTensors | None,
|
||||
inputs_embeds: torch.Tensor | None = None,
|
||||
) -> torch.Tensor | IntermediateTensors:
|
||||
if get_pp_group().is_first_rank:
|
||||
if inputs_embeds is not None:
|
||||
hidden_states = inputs_embeds
|
||||
else:
|
||||
hidden_states = self.embed_input_ids(input_ids)
|
||||
residual = None
|
||||
else:
|
||||
assert intermediate_tensors is not None
|
||||
hidden_states = intermediate_tensors["hidden_states"]
|
||||
residual = intermediate_tensors["residual"]
|
||||
|
||||
cla_factor = _get_cla_factor(self.config)
|
||||
prev_kv_states = None
|
||||
aux_hidden_states = self._maybe_add_hidden_state([], 0, hidden_states, residual)
|
||||
for i, layer in enumerate(islice(self.layers, self.start_layer, self.end_layer)):
|
||||
hidden_states, residual, kv_states = layer(
|
||||
positions,
|
||||
hidden_states,
|
||||
residual,
|
||||
prev_kv_states,
|
||||
)
|
||||
|
||||
if getattr(self.config, "use_cla", False) and i % cla_factor == 0:
|
||||
prev_kv_states = kv_states
|
||||
else:
|
||||
prev_kv_states = None
|
||||
|
||||
self._maybe_add_hidden_state(aux_hidden_states, i + 1, hidden_states, residual)
|
||||
|
||||
if not get_pp_group().is_last_rank:
|
||||
return IntermediateTensors({"hidden_states": hidden_states, "residual": residual})
|
||||
|
||||
hidden_states, _ = self.norm(hidden_states, residual)
|
||||
|
||||
if len(aux_hidden_states) > 0:
|
||||
return hidden_states, aux_hidden_states
|
||||
return hidden_states
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
import gc
|
||||
import math
|
||||
import typing
|
||||
@@ -45,13 +45,6 @@ from vllm.model_executor.model_loader.weight_utils import (
|
||||
default_weight_loader,
|
||||
maybe_remap_kv_scale_name,
|
||||
)
|
||||
from vllm.model_executor.models.hunyuan_v1 import (
|
||||
HunYuanMLP,
|
||||
HunYuanModel,
|
||||
HunYuanSparseMoeBlock,
|
||||
_get_cla_factor,
|
||||
_is_moe,
|
||||
)
|
||||
from vllm.model_executor.models.interfaces import (
|
||||
MultiModalEmbeddings,
|
||||
SupportsLoRA,
|
||||
@@ -63,6 +56,7 @@ from vllm.model_executor.models.interfaces import (
|
||||
from vllm.model_executor.models.utils import (
|
||||
AutoWeightsLoader,
|
||||
PPMissingLayer,
|
||||
WeightsMapper,
|
||||
_merge_multimodal_embeddings,
|
||||
is_pp_missing_parameter,
|
||||
maybe_prefix,
|
||||
@@ -78,7 +72,6 @@ from vllm.multimodal.parse import (
|
||||
)
|
||||
from vllm.multimodal.processing import (
|
||||
BaseDummyInputsBuilder,
|
||||
BaseMultiModalProcessor,
|
||||
BaseProcessingInfo,
|
||||
PromptReplacement,
|
||||
PromptUpdate,
|
||||
@@ -91,6 +84,14 @@ from vllm.v1.outputs import SamplerOutput
|
||||
from vllm.v1.sample.metadata import SamplingMetadata
|
||||
from vllm.v1.sample.sampler import Sampler
|
||||
|
||||
from vllm_omni.inputs.mm_processor import OmniMultiModalProcessor
|
||||
from vllm_omni.model_executor.models.hunyuan_image3._hunyuan_v1_vendored import (
|
||||
HunYuanMLP,
|
||||
HunYuanModel,
|
||||
HunYuanSparseMoeBlock,
|
||||
_get_cla_factor,
|
||||
_is_moe,
|
||||
)
|
||||
from vllm_omni.model_executor.models.hunyuan_image3.autoencoder_kl_3d import AutoencoderKLConv3D
|
||||
from vllm_omni.model_executor.models.hunyuan_image3.siglip2 import LightProjector, Siglip2VisionTransformer
|
||||
|
||||
@@ -1027,7 +1028,7 @@ class HunyuanImage3DummyInputsBuilder(BaseDummyInputsBuilder[HunyuanImage3Proces
|
||||
}
|
||||
|
||||
|
||||
class HunyuanImage3MultiModalProcessor(BaseMultiModalProcessor[HunyuanImage3ProcessingInfo]):
|
||||
class HunyuanImage3MultiModalProcessor(OmniMultiModalProcessor[HunyuanImage3ProcessingInfo]):
|
||||
"""Multimodal processor for HunyuanImage3 model."""
|
||||
|
||||
def _call_hf_processor(
|
||||
@@ -1046,15 +1047,6 @@ class HunyuanImage3MultiModalProcessor(BaseMultiModalProcessor[HunyuanImage3Proc
|
||||
batch_feature["vae_generator_seed"] = torch.full((len(images),), int(vae_generator_seed), dtype=torch.long)
|
||||
return batch_feature
|
||||
|
||||
def _hf_processor_applies_updates(
|
||||
self,
|
||||
prompt_text: str,
|
||||
mm_items: MultiModalDataItems,
|
||||
hf_processor_mm_kwargs: Mapping[str, object],
|
||||
tokenization_kwargs: Mapping[str, object],
|
||||
) -> bool:
|
||||
return False
|
||||
|
||||
def _get_mm_fields_config(
|
||||
self,
|
||||
hf_inputs: BatchFeature,
|
||||
@@ -2180,12 +2172,11 @@ class HunyuanImage3ForConditionalGeneration(nn.Module, SupportsMultiModal, Suppo
|
||||
"timestep_r_emb",
|
||||
]
|
||||
skip_prefixes.extend(unexpected_keywords)
|
||||
loader = AutoWeightsLoader(
|
||||
self,
|
||||
skip_prefixes=skip_prefixes,
|
||||
)
|
||||
loader = AutoWeightsLoader(self)
|
||||
|
||||
loaded_params = loader.load_weights(weights)
|
||||
loaded_params = loader.load_weights(
|
||||
weights, mapper=WeightsMapper(orig_to_new_prefix={name: None for name in (skip_prefixes or ())})
|
||||
)
|
||||
return loaded_params
|
||||
|
||||
def get_language_model(self) -> torch.nn.Module:
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
# Copyright 2025 Xiaomi Corporation.
|
||||
import os
|
||||
from collections.abc import Iterable, Mapping, Sequence
|
||||
@@ -27,14 +30,13 @@ from vllm.multimodal.inputs import (
|
||||
from vllm.multimodal.parse import AudioProcessorItems, MultiModalDataItems, MultiModalDataParser
|
||||
from vllm.multimodal.processing import (
|
||||
BaseDummyInputsBuilder,
|
||||
BaseMultiModalProcessor,
|
||||
BaseProcessingInfo,
|
||||
ProcessorInputs,
|
||||
PromptReplacement,
|
||||
PromptUpdate,
|
||||
PromptUpdateDetails,
|
||||
)
|
||||
from vllm.multimodal.utils import group_mm_kwargs_by_modality
|
||||
from vllm.multimodal.utils import group_and_batch_mm_kwargs
|
||||
from vllm.sequence import IntermediateTensors
|
||||
from vllm.utils.cache import LRUCache
|
||||
from vllm.utils.collection_utils import is_list_of
|
||||
@@ -42,6 +44,7 @@ from vllm.utils.platform_utils import is_pin_memory_available
|
||||
from vllm.v1.sample.metadata import SamplingMetadata
|
||||
from vllm.v1.sample.sampler import Sampler
|
||||
|
||||
from vllm_omni.inputs.mm_processor import OmniMultiModalProcessor
|
||||
from vllm_omni.model_executor.custom_process_mixin import CustomProcessMixin
|
||||
from vllm_omni.model_executor.models.mimo_audio.config_mimo_audio import (
|
||||
NO_INTERLEAVE_NEXT_TOKEN_ID,
|
||||
@@ -348,7 +351,7 @@ class MiMoAudioDataParser(MultiModalDataParser):
|
||||
return AudioProcessorItems(new_audios)
|
||||
|
||||
|
||||
class MiMoAudioLLMMultiModalProcessor(BaseMultiModalProcessor[MiMoAudioLLMProcessingInfo]):
|
||||
class MiMoAudioLLMMultiModalProcessor(OmniMultiModalProcessor[MiMoAudioLLMProcessingInfo]):
|
||||
def _call_hf_processor(
|
||||
self,
|
||||
prompt: str,
|
||||
@@ -484,7 +487,7 @@ class MiMoAudioLLMMultiModalProcessor(BaseMultiModalProcessor[MiMoAudioLLMProces
|
||||
return [
|
||||
PromptReplacement(
|
||||
modality="audio",
|
||||
target=audio_token,
|
||||
target=[audio_token_id],
|
||||
replacement=get_replacement_mimo_audio,
|
||||
)
|
||||
]
|
||||
@@ -626,7 +629,7 @@ class MiMoAudioForConditionalGeneration(
|
||||
)
|
||||
mm_kwargs.append((mm_feature.modality, mm_item))
|
||||
|
||||
for modality, num_items, mm_kwargs_group in group_mm_kwargs_by_modality(
|
||||
for modality, num_items, mm_kwargs_group in group_and_batch_mm_kwargs(
|
||||
mm_kwargs,
|
||||
device=self.device,
|
||||
pin_memory=self.pin_memory,
|
||||
|
||||
@@ -390,35 +390,25 @@ class MimoAudioDummyInputsBuilder(BaseDummyInputsBuilder[MimoAudioProcessingInfo
|
||||
|
||||
|
||||
class MimoAudioMultiModalProcessor(BaseMultiModalProcessor[MimoAudioProcessingInfo]):
|
||||
def _call_hf_processor(
|
||||
def _preprocess_hf_mm_data(
|
||||
self,
|
||||
prompt: str,
|
||||
mm_data: Mapping[str, object],
|
||||
mm_kwargs: Mapping[str, Any],
|
||||
tok_kwargs: Mapping[str, object],
|
||||
) -> BatchFeature:
|
||||
hf_processor_mm_kwargs: Mapping[str, object],
|
||||
) -> tuple[Mapping[str, object], Mapping[str, object]]:
|
||||
mm_data, hf_processor_mm_kwargs = super()._preprocess_hf_mm_data(
|
||||
mm_data,
|
||||
hf_processor_mm_kwargs,
|
||||
)
|
||||
mm_data = dict(mm_data)
|
||||
audios = mm_data.pop("audios", [])
|
||||
if audios:
|
||||
mm_data["audio"] = audios
|
||||
|
||||
# Text-only input not supported in composite processor
|
||||
if not mm_data.get("audio", []):
|
||||
prompt_ids = self.info.get_tokenizer().encode(prompt)
|
||||
prompt_ids = self._apply_hf_processor_tokens_only(prompt_ids)
|
||||
return BatchFeature(dict(input_ids=[prompt_ids]), tensor_type="pt")
|
||||
|
||||
feature_extractor = self.info.get_feature_extractor(**mm_kwargs)
|
||||
mm_kwargs = dict(
|
||||
**mm_kwargs,
|
||||
sampling_rate=feature_extractor.sampling_rate,
|
||||
)
|
||||
|
||||
return super()._call_hf_processor(
|
||||
prompt=prompt,
|
||||
mm_data=mm_data,
|
||||
mm_kwargs=mm_kwargs,
|
||||
tok_kwargs=tok_kwargs,
|
||||
)
|
||||
feature_extractor = self.info.get_feature_extractor(**hf_processor_mm_kwargs)
|
||||
hf_processor_mm_kwargs = {
|
||||
**hf_processor_mm_kwargs,
|
||||
"sampling_rate": feature_extractor.sampling_rate,
|
||||
}
|
||||
return mm_data, hf_processor_mm_kwargs
|
||||
|
||||
def _get_mm_fields_config(
|
||||
self,
|
||||
@@ -480,7 +470,7 @@ class MimoAudioMultiModalProcessor(BaseMultiModalProcessor[MimoAudioProcessingIn
|
||||
return [
|
||||
PromptReplacement(
|
||||
modality="audio",
|
||||
target=audio_token,
|
||||
target=[audio_token_id],
|
||||
replacement=get_replacement_mimo_audio,
|
||||
)
|
||||
]
|
||||
|
||||
Loaded 100 of 120 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user