[Rebase] Rebase to vllm 0.12.0 (#335)

Signed-off-by: Rein Yang <ruiruyang2@gmail.com>
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
Signed-off-by: Isotr0py <mozf@mail2.sysu.edu.cn>
Co-authored-by: Rein Yang <ruiruyang2@gmail.com>
Co-authored-by: Isotr0py <mozf@mail2.sysu.edu.cn>
This commit is contained in:
authored and GitHub committed 2025-12-19 16:42:02 +08:00
1 parent 9464e1479d
commit 8939eb835d
48 files changed
+1649 -2473

No files matched your search

+1 -1
View File
@@ -47,7 +47,7 @@ fi
VENV_PYTHON="${VENV_DIR}/bin/python"
[[ -x "${VENV_PYTHON}" ]] || { echo "Python not found in ${VENV_DIR}"; exit 1; }
"${UV_BIN}" pip install --python "${VENV_PYTHON}" vllm==0.11.0
"${UV_BIN}" pip install --python "${VENV_PYTHON}" vllm==0.12.0
"${UV_BIN}" pip install --python "${VENV_PYTHON}" -e ".[dev]"
"${VENV_PYTHON}" -m pytest -v -s tests/entrypoints/
"${VENV_PYTHON}" -m pytest -v -s tests/diffusion/cache/
+1
View File
@@ -221,6 +221,7 @@ datasets/
outputs/
results/
generated/
output_*/
# Configuration overrides
configs/local.yaml
+1 -1
View File
@@ -1,5 +1,5 @@
ARG VLLM_BASE_IMAGE=vllm/vllm-openai
ARG VLLM_BASE_TAG=v0.11.0
ARG VLLM_BASE_TAG=v0.12.0
FROM ${VLLM_BASE_IMAGE}:${VLLM_BASE_TAG}
ARG APP_DIR=/workspace/vllm-omni
WORKDIR ${APP_DIR}
@@ -18,9 +18,9 @@ Therefore, it is recommended to install vLLM and vLLM-Omni with a **fresh new**
#### Installation of vLLM
vLLM-Omni is built based on vLLM v0.11.0. Please install it with command below.
vLLM-Omni is built based on vLLM v0.12.0. Please install it with command below.
```bash
uv pip install vllm==0.11.0 --torch-backend=auto
uv pip install vllm==0.12.0 --torch-backend=auto
```
#### Installation of vLLM-Omni
@@ -34,10 +34,10 @@ uv pip install vllm-omni
# --8<-- [start:build-wheel-from-source]
#### Installation of vLLM
If you do not need to modify source code of vLLM, you can directly install the stable 0.11.0 release version of the library
If you do not need to modify source code of vLLM, you can directly install the stable 0.12.0 release version of the library
```bash
uv pip install vllm==0.11.0 --torch-backend=auto
uv pip install vllm==0.12.0 --torch-backend=auto
```
#### Installation of vLLM-Omni
@@ -54,11 +54,11 @@ If you want to check, modify or debug with source code of vLLM, install the libr
```bash
git clone https://github.com/vllm-project/vllm.git
cd vllm
git checkout v0.11.0
git checkout v0.12.0
```
Set up environment variables to get pre-built wheels. If there are internet problems, just download the whl file manually. And set `VLLM_PRECOMPILED_WHEEL_LOCATION` as your local absolute path of whl file.
```bash
export VLLM_PRECOMPILED_WHEEL_LOCATION=https://github.com/vllm-project/vllm/releases/download/v0.11.0/vllm-0.11.0-cp38-abi3-manylinux1_x86_64.whl
export VLLM_PRECOMPILED_WHEEL_LOCATION=https://github.com/vllm-project/vllm/releases/download/v0.12.0/vllm-0.12.0-cp38-abi3-manylinux_2_31_x86_64.whl
```
Install vllm with command below (If you have no existing PyTorch).
```bash
@@ -89,7 +89,7 @@ docker run --runtime nvidia --gpus 2 \
--env "HF_TOKEN=$HF_TOKEN" \
-p 8091:8091 \
--ipc=host \
vllm/vllm-omni:v0.11.0rc1 \
vllm/vllm-omni:v0.12.0rc1 \
--model Qwen/Qwen3-Omni-30B-A3B-Instruct --port 8091
```
@@ -19,7 +19,7 @@ vLLM-Omni current recommends the steps in under setup through Docker Images.
#### Get the correct docker environment
The v0.11.0 releases is on Oct 3, 2025, so we will pull a AMD nightly docker image close to the release period. `rocm/vllm-dev:nightly_main_20251005` is picked as it has a correct `transformers` version that supports qwen3_omni.
The v0.12.0 releases is on Dec 3, 2025, so we will pull a AMD nightly docker image close to the release period. `rocm/vllm-dev:nightly_main_20251205` is picked as it has a correct `transformers` version that supports qwen3_omni.
`docker pull rocm/vllm-dev:nightly_main_20251005`
@@ -51,18 +51,18 @@ sudo apt install ffmpeg -y
```
#### Installation of vLLM
Install the v0.11.0 release of vLLM as vLLM-Omni is currently depend on v0.11.0.
Install the v0.12.0 release of vLLM as vLLM-Omni is currently depend on v0.12.0.
```bash
# uninstall existing vllm which is based on the main branch, not on the releases branch
python3 -m pip uninstall -y vllm
git clone https://github.com/vllm-project/vllm.git vllm0110
cd vllm0110/
git clone https://github.com/vllm-project/vllm.git vllm0120
cd vllm0120/
# checkout the version tag v0.11.0
git checkout -b v0.11.0 v0.11.0
# checkout the version tag v0.12.0
git checkout -b v0.12.0 v0.12.0
# you should see 0.11.0
# you should see 0.12.0
python -c "import setuptools_scm; print(setuptools_scm.get_version())"
# PYTORCH_ROCM_ARCH=<your-gpu-arch> python3 setup.py develop
@@ -92,7 +92,7 @@ pip install vllm-omni
# clone and checkout to the release version.
git clone https://github.com/vllm-project/vllm-omni.git
cd vllm-omni
git checkout v0.11.0rc1
git checkout v0.12.0rc1
pip install -e .
```
@@ -13,10 +13,10 @@ export DEVICE0=/dev/davinci0
export DEVICE1=/dev/davinci1
# Update the vllm-ascend image
# Atlas A2:
# export IMAGE=quay.io/ascend/vllm-ascend:v0.11.0rc2
# export IMAGE=quay.io/ascend/vllm-ascend:v0.12.0rc1
# Atlas A3:
# export IMAGE=quay.io/ascend/vllm-ascend:v0.11.0rc2-a3
export IMAGE=quay.io/ascend/vllm-ascend:v0.11.0rc2
# export IMAGE=quay.io/ascend/vllm-ascend:v0.12.0rc1-a3
export IMAGE=quay.io/ascend/vllm-ascend:v0.12.0rc1
docker run --rm \
--name vllm-omni-npu \
--shm-size=1g \
@@ -17,7 +17,7 @@ from vllm.assets.image import ImageAsset
from vllm.assets.video import VideoAsset, video_to_ndarrays
from vllm.multimodal.image import convert_image_mode
from vllm.sampling_params import SamplingParams
from vllm.utils import FlexibleArgumentParser
from vllm.utils.argparse_utils import FlexibleArgumentParser
from vllm_omni.entrypoints.omni import Omni
@@ -119,7 +119,7 @@ def get_use_audio_in_video_query(
question = "Describe the content of the video, then convert what the baby say into text."
prompt = (
f"<|im_start|>system\n{default_system}<|im_end|>\n"
"<|im_start|>user\n<|vision_bos|><|VIDEO|><|vision_eos|>"
"<|im_start|>user\n<|vision_bos|><|VIDEO|><|vision_eos|><|audio_bos|><|AUDIO|><|audio_eos|>"
f"{question}<|im_end|>\n"
f"<|im_start|>assistant\n"
)
@@ -319,7 +319,6 @@ def main(args):
query_result = query_func(audio_path=audio_path, sampling_rate=sampling_rate)
else:
query_result = query_func()
omni_llm = Omni(
model=model_name,
log_stats=args.enable_stats,
@@ -17,7 +17,7 @@ from vllm.assets.audio import AudioAsset
from vllm.assets.image import ImageAsset
from vllm.assets.video import VideoAsset, video_to_ndarrays
from vllm.multimodal.image import convert_image_mode
from vllm.utils import FlexibleArgumentParser
from vllm.utils.argparse_utils import FlexibleArgumentParser
from vllm_omni.entrypoints.omni import Omni
@@ -281,7 +281,7 @@ def parse_args():
"--query-type",
"-q",
type=str,
default="mixed_modalities",
default="use_video",
choices=query_map.keys(),
help="Query type.",
)
@@ -4,7 +4,7 @@ import os
import requests
from openai import OpenAI
from vllm.assets.audio import AudioAsset
from vllm.utils import FlexibleArgumentParser
from vllm.utils.argparse_utils import FlexibleArgumentParser
# Modify OpenAI's API key and API base to use vLLM's API server.
openai_api_key = "EMPTY"
@@ -5,7 +5,7 @@ from typing import NamedTuple
import requests
from openai import OpenAI
from vllm.assets.audio import AudioAsset
from vllm.utils import FlexibleArgumentParser
from vllm.utils.argparse_utils import FlexibleArgumentParser
# Modify OpenAI's API key and API base to use vLLM's API server.
openai_api_key = "EMPTY"
+2 -2
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "vllm-omni"
version = "0.11.0rc1"
version = "0.12.0rc1"
description = "A framework for efficient model inference with omni-modality models"
readme = "README.md"
requires-python = ">=3.10,<3.14"
@@ -37,7 +37,7 @@ dependencies = [
"gradio==5.50",
"soundfile>=0.13.1",
"cache-dit==1.1.8",
# "vllm==0.11.0", # TODO: fix the entrypoints overwrite problem
# "vllm==0.12.0", # TODO: fix the entrypoints overwrite problem
]
[project.optional-dependencies]
@@ -44,7 +44,7 @@ stage_args:
model_arch: Qwen3OmniMoeForConditionalGeneration
worker_cls: vllm_omni.worker.gpu_ar_worker.GPUARWorker
scheduler_cls: vllm_omni.core.sched.omni_ar_scheduler.OmniARScheduler
gpu_memory_utilization: 0.5
gpu_memory_utilization: 0.32
enforce_eager: true
trust_remote_code: true
engine_output_type: latent # Output codec codes for code2wav
@@ -27,7 +27,7 @@ test_params = [(model, stage_config) for model in models for stage_config in sta
def test_video_to_audio(omni_runner: type[OmniRunner], test_config) -> None:
"""Test processing video, generating audio output."""
model, stage_config_path = test_config
with omni_runner(model, seed=42, stage_configs_path=stage_config_path) as runner:
with omni_runner(model, seed=42, stage_configs_path=stage_config_path, init_sleep_seconds=90) as runner:
# Prepare inputs
question = "Describe the video briefly."
video = VideoAsset(name="baby_reading", num_frames=4).np_ndarrays
+1 -1
View File
@@ -109,7 +109,7 @@ def omni_server(request):
Multi-stage initialization can take 10-20+ minutes.
"""
model, stage_config_path = request.param
with OmniServer(model, ["--stage-configs-path", stage_config_path]) as server:
with OmniServer(model, ["--stage-configs-path", stage_config_path, "--init-sleep-seconds", "90"]) as server:
yield server
+2 -2
View File
@@ -393,12 +393,12 @@ def mock_get_config(monkeypatch):
)
# Also mock in processor module if it's imported
monkeypatch.setattr(
"vllm_omni.engine.processor.length_from_prompt_token_ids_or_embeds",
"vllm_omni.engine.input_processor.length_from_prompt_token_ids_or_embeds",
_mock_length_from_prompt_token_ids_or_embeds,
raising=False,
)
# If processor module is already imported, patch it directly
processor_module_path = "vllm_omni.engine.processor"
processor_module_path = "vllm_omni.engine.input_processor"
if processor_module_path in sys.modules:
processor_module = sys.modules[processor_module_path]
setattr(
+67 -86
View File
@@ -1,17 +1,17 @@
import json
import warnings
from importlib.util import find_spec
from typing import Any, Literal
from typing import Any
import torch
import vllm.envs as envs
from omegaconf import DictConfig, OmegaConf
from pydantic import ConfigDict
from pydantic.dataclasses import dataclass
from vllm.attention.backends.registry import AttentionBackendEnum
from vllm.config import ModelConfig, config
from vllm.config.model import (
_RUNNER_CONVERTS,
_RUNNER_TASKS,
ConvertOption,
ConvertType,
RunnerOption,
TaskOption,
@@ -27,9 +27,14 @@ from vllm.transformers_utils.config import (
get_hf_image_processor_config,
get_hf_text_config,
get_pooling_config,
is_interleaved,
)
from vllm.transformers_utils.utils import maybe_model_redirect
from vllm.transformers_utils.gguf_utils import (
maybe_patch_hf_config_from_gguf,
)
from vllm.transformers_utils.utils import (
is_gguf,
maybe_model_redirect,
)
import vllm_omni.model_executor.models as me_models
@@ -87,29 +92,19 @@ class OmniModelConfig(ModelConfig):
def __post_init__(
self,
# Multimodal config init vars
limit_mm_per_prompt: dict[str, int] | None,
limit_mm_per_prompt: dict[str, int | dict[str, int]] | None,
enable_mm_embeds: bool | None,
media_io_kwargs: dict[str, dict[str, Any]] | None,
mm_processor_kwargs: dict[str, Any] | None,
mm_processor_cache_gb: float | None,
mm_processor_cache_type: MMCacheType | None,
mm_shm_cache_max_object_size_mb: int | None,
mm_encoder_tp_mode: MMEncoderTPMode | None,
mm_encoder_attn_backend: AttentionBackendEnum | str | None,
interleave_mm_strings: bool | None,
skip_mm_profiling: bool | None,
video_pruning_rate: float | None,
) -> None:
# Set the default seed to 0 in V1.
if envs.VLLM_USE_V1 and self.seed is None:
self.seed = 0
if not envs.VLLM_ENABLE_V1_MULTIPROCESSING:
logger.warning(
"The global random seed is set to %d. Since "
"VLLM_ENABLE_V1_MULTIPROCESSING is set to False, this may "
"affect the random state of the Python process that "
"launched vLLM.",
self.seed,
)
# Keep set served_model_name before maybe_model_redirect(self.model)
self.served_model_name = get_served_model_name(self.model, self.served_model_name)
self.model = maybe_model_redirect(self.model)
@@ -124,53 +119,36 @@ class OmniModelConfig(ModelConfig):
self.hf_config_path = maybe_model_redirect(self.hf_config_path)
if callable(self.hf_overrides):
hf_overrides_kw: dict[str, Any] = {}
hf_overrides_kw = {}
hf_overrides_fn = self.hf_overrides
dict_overrides: dict[str, Any] = {}
else:
# Separate dict overrides from flat ones; convert OmegaConf to dict
# Separate dict overrides from flat ones
# We'll determine how to apply dict overrides after loading the config
hf_overrides_kw = {}
dict_overrides = {}
for key, value in self.hf_overrides.items():
if isinstance(value, DictConfig):
value = OmegaConf.to_container(value, resolve=True, structured_config_mode=None)
if isinstance(value, dict):
dict_overrides[key] = value
else:
hf_overrides_kw[key] = value
hf_overrides_fn = None
if self.rope_scaling:
hf_override: dict[str, Any] = {"rope_scaling": self.rope_scaling}
hf_overrides_kw.update(hf_override)
hf_overrides_str = json.dumps(hf_overrides_kw)
msg = (
"`--rope-scaling` will be removed in a future release. "
f"'Please instead use `--hf-overrides '{hf_overrides_str}'`"
)
warnings.warn(DeprecationWarning(msg), stacklevel=2)
if self.rope_theta is not None:
hf_override = {"rope_theta": self.rope_theta}
hf_overrides_kw.update(hf_override)
hf_overrides_str = json.dumps(hf_overrides_kw)
msg = (
"`--rope-theta` will be removed in a future release. "
f"'Please instead use `--hf-overrides '{hf_overrides_str}'`"
)
warnings.warn(DeprecationWarning(msg), stacklevel=2)
self.maybe_pull_model_tokenizer_for_runai(self.model, self.tokenizer)
if (backend := envs.VLLM_ATTENTION_BACKEND) and backend == "FLASHINFER" and find_spec("flashinfer") is None:
raise ValueError(
"VLLM_ATTENTION_BACKEND is set to FLASHINFER, but flashinfer "
"module was not found. See "
"https://github.com/vllm-project/vllm/blob/main/docker/Dockerfile "
"https://github.com/vllm-project/vllm/blob/main/docker/Dockerfile " # noqa: E501
"for instructions on how to install it."
)
if self.override_attention_dtype is not None and not current_platform.is_rocm():
warnings.warn("override-attention-dtype is set but not using ROCm platform", stacklevel=2)
warnings.warn(
"override-attention-dtype is set but not using ROCm platform",
stacklevel=2,
)
if self.enable_sleep_mode and not current_platform.is_sleep_mode_available():
raise ValueError("Sleep mode is not supported on current platform.")
@@ -184,11 +162,14 @@ class OmniModelConfig(ModelConfig):
hf_overrides_kw=hf_overrides_kw,
hf_overrides_fn=hf_overrides_fn,
)
if dict_overrides:
self._apply_dict_overrides(hf_config, dict_overrides)
hf_config = maybe_patch_hf_config_from_gguf(
self.model,
hf_config,
)
self.hf_config = hf_config
if dict_overrides:
self._apply_dict_overrides(hf_config, dict_overrides)
self.hf_text_config = self.draw_hf_text_config()
self.attention_chunk_size = getattr(self.hf_text_config, "attention_chunk_size", None)
self.encoder_config = self._get_encoder_config()
@@ -216,7 +197,7 @@ class OmniModelConfig(ModelConfig):
if self.task is not None:
runner: RunnerOption = "auto"
convert: ConvertType | Literal["auto"] = "auto" # type: ignore[name-defined]
convert: ConvertOption = "auto"
msg_prefix = (
"The 'task' option has been deprecated and will be removed in v0.13.0 or v1.0, whichever comes first."
)
@@ -228,7 +209,7 @@ class OmniModelConfig(ModelConfig):
if is_generative_model and is_pooling_model:
if is_generative_task:
runner = "generate"
convert = "auto" # type: ignore[assignment]
convert = "auto"
msg_hint = (
"Please replace this option with `--runner "
"generate` to continue using this model "
@@ -236,7 +217,7 @@ class OmniModelConfig(ModelConfig):
)
elif is_pooling_task:
runner = "pooling"
convert = "auto" # type: ignore[assignment]
convert = "auto"
msg_hint = (
"Please replace this option with `--runner "
"pooling` to continue using this model "
@@ -247,7 +228,7 @@ class OmniModelConfig(ModelConfig):
elif is_generative_model or is_pooling_model:
if is_generative_task:
runner = "generate"
convert = "auto" # type: ignore[assignment]
convert = "auto"
msg_hint = "Please remove this option"
elif is_pooling_task:
runner = "pooling"
@@ -260,12 +241,29 @@ class OmniModelConfig(ModelConfig):
else: # task == "auto"
pass
else:
raise AssertionError(
f"The model should be a generative or pooling model when task is set to {self.task!r}."
)
# Neither generative nor pooling model - try to convert if possible
if is_pooling_task:
runner = "pooling"
convert = _task_to_convert(self.task)
msg_hint = (
"Please replace this option with `--runner pooling "
f"--convert {convert}` to continue using this model "
"as a pooling model."
)
else:
debug_info = {
"architectures": architectures,
"is_generative_model": is_generative_model,
"is_pooling_model": is_pooling_model,
}
raise AssertionError(
"The model should be a generative or "
"pooling model when task is set to "
f"{self.task!r}. Found: {debug_info}"
)
self.runner = runner
self.convert = convert # type: ignore[assignment]
self.convert = convert
msg = f"{msg_prefix} {msg_hint}"
warnings.warn(msg, DeprecationWarning, stacklevel=2)
@@ -276,6 +274,7 @@ class OmniModelConfig(ModelConfig):
if self.runner_type == "generate" and not is_generative_model:
generate_converts = _RUNNER_CONVERTS["generate"]
if self.convert_type not in generate_converts:
# Currently we don't have any converters for generative models
raise ValueError("This model does not support `--runner generate`.")
if self.runner_type == "pooling" and not is_pooling_model:
pooling_converts = _RUNNER_CONVERTS["pooling"]
@@ -296,23 +295,12 @@ class OmniModelConfig(ModelConfig):
# Init pooler config if needed
if self.runner_type == "pooling":
if self.override_pooler_config is not None:
logger.warning_once(
"`override_pooler_config` is deprecated and will be "
"removed in v0.12.0 or v1.0.0, whichever is sooner. "
"Please use `pooler_config` instead."
)
if isinstance(self.override_pooler_config, dict):
self.pooler_config = PoolerConfig(**self.override_pooler_config)
else:
self.pooler_config = self.override_pooler_config
if self.pooler_config is None:
self.pooler_config = PoolerConfig()
base_config = get_pooling_config(self.model, self.revision)
if base_config is not None:
# Only set values that are not overridden by the user
for k, v in base_config.items():
if getattr(self.pooler_config, k) is None:
setattr(self.pooler_config, k, v)
@@ -329,23 +317,6 @@ class OmniModelConfig(ModelConfig):
revision=self.revision,
)
# Interleaved attention is not supported by some backends in V0
if (
not self.disable_sliding_window
and is_interleaved(self.hf_text_config)
and not envs.VLLM_USE_V1
and (backend := envs.VLLM_ATTENTION_BACKEND) in ("XFORMERS", "FLASHINFER")
):
logger.warning_once(
"%s has interleaved attention, which is currently not "
"supported by the %s backend. Disabling sliding window and "
"capping the max length to the sliding window size (%d).",
self.hf_text_config.model_type,
backend,
self.hf_text_config.sliding_window,
)
self.disable_sliding_window = True
self.original_max_model_len = self.max_model_len
self.max_model_len = self.get_and_verify_max_len(self.max_model_len)
# Init multimodal config if needed
@@ -359,12 +330,14 @@ class OmniModelConfig(ModelConfig):
mm_config_kwargs = dict(
limit_per_prompt=limit_mm_per_prompt,
enable_mm_embeds=enable_mm_embeds,
media_io_kwargs=media_io_kwargs,
mm_processor_kwargs=mm_processor_kwargs,
mm_processor_cache_gb=mm_processor_cache_gb,
mm_processor_cache_type=mm_processor_cache_type,
mm_shm_cache_max_object_size_mb=mm_shm_cache_max_object_size_mb,
mm_encoder_tp_mode=mm_encoder_tp_mode,
mm_encoder_attn_backend=mm_encoder_attn_backend,
interleave_mm_strings=interleave_mm_strings,
skip_mm_profiling=skip_mm_profiling,
video_pruning_rate=video_pruning_rate,
@@ -374,12 +347,20 @@ class OmniModelConfig(ModelConfig):
self.multimodal_config = MultiModalConfig(**mm_config_kwargs)
# Multimodal GGUF models must use original repo for mm processing
if is_gguf(self.tokenizer) and self.is_multimodal_model:
raise ValueError(
"Loading a multimodal GGUF model needs to use original "
"tokenizer. Please specify the unquantized hf model's "
"repo name or path using the --tokenizer argument."
)
if self.disable_sliding_window:
# Set after get_and_verify_max_len to ensure that max_model_len
# can be correctly capped to sliding window size
self.hf_text_config.sliding_window = None
if not self.skip_tokenizer_init:
self._verify_tokenizer_mode()
# Avoid running try_verify_and_update_config multiple times
self.config_updated = False
self._verify_quantization()
+54 -38
View File
@@ -1,7 +1,10 @@
from __future__ import annotations
from collections import defaultdict
from time import time
from vllm.distributed.kv_events import KVEventBatch
from vllm.distributed.kv_transfer.kv_connector.v1.metrics import KVConnectorStats
from vllm.logger import init_logger
from vllm.v1.core.sched.output import SchedulerOutput
from vllm.v1.core.sched.scheduler import Scheduler as VLLMScheduler
@@ -63,14 +66,6 @@ class OmniARScheduler(VLLMScheduler):
scheduler_output: SchedulerOutput,
model_runner_output: ModelRunnerOutput,
) -> dict[int, EngineCoreOutputs]:
"""Override update_from_output to fix check_stop bug when output_token_ids is empty.
The original vLLM implementation calls check_stop when pooler_outputs exist,
but doesn't check if output_token_ids is empty. This causes IndexError during
prefill phase when pooler_outputs exist but output_token_ids is still empty.
This override adds a safety check before calling check_stop.
"""
sampled_token_ids = model_runner_output.sampled_token_ids
logprobs = model_runner_output.logprobs
prompt_logprobs_dict = model_runner_output.prompt_logprobs_dict
@@ -81,7 +76,20 @@ class OmniARScheduler(VLLMScheduler):
outputs: dict[int, list[EngineCoreOutput]] = defaultdict(list)
spec_decoding_stats: SpecDecodingStats | None = None
kv_connector_stats = kv_connector_output.kv_connector_stats if kv_connector_output else None
kv_connector_stats: KVConnectorStats | None = (
kv_connector_output.kv_connector_stats if kv_connector_output else None
)
if kv_connector_stats and self.connector:
kv_stats = self.connector.get_kv_connector_stats()
if kv_stats:
kv_connector_stats = kv_connector_stats.aggregate(kv_stats)
failed_kv_load_req_ids = None
if kv_connector_output and kv_connector_output.invalid_block_ids:
# These blocks contain externally computed tokens that failed to
# load. Identify affected requests and adjust their computed token
# count to trigger recomputation of the invalid blocks.
failed_kv_load_req_ids = self._handle_invalid_blocks(kv_connector_output.invalid_block_ids)
# NOTE(woosuk): As len(num_scheduled_tokens) can be up to 1K or more,
# the below loop can be a performance bottleneck. We should do our best
@@ -90,6 +98,9 @@ class OmniARScheduler(VLLMScheduler):
stopped_preempted_reqs: set[Request] = set()
for req_id, num_tokens_scheduled in num_scheduled_tokens.items():
assert num_tokens_scheduled > 0
if failed_kv_load_req_ids and req_id in failed_kv_load_req_ids:
# Skip requests that were recovered from KV load failure
continue
request = self.requests.get(req_id)
if request is None:
# The request is already finished. This can happen if the
@@ -110,9 +121,16 @@ class OmniARScheduler(VLLMScheduler):
# tokens and rejections. If some tokens are rejected,
# num_computed_tokens is decreased by the number of rejected
# tokens.
request.num_computed_tokens -= num_rejected
if request.num_computed_tokens > 0:
request.num_computed_tokens -= num_rejected
# If async scheduling, num_output_placeholders also includes
# the scheduled spec tokens count and so is similarly adjusted.
if request.num_output_placeholders > 0:
request.num_output_placeholders -= num_rejected
spec_decoding_stats = self.make_spec_decoding_stats(
spec_decoding_stats, num_draft_tokens=num_draft_tokens, num_accepted_tokens=num_accepted
spec_decoding_stats,
num_draft_tokens=num_draft_tokens,
num_accepted_tokens=num_accepted,
)
stopped = False
@@ -125,28 +143,12 @@ class OmniARScheduler(VLLMScheduler):
if new_token_ids:
new_token_ids, stopped = self._update_request_with_output(request, new_token_ids)
"""
# Stop checking for pooler models.
pooler_output = None
if pooler_outputs:
pooler_output = pooler_outputs[req_index]
stopped = check_stop(request, self.max_model_len,
pooler_output)
"""
# Stop checking for pooler models.
# FIX: Check if output_token_ids is empty before calling check_stop
# to prevent IndexError during prefill phase when pooler_outputs exist
# but output_token_ids is still empty.
pooler_output = None
if pooler_outputs:
pooler_output = pooler_outputs[req_index]
# Only call check_stop if output_token_ids is not empty
# This prevents IndexError when accessing output_token_ids[-1]
if request.output_token_ids:
stopped = check_stop(request, self.max_model_len, pooler_output)
# If output_token_ids is empty, we don't call check_stop
# and keep stopped=False (don't stop during prefill phase)
if stopped:
kv_transfer_params = self._free_request(request)
@@ -157,17 +159,13 @@ class OmniARScheduler(VLLMScheduler):
# Extract sample logprobs if needed.
if request.sampling_params is not None and request.sampling_params.logprobs is not None and logprobs:
# NOTE: once we support N tokens per step (spec decode),
# the outer lists can be of length > 1.
new_logprobs = logprobs.slice(req_index, req_index + 1)
new_logprobs = logprobs.slice_request(req_index, len(new_token_ids))
if new_token_ids and self.structured_output_manager.should_advance(request):
# NOTE: structured_output_request
# should not be None if use_structured_output, we have
# checked above, so safe to ignore type warning
request.structured_output_request.grammar.accept_tokens( # type: ignore[union-attr]
req_id, new_token_ids
)
struct_output_request = request.structured_output_request
assert struct_output_request is not None
assert struct_output_request.grammar is not None
struct_output_request.grammar.accept_tokens(req_id, new_token_ids)
if num_nans_in_logits is not None and req_id in num_nans_in_logits:
request.num_nans_in_logits = num_nans_in_logits[req_id]
@@ -189,6 +187,7 @@ class OmniARScheduler(VLLMScheduler):
kv_transfer_params=kv_transfer_params,
trace_headers=request.trace_headers,
num_cached_tokens=request.num_cached_tokens,
num_nans_in_logits=request.num_nans_in_logits,
)
)
else:
@@ -203,8 +202,25 @@ class OmniARScheduler(VLLMScheduler):
self.waiting.remove_requests(stopped_preempted_reqs)
# KV Connector: update state for finished KV Transfers.
if model_runner_output.kv_connector_output:
self._update_from_kv_xfer_finished(model_runner_output.kv_connector_output)
if kv_connector_output:
self._update_from_kv_xfer_finished(kv_connector_output)
# collect KV cache events from KV cache manager
events = self.kv_cache_manager.take_events()
# collect KV cache events from connector
if self.connector is not None:
connector_events = self.connector.take_events()
if connector_events:
if events is None:
events = list(connector_events)
else:
events.extend(connector_events)
# publish collected KV cache events
if events:
batch = KVEventBatch(ts=time.time(), events=events)
self.kv_event_publisher.publish(batch)
# Create EngineCoreOutputs for all clients that have requests with
# outputs in this step.
@@ -1,17 +1,15 @@
import time
from collections import defaultdict
from vllm.distributed.kv_events import KVEventBatch
from vllm.v1.core.kv_cache_manager import KVCacheBlocks
from vllm.v1.core.sched.output import SchedulerOutput
from vllm.v1.core.sched.request_queue import create_request_queue
from vllm.v1.core.sched.scheduler import (
Request,
RequestStatus,
SchedulerOutput,
SpecDecodingStats,
)
from vllm.v1.core.sched.scheduler import Scheduler as VLLMScheduler
from vllm.v1.core.sched.utils import remove_all
from vllm.v1.engine import EngineCoreEventType, EngineCoreOutput, EngineCoreOutputs
from vllm.v1.request import Request, RequestStatus
from vllm.v1.spec_decode.metrics import SpecDecodingStats
from vllm_omni.core.sched.output import OmniNewRequestData
from vllm_omni.outputs import OmniModelRunnerOutput
@@ -26,15 +24,10 @@ class OmniGenerationScheduler(VLLMScheduler):
default vLLM scheduling.
"""
# Select requests with zero prompt and using pooling
# (diffusion results returned via pooler_output)
token_budget = self.max_num_scheduled_tokens
capacity = self.max_num_running_reqs - len(self.running)
scheduled_timestamp = time.monotonic()
scheduled_new_reqs: list[Request] = []
scheduled_resumed_reqs: list[Request] = []
scheduled_running_reqs: list[Request] = []
req_to_new_blocks: dict[str, KVCacheBlocks] = {}
num_scheduled_tokens: dict[str, int] = {}
@@ -46,7 +39,7 @@ class OmniGenerationScheduler(VLLMScheduler):
# Fast path selection and scheduling (treat all as diffusion requests,
# independent of pooling_params)
while self.waiting and token_budget > 0 and capacity > 0:
while self.waiting and token_budget > 0 and len(self.running) < self.max_num_running_reqs:
request = self.waiting.peek_request()
# Uniformly treat as diffusion. A feature flag can be added later
# via config or request tag.
@@ -81,7 +74,6 @@ class OmniGenerationScheduler(VLLMScheduler):
req_to_new_blocks[request.request_id] = new_blocks
num_scheduled_tokens[request.request_id] = num_new_tokens
token_budget -= num_new_tokens
capacity -= 1
scheduled_new_reqs.append(request)
# Return skipped waiting requests
@@ -96,25 +88,31 @@ class OmniGenerationScheduler(VLLMScheduler):
num_common_prefix_blocks = [0] * len(self.kv_cache_config.kv_cache_groups)
if self.running:
any_request = self.running[0]
num_common_prefix_blocks = self.kv_cache_manager.get_num_common_prefix_blocks(
any_request, len(self.running)
)
num_common_prefix_blocks = self.kv_cache_manager.get_num_common_prefix_blocks(any_request.request_id)
# Assemble SchedulerOutput
new_reqs_data = [
OmniNewRequestData.from_request(req, req_to_new_blocks[req.request_id].get_block_ids())
for req in scheduled_new_reqs
]
# Assemble SchedulerOutput (align with v0.12.0)
if self.use_v2_model_runner:
# No resumed reqs in fast path; pass prefill_token_ids for new reqs.
new_reqs_data = [
OmniNewRequestData.from_request(
req,
req_to_new_blocks[req.request_id].get_block_ids(),
getattr(req, "_all_token_ids", None),
)
for req in scheduled_new_reqs
]
else:
new_reqs_data = [
OmniNewRequestData.from_request(req, req_to_new_blocks[req.request_id].get_block_ids())
for req in scheduled_new_reqs
]
# No running/resumed reqs scheduled in our fast path
cached_reqs_data = self._make_cached_request_data(
scheduled_running_reqs,
scheduled_resumed_reqs,
num_scheduled_tokens,
scheduled_spec_decode_tokens,
req_to_new_blocks,
)
scheduled_requests = scheduled_new_reqs + scheduled_running_reqs + scheduled_resumed_reqs
structured_output_request_ids, grammar_bitmask = self.get_grammar_bitmask(
scheduled_requests, scheduled_spec_decode_tokens
running_reqs=[],
resumed_reqs=[],
num_scheduled_tokens=num_scheduled_tokens,
spec_decode_tokens=scheduled_spec_decode_tokens,
req_to_new_blocks=req_to_new_blocks,
)
total_num_scheduled_tokens = sum(num_scheduled_tokens.values())
@@ -128,14 +126,21 @@ class OmniGenerationScheduler(VLLMScheduler):
num_common_prefix_blocks=num_common_prefix_blocks,
finished_req_ids=self.finished_req_ids,
free_encoder_mm_hashes=self.encoder_cache_manager.get_freed_mm_hashes(),
structured_output_request_ids=structured_output_request_ids,
grammar_bitmask=grammar_bitmask,
preempted_req_ids=set(),
)
# Record the request ids scheduled in this step (v0.12.0 behavior).
self.prev_step_scheduled_req_ids.clear()
self.prev_step_scheduled_req_ids.update(num_scheduled_tokens.keys())
# KVTransfer: package metadata
if self.connector is not None:
meta = self.connector.build_connector_meta(scheduler_output)
scheduler_output.kv_connector_metadata = meta
# EC Connector: package metadata
if self.ec_connector is not None:
ec_meta = self.ec_connector.build_connector_meta(scheduler_output)
scheduler_output.ec_connector_metadata = ec_meta
# Update internal state (advance num_computed_tokens, free encoder inputs,
# etc.)
@@ -171,6 +176,15 @@ class OmniGenerationScheduler(VLLMScheduler):
outputs: dict[int, list[EngineCoreOutput]] = defaultdict(list)
spec_decoding_stats: SpecDecodingStats | None = None
kv_connector_stats = kv_connector_output.kv_connector_stats if kv_connector_output else None
# Merge connector-side stats (align with v0.12.0)
if kv_connector_stats and self.connector:
kv_stats = self.connector.get_kv_connector_stats()
if kv_stats:
kv_connector_stats = kv_connector_stats.aggregate(kv_stats)
failed_kv_load_req_ids = None
if kv_connector_output and getattr(kv_connector_output, "invalid_block_ids", None):
failed_kv_load_req_ids = self._handle_invalid_blocks(kv_connector_output.invalid_block_ids)
# NOTE(woosuk): As len(num_scheduled_tokens) can be up to 1K or more,
# the below loop can be a performance bottleneck. We should do our best
@@ -179,6 +193,9 @@ class OmniGenerationScheduler(VLLMScheduler):
stopped_preempted_reqs: set[Request] = set()
for req_id, num_tokens_scheduled in num_scheduled_tokens.items():
assert num_tokens_scheduled > 0
if failed_kv_load_req_ids and req_id in failed_kv_load_req_ids:
# Skip requests that were recovered from KV load failure
continue
request = self.requests.get(req_id)
if request is None:
# The request is already finished. This can happen if the
@@ -199,7 +216,8 @@ class OmniGenerationScheduler(VLLMScheduler):
# tokens and rejections. If some tokens are rejected,
# num_computed_tokens is decreased by the number of rejected
# tokens.
request.num_computed_tokens -= num_rejected
if request.num_computed_tokens > 0:
request.num_computed_tokens -= num_rejected
spec_decoding_stats = self.make_spec_decoding_stats(
spec_decoding_stats,
num_draft_tokens=num_draft_tokens,
@@ -227,9 +245,7 @@ class OmniGenerationScheduler(VLLMScheduler):
# Extract sample logprobs if needed.
if request.sampling_params is not None and request.sampling_params.logprobs is not None and logprobs:
# NOTE: once we support N tokens per step (spec decode),
# the outer lists can be of length > 1.
new_logprobs = logprobs.slice(req_index, req_index + 1)
new_logprobs = logprobs.slice_request(req_index, len(new_token_ids))
if new_token_ids and self.structured_output_manager.should_advance(request):
# NOTE: structured_output_request should not be None if
@@ -245,12 +261,6 @@ class OmniGenerationScheduler(VLLMScheduler):
# Get prompt logprobs for this request.
prompt_logprobs_tensors = prompt_logprobs_dict.get(req_id)
# Convert pooler_output tensor to dict format expected by OmniEngineCoreOutput
pooling_output_dict = None
if pooler_output is not None:
# Wrap tensor in dict to match OmniEngineCoreOutput.pooling_output type
# which expects Optional[dict[str, torch.Tensor]]
pooling_output_dict = {"model_outputs": pooler_output}
if new_token_ids or pooler_output is not None or kv_transfer_params:
# Add EngineCoreOutput for this Request.
outputs[request.client_index].append(
@@ -260,12 +270,13 @@ class OmniGenerationScheduler(VLLMScheduler):
finish_reason=request.get_finished_reason(),
new_logprobs=new_logprobs,
new_prompt_logprobs_tensors=prompt_logprobs_tensors,
pooling_output=pooling_output_dict,
pooling_output=pooler_output,
stop_reason=request.stop_reason,
events=request.take_events(),
kv_transfer_params=kv_transfer_params,
trace_headers=request.trace_headers,
num_cached_tokens=request.num_cached_tokens,
num_nans_in_logits=request.num_nans_in_logits,
)
)
else:
@@ -280,8 +291,21 @@ class OmniGenerationScheduler(VLLMScheduler):
self.waiting.remove_requests(stopped_preempted_reqs)
# KV Connector: update state for finished KV Transfers.
if model_runner_output.kv_connector_output:
self._update_from_kv_xfer_finished(model_runner_output.kv_connector_output)
if kv_connector_output:
self._update_from_kv_xfer_finished(kv_connector_output)
# Collect and publish KV cache events (align with v0.12.0)
events = self.kv_cache_manager.take_events()
if self.connector is not None:
connector_events = self.connector.take_events()
if connector_events:
if events is None:
events = list(connector_events)
else:
events.extend(connector_events)
if events:
batch = KVEventBatch(ts=time.time(), events=events)
self.kv_event_publisher.publish(batch)
# Create EngineCoreOutputs for all clients that have requests with
# outputs in this step.
+2
View File
@@ -29,6 +29,7 @@ class OmniNewRequestData(NewRequestData):
cls,
request: Request,
block_ids: tuple[list[int], ...],
prefill_token_ids: list[int] | None = None,
) -> "OmniNewRequestData":
"""Create OmniNewRequestData from a Request object.
@@ -49,5 +50,6 @@ class OmniNewRequestData(NewRequestData):
num_computed_tokens=request.num_computed_tokens,
lora_request=request.lora_request,
prompt_embeds=request.prompt_embeds,
prefill_token_ids=prefill_token_ids,
additional_information=request.additional_information,
)
@@ -13,7 +13,6 @@ from torch import nn
from vllm.config import ModelConfig
from vllm.config.load import LoadConfig
from vllm.logger import init_logger
from vllm.model_executor.model_loader.utils import set_default_torch_dtype
from vllm.model_executor.model_loader.weight_utils import (
download_safetensors_index_file_from_hf,
download_weights_from_hf,
@@ -22,6 +21,7 @@ from vllm.model_executor.model_loader.weight_utils import (
maybe_download_from_modelscope,
safetensors_weights_iterator,
)
from vllm.utils.torch_utils import set_default_torch_dtype
from vllm_omni.diffusion.data import OmniDiffusionConfig
from vllm_omni.diffusion.registry import initialize_model
+1 -1
View File
@@ -9,7 +9,7 @@ import zmq
from vllm.config import LoadConfig, VllmConfig, set_current_vllm_config
from vllm.distributed.device_communicators.shm_broadcast import MessageQueue
from vllm.logger import init_logger
from vllm.utils import DeviceMemoryProfiler, GiB_bytes
from vllm.utils.mem_utils import DeviceMemoryProfiler, GiB_bytes
from vllm_omni.diffusion.cache.selector import get_cache_backend
from vllm_omni.diffusion.data import (
+1 -1
View File
@@ -14,7 +14,7 @@ from vllm.distributed.parallel_state import (
initialize_model_parallel,
)
from vllm.logger import init_logger
from vllm.utils import DeviceMemoryProfiler, GiB_bytes
from vllm.utils.mem_utils import DeviceMemoryProfiler, GiB_bytes
from vllm_omni.diffusion.data import (
SHUTDOWN_MESSAGE,
@@ -292,7 +292,7 @@ def build_stage_connectors(
from .config import ConnectorSpec
connectors: dict[tuple[str, str], Any] = {}
# 将字典格式的配置转换为ConnectorSpec对象
# Convert dictionary-formatted config to ConnectorSpec objects
stage_connector_specs = {}
for input_key, config in connectors_config.items():
if not input_key.startswith("from_stage_"):
@@ -310,7 +310,7 @@ def build_stage_connectors(
stage_connector_specs[(str(from_stage), str(stage_id))] = connector_spec
try:
# 使用统一的连接器创建逻辑
# Use unified connector creation logic
connectors = create_connectors_from_config(stage_connector_specs)
except Exception as exc: # pragma: no cover - defensive logging
# Fail fast so the stage does not start with missing connectors.
+4
View File
@@ -102,6 +102,10 @@ class OmniEngineCoreOutput(
# The number of tokens with prefix cache hits.
num_cached_tokens: int = 0
# The number of NaNs in logits.
# A value greater than 0 indicates that the output is corrupted.
num_nans_in_logits: int = 0
@property
def finished(self) -> bool:
return self.finish_reason is not None
@@ -1,6 +1,6 @@
import time
from collections.abc import Mapping
from typing import Any
from typing import Any, cast
import torch
from vllm.config import VllmConfig
@@ -9,14 +9,14 @@ from vllm.inputs.parse import split_enc_dec_inputs
from vllm.logger import init_logger
from vllm.lora.request import LoRARequest
from vllm.multimodal import MULTIMODAL_REGISTRY, MultiModalRegistry
from vllm.multimodal.inputs import MultiModalFeatureSpec
from vllm.multimodal.inputs import MultiModalFeatureSpec, MultiModalUUIDDict
from vllm.multimodal.utils import argsort_mm_positions
from vllm.platforms import current_platform
from vllm.pooling_params import PoolingParams
from vllm.sampling_params import SamplingParams
from vllm.transformers_utils.tokenizer import AnyTokenizer
from vllm.tokenizers import TokenizerLike
from vllm.utils import length_from_prompt_token_ids_or_embeds
from vllm.v1.engine.processor import Processor
from vllm.v1.engine.input_processor import InputProcessor
from vllm_omni.engine import (
AdditionalInformationEntry,
@@ -29,7 +29,7 @@ from vllm_omni.inputs.preprocess import OmniInputPreprocessor
logger = init_logger(__name__)
class OmniProcessor(Processor):
class OmniInputProcessor(InputProcessor):
"""Processor for omni models, handling multimodal inputs and embeddings.
Extends the base vLLM Processor with support for processing prompt
@@ -75,7 +75,7 @@ class OmniProcessor(Processor):
def __init__(
self,
vllm_config: VllmConfig,
tokenizer: AnyTokenizer,
tokenizer: TokenizerLike,
mm_registry: MultiModalRegistry = MULTIMODAL_REGISTRY,
):
super().__init__(vllm_config, tokenizer, mm_registry)
@@ -127,8 +127,6 @@ class OmniProcessor(Processor):
ValueError: If data_parallel_rank is out of range or prompt_embeds
has incorrect shape
"""
# TODO(woosuk): Support pooling models.
# TODO(woosuk): Support encoder-decoder models.
self._validate_lora(lora_request)
self._validate_params(params)
@@ -158,7 +156,7 @@ class OmniProcessor(Processor):
# if provided.
self._validate_multi_modal_uuids(prompt)
if isinstance(prompt, dict):
mm_uuids = prompt.get("multi_modal_uuids")
mm_uuids = cast(MultiModalUUIDDict | None, prompt.get("multi_modal_uuids"))
else:
mm_uuids = None
@@ -171,11 +169,13 @@ class OmniProcessor(Processor):
tokenization_kwargs=tokenization_kwargs,
mm_uuids=mm_uuids,
)
current_platform.validate_request(
prompt=prompt,
params=params,
processed_inputs=processed_inputs,
)
eos_token_id = self.input_preprocessor.get_eos_token_id()
encoder_inputs, decoder_inputs = split_enc_dec_inputs(processed_inputs)
@@ -185,7 +185,7 @@ class OmniProcessor(Processor):
# discriminated unions of TypedDicts, because of how it handles
# inheritance of TypedDict. If we explicitly extract the items we want
# we can avoid type errors from using `dict.get` later in the method.
prompt_str: str | None = None if decoder_inputs["type"] == "embeds" else decoder_inputs.get("prompt")
_prompt_str: str | None = None if decoder_inputs["type"] == "embeds" else decoder_inputs.get("prompt")
prompt_token_ids = decoder_inputs["prompt_token_ids"] if decoder_inputs["type"] != "embeds" else None
prompt_embeds = decoder_inputs["prompt_embeds"] if decoder_inputs["type"] == "embeds" else None
@@ -266,7 +266,7 @@ class OmniProcessor(Processor):
entries[key] = entry
additional_information_payload = AdditionalInformationPayload(entries=entries)
return prompt_str, OmniEngineCoreRequest(
return OmniEngineCoreRequest(
request_id=request_id,
prompt_token_ids=prompt_token_ids,
mm_features=mm_features,
+28 -4
View File
@@ -6,7 +6,7 @@ import torch
from vllm.logger import init_logger
from vllm.outputs import PoolingRequestOutput
from vllm.sampling_params import RequestOutputKind
from vllm.transformers_utils.tokenizer import AnyTokenizer
from vllm.tokenizers import TokenizerLike
from vllm.v1.engine import EngineCoreOutput, EngineCoreRequest, FinishReason
from vllm.v1.engine.detokenizer import IncrementalDetokenizer
from vllm.v1.engine.logprobs import LogprobsProcessor
@@ -40,13 +40,14 @@ class OmniRequestState(RequestState):
@classmethod
def from_new_request(
cls,
tokenizer: AnyTokenizer,
tokenizer: TokenizerLike,
request: EngineCoreRequest,
prompt: str | None,
parent_req: ParentRequest | None,
request_index: int,
queue: Any | None,
log_stats: bool,
stream_interval: int,
) -> "OmniRequestState":
if sampling_params := request.sampling_params:
if not sampling_params.detokenize:
@@ -92,6 +93,7 @@ class OmniRequestState(RequestState):
arrival_time=request.arrival_time,
queue=queue,
log_stats=log_stats,
stream_interval=stream_interval,
)
def add_multimodal_tensor(self, payload: Any | None, mm_type: str | None) -> None:
@@ -196,6 +198,26 @@ class OmniRequestState(RequestState):
if not finished and final_only:
return None
if self.stream_interval > 1:
assert self.detokenizer is not None
# Send output request only when
# 1. It has finished, or
# 2. It is the first token, or
# 3. It has reached the stream interval number of tokens
if not (
finished
or self.sent_tokens_offset == 0
or len(self.detokenizer.output_token_ids) - self.sent_tokens_offset >= self.stream_interval
):
return None
if self.output_kind == RequestOutputKind.DELTA:
# Send tokens from the offset in DELTA mode, otherwise all
# tokens are sent.
new_token_ids = self.detokenizer.output_token_ids[self.sent_tokens_offset :]
self.sent_tokens_offset = len(self.detokenizer.output_token_ids)
request_id = self.request_id
output = self._new_completion_output(new_token_ids, finish_reason, stop_reason)
@@ -247,7 +269,7 @@ class MultimodalOutputProcessor(VLLMOutputProcessor):
def __init__(
self,
tokenizer: AnyTokenizer,
tokenizer: TokenizerLike,
log_stats: bool,
engine_core_output_type: str | None = None,
):
@@ -314,9 +336,9 @@ class MultimodalOutputProcessor(VLLMOutputProcessor):
request_index=request_index,
queue=queue,
log_stats=self.log_stats,
stream_interval=self.stream_interval,
)
self.request_states[request_id] = req_state
self.lora_states.add_request(req_state)
if parent_req:
self.parent_requests[parent_req.request_id] = parent_req
@@ -422,6 +444,8 @@ class MultimodalOutputProcessor(VLLMOutputProcessor):
parent_req = req_state.parent_req
if parent_req and not parent_req.child_requests:
self.parent_requests.pop(parent_req.request_id, None)
if not self.request_states:
self._requests_drained.set()
if not eco.finished:
reqs_to_abort.append(req_id)
self._update_stats_from_finished(req_state, finish_reason, iteration_stats)
+90 -27
View File
@@ -19,12 +19,13 @@ from vllm.inputs.preprocess import InputPreprocessor
from vllm.logger import init_logger
from vllm.lora.request import LoRARequest
from vllm.multimodal import MULTIMODAL_REGISTRY, MultiModalRegistry
from vllm.plugins.io_processors import get_io_processor
from vllm.sampling_params import SamplingParams
from vllm.tokenizers import TokenizerLike, init_tokenizer_from_config
from vllm.tracing import init_tracer
from vllm.transformers_utils.config import maybe_register_config_serialize_by_value
from vllm.transformers_utils.tokenizer import AnyTokenizer, init_tokenizer_from_configs
from vllm.usage.usage_lib import UsageContext
from vllm.utils import Device, deprecate_kwargs
from vllm.utils.func_utils import deprecate_kwargs
from vllm.v1.engine.async_llm import AsyncLLM
from vllm.v1.engine.core_client import EngineCoreClient
from vllm.v1.engine.exceptions import EngineDeadError
@@ -44,8 +45,8 @@ from vllm_omni.distributed.ray_utils.utils import (
try_close_ray,
)
from vllm_omni.engine.arg_utils import AsyncOmniEngineArgs
from vllm_omni.engine.input_processor import OmniInputProcessor
from vllm_omni.engine.output_processor import MultimodalOutputProcessor
from vllm_omni.engine.processor import OmniProcessor
from vllm_omni.entrypoints.client_request_state import ClientRequestState
from vllm_omni.entrypoints.log_utils import (
OrchestratorMetrics,
@@ -114,6 +115,10 @@ class AsyncOmni(EngineClient):
self.batch_timeout = cli_args.batch_timeout
self._enable_stats: bool = bool(cli_args.log_stats)
# Pause / resume state for async RL workflows.
self._pause_cond = asyncio.Condition()
self._paused = False
base_engine_args = AsyncOmniEngineArgs.from_cli_args(cli_args).__dict__.copy()
if cli_args.stage_configs_path is None:
@@ -143,6 +148,23 @@ class AsyncOmni(EngineClient):
self._stats_file, self._overall_stats_file = init_stats_paths(self._enable_stats, self._log_file)
self._initialize_stages(model, cli_args.init_sleep_seconds, cli_args.shm_threshold_bytes, cli_args.init_timeout)
# Here is a duplicated init for the first stage to get the tokenizer,
# input_processor, and io_processor
# TODO: Refactor the code to avoid the duplicated init, we need to pass
# the tokenizer, input_processor, and io_processor back here from the
# first stage.
vllm_config = self.stage_list[0].vllm_config
self.tokenizer = init_tokenizer_from_config(model_config=vllm_config.model_config)
self.input_processor = OmniInputProcessor(
vllm_config=vllm_config,
tokenizer=self.tokenizer,
)
self.model_config = vllm_config.model_config
self.io_processor = get_io_processor(
vllm_config,
self.model_config.io_processor_plugin,
)
def _initialize_stages(
self,
model: str,
@@ -304,6 +326,10 @@ class AsyncOmni(EngineClient):
Raises:
ValueError: If sampling_params_list has incorrect length.
"""
# Wait until generation is resumed if the engine is paused.
async with self._pause_cond:
await self._pause_cond.wait_for(lambda: not self._paused)
logger.debug("[Orchestrator] generate() called")
# Start output handler on the first call to generate()
@@ -634,7 +660,7 @@ class AsyncOmni(EngineClient):
async def get_input_preprocessor(self) -> InputPreprocessor:
return None
async def get_tokenizer(self) -> AnyTokenizer:
async def get_tokenizer(self) -> TokenizerLike:
for stage in self.stage_list:
if stage.is_comprehension:
return stage.tokenizer
@@ -655,7 +681,7 @@ class AsyncOmni(EngineClient):
async def reset_mm_cache(self) -> None:
pass
async def reset_prefix_cache(self, device: Device | None = None) -> None:
async def reset_prefix_cache(self, reset_running_requests: bool = False) -> bool:
pass
async def sleep(self, level: int = 1) -> None:
@@ -686,6 +712,54 @@ class AsyncOmni(EngineClient):
async def stop_profile(self) -> None:
raise NotImplementedError("stop_profile() is not implemented for AsyncOmni")
async def pause_generation(
self,
*,
wait_for_inflight_requests: bool = False,
clear_cache: bool = True,
) -> None:
"""
Pause generation to allow model weight updates.
New generation/encoding requests are blocked until resume.
Args:
wait_for_inflight_requests: When ``True`` waits for in-flight
requests to finish before pausing. When ``False`` (default),
immediately aborts any in-flight requests.
clear_cache: Whether to clear KV cache and prefix cache after
draining. Set to ``False`` to preserve cache for faster resume.
Default is ``True`` (clear caches).
"""
async with self._pause_cond:
if self._paused:
return
self._paused = True
# Note: AsyncOmni uses a stage-based architecture without a central
# output_processor. For now, we simply set the pause flag and let
# new requests wait. In-flight requests will complete naturally.
# TODO: Implement request abortion for stages if needed.
# Clear cache if requested
if clear_cache:
await self.reset_prefix_cache()
await self.reset_mm_cache()
async def resume_generation(self) -> None:
"""Resume generation after :meth:`pause_generation`."""
async with self._pause_cond:
self._paused = False
self._pause_cond.notify_all() # Wake up all waiting requests
async def is_paused(self) -> bool:
"""Return whether the engine is currently paused."""
async with self._pause_cond:
return self._paused
class AsyncOmniStageLLM(AsyncLLM):
"""Async single-stage LLM engine for use within a stage worker process.
@@ -749,14 +823,6 @@ class AsyncOmniStageLLM(AsyncLLM):
Returns:
None
"""
if not envs.VLLM_USE_V1:
raise ValueError(
"Using V1 AsyncLLMEngine, but envs.VLLM_USE_V1=False. "
"This should not happen. As a workaround, try using "
"AsyncLLMEngine.from_vllm_config(...) or explicitly set "
"VLLM_USE_V1=0 or 1 and report this issue on Github."
)
# Ensure we can serialize custom transformer configs
maybe_register_config_serialize_by_value()
@@ -773,28 +839,33 @@ class AsyncOmniStageLLM(AsyncLLM):
)
if self.model_config.skip_tokenizer_init:
self.tokenizer = None
tokenizer = None
else:
# Tokenizer (+ ensure liveness if running in another process).
self.tokenizer = init_tokenizer_from_configs(model_config=vllm_config.model_config)
tokenizer = init_tokenizer_from_config(model_config=vllm_config.model_config)
# Processor (converts Inputs --> EngineCoreRequests).
self.processor = OmniProcessor(
# InputProcessor (converts Inputs --> EngineCoreRequests).
self.input_processor = OmniInputProcessor(
vllm_config=vllm_config,
tokenizer=self.tokenizer,
tokenizer=tokenizer,
mm_registry=mm_registry,
)
# OutputProcessor (converts EngineCoreOutputs --> RequestOutput).
self.output_processor = MultimodalOutputProcessor(
tokenizer=self.tokenizer,
tokenizer=tokenizer,
log_stats=self.log_stats,
engine_core_output_type=engine_args.engine_output_type,
)
if self.observability_config.otlp_traces_endpoint is not None:
tracer = init_tracer("vllm.llm_engine", self.observability_config.otlp_traces_endpoint)
self.output_processor.tracer = tracer
# Pause / resume state for async RL workflows.
self._pause_cond = asyncio.Condition()
self._paused = False
# EngineCore (starts the engine in background process).
self.engine_core = EngineCoreClient.make_async_mp_client(
vllm_config=vllm_config,
@@ -862,14 +933,6 @@ class AsyncOmniStageLLM(AsyncLLM):
client_index: int = 0,
disable_log_requests: bool = True, # Deprecated, will be removed
) -> "AsyncLLM":
if not envs.VLLM_USE_V1:
raise ValueError(
"Using V1 AsyncLLMEngine, but envs.VLLM_USE_V1=False. "
"This should not happen. As a workaround, try using "
"AsyncLLMEngine.from_vllm_config(...) or explicitly set "
"VLLM_USE_V1=0 or 1 and report this issue on Github."
)
# Create the LLMEngine.
return cls(
vllm_config=vllm_config,
+1 -1
View File
@@ -16,7 +16,7 @@ def main():
return
else:
from vllm.entrypoints.utils import VLLM_SUBCMD_PARSER_EPILOG, cli_env_setup
from vllm.utils import FlexibleArgumentParser
from vllm.utils.argparse_utils import FlexibleArgumentParser
import vllm_omni.entrypoints.cli.serve
+1 -1
View File
@@ -12,7 +12,7 @@ from vllm.entrypoints.cli.types import CLISubcommand
from vllm.entrypoints.openai.cli_args import make_arg_parser, validate_parsed_serve_args
from vllm.entrypoints.utils import VLLM_SUBCMD_PARSER_EPILOG
from vllm.logger import init_logger
from vllm.utils import FlexibleArgumentParser
from vllm.utils.argparse_utils import FlexibleArgumentParser
from vllm_omni.entrypoints.openai.api_server import omni_run_server
+5 -3
View File
@@ -17,7 +17,7 @@ from vllm.logger import init_logger
from vllm.plugins.io_processors import get_io_processor
from vllm.sampling_params import SamplingParams
from vllm.usage.usage_lib import UsageContext
from vllm.utils import Counter
from vllm.utils.counter import Counter
from vllm.v1.engine.llm_engine import LLMEngine
from vllm_omni.distributed.omni_connectors import (
@@ -33,8 +33,8 @@ from vllm_omni.distributed.ray_utils.utils import (
try_close_ray,
)
from vllm_omni.engine.arg_utils import OmniEngineArgs
from vllm_omni.engine.input_processor import OmniInputProcessor
from vllm_omni.engine.output_processor import MultimodalOutputProcessor
from vllm_omni.engine.processor import OmniProcessor
from vllm_omni.entrypoints.log_utils import (
OrchestratorMetrics,
configure_orchestrator_logger,
@@ -624,7 +624,7 @@ class OmniStageLLM(LLM):
log_stats=self.llm_engine.log_stats,
engine_core_output_type=engine_args.engine_output_type,
)
self.llm_engine.processor = OmniProcessor(
self.llm_engine.input_processor = OmniInputProcessor(
vllm_config=self.llm_engine.vllm_config, tokenizer=self.llm_engine.tokenizer
)
self.engine_class = type(self.llm_engine)
@@ -641,3 +641,5 @@ class OmniStageLLM(LLM):
# Load the Input/Output processor plugin if any
io_processor_plugin = self.llm_engine.model_config.io_processor_plugin
self.io_processor = get_io_processor(self.llm_engine.vllm_config, io_processor_plugin)
self.model_config = self.llm_engine.model_config
self.input_processor = self.llm_engine.input_processor
+2 -2
View File
@@ -22,7 +22,7 @@ from vllm.inputs import TextPrompt
from vllm.inputs.preprocess import InputPreprocessor
from vllm.logger import init_logger
from vllm.sampling_params import SamplingParams
from vllm.transformers_utils.tokenizer import AnyTokenizer
from vllm.tokenizers import TokenizerLike
from vllm.usage.usage_lib import UsageContext
from vllm.v1.engine import EngineCoreOutput
from vllm.v1.engine.async_llm import AsyncLLM
@@ -112,7 +112,7 @@ class OmniStage:
"""
self.vllm_config = vllm_config
def set_tokenizer(self, tokenizer: AnyTokenizer) -> None:
def set_tokenizer(self, tokenizer: TokenizerLike) -> None:
"""Set the tokenizer for this stage.
Args:
+4 -11
View File
@@ -37,8 +37,8 @@ from vllm.entrypoints.openai.tool_parsers import ToolParserManager
from vllm.entrypoints.tool_server import DemoToolServer, MCPToolServer, ToolServer
from vllm.entrypoints.utils import load_aware_call, with_cancellation
from vllm.logger import init_logger
from vllm.transformers_utils.tokenizer import MistralTokenizer
from vllm.utils import decorate_logs
from vllm.tokenizers import MistralTokenizer
from vllm.utils.system_utils import decorate_logs
from vllm_omni.diffusion.data import DiffusionParallelConfig, OmniDiffusionConfig
from vllm_omni.diffusion.utils.hf_utils import is_diffusion_model
@@ -305,13 +305,8 @@ async def build_async_omni_from_stage_config(
"""
# V1 AsyncLLM.
assert envs.VLLM_USE_V1
if disable_frontend_multiprocessing:
logger.warning(
"V1 is enabled, but got --disable-frontend-multiprocessing. "
"To disable frontend multiprocessing, set VLLM_USE_V1=0."
)
logger.warning("V1 is enabled, but got --disable-frontend-multiprocessing.")
async_omni: EngineClient | None = None
@@ -358,7 +353,7 @@ async def omni_init_app_state(
state.engine_client = engine_client
state.log_stats = not args.disable_log_stats
state.vllm_config = vllm_config
model_config = vllm_config.model_config
_model_config = vllm_config.model_config
state.log_stats = not args.disable_log_stats
# For omni models
@@ -416,14 +411,12 @@ async def omni_init_app_state(
state.openai_serving_models = OpenAIServingModels(
engine_client=engine_client,
model_config=model_config,
base_model_paths=base_model_paths,
lora_modules=lora_modules,
)
await state.openai_serving_models.init_static_loras()
state.openai_serving_chat = OmniOpenAIServingChat(
engine_client,
model_config,
state.openai_serving_models,
args.response_role,
request_logger=request_logger,
+8 -7
View File
@@ -61,13 +61,14 @@ from vllm.logger import init_logger
from vllm.lora.request import LoRARequest
from vllm.outputs import RequestOutput
from vllm.sampling_params import SamplingParams
from vllm.transformers_utils.tokenizer import AnyTokenizer, MistralTokenizer
from vllm.transformers_utils.tokenizers import (
from vllm.tokenizers import TokenizerLike
from vllm.tokenizers.mistral import (
MistralTokenizer,
maybe_serialize_tool_calls,
truncate_tool_call_ids,
validate_request_params,
)
from vllm.utils import as_list
from vllm.utils.collection_utils import as_list
from vllm_omni.entrypoints.chat_utils import parse_chat_messages_futures
from vllm_omni.outputs import OmniRequestOutput
@@ -290,7 +291,7 @@ class OmniOpenAIServingChat(OpenAIServingChat):
async def _preprocess_chat(
self,
request: ChatLikeRequest | ResponsesRequest,
tokenizer: AnyTokenizer,
tokenizer: TokenizerLike,
messages: list[ChatCompletionMessageParam],
chat_template: str | None,
chat_template_content_format: ChatTemplateContentFormatOption,
@@ -299,7 +300,7 @@ class OmniOpenAIServingChat(OpenAIServingChat):
tool_dicts: list[dict[str, Any]] | None = None,
documents: list[dict[str, str]] | None = None,
chat_template_kwargs: dict[str, Any] | None = None,
tool_parser: Callable[[AnyTokenizer], ToolParser] | None = None,
tool_parser: Callable[[TokenizerLike], ToolParser] | None = None,
add_special_tokens: bool = False,
) -> tuple[
list[ConversationMessage],
@@ -450,7 +451,7 @@ class OmniOpenAIServingChat(OpenAIServingChat):
request_id: str,
model_name: str,
conversation: list[ConversationMessage],
tokenizer: AnyTokenizer,
tokenizer: TokenizerLike,
request_metadata: RequestResponseMetadata,
) -> ErrorResponse | ChatCompletionResponse:
created_time = int(time.time())
@@ -542,7 +543,7 @@ class OmniOpenAIServingChat(OpenAIServingChat):
self,
request: ChatCompletionRequest,
omni_outputs: OmniRequestOutput,
tokenizer: AnyTokenizer,
tokenizer: TokenizerLike,
conversation: list[ConversationMessage],
role: str,
):
+1 -1
View File
@@ -36,7 +36,7 @@ class OmniInputPreprocessor(InputPreprocessor):
inputs = self._process_multimodal(
prompt_token_ids,
multi_modal_data,
parsed_content.get("mm_processor_kwargs"),
parsed_content.get("mm_processor_kwargs") or {},
tokenization_kwargs=tokenization_kwargs,
mm_uuids=mm_uuids,
)
@@ -15,6 +15,12 @@ from transformers.models.qwen2_5_omni.configuration_qwen2_5_omni import (
from vllm.config import VllmConfig
from vllm.logger import init_logger
from vllm.model_executor.models.interfaces import SupportsMRoPE, SupportsMultiModal, SupportsPP
from vllm.model_executor.models.qwen2_5_omni_thinker import (
Qwen2_5OmniConditionalGenerationMixin,
Qwen2_5OmniThinkerDummyInputsBuilder,
Qwen2_5OmniThinkerMultiModalProcessor,
Qwen2_5OmniThinkerProcessingInfo,
)
from vllm.model_executor.models.utils import init_vllm_registered_model, maybe_prefix
# from vllm.model_executor.models.qwen2_code2wav_dit import Qwen2Code2wav
@@ -26,12 +32,6 @@ from vllm.v1.sample.sampler import Sampler
from vllm_omni.model_executor.model_loader.weight_utils import download_weights_from_hf_specific
from vllm_omni.model_executor.models.output_templates import OmniOutput
from vllm_omni.model_executor.models.qwen2_5_omni.qwen2_5_omni_thinker import (
Qwen2_5OmniConditionalGenerationMixin,
Qwen2_5OmniThinkerDummyInputsBuilder,
Qwen2_5OmniThinkerMultiModalProcessor,
Qwen2_5OmniThinkerProcessingInfo,
)
from vllm_omni.model_executor.models.utils import add_prefix_to_loaded_weights, split_list_into_ranges
from vllm_omni.model_executor.models.vision import get_llm_pos_ids_for_vision
from vllm_omni.utils.platform_utils import is_npu
@@ -161,18 +161,21 @@ class Qwen2_5OmniForConditionalGeneration(
return self.model.sampler
return Sampler()
def get_input_embeddings(
def embed_input_ids(
self,
input_ids: torch.Tensor,
multimodal_embeddings=None,
is_multimodal=None,
) -> torch.Tensor:
if self.model_stage == "code2wav":
return torch.zeros_like(input_ids).reshape(-1, 1).repeat(1, self.vllm_config.model_config.get_hidden_size())
return self.model.get_input_embeddings(input_ids, multimodal_embeddings)
return self.model.embed_input_ids(
input_ids=input_ids, multimodal_embeddings=multimodal_embeddings, is_multimodal=is_multimodal
)
def get_multimodal_embeddings(self, **kwargs):
def embed_multimodal(self, **kwargs):
# Delegate to thinker model for multimodal processing
return self.model.get_multimodal_embeddings(**kwargs)
return self.model.embed_multimodal(**kwargs)
def last_index_of(self, list, value):
return len(list) - 1 - list[::-1].index(value)
@@ -258,7 +261,7 @@ class Qwen2_5OmniForConditionalGeneration(
# Text-only path
return OmniOutput(
text_hidden_states=(text_hidden_states.squeeze(0) if added_batch_dim else text_hidden_states),
text_hidden_states=(text_hidden_states.reshape(-1, text_hidden_states.shape[-1])),
multimodal_outputs=None,
)
@@ -288,7 +291,7 @@ class Qwen2_5OmniForConditionalGeneration(
# Ensure we have base embeddings when only ids are provided
if inputs_embeds is None and input_ids is not None:
inputs_embeds = self.talker.get_input_embeddings(input_ids)
inputs_embeds = self.talker.embed_input_ids(input_ids)
# ------- Request-scoped additional information (no cross-request concat) -------
request_ids: list[str] | None = kwargs.get("request_ids") # ordered
@@ -456,7 +459,7 @@ class Qwen2_5OmniForConditionalGeneration(
dtype=torch.bfloat16,
).to(self._module_device(self.model)),
self.talker.thinker_to_talker_proj(
self.talker.get_input_embeddings(
self.talker.embed_input_ids(
torch.tensor([TALKER_CODEC_BOS_TOKEN_ID, TALKER_CODEC_EOS_TOKEN_ID])
.to(torch.bfloat16)
.to(self._module_device(self.model))
@@ -790,7 +793,7 @@ class Qwen2_5OmniForConditionalGeneration(
output_prompt_embeds,
output_token_ids,
):
processed_output_token_embeds = output_prompt_embeds + self.talker.get_input_embeddings(
processed_output_token_embeds = output_prompt_embeds + self.talker.embed_input_ids(
output_token_ids
) # for decode
return output_token_ids, processed_output_token_embeds
@@ -12,7 +12,6 @@ from vllm.logger import init_logger
from vllm.model_executor.layers.linear import ColumnParallelLinear
from vllm.model_executor.models.interfaces import MultiModalEmbeddings, SupportsMultiModal, SupportsPP
from vllm.model_executor.models.qwen2_5_omni_thinker import (
Qwen2_5OmniConditionalGenerationMixin,
Qwen2_5OmniThinkerDummyInputsBuilder,
Qwen2_5OmniThinkerMultiModalProcessor,
Qwen2_5OmniThinkerProcessingInfo,
@@ -23,7 +22,6 @@ from vllm.model_executor.models.utils import (
WeightsMapper,
init_vllm_registered_model,
maybe_prefix,
merge_multimodal_embeddings,
)
from vllm.multimodal import MULTIMODAL_REGISTRY
from vllm.sequence import IntermediateTensors
@@ -31,6 +29,8 @@ from vllm.v1.outputs import SamplerOutput
from vllm.v1.sample.metadata import SamplingMetadata
from vllm.v1.sample.sampler import Sampler
from vllm_omni.model_executor.models.qwen2_5_omni.qwen2_5_omni_thinker import Qwen2_5OmniConditionalGenerationMixin
@MULTIMODAL_REGISTRY.register_processor(
Qwen2_5OmniThinkerMultiModalProcessor,
@@ -58,7 +58,7 @@ class Qwen2_5OmniTalkerForConditionalGeneration(
super().__init__()
config: Qwen2_5OmniTalkerConfig = vllm_config.model_config.hf_config
quant_config = vllm_config.quant_config
self.vllm_config = vllm_config
self.prefix = prefix
self.quant_config = quant_config
@@ -103,26 +103,24 @@ class Qwen2_5OmniTalkerForConditionalGeneration(
return Sampler()
def get_input_embeddings(
def embed_input_ids(
self,
input_ids: torch.Tensor,
multimodal_embeddings: MultiModalEmbeddings | None = None,
*,
is_multimodal: torch.Tensor | None = None,
handle_oov_mm_token: bool = False,
) -> torch.Tensor:
inputs_embeds = self.language_model.get_input_embeddings(input_ids)
if multimodal_embeddings is not None and len(multimodal_embeddings) != 0:
# TODO (ywang96): support overlapping modalitiy embeddings so that
# `use_audio_in_video` will work on V1.
inputs_embeds = merge_multimodal_embeddings(
input_ids,
inputs_embeds,
multimodal_embeddings,
[
self.config.image_token_index,
self.config.video_token_index,
self.config.audio_token_index,
],
)
return inputs_embeds
# This is to satisfy the type checker for each overload
if multimodal_embeddings is None or is_multimodal is None:
return super().embed_input_ids(input_ids)
return super().embed_input_ids(
input_ids,
multimodal_embeddings=multimodal_embeddings,
is_multimodal=is_multimodal,
handle_oov_mm_token=handle_oov_mm_token,
)
def forward(
self,
@@ -139,7 +137,7 @@ class Qwen2_5OmniTalkerForConditionalGeneration(
inputs_embeds = None
elif inputs_embeds is None:
# for profile_run:
inputs_embeds = self.get_input_embeddings(input_ids)
inputs_embeds = self.embed_input_ids(input_ids)
input_ids = None
@@ -213,7 +211,7 @@ class Qwen2_5OmniTalkerForConditionalGeneration(
mm_input_by_modality["audio"] = self._parse_and_validate_audio_input(**kwargs)
return mm_input_by_modality
def get_multimodal_embeddings(self, **kwargs: object) -> MultiModalEmbeddings:
def embed_multimodal(self, **kwargs: object) -> MultiModalEmbeddings:
mm_input_by_modality = self._parse_and_validate_multimodal_inputs(**kwargs)
if not mm_input_by_modality:
return []
File diff suppressed because it is too large. Load diff
@@ -3,7 +3,10 @@ from collections.abc import Iterable
import torch
from torch import nn
from transformers import Qwen2Config
from vllm.attention import Attention, AttentionType
from vllm.attention.backends.abstract import (
AttentionType,
)
from vllm.attention.layer import Attention
from vllm.compilation.decorators import support_torch_compile
from vllm.config import CacheConfig, VllmConfig
from vllm.distributed import get_pp_group, get_tensor_model_parallel_world_size
@@ -125,12 +128,15 @@ class Qwen2Attention(nn.Module):
prefix=f"{prefix}.o_proj",
)
self.rotary_emb = get_rope(
self.head_dim,
self.rotary_pos_emb = get_rope(
head_size=self.head_dim,
rotary_dim=self.head_dim,
max_position=max_position,
base=self.rope_theta,
rope_scaling=rope_scaling,
is_neox_style=True,
rope_parameters={
"base": self.rope_theta,
**rope_scaling,
},
)
self.attn = Attention(
self.num_heads,
@@ -150,7 +156,7 @@ class Qwen2Attention(nn.Module):
) -> 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)
q, k = self.rotary_pos_emb(positions, q, k)
attn_output = self.attn(q, k, v)
output, _ = self.o_proj(attn_output)
return output
@@ -294,7 +300,7 @@ class Qwen2Model(nn.Module):
else:
self.norm = PPMissingLayer()
def get_input_embeddings(self, input_ids: torch.Tensor) -> torch.Tensor:
def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
return self.embed_tokens(input_ids)
def forward(
@@ -308,7 +314,7 @@ class Qwen2Model(nn.Module):
if inputs_embeds is not None:
hidden_states = inputs_embeds
else:
hidden_states = self.get_input_embeddings(input_ids)
hidden_states = self.embed_input_ids(input_ids)
residual = None
else:
assert intermediate_tensors is not None
@@ -420,8 +426,8 @@ class Qwen2ForCausalLM(nn.Module, SupportsLoRA, SupportsPP):
self.make_empty_intermediate_tensors = self.model.make_empty_intermediate_tensors
def get_input_embeddings(self, input_ids: torch.Tensor) -> torch.Tensor:
return self.model.get_input_embeddings(input_ids)
def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
return self.model.embed_input_ids(input_ids)
def forward(
self,
@@ -18,6 +18,12 @@ from transformers.models.qwen3_omni_moe.configuration_qwen3_omni_moe import (
from vllm.config import VllmConfig
from vllm.logger import init_logger
from vllm.model_executor.models.interfaces import SupportsMultiModal, SupportsPP
from vllm.model_executor.models.qwen3_omni_moe_thinker import (
Qwen3OmniMoeConditionalGenerationMixin,
Qwen3OmniMoeThinkerDummyInputsBuilder,
Qwen3OmniMoeThinkerMultiModalProcessor,
Qwen3OmniMoeThinkerProcessingInfo,
)
from vllm.model_executor.models.utils import init_vllm_registered_model, maybe_prefix
from vllm.multimodal import MULTIMODAL_REGISTRY
from vllm.sequence import IntermediateTensors
@@ -28,13 +34,6 @@ from vllm.v1.sample.sampler import Sampler
from vllm_omni.model_executor.models.output_templates import OmniOutput
from vllm_omni.model_executor.models.utils import add_prefix_to_loaded_weights
from .qwen3_omni_moe_thinker import (
Qwen3OmniMoeConditionalGenerationMixin,
Qwen3OmniMoeThinkerDummyInputsBuilder,
Qwen3OmniMoeThinkerMultiModalProcessor,
Qwen3OmniMoeThinkerProcessingInfo,
)
# Special token IDs for Qwen3 Omni MoE
# Reference: https://huggingface.co/Qwen/Qwen3-Omni-30B-A3B-Instruct/blob/main/tokenizer_config.json
@@ -226,20 +225,21 @@ class Qwen3OmniMoeForConditionalGeneration(
return self.model.sampler
return Sampler()
def get_input_embeddings(
def embed_input_ids(
self,
input_ids: torch.Tensor,
multimodal_embeddings=None,
is_multimodal=None,
) -> torch.Tensor:
"""Get input embeddings for the active model stage."""
if self.model_stage == "code2wav":
# Code2wav doesn't use text embeddings
return torch.zeros_like(input_ids).reshape(-1, 1).repeat(1, self.vllm_config.model_config.get_hidden_size())
return self.model.get_input_embeddings(input_ids, multimodal_embeddings)
return self.model.embed_input_ids(
input_ids=input_ids, multimodal_embeddings=multimodal_embeddings, is_multimodal=is_multimodal
)
def get_multimodal_embeddings(self, **kwargs):
def embed_multimodal(self, **kwargs):
"""Delegate to active model for multimodal processing."""
return self.model.get_multimodal_embeddings(**kwargs)
return self.model.embed_multimodal(**kwargs)
# ==================== Forward Pass ====================
def _get_talker_suppressed_tokens(self):
@@ -281,16 +281,16 @@ class Qwen3OmniMoeForConditionalGeneration(
# ========== Stage 1: Thinker ==========
if self.model_stage == "thinker":
# Normalize to batched inputs if needed
added_batch_dim = False
_added_batch_dim = False
if input_ids is not None and input_ids.ndim == 1:
input_ids = input_ids.unsqueeze(0)
added_batch_dim = True
_added_batch_dim = True
if positions is not None and positions.ndim == 1:
positions = positions.unsqueeze(0)
added_batch_dim = True
_added_batch_dim = True
if inputs_embeds is not None and inputs_embeds.ndim == 2:
inputs_embeds = inputs_embeds.unsqueeze(0)
added_batch_dim = True
_added_batch_dim = True
thinker_dev = self._module_device(self.thinker)
@@ -301,7 +301,7 @@ class Qwen3OmniMoeForConditionalGeneration(
dtype=torch.long,
device=thinker_dev,
).unsqueeze(0)
added_batch_dim = True
_added_batch_dim = True
# Move to thinker device
if input_ids is not None and input_ids.device != thinker_dev:
@@ -337,7 +337,7 @@ class Qwen3OmniMoeForConditionalGeneration(
device=self._module_device(self.thinker),
dtype=torch.long,
)
thinker_tts_embeds = self.thinker.get_input_embeddings(tts_tokens) # [1,3,thinker_hidden]
thinker_tts_embeds = self.thinker.embed_input_ids(tts_tokens) # [1,3,thinker_hidden]
if (
isinstance(thinker_tts_embeds, torch.Tensor)
and thinker_tts_embeds.ndim == 3
@@ -353,7 +353,7 @@ class Qwen3OmniMoeForConditionalGeneration(
# Return text-only output (with multimodal sidecar)
return OmniOutput(
text_hidden_states=(text_hidden_states.squeeze(0) if added_batch_dim else text_hidden_states),
text_hidden_states=(text_hidden_states.reshape(-1, text_hidden_states.shape[-1])),
multimodal_outputs=multimodal_outputs,
)
@@ -379,7 +379,7 @@ class Qwen3OmniMoeForConditionalGeneration(
# Ensure we have base embeddings when only ids are provided
if inputs_embeds is None and input_ids is not None:
inputs_embeds = self.talker.get_input_embeddings(input_ids)
inputs_embeds = self.talker.embed_input_ids(input_ids)
# ------- Request-scoped additional information (no cross-request concat) -------
request_ids: list[str] | None = kwargs.get("request_ids") # ordered
@@ -748,8 +748,6 @@ class Qwen3OmniMoeForConditionalGeneration(
- hidden_projection: Used at runtime for multimodal hidden states (audio/image/video)
from thinker's last layer, not needed for special token initialization
"""
# Get embeddings from both models
# self.thinker_embedding = self.thinker.model.get_input_embeddings()
self.talker_embedding = self._load_talker_embedding()
# Get configuration
@@ -960,9 +958,7 @@ class Qwen3OmniMoeForConditionalGeneration(
device=tts_pad_embed.device,
dtype=torch.bfloat16,
),
self.talker.get_input_embeddings(codec_special_tokens).to(
device=tts_pad_embed.device, dtype=torch.bfloat16
),
self.talker.embed_input_ids(codec_special_tokens).to(device=tts_pad_embed.device, dtype=torch.bfloat16),
),
dim=0,
)
@@ -1059,7 +1055,7 @@ class Qwen3OmniMoeForConditionalGeneration(
logitsprocs=LogitsProcessors(),
)
# Use active model for logits computation
logits = self.model.compute_logits(hidden_states, sampling_metadata) # V, d
logits = self.model.compute_logits(hidden_states) # V, d
# Talker: suppress tokens by setting their probability to ~1e-9 (finite very small),
# implemented by assigning their logits to log(1e-9).
@@ -23,6 +23,7 @@ from vllm.model_executor.models.qwen2_5_omni_thinker import (
Qwen2_5OmniThinkerDummyInputsBuilder,
)
from vllm.model_executor.models.qwen3_moe import Qwen3MoeMLP
from vllm.model_executor.models.qwen3_omni_moe_thinker import Qwen3Omni_VisionTransformer
from vllm.model_executor.models.utils import (
AutoWeightsLoader,
WeightsMapper,
@@ -31,14 +32,12 @@ from vllm.model_executor.models.utils import (
)
from vllm.multimodal import MULTIMODAL_REGISTRY
from vllm.sequence import IntermediateTensors
from vllm.v1.sample.metadata import SamplingMetadata
from vllm_omni.model_executor.models.qwen3_omni.qwen3_omni_moe_code_predictor_mtp import (
Qwen3OmniMoeTalkerCodePredictor,
)
from vllm_omni.model_executor.models.qwen3_omni.qwen3_omni_moe_thinker import (
Qwen3MoeLLMForCausalLM,
Qwen3Omni_VisionTransformer,
Qwen3OmniMoeConditionalGenerationMixin,
Qwen3OmniMoeThinkerMultiModalProcessor,
Qwen3OmniMoeThinkerProcessingInfo,
@@ -106,9 +105,11 @@ class Qwen3OmniMoeTalkerForConditionalGeneration(
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
super().__init__()
talker_config: Qwen3OmniMoeTalkerConfig = vllm_config.model_config.hf_config
talker_config.text_config.rope_parameters = talker_config.text_config.rope_scaling
talker_config.text_config.rope_parameters["rope_theta"] = talker_config.text_config.rope_theta
self.quant_config = vllm_config.quant_config
self.prefix = prefix
self.vllm_config = vllm_config
self.config = talker_config
self.vocab_size = talker_config.text_config.vocab_size
self.router_aux_loss_coef = talker_config.text_config.router_aux_loss_coef
@@ -193,7 +194,7 @@ class Qwen3OmniMoeTalkerForConditionalGeneration(
pos_codes = [layer0_code] # Start with layer 0: [batch, 1]
# Initial input: [last_talker_hidden, layer0_embed]
layer0_embed = self.get_input_embeddings(layer0_code)
layer0_embed = self.embed_input_ids(layer0_code)
prev_embed = layer0_embed # Track previous layer embedding
try:
current_input = torch.cat([last_talker_hidden, prev_embed], dim=1) # [batch, 2, hidden_size]
@@ -260,7 +261,7 @@ class Qwen3OmniMoeTalkerForConditionalGeneration(
for pos in range(seq_len):
# Layer 0 embedding
layer0_code = result_codes[:, 0, pos : pos + 1] # [batch, 1]
layer0_embed = self.get_input_embeddings(layer0_code) # [batch, 1, hidden_size]
layer0_embed = self.embed_input_ids(layer0_code) # [batch, 1, hidden_size]
# mid layers hidden states (from CodePredictor)
mid_residual_hiddens = middle_hidden_states[pos] # [batch, num_code_groups-2, hidden_size]
@@ -440,7 +441,6 @@ class Qwen3OmniMoeTalkerForConditionalGeneration(
def compute_logits(
self,
hidden_states: torch.Tensor,
sampling_metadata: SamplingMetadata = None,
) -> torch.Tensor | None:
"""Compute logits for audio codec codes (layer 0 of RVQ).
@@ -471,7 +471,7 @@ class Qwen3OmniMoeTalkerForConditionalGeneration(
mm_input_by_modality["audio"] = self._parse_and_validate_audio_input(**kwargs)
return mm_input_by_modality
def get_multimodal_embeddings(self, **kwargs: object) -> MultiModalEmbeddings | None:
def embed_multimodal(self, **kwargs: object) -> MultiModalEmbeddings | None:
mm_input_by_modality = self._parse_and_validate_multimodal_inputs(**kwargs)
if not mm_input_by_modality:
return []
@@ -504,9 +504,14 @@ class Qwen3OmniMoeTalkerForConditionalGeneration(
multimodal_embeddings += tuple(audio_embeddings)
return multimodal_embeddings
def get_input_embeddings(self, input_ids: torch.Tensor, multimodal_embeddings: MultiModalEmbeddings | None = None):
def embed_input_ids(
self,
input_ids: torch.Tensor,
multimodal_embeddings: MultiModalEmbeddings | None = None,
is_multimodal: bool = False,
):
"""Get the input embedding layer (for codec tokens)."""
return self.language_model.get_input_embeddings(input_ids, multimodal_embeddings)
return self.language_model.embed_input_ids(input_ids)
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
"""Load weights for the talker model.
@@ -695,10 +700,9 @@ class Qwen3OmniMoeModel(Qwen3MoeLLMForCausalLM):
return forward_with_shared_expert
def get_input_embeddings(
def embed_input_ids(
self,
input_ids: torch.Tensor,
multimodal_embeddings: MultiModalEmbeddings | None = None,
generation_steps=None,
**kwargs: object,
) -> torch.Tensor:
return self.model.codec_embedding(input_ids)
@@ -22,15 +22,13 @@
# limitations under the License.
"""Inference-only Qwen3-Omni-Moe model (thinker part)."""
import math
from collections.abc import Callable, Iterable, Mapping, Sequence
from collections.abc import Iterable, Mapping, Sequence
from functools import partial
from typing import Any
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
from packaging.version import Version
from transformers import PretrainedConfig
from transformers import __version__ as TRANSFORMERS_VERSION
@@ -46,52 +44,44 @@ from transformers.models.qwen3_omni_moe.processing_qwen3_omni_moe import (
Qwen3OmniMoeProcessor,
)
from transformers.models.whisper import WhisperFeatureExtractor
from vllm.attention.utils.fa_utils import get_flash_attn_version
from vllm.compilation.decorators import support_torch_compile
from vllm.config import VllmConfig
from vllm.distributed import get_pp_group
from vllm.logger import init_logger
from vllm.model_executor.layers.activation import _ACTIVATION_REGISTRY
from vllm.model_executor.layers.linear import (
ColumnParallelLinear,
ReplicatedLinear,
RowParallelLinear,
)
from vllm.model_executor.layers.logits_processor import LogitsProcessor
from vllm.model_executor.layers.quantization import QuantizationConfig
from vllm.model_executor.layers.vocab_parallel_embedding import ParallelLMHead
from vllm.model_executor.model_loader.weight_utils import default_weight_loader
from vllm.model_executor.models.interfaces import (
MultiModalEmbeddings,
SupportsMRoPE,
SupportsMultiModal,
SupportsPP,
)
from vllm.model_executor.models.qwen2_5_omni_thinker import (
Qwen2_5OmniConditionalGenerationMixin,
Qwen2_5OmniAudioFeatureInputs,
Qwen2_5OmniThinkerDummyInputsBuilder,
Qwen2_5OmniThinkerMultiModalProcessor,
)
from vllm.model_executor.models.qwen2_5_vl import (
Qwen2_5_VisionAttention,
Qwen2_5_VisionRotaryEmbedding,
Qwen2_5_VLProcessingInfo,
)
from vllm.model_executor.models.qwen2_audio import Qwen2AudioInputs, Qwen2AudioProcessingInfo
from vllm.model_executor.models.qwen2_audio import Qwen2AudioProcessingInfo
from vllm.model_executor.models.qwen3_moe import Qwen3MoeForCausalLM
from vllm.model_executor.models.qwen3_moe import Qwen3MoeModel as _Qwen3MoeLLMModel
from vllm.model_executor.models.qwen3_omni_moe_thinker import (
Qwen3Omni_VisionTransformer,
_get_feat_extract_output_lengths,
)
from vllm.model_executor.models.utils import (
AutoWeightsLoader,
WeightsMapper,
_merge_multimodal_embeddings,
flatten_bn,
maybe_prefix,
)
from vllm.model_executor.models.vision import (
get_vit_attn_backend,
get_llm_pos_ids_for_vision,
)
from vllm.multimodal import MULTIMODAL_REGISTRY
from vllm.multimodal.inputs import (
MultiModalKwargsItems,
)
from vllm.multimodal.inputs import MultiModalFeatureSpec, MultiModalKwargsItems
from vllm.multimodal.parse import AudioProcessorItems, MultiModalDataItems
from vllm.multimodal.processing import (
MultiModalPromptUpdates,
@@ -100,504 +90,20 @@ from vllm.multimodal.processing import (
PromptUpdate,
PromptUpdateDetails,
)
from vllm.platforms import _Backend
from vllm.sequence import IntermediateTensors
from vllm.v1.sample.metadata import SamplingMetadata
from .qwen3_moe import Qwen3MoeModel
from vllm_omni.model_executor.models.qwen2_5_omni.qwen2_5_omni_thinker import (
Qwen2_5OmniConditionalGenerationMixin,
)
try:
import flash_attn
except (ImportError, ModuleNotFoundError):
flash_attn = None
from torch import Tensor
logger = init_logger(__name__)
# Local helper functions (not available in vLLM)
def conv3d_to_linear_weight(weight: torch.Tensor) -> torch.Tensor:
"""Convert 3D convolutional weight to linear weight for patch embedding."""
# Weight shape: (out_channels, in_channels, t, h, w)
# Need to flatten spatial dimensions: (out_channels, in_channels * t * h * w)
flattened_weight = weight.flatten(1) # Flatten all dims except out_channels
return flattened_weight
def get_llm_pos_ids_for_vision(
start_idx: int,
vision_idx: int,
spatial_merge_size: int,
t_index: list[torch.Tensor],
grid_hs: list[torch.Tensor],
grid_ws: list[torch.Tensor],
) -> torch.Tensor:
"""Generate 3D position IDs for vision tokens (temporal, height, width)."""
llm_grid_h = grid_hs[vision_idx] // spatial_merge_size
llm_grid_w = grid_ws[vision_idx] // spatial_merge_size
h_index = torch.arange(llm_grid_h).view(1, -1, 1).expand(len(t_index), -1, llm_grid_w).flatten().float()
w_index = torch.arange(llm_grid_w).view(1, 1, -1).expand(len(t_index), llm_grid_h, -1).flatten().float()
t_index_tensor = torch.Tensor(t_index).view(-1, 1).expand(-1, llm_grid_h * llm_grid_w).flatten().float()
llm_pos_ids = torch.stack([t_index_tensor, h_index, w_index])
llm_pos_ids = llm_pos_ids + start_idx
return llm_pos_ids
# Type aliases for audio inputs
Qwen2_5OmniAudioFeatureInputs = Qwen2AudioInputs
def _get_feat_extract_output_lengths(input_lengths: torch.Tensor):
input_lengths_leave = input_lengths % 100
feat_lengths = (input_lengths_leave - 1) // 2 + 1
output_lengths = ((feat_lengths - 1) // 2 + 1 - 1) // 2 + 1 + (input_lengths // 100) * 13
return feat_lengths, output_lengths
class Qwen3_VisionPatchEmbed(nn.Module):
def __init__(
self,
patch_size: int = 14,
temporal_patch_size: int = 2,
in_channels: int = 3,
hidden_size: int = 1152,
) -> None:
super().__init__()
self.patch_size = patch_size
self.temporal_patch_size = temporal_patch_size
self.hidden_size = hidden_size
kernel_size = (temporal_patch_size, patch_size, patch_size)
self.proj = ReplicatedLinear(
in_channels * math.prod(kernel_size),
hidden_size,
bias=True,
return_bias=False,
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
L, C = x.shape
x = self.proj(x)
return x
class Qwen3_VisionMLP(nn.Module):
def __init__(
self,
in_features: int,
hidden_features: int,
bias: bool = False,
act_fn: Callable[[torch.Tensor], torch.Tensor] = F.silu,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
):
super().__init__()
self.linear_fc1 = ColumnParallelLinear(
in_features,
hidden_features,
bias=bias,
quant_config=quant_config,
return_bias=False,
prefix=f"{prefix}.linear_fc1",
)
self.linear_fc2 = RowParallelLinear(
hidden_features,
in_features,
bias=bias,
quant_config=quant_config,
return_bias=False,
prefix=f"{prefix}.linear_fc2",
)
self.act_fn = act_fn
def forward(self, x: torch.Tensor):
mlp_output = self.linear_fc2(self.act_fn(self.linear_fc1(x)))
return mlp_output
class Qwen3_VisionBlock(nn.Module):
def __init__(
self,
dim: int,
num_heads: int,
mlp_hidden_dim: int,
act_fn: Callable[[torch.Tensor], torch.Tensor] = F.silu,
norm_layer: Callable[[int], nn.Module] | None = None,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
) -> None:
super().__init__()
if norm_layer is None:
norm_layer = partial(nn.LayerNorm, eps=1e-6)
self.norm1 = norm_layer(dim)
self.norm2 = norm_layer(dim)
self.attn = Qwen2_5_VisionAttention(
embed_dim=dim,
num_heads=num_heads,
projection_size=dim,
quant_config=quant_config,
prefix=f"{prefix}.attn",
)
self.mlp = Qwen3_VisionMLP(
dim,
mlp_hidden_dim,
act_fn=act_fn,
bias=True,
quant_config=quant_config,
prefix=f"{prefix}.mlp",
)
def forward(
self,
x: torch.Tensor,
cu_seqlens: torch.Tensor,
rotary_pos_emb: torch.Tensor,
max_seqlen: int | None = None, # Only used for Flash Attention
seqlens: list[int] | None = None, # Only used for xFormers
) -> torch.Tensor:
x = x + self.attn(
self.norm1(x),
cu_seqlens=cu_seqlens,
rotary_pos_emb=rotary_pos_emb,
max_seqlen=max_seqlen,
seqlens=seqlens,
)
x = x + self.mlp(self.norm2(x))
return x
class Qwen3_VisionPatchMerger(nn.Module):
def __init__(
self,
d_model: int,
context_dim: int,
norm_layer: Callable[[int], nn.Module] | None = None,
spatial_merge_size: int = 2,
use_postshuffle_norm: bool = False,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
) -> None:
super().__init__()
self.hidden_size = context_dim * (spatial_merge_size**2)
self.use_postshuffle_norm = use_postshuffle_norm
if self.use_postshuffle_norm:
context_dim = self.hidden_size
if norm_layer is None:
norm_layer = partial(nn.LayerNorm, eps=1e-6)
self.use_postshuffle_norm = use_postshuffle_norm
self.ln_q = norm_layer(self.hidden_size if use_postshuffle_norm else context_dim)
self.mlp = nn.ModuleList(
[
ColumnParallelLinear(
self.hidden_size,
self.hidden_size,
bias=True,
quant_config=quant_config,
prefix=f"{prefix}.mlp.0",
),
nn.GELU(),
RowParallelLinear(
self.hidden_size,
d_model,
bias=True,
quant_config=quant_config,
prefix=f"{prefix}.mlp.2",
),
]
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
if self.use_postshuffle_norm:
x = self.ln_q(x.view(-1, self.hidden_size))
else:
x = self.ln_q(x).view(-1, self.hidden_size)
mlp_fc1, mlp_act, mlp_fc2 = self.mlp
x_parallel, _ = mlp_fc1(x)
x_parallel = mlp_act(x_parallel)
out, _ = mlp_fc2(x_parallel)
return out
class Qwen3Omni_VisionTransformer(nn.Module):
def __init__(
self,
vision_config,
norm_eps: float = 1e-6,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
attn_backend_override: _Backend | None = None,
) -> None:
super().__init__()
self.hidden_size = vision_config.hidden_size
self.num_heads = vision_config.num_heads
self.image_size = vision_config.image_size
self.patch_size = vision_config.patch_size
self.spatial_merge_size = vision_config.spatial_merge_size
self.spatial_merge_unit = self.spatial_merge_size**2
self.temporal_patch_size = vision_config.temporal_patch_size
self.num_grid_per_side = self.image_size // self.patch_size
self.apply_vit_abs_pos_embed = vision_config.apply_vit_abs_pos_embed
self.deepstack_visual_indexes = vision_config.deepstack_visual_indexes
self.patch_embed = Qwen3_VisionPatchEmbed(
patch_size=self.patch_size,
temporal_patch_size=self.temporal_patch_size,
in_channels=vision_config.in_channels,
hidden_size=self.hidden_size,
)
# vit pos embedding, TODO: spatial_patch_size vs patch_size
if self.apply_vit_abs_pos_embed:
self.pos_embed = nn.Embedding(self.num_grid_per_side**2, self.hidden_size)
else:
self.pos_embed = nn.Parameter(torch.empty([1, self.num_grid_per_side**2, self.hidden_size]))
norm_layer = partial(nn.LayerNorm, eps=norm_eps)
head_dim = self.hidden_size // self.num_heads
self.rotary_pos_emb = Qwen2_5_VisionRotaryEmbedding(head_dim // 2)
self.blocks = nn.ModuleList(
[
Qwen3_VisionBlock(
dim=self.hidden_size,
num_heads=self.num_heads,
mlp_hidden_dim=vision_config.intermediate_size,
act_fn=_ACTIVATION_REGISTRY[vision_config.hidden_act],
norm_layer=norm_layer,
quant_config=quant_config,
prefix=f"{prefix}.blocks.{layer_idx}",
)
for layer_idx in range(vision_config.depth)
]
)
self.merger = Qwen3_VisionPatchMerger(
d_model=vision_config.out_hidden_size,
context_dim=self.hidden_size,
norm_layer=norm_layer,
spatial_merge_size=self.spatial_merge_size,
quant_config=quant_config,
prefix=f"{prefix}.merger",
)
if self.deepstack_visual_indexes is not None:
self.merger_list = nn.ModuleList(
[
Qwen3_VisionPatchMerger(
d_model=vision_config.out_hidden_size,
context_dim=self.hidden_size,
spatial_merge_size=self.spatial_merge_size,
use_postshuffle_norm=True,
norm_layer=norm_layer,
quant_config=quant_config,
prefix=f"{prefix}.merger_list.{layer_idx}",
)
for layer_idx in range(len(self.deepstack_visual_indexes))
]
)
self.attn_backend = get_vit_attn_backend(
head_size=head_dim,
dtype=torch.get_default_dtype(),
# attn_backend_override=attn_backend_override,
)
if self.attn_backend != _Backend.FLASH_ATTN and get_flash_attn_version() is not None:
self.attn_backend = _Backend.FLASH_ATTN
@property
def dtype(self) -> torch.dtype:
return self.patch_embed.proj.weight.dtype
@property
def device(self) -> torch.device:
return self.patch_embed.proj.weight.device
def rot_pos_emb(self, grid_thw):
pos_ids = []
for t, h, w in grid_thw:
hpos_ids = torch.arange(h).unsqueeze(1).expand(-1, w)
hpos_ids = hpos_ids.reshape(
h // self.spatial_merge_size,
self.spatial_merge_size,
w // self.spatial_merge_size,
self.spatial_merge_size,
)
hpos_ids = hpos_ids.permute(0, 2, 1, 3)
hpos_ids = hpos_ids.flatten()
wpos_ids = torch.arange(w).unsqueeze(0).expand(h, -1)
wpos_ids = wpos_ids.reshape(
h // self.spatial_merge_size,
self.spatial_merge_size,
w // self.spatial_merge_size,
self.spatial_merge_size,
)
wpos_ids = wpos_ids.permute(0, 2, 1, 3)
wpos_ids = wpos_ids.flatten()
pos_ids.append(torch.stack([hpos_ids, wpos_ids], dim=-1).repeat(t, 1))
pos_ids = torch.cat(pos_ids, dim=0)
max_grid_size = grid_thw[:, 1:].max()
rotary_pos_emb_full = self.rotary_pos_emb(max_grid_size)
rotary_pos_emb = rotary_pos_emb_full[pos_ids].flatten(1)
return rotary_pos_emb
def fast_pos_embed_interpolate(self, grid_thw: list[list[int]]) -> torch.Tensor:
num_grid_per_side = self.num_grid_per_side
m_size = self.spatial_merge_size
hidden_dim = self.pos_embed.embedding_dim
outputs = []
for t, h, w in grid_thw:
h_idxs = torch.linspace(0, num_grid_per_side - 1, h, dtype=torch.float32, device=self.device)
w_idxs = torch.linspace(0, num_grid_per_side - 1, w, dtype=torch.float32, device=self.device)
h_floor = h_idxs.to(torch.long)
w_floor = w_idxs.to(torch.long)
h_ceil = torch.clamp(h_floor + 1, max=num_grid_per_side - 1)
w_ceil = torch.clamp(w_floor + 1, max=num_grid_per_side - 1)
dh = h_idxs - h_floor
dw = w_idxs - w_floor
# Create meshgrid view for all h, w vars
dh_grid, dw_grid = torch.meshgrid(dh, dw, indexing="ij")
h_floor_grid, w_floor_grid = torch.meshgrid(h_floor, w_floor, indexing="ij")
h_ceil_grid, w_ceil_grid = torch.meshgrid(h_ceil, w_ceil, indexing="ij")
h_floor_grid_idx = h_floor_grid * num_grid_per_side
h_ceil_grid_idx = h_ceil_grid * num_grid_per_side
# original computation of weights
# w00 = (1 - dh_grid) * (1 - dw_grid)
# w01 = (1 - dh_grid) * dw_grid
# w10 = dh_grid * (1 - dw_grid)
# w11 = dh_grid * dw_grid
# we reuse w11 here to avoid duplicate
# dh_grid * dw_grid computation
w11 = dh_grid * dw_grid
w10 = dh_grid - w11
w01 = dw_grid - w11
w00 = 1 - dh_grid - dw_grid + w11
idx00 = h_floor_grid_idx + w_floor_grid
idx01 = h_floor_grid_idx + w_ceil_grid
idx10 = h_ceil_grid_idx + w_floor_grid
idx11 = h_ceil_grid_idx + w_ceil_grid
indices = torch.stack([idx00, idx01, idx10, idx11], dim=0).reshape(4, -1)
weights = torch.stack([w00, w01, w10, w11], dim=0).reshape(4, -1, 1)
weights = weights.to(dtype=self.dtype, device=self.device)
embeds = self.pos_embed(indices)
weighted_embeds = embeds * weights
p0, p1, p2, p3 = weighted_embeds.unbind(dim=0)
combined = p0 + p1 + p2 + p3
combined = combined.view(h * w, hidden_dim)
repeated = combined.unsqueeze(0).expand(t, -1, -1).contiguous()
repeated = repeated.view(t, h // m_size, m_size, w // m_size, m_size, hidden_dim)
repeated = repeated.permute(0, 1, 3, 2, 4, 5).reshape(-1, hidden_dim)
outputs.append(repeated)
return torch.cat(outputs, dim=0)
def compute_attn_mask_seqlen(
self,
cu_seqlens: torch.Tensor,
) -> tuple[int | None, list[int] | None]:
max_seqlen, seqlens = None, None
if self.attn_backend == _Backend.FLASH_ATTN:
max_seqlen = (cu_seqlens[1:] - cu_seqlens[:-1]).max().item()
elif self.attn_backend == _Backend.XFORMERS:
seqlens = (cu_seqlens[1:] - cu_seqlens[:-1]).tolist()
return max_seqlen, seqlens
def forward(
self,
x: torch.Tensor,
grid_thw: list[list[int]],
) -> torch.Tensor:
hidden_states = x.to(device=self.device, dtype=self.dtype)
hidden_states = self.patch_embed(hidden_states)
if self.apply_vit_abs_pos_embed:
pos_embeds = self.fast_pos_embed_interpolate(grid_thw)
hidden_states = hidden_states + pos_embeds
rotary_pos_emb = self.rot_pos_emb(grid_thw)
cu_seqlens = torch.repeat_interleave(grid_thw[:, 1] * grid_thw[:, 2], grid_thw[:, 0]).cumsum(
dim=0,
dtype=grid_thw.dtype if torch.jit.is_tracing() else torch.int32,
)
cu_seqlens = F.pad(cu_seqlens, (1, 0), value=0)
hidden_states = hidden_states.unsqueeze(1)
rotary_pos_emb = rotary_pos_emb.to(hidden_states.device)
max_seqlen, seqlens = self.compute_attn_mask_seqlen(cu_seqlens)
hidden_states_list = []
deepstack_visual_indexes = self.deepstack_visual_indexes
for layer_num, blk in enumerate(self.blocks):
hidden_states = blk(
hidden_states,
cu_seqlens=cu_seqlens,
rotary_pos_emb=rotary_pos_emb,
max_seqlen=max_seqlen,
seqlens=seqlens,
)
if deepstack_visual_indexes is not None and layer_num in deepstack_visual_indexes:
hidden_states_list.append(hidden_states)
hidden_states = self.merger(hidden_states)
# processing deepstack
if deepstack_visual_indexes is not None:
processed_hidden_states_list = [hidden_states]
for idx, x in enumerate(hidden_states_list):
x = self.merger_list[idx](x)
processed_hidden_states_list.append(x)
# we cat the original visual features and deepstack features
# along the feature dim
hidden_states = torch.cat(
processed_hidden_states_list, dim=1
) # [seq_len, hidden_size * (1 + depth_of_deepstack)]
return hidden_states
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
stacked_params_mapping = [
# (param_name, shard_name, shard_id)
("attn.qkv.", "attn.q.", "q"),
("attn.qkv.", "attn.k.", "k"),
("attn.qkv.", "attn.v.", "v"),
]
params_dict = dict(self.named_parameters(remove_duplicate=False))
loaded_params: set[str] = set()
for name, loaded_weight in weights:
if name.endswith("patch_embed.proj.weight"):
loaded_weight = conv3d_to_linear_weight(loaded_weight)
for param_name, weight_name, shard_id in stacked_params_mapping:
if weight_name not in name:
continue
name = name.replace(weight_name, param_name)
param = params_dict[name]
weight_loader = param.weight_loader
weight_loader(param, loaded_weight, shard_id)
break
else:
param = params_dict[name]
weight_loader = getattr(param, "weight_loader", default_weight_loader)
weight_loader(param, loaded_weight)
loaded_params.add(name)
return loaded_params
@support_torch_compile(
dynamic_arg_dims={
"input_ids": 0,
@@ -607,44 +113,7 @@ class Qwen3Omni_VisionTransformer(nn.Module):
"deepstack_input_embeds": 0,
}
)
class Qwen3MoeLLMModel(Qwen3MoeModel):
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
super().__init__(vllm_config=vllm_config, prefix=prefix)
# from transformers.models.qwen3_omni_moe.modeling_qwen3_omni_moe import Qwen3OmniMoeTextRMSNorm
# config = vllm_config.model_config.hf_config
# self.norm = Qwen3OmniMoeTextRMSNorm(config.hidden_size, eps=config.rms_norm_eps)
self.deepstack_multiscale_layer_start = 1
def _deepstack_process(self, hidden_states: torch.Tensor, visual_embeds: torch.Tensor) -> torch.Tensor:
"""
Process deepstack visual embeddings by selectively adding them only to visual positions.
This matches the transformers implementation which uses visual_pos_masks.
Args:
hidden_states: Hidden states tensor [num_tokens, hidden_size]
visual_embeds: Visual embeddings tensor [num_tokens, hidden_size] (zero-padded for non-visual positions)
Returns:
Modified hidden_states with visual embeddings added only to visual positions
"""
# Derive visual position mask from visual_embeds (non-zero positions are visual)
# Use a small threshold to handle floating point precision issues
visual_pos_masks = torch.any(torch.abs(visual_embeds) > 1e-8, dim=-1)
if visual_pos_masks.any():
visual_pos_masks = visual_pos_masks.to(hidden_states.device)
visual_embeds = visual_embeds.to(hidden_states.device, hidden_states.dtype)
# Match transformers implementation EXACTLY (lines 2966-2969):
# In transformers: local_this = hidden_states[visual_pos_masks, :].clone() + visual_embeds
# Note: visual_embeds in transformers is NOT indexed, meaning it's already shaped
# [num_visual_tokens, hidden_size]. In vllm-omni, visual_embeds is [num_tokens, hidden_size]
# zero-padded, so we extract only visual positions to match transformers behavior.
visual_embeds_only = visual_embeds[visual_pos_masks, :] # Extract only visual positions
local_this = hidden_states[visual_pos_masks, :].clone() + visual_embeds_only
hidden_states[visual_pos_masks, :] = local_this
return hidden_states
class Qwen3MoeLLMModel(_Qwen3MoeLLMModel):
def forward(
self,
input_ids: torch.Tensor,
@@ -660,7 +129,7 @@ class Qwen3MoeLLMModel(Qwen3MoeModel):
if inputs_embeds is not None:
hidden_states = inputs_embeds
else:
hidden_states = self.get_input_embeddings(input_ids)
hidden_states = self.embed_input_ids(input_ids)
residual = None
else:
assert intermediate_tensors is not None
@@ -683,11 +152,7 @@ class Qwen3MoeLLMModel(Qwen3MoeModel):
)
if deepstack_input_embeds is not None and layer_idx in range(0, len(deepstack_input_embeds)):
# Use _deepstack_process to match transformers implementation
# This selectively adds visual embeddings only to visual positions
hidden_states = self._deepstack_process(
hidden_states, deepstack_input_embeds[f"deepstack_input_embeds_{layer_idx}"]
)
hidden_states = hidden_states + deepstack_input_embeds[f"deepstack_input_embeds_{layer_idx}"]
if not get_pp_group().is_last_rank:
return IntermediateTensors({"hidden_states": hidden_states, "residual": residual})
@@ -856,7 +321,7 @@ class Qwen3OmniMoeThinkerMultiModalProcessor(
# Derive audio placeholders from video placeholders
mm_placeholders = self._derive_audio_from_video_placeholders(mm_placeholders, mm_prompt_updates)
else:
prompt_ids, prompt, mm_placeholders = self._apply_prompt_updates(
prompt_ids, mm_placeholders = self._apply_prompt_updates(
prompt_ids,
mm_prompt_updates,
)
@@ -866,7 +331,7 @@ class Qwen3OmniMoeThinkerMultiModalProcessor(
mm_item_counts,
)
return prompt_ids, prompt, mm_placeholders
return prompt_ids, mm_placeholders
def get_updates_use_audio_in_video(
self,
@@ -962,7 +427,6 @@ class Qwen3OmniMoeThinkerMultiModalProcessor(
def get_replacement_qwen2_vision(item_idx: int, modality: str):
grid_thw = out_mm_data[f"{modality}_grid_thw"][item_idx]
# grid_thw = out_mm_kwargs[f"{modality}_grid_thw"][item_idx]
assert isinstance(grid_thw, torch.Tensor)
merge_length = image_processor.merge_size**2
@@ -974,8 +438,6 @@ class Qwen3OmniMoeThinkerMultiModalProcessor(
def get_replacement_qwen2_use_audio_in_video(item_idx: int):
nonlocal audio_in_video_item_idx
# audio_num_features = audio_output_lengths[audio_item_idx + item_idx]
# video_grid_thw = out_mm_kwargs["video_grid_thw"][item_idx]
audio_num_features = audio_output_lengths[audio_in_video_item_idx + item_idx]
video_grid_thw = out_mm_data["video_grid_thw"][item_idx]
@@ -1022,7 +484,7 @@ class Qwen3OmniMoeThinkerMultiModalProcessor(
def _derive_audio_from_video_placeholders(
self,
placeholders: Mapping[str, list[PlaceholderFeaturesInfo]],
mm_prompt_updates: Mapping[str, list[PromptUpdateDetails]],
mm_prompt_updates: MultiModalPromptUpdates,
) -> Mapping[str, list[PlaceholderFeaturesInfo]]:
"""
Helper to derive audio placeholders from video placeholders when
@@ -1116,8 +578,6 @@ class Qwen3OmniMoeConditionalGenerationMixin(Qwen2_5OmniConditionalGenerationMix
input_features = audio_input["input_features"]
audio_feature_lengths = audio_input["audio_feature_lengths"]
audio_feature_lengths = flatten_bn(audio_feature_lengths, concat=True)
audio_feat_lengths, audio_output_lengths = _get_feat_extract_output_lengths(audio_feature_lengths)
audio_outputs = self.audio_tower(
@@ -1138,6 +598,7 @@ class Qwen3OmniMoeThinkerForConditionalGeneration(
nn.Module,
SupportsMultiModal,
SupportsPP,
SupportsMRoPE,
Qwen3OmniMoeConditionalGenerationMixin,
):
merge_by_field_config = True
@@ -1163,6 +624,7 @@ class Qwen3OmniMoeThinkerForConditionalGeneration(
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
super().__init__()
self.vllm_config = vllm_config # needed for torch compile forward context
thinker_config: Qwen3OmniMoeThinkerConfig = vllm_config.model_config.hf_config
quant_config = vllm_config.quant_config
multimodal_config = vllm_config.model_config.multimodal_config
@@ -1184,24 +646,18 @@ class Qwen3OmniMoeThinkerForConditionalGeneration(
self.audio_tower = Qwen3OmniMoeAudioEncoder(thinker_config.audio_config)
# attn_backend_override = (
# multimodal_config.mm_encoder_attn_backend
# if multimodal_config is not None
# else None
# )
attn_backend_override = multimodal_config.mm_encoder_attn_backend if multimodal_config is not None else None
self.visual = Qwen3Omni_VisionTransformer(
vision_config=thinker_config.vision_config,
norm_eps=getattr(thinker_config.text_config, "rms_norm_eps", 1e-6),
quant_config=quant_config,
prefix=maybe_prefix(prefix, "visual"),
# attn_backend_override=attn_backend_override,
attn_backend_override=attn_backend_override,
)
self.quant_config = quant_config
lm_vllm_config = vllm_config.with_hf_config(thinker_config.text_config, architectures=["Qwen3MoeForCausalLM"])
self.language_model = Qwen3MoeLLMForCausalLM(
vllm_config=lm_vllm_config,
vllm_config=vllm_config.with_hf_config(thinker_config.text_config, architectures=["Qwen3MoeForCausalLM"]),
prefix=maybe_prefix(prefix, "language_model"),
)
@@ -1274,7 +730,7 @@ class Qwen3OmniMoeThinkerForConditionalGeneration(
def get_language_model(self) -> torch.nn.Module:
return self.language_model
def get_multimodal_embeddings(self, **kwargs: object) -> MultiModalEmbeddings | None:
def embed_multimodal(self, **kwargs: object) -> MultiModalEmbeddings | None:
mm_input_by_modality = self._parse_and_validate_multimodal_inputs(**kwargs)
if not mm_input_by_modality:
return []
@@ -1298,27 +754,7 @@ class Qwen3OmniMoeThinkerForConditionalGeneration(
multimodal_embeddings += tuple(audio_embeddings)
return multimodal_embeddings
def _get_text_embeddings(
self,
input_ids: Tensor,
embed_input_ids: Callable[[Tensor], Tensor],
*,
is_multimodal: Tensor | None,
handle_oov_mm_token: bool,
) -> Tensor:
if handle_oov_mm_token and is_multimodal is not None:
is_text = ~is_multimodal
text_embeds = embed_input_ids(input_ids[is_text])
return torch.empty(
(input_ids.shape[0], text_embeds.shape[1]),
dtype=text_embeds.dtype,
device=text_embeds.device,
).masked_scatter_(is_text.unsqueeze_(-1), text_embeds)
return embed_input_ids(input_ids)
def get_input_embeddings(
def embed_input_ids(
self,
input_ids: torch.Tensor,
multimodal_embeddings: MultiModalEmbeddings | None = None,
@@ -1326,18 +762,9 @@ class Qwen3OmniMoeThinkerForConditionalGeneration(
is_multimodal: torch.Tensor | None = None,
handle_oov_mm_token: bool = False,
) -> torch.Tensor:
# Compute is_multimodal if not provided and we have multimodal embeddings
if is_multimodal is None and multimodal_embeddings is not None and len(multimodal_embeddings) > 0:
# Create a boolean tensor marking which positions are multimodal tokens
is_multimodal = (
(input_ids == self.config.image_token_id)
| (input_ids == self.config.video_token_id)
| (input_ids == self.config.audio_token_id)
)
inputs_embeds = self._get_text_embeddings(
inputs_embeds = self._embed_text_input_ids(
input_ids,
self.language_model.get_input_embeddings,
self.language_model.embed_input_ids,
is_multimodal=is_multimodal,
handle_oov_mm_token=handle_oov_mm_token,
)
@@ -1406,7 +833,6 @@ class Qwen3OmniMoeThinkerForConditionalGeneration(
positions: torch.Tensor,
intermediate_tensors: IntermediateTensors | None = None,
inputs_embeds: torch.Tensor | None = None,
*,
capture_layer_indices: Sequence[int] | None = None,
return_hidden_states: bool = False,
**kwargs: object,
@@ -1415,7 +841,7 @@ class Qwen3OmniMoeThinkerForConditionalGeneration(
inputs_embeds = None
if self.use_deepstack and inputs_embeds is not None and get_pp_group().is_first_rank:
deepstack_input_embeds = self._get_deepstack_input_embeds(input_ids.size(1))
deepstack_input_embeds = self._get_deepstack_input_embeds(inputs_embeds.size(0))
else:
deepstack_input_embeds = None
@@ -1431,14 +857,13 @@ class Qwen3OmniMoeThinkerForConditionalGeneration(
)
if inputs_embeds is not None and get_pp_group().is_first_rank:
self._clear_deepstack_input_embeds(input_ids.size(1))
self._clear_deepstack_input_embeds(inputs_embeds.size(0))
return hidden_states.unsqueeze(0), captured_hidden_states
return hidden_states, captured_hidden_states
def compute_logits(
self,
hidden_states: torch.Tensor,
sampling_metadata: SamplingMetadata = None,
) -> torch.Tensor | None:
return self.language_model.compute_logits(hidden_states)
@@ -1449,55 +874,48 @@ class Qwen3OmniMoeThinkerForConditionalGeneration(
)
loaded_weights = loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
# Log load summary
try:
total_bytes = 0
for name, param in self.named_parameters():
if param is not None and param.data is not None:
total_bytes += param.data.numel() * param.data.element_size()
device = next(self.parameters()).device
logger.info(
"[Model Loaded] name=%s, success=%s, size=%.2f MB, device=%s",
self.__class__.__name__,
True,
total_bytes / (1024**2),
str(device),
)
except Exception:
pass
return loaded_weights
def get_mrope_input_positions(
self,
input_tokens: list[int],
hf_config: PretrainedConfig,
image_grid_thw: list[list[int]] | torch.Tensor | None,
video_grid_thw: list[list[int]] | torch.Tensor | None,
second_per_grid_ts: list[float] | None = None,
context_len: int = 0,
seq_len: int | None = None,
audio_feature_lengths: torch.Tensor | None = None,
use_audio_in_video: bool = False,
mm_features: list[MultiModalFeatureSpec],
) -> tuple[torch.Tensor, int]:
config = hf_config.thinker_config
if isinstance(image_grid_thw, list):
image_grid_thw = torch.tensor(image_grid_thw)
if isinstance(video_grid_thw, list):
video_grid_thw = torch.tensor(video_grid_thw)
kwargs = MultiModalFeatureSpec.gather_kwargs(
mm_features,
{
"image_grid_thw",
"video_grid_thw",
"second_per_grid_ts",
"audio_feature_lengths",
"use_audio_in_video",
},
)
image_grid_thw = kwargs.get("image_grid_thw", [])
video_grid_thw = kwargs.get("video_grid_thw", [])
second_per_grid_ts = kwargs.get("second_per_grid_ts", [])
audio_feature_lengths = kwargs.get("audio_feature_lengths", [])
use_audio_in_video = any(kwargs.get("use_audio_in_video", []))
image_grid_thw = (torch.stack if image_grid_thw else torch.tensor)(image_grid_thw)
video_grid_thw = (torch.stack if video_grid_thw else torch.tensor)(video_grid_thw)
input_ids = torch.tensor(input_tokens)
if input_ids is None or input_ids.ndim != 1:
raise ValueError("_omni3_get_input_positions_tensor expects 1D input_ids")
seq_len = input_ids.shape[0]
if audio_feature_lengths is not None and not isinstance(audio_feature_lengths, torch.Tensor):
audio_feature_lengths = torch.as_tensor(audio_feature_lengths, dtype=torch.long)
if isinstance(audio_feature_lengths, list):
audio_feature_lengths = torch.tensor(audio_feature_lengths, dtype=torch.long)
if not len(second_per_grid_ts) and len(video_grid_thw):
second_per_grid_ts = 2.0
second_per_grids = torch.ones(len(video_grid_thw), dtype=torch.float32) * second_per_grid_ts
else:
second_per_grids = torch.tensor(second_per_grid_ts, dtype=torch.float32)
config = self.config
spatial_merge_size = config.vision_config.spatial_merge_size
image_token_id = config.image_token_id
video_token_id = config.video_token_id
+2 -2
View File
@@ -10,12 +10,12 @@ _OMNI_MODELS = {
"qwen2_5_omni",
"qwen2_5_omni_thinker",
"Qwen2_5OmniThinkerForConditionalGeneration",
), # noqa: E501
),
"Qwen2_5OmniTalkerModel": (
"qwen2_5_omni",
"qwen2_5_omni_talker",
"Qwen2_5OmniTalkerForConditionalGeneration",
), # noqa: E501
),
"Qwen2_5OmniToken2WavModel": (
"qwen2_5_omni",
"qwen2_5_omni_token2wav",
+1 -5
View File
@@ -2,7 +2,6 @@ from collections.abc import Callable
from typing import TYPE_CHECKING
from vllm.v1.request import Request
from vllm.v1.structured_output.request import StructuredOutputRequest
if TYPE_CHECKING:
from vllm.v1.core.kv_cache_utils import BlockHash
@@ -57,19 +56,16 @@ class OmniRequest(Request):
request_id=request.request_id,
client_index=request.client_index,
prompt_token_ids=request.prompt_token_ids,
prompt_embeds=request.prompt_embeds,
mm_features=request.mm_features,
sampling_params=request.sampling_params,
pooling_params=request.pooling_params,
eos_token_id=request.eos_token_id,
arrival_time=request.arrival_time,
lora_request=request.lora_request,
structured_output_request=(
StructuredOutputRequest(sampling_params=request.sampling_params) if request.sampling_params else None
),
cache_salt=request.cache_salt,
priority=request.priority,
trace_headers=request.trace_headers,
block_hasher=block_hasher,
prompt_embeds=request.prompt_embeds,
additional_information=request.additional_information,
)
+227 -134
View File
@@ -6,11 +6,17 @@ and also outputs sampled tokens.
from __future__ import annotations
from copy import copy
from typing import Any, NamedTuple
import numpy as np
import torch
from vllm.forward_context import BatchDescriptor
from vllm.config import CUDAGraphMode
from vllm.forward_context import set_forward_context
from vllm.logger import init_logger
from vllm.v1.core.sched.output import SchedulerOutput
from vllm.v1.core.sched.output import GrammarOutput, SchedulerOutput
from vllm.v1.outputs import AsyncModelRunnerOutput
from vllm.v1.spec_decode.eagle import EagleProposer
from vllm.v1.structured_output.utils import apply_grammar_bitmask
from vllm.v1.utils import record_function_or_nullcontext
from vllm.v1.worker.gpu_model_runner import (
@@ -20,7 +26,6 @@ from vllm.v1.worker.gpu_model_runner import (
get_pp_group,
get_tp_group,
has_kv_transfer_group,
set_forward_context,
)
from vllm.v1.worker.utils import is_residual_scattered_for_sp
@@ -30,14 +35,25 @@ from vllm_omni.worker.gpu_model_runner import OmniGPUModelRunner
logger = init_logger(__name__)
class ExecuteModelState(NamedTuple):
scheduler_output: SchedulerOutput
logits: torch.Tensor | None
spec_decode_metadata: Any
spec_decode_common_attn_metadata: Any
hidden_states: torch.Tensor
sample_hidden_states: torch.Tensor
aux_hidden_states: list[torch.Tensor] | None
ec_connector_output: Any
multimodal_outputs: Any
class GPUARModelRunner(OmniGPUModelRunner):
"""Autoregressive GPU model runner that returns hidden states per request.
This runner follows the same preparation and forward path as GPUModelRunner
(inputs assembly, multi-modal handling, TP/PP/DP integration, CUDA graphs),
and additionally performs lightweight sampling so that sampled tokens are
available in outputs. Hidden representations are taken at the same indices
that GPUModelRunner would use for sampling/logits (i.e. `logits_indices`).
Follows the v0.12 two-phase execute/sample flow from GPUModelRunner, and
reuses Omni hooks for additional_information / multimodal outputs. This
class only overrides sample_tokens to expose hidden states + multimodal
outputs per request while keeping Async output semantics.
"""
def __init__(self, *args, **kwargs):
@@ -65,19 +81,14 @@ class GPUARModelRunner(OmniGPUModelRunner):
self,
scheduler_output: SchedulerOutput,
intermediate_tensors: IntermediateTensors | None = None,
) -> OmniModelRunnerOutput | AsyncModelRunnerOutput | IntermediateTensors:
) -> OmniModelRunnerOutput | AsyncModelRunnerOutput | IntermediateTensors | None:
with record_function_or_nullcontext("Preprocess"):
with self.synchronize_input_prep():
# Update persistent batch states.
self._update_states(scheduler_output)
# Decode per-request prompt_embeds / additional_information payloads
# (if present) into CPU tensors
self._decode_and_store_request_payloads(scheduler_output)
if not scheduler_output.total_num_scheduled_tokens:
if not has_kv_transfer_group():
# Return empty ModelRunnerOutput if no work to do.
return EMPTY_MODEL_RUNNER_OUTPUT
return self.kv_connector_no_forward(scheduler_output, self.vllm_config)
if self.cache_config.kv_sharing_fast_prefill:
@@ -87,64 +98,83 @@ class GPUARModelRunner(OmniGPUModelRunner):
"it when the requests need prompt logprobs"
)
# Prepare the decoder inputs.
num_reqs = self.input_batch.num_reqs
req_ids = self.input_batch.req_ids
tokens = [scheduler_output.num_scheduled_tokens[i] for i in req_ids]
num_scheduled_tokens_np = np.array(tokens, dtype=np.int32)
max_num_scheduled_tokens = int(num_scheduled_tokens_np.max())
num_tokens_unpadded = scheduler_output.total_num_scheduled_tokens
logits_indices, spec_decode_metadata = self._prepare_inputs(
scheduler_output,
num_scheduled_tokens_np,
)
(
cudagraph_mode,
batch_desc,
ubatch_slices,
num_tokens_across_dp,
) = self._determine_batch_execution_and_padding(
num_tokens=num_tokens_unpadded,
num_reqs=num_reqs,
num_scheduled_tokens_np=num_scheduled_tokens_np,
max_num_scheduled_tokens=max_num_scheduled_tokens,
use_cascade_attn=False,
)
num_tokens_padded = batch_desc.num_tokens
num_reqs_padded = batch_desc.num_reqs if batch_desc.num_reqs is not None else num_reqs
use_spec_decode = len(scheduler_output.scheduled_spec_decode_tokens) > 0
pad_attn = cudagraph_mode == CUDAGraphMode.FULL
(
attn_metadata,
logits_indices,
spec_decode_metadata,
num_scheduled_tokens_np,
spec_decode_common_attn_metadata,
max_query_len,
ubatch_slices,
num_tokens_after_padding,
) = self._prepare_inputs(scheduler_output)
) = self._build_attention_metadata(
num_tokens=num_tokens_unpadded,
num_tokens_padded=num_tokens_padded if pad_attn else None,
num_reqs=num_reqs,
num_reqs_padded=num_reqs_padded if pad_attn else None,
max_query_len=max_num_scheduled_tokens,
ubatch_slices=ubatch_slices,
logits_indices=logits_indices,
use_spec_decode=use_spec_decode,
num_scheduled_tokens=scheduler_output.num_scheduled_tokens,
cascade_attn_prefix_lens=None,
)
(
num_scheduled_tokens,
num_input_tokens,
num_tokens_across_dp,
input_ids,
inputs_embeds,
positions,
intermediate_tensors,
model_kwargs,
per_req_additional_information,
ec_connector_output,
) = self._preprocess(
scheduler_output,
num_scheduled_tokens_np,
num_tokens_padded,
intermediate_tensors,
ubatch_slices,
num_tokens_after_padding,
)
uniform_decode = (max_query_len == self.uniform_decode_query_len) and (
num_scheduled_tokens == self.input_batch.num_reqs * max_query_len
)
batch_descriptor = BatchDescriptor(num_tokens=num_input_tokens, uniform_decode=uniform_decode)
cudagraph_runtime_mode, batch_descriptor = self.cudagraph_dispatcher.dispatch(batch_descriptor)
if self.calculate_kv_scales:
cudagraph_mode = CUDAGraphMode.NONE
self.calculate_kv_scales = False
# This is currently to get around the assert in the DPMetadata
# where it wants `num_tokens_across_dp` to align with `num_tokens`
if ubatch_slices is not None:
num_input_tokens = ubatch_slices[0].num_tokens
# Run the model.
# Use persistent buffers for CUDA graphs.
with (
set_forward_context(
attn_metadata,
self.vllm_config,
num_tokens=num_input_tokens,
num_tokens=num_tokens_padded,
num_tokens_across_dp=num_tokens_across_dp,
cudagraph_runtime_mode=cudagraph_runtime_mode,
batch_descriptor=batch_descriptor,
cudagraph_runtime_mode=cudagraph_mode,
batch_descriptor=batch_desc,
ubatch_slices=ubatch_slices,
),
record_function_or_nullcontext("Forward"),
self.maybe_get_kv_connector_output(scheduler_output) as kv_connector_output,
):
model_kwargs_extra = self._build_model_kwargs_extra(per_req_additional_information, num_scheduled_tokens_np)
model_output = self.model(
model_output = self._model_forward(
input_ids=input_ids,
positions=positions,
intermediate_tensors=intermediate_tensors,
@@ -153,10 +183,9 @@ class GPUARModelRunner(OmniGPUModelRunner):
sampling_metadata=self.input_batch.sampling_metadata,
logits_index=logits_indices,
sampler=self.sampler,
**model_kwargs_extra,
)
with record_function_or_nullcontext("Postprocess"):
with record_function_or_nullcontext("gpu_model_runner: postprocess"):
if self.use_aux_hidden_state_outputs:
# True when EAGLE 3 is used.
hidden_states, aux_hidden_states = model_output
@@ -165,38 +194,42 @@ class GPUARModelRunner(OmniGPUModelRunner):
hidden_states = model_output
aux_hidden_states = None
# hidden_states, multimodal_outputs = self.extract_multimodal_outputs(
# hidden_states
# )
multimodal_outputs = model_output.multimodal_outputs
hidden_states = model_output.text_hidden_states
# The model side may return per-request additional_information updates (model-agnostic channel).
# Convention: multimodal_outputs["additional_information_update"] is a list[dict] in batch order;
# the runner merges it into the corresponding request's additional_information_cpu for subsequent decode.
self._process_additional_information_updates(multimodal_outputs)
if multimodal_outputs is not None:
keys_or_type = (
list(multimodal_outputs.keys())
if isinstance(multimodal_outputs, dict)
else type(multimodal_outputs)
)
logger.debug(f"[AR] execute_model: multimodal_outputs keys = {keys_or_type}")
else:
logger.debug("[AR] execute_model: multimodal_outputs is None")
if not self.broadcast_pp_output:
# Common case.
if not get_pp_group().is_last_rank:
# Return the intermediate tensors.
assert isinstance(hidden_states, IntermediateTensors)
hidden_states.kv_connector_output = kv_connector_output
return hidden_states
if self.is_pooling_model:
# Return the pooling output.
output = self._pool(hidden_states, num_scheduled_tokens, num_scheduled_tokens_np)
output = self._pool(
hidden_states,
num_tokens_padded,
num_scheduled_tokens_np,
)
output.kv_connector_output = kv_connector_output
return output
sample_hidden_states = hidden_states[logits_indices]
logits = self.model.compute_logits(sample_hidden_states)
else:
# Rare case.
assert not self.is_pooling_model
if not get_pp_group().is_last_rank:
all_gather_tensors = {
"residual": not is_residual_scattered_for_sp(self.vllm_config, num_input_tokens)
"residual": not is_residual_scattered_for_sp(self.vllm_config, num_tokens_padded)
}
get_pp_group().send_tensor_dict(
hidden_states.tensors,
@@ -208,26 +241,71 @@ class GPUARModelRunner(OmniGPUModelRunner):
sample_hidden_states = hidden_states[logits_indices]
logits = self.model.compute_logits(sample_hidden_states)
model_output_broadcast_data = {}
model_output_broadcast_data: dict[str, Any] = {}
if logits is not None:
model_output_broadcast_data["logits"] = logits.contiguous()
model_output_broadcast_data = get_pp_group().broadcast_tensor_dict(
broadcasted = get_pp_group().broadcast_tensor_dict(
model_output_broadcast_data, src=len(get_pp_group().ranks) - 1
)
assert model_output_broadcast_data is not None
logits = model_output_broadcast_data["logits"]
assert broadcasted is not None
logits = broadcasted["logits"]
# Apply structured output bitmasks if present
if scheduler_output.grammar_bitmask is not None:
apply_grammar_bitmask(scheduler_output, self.input_batch, logits, self.device)
self.execute_model_state = ExecuteModelState(
scheduler_output,
logits,
spec_decode_metadata,
spec_decode_common_attn_metadata,
hidden_states,
sample_hidden_states,
aux_hidden_states,
ec_connector_output,
multimodal_outputs,
)
self.kv_connector_output = kv_connector_output
return None
with record_function_or_nullcontext("Sample"):
@torch.inference_mode()
def sample_tokens(
self,
grammar_output: GrammarOutput | None,
) -> OmniModelRunnerOutput | AsyncModelRunnerOutput | IntermediateTensors:
kv_connector_output = self.kv_connector_output
self.kv_connector_output = None
if self.execute_model_state is None:
if not kv_connector_output:
return None # type: ignore[return-value]
if kv_connector_output.is_empty():
return EMPTY_MODEL_RUNNER_OUTPUT
output = copy(EMPTY_MODEL_RUNNER_OUTPUT)
output.kv_connector_output = kv_connector_output
return output
(
scheduler_output,
logits,
spec_decode_metadata,
spec_decode_common_attn_metadata,
hidden_states,
sample_hidden_states,
aux_hidden_states,
ec_connector_output,
multimodal_outputs,
) = self.execute_model_state
self.execute_model_state = None
if grammar_output is not None:
apply_grammar_bitmask(scheduler_output, grammar_output, self.input_batch, logits)
with record_function_or_nullcontext("gpu_model_runner: sample"):
sampler_output = self._sample(logits, spec_decode_metadata)
self.input_batch.prev_sampled_token_ids = None
def propose_draft_token_ids(sampled_token_ids):
assert spec_decode_common_attn_metadata is not None
with record_function_or_nullcontext("Draft"):
with record_function_or_nullcontext("gpu_model_runner: draft"):
self._draft_token_ids = self.propose_draft_token_ids(
scheduler_output,
sampled_token_ids,
@@ -239,30 +317,40 @@ class GPUARModelRunner(OmniGPUModelRunner):
spec_decode_common_attn_metadata,
)
spec_config = self.speculative_config
use_padded_batch_for_eagle = (
self.speculative_config
and self.speculative_config.use_eagle()
and not self.speculative_config.disable_padded_drafter_batch
spec_config is not None and spec_config.use_eagle() and not spec_config.disable_padded_drafter_batch
)
effective_drafter_max_model_len = self.max_model_len
if effective_drafter_max_model_len is None:
effective_drafter_max_model_len = self.model_config.max_model_len
if (
self.speculative_config
and self.speculative_config.draft_model_config is not None
and self.speculative_config.draft_model_config.max_model_len is not None
spec_config is not None
and spec_config.draft_model_config is not None
and spec_config.draft_model_config.max_model_len is not None
):
effective_drafter_max_model_len = self.speculative_config.draft_model_config.max_model_len
effective_drafter_max_model_len = spec_config.draft_model_config.max_model_len
input_fits_in_drafter = spec_decode_common_attn_metadata and (
spec_decode_common_attn_metadata.seq_lens.max() + self.speculative_config.num_speculative_tokens
<= effective_drafter_max_model_len
spec_decode_common_attn_metadata.max_seq_len + self.num_spec_tokens <= effective_drafter_max_model_len
)
if use_padded_batch_for_eagle and input_fits_in_drafter:
# EAGLE speculative decoding can use the GPU sampled tokens
# as inputs, and does not need to wait for bookkeeping to finish.
propose_draft_token_ids(sampler_output.sampled_token_ids)
if use_padded_batch_for_eagle:
assert self.speculative_config is not None
assert isinstance(self.drafter, EagleProposer)
sampled_token_ids = sampler_output.sampled_token_ids
if input_fits_in_drafter:
propose_draft_token_ids(sampled_token_ids)
elif self.valid_sampled_token_count_event is not None:
assert spec_decode_common_attn_metadata is not None
next_token_ids, valid_sampled_tokens_count = self.drafter.prepare_next_token_ids_padded(
spec_decode_common_attn_metadata,
sampled_token_ids,
self.requests,
self.input_batch,
self.discard_request_mask.gpu,
)
self._copy_valid_sampled_token_count(next_token_ids, valid_sampled_tokens_count)
with record_function_or_nullcontext("Bookkeep"):
with record_function_or_nullcontext("gpu_model_runner: bookkeep"):
(
num_nans_in_logits,
logprobs_lists,
@@ -276,82 +364,91 @@ class GPUARModelRunner(OmniGPUModelRunner):
sampler_output,
logits,
hidden_states,
num_scheduled_tokens,
scheduler_output.total_num_scheduled_tokens,
spec_decode_metadata,
)
if self.speculative_config and not use_padded_batch_for_eagle and input_fits_in_drafter:
# ngram and other speculative decoding methods use the sampled
# tokens on the CPU, so they are run after bookkeeping.
propose_draft_token_ids(valid_sampled_token_ids)
with record_function_or_nullcontext("EPLB"):
with record_function_or_nullcontext("gpu_model_runner: eplb"):
self.eplb_step()
# Convert to per-request tensors on CPU
self._process_additional_information_updates(multimodal_outputs)
hidden_states_cpu = hidden_states.detach().to("cpu").contiguous()
# pooler_output: list[torch.Tensor | None] = []
pooler_output: list[torch.Tensor | None] = []
prev_logits_index = 0
for rid, logits_index in zip(req_ids_output_copy, logits_indices):
# Base payload: hidden slice for this request in this iteration
hidden_slice = hidden_states_cpu[prev_logits_index : logits_index + 1]
num_scheduled_tokens_np = getattr(self, "_omni_num_scheduled_tokens_np", None)
if num_scheduled_tokens_np is None:
req_ids = self.input_batch.req_ids
num_scheduled_tokens_np = np.array(
[scheduler_output.num_scheduled_tokens[rid] for rid in req_ids],
dtype=np.int32,
)
pooler_output: list[dict[str, object]] = []
for rid in req_ids_output_copy:
idx = req_id_to_index_output_copy[rid]
start = int(self.query_start_loc.cpu[idx])
sched = int(num_scheduled_tokens_np[idx])
end = start + sched
hidden_slice = hidden_states_cpu[start:end]
payload: dict[str, object] = {"hidden": hidden_slice}
# Merge multimodal_outputs if present
if isinstance(multimodal_outputs, dict) and multimodal_outputs:
mm_payload: dict[str, object] = {}
for k, v in multimodal_outputs.items():
try:
# Case 1: tensor aligned on token dimension
if isinstance(v, torch.Tensor) and v.shape[0] == hidden_states_cpu.shape[0]:
mm_payload[k] = v.detach().to("cpu")[prev_logits_index : logits_index + 1].contiguous()
elif isinstance(v, torch.Tensor) and v.shape[0] != hidden_states_cpu.shape[0]:
logger.error(
f"Error in merge multimodal outputs: Tensor dimension mismatch, \
{v.shape} != {hidden_states_cpu.shape} for {k}"
)
# Case 2: nested dict of tensors aligned on token dimension (e.g., selected_hidden_layers)
mm_payload[k] = v.detach().to("cpu")[start:end].contiguous()
elif isinstance(v, dict):
sub_dict: dict[str, torch.Tensor] = {}
for sk, sv in v.items():
if isinstance(sv, torch.Tensor) and sv.shape[0] == hidden_states_cpu.shape[0]:
sub_dict[str(sk)] = (
sv.detach().to("cpu")[prev_logits_index : logits_index + 1].contiguous()
)
sub_dict[str(sk)] = sv.detach().to("cpu")[start:end].contiguous()
if sub_dict:
mm_payload[k] = sub_dict
elif isinstance(v, list):
element = v[0]
if isinstance(element, torch.Tensor):
element = element.detach().to("cpu").contiguous()
multimodal_outputs[k] = v[1:] if len(v) > 1 else v
mm_payload[k] = element
except Exception as e:
# Best-effort; skip malformed entries
logger.error(f"Error in merge multimodal outputs: {e}")
if mm_payload:
payload.update(mm_payload)
pooler_output.append(payload) # type: ignore[arg-type]
prev_logits_index = logits_index + 1
output = OmniModelRunnerOutput(
req_ids=req_ids_output_copy,
req_id_to_index=req_id_to_index_output_copy,
sampled_token_ids=valid_sampled_token_ids,
logprobs=logprobs_lists,
prompt_logprobs_dict=prompt_logprobs_dict,
pooler_output=(pooler_output if self.vllm_config.model_config.engine_output_type != "text" else None),
kv_connector_output=kv_connector_output,
num_nans_in_logits=num_nans_in_logits,
)
pooler_output.append(payload)
with record_function_or_nullcontext("gpu_model_runner: ModelRunnerOutput"):
output = OmniModelRunnerOutput(
req_ids=req_ids_output_copy,
req_id_to_index=req_id_to_index_output_copy,
sampled_token_ids=valid_sampled_token_ids,
logprobs=logprobs_lists,
prompt_logprobs_dict=prompt_logprobs_dict,
pooler_output=(pooler_output if self.vllm_config.model_config.engine_output_type != "text" else None),
kv_connector_output=kv_connector_output,
ec_connector_output=ec_connector_output if self.supports_mm_inputs else None,
num_nans_in_logits=num_nans_in_logits,
)
if not self.use_async_scheduling:
return output
with record_function_or_nullcontext("gpu_model_runner: AsyncGPUModelRunnerOutput"):
async_output = AsyncGPUModelRunnerOutput(
model_runner_output=output,
sampled_token_ids=sampler_output.sampled_token_ids,
logprobs_tensors=sampler_output.logprobs_tensors,
invalid_req_indices=invalid_req_indices,
async_output_copy_stream=self.async_output_copy_stream,
vocab_size=self.input_batch.vocab_size,
)
with record_function_or_nullcontext("gpu_model_runner: set_async_sampled_token_ids"):
# Save ref of sampled_token_ids CPU tensor if the batch contains
# any requests with sampling params that require output ids.
self.input_batch.set_async_sampled_token_ids(
async_output.sampled_token_ids_cpu,
async_output.async_copy_ready_event,
)
return AsyncGPUModelRunnerOutput(
model_runner_output=output,
sampled_token_ids=sampler_output.sampled_token_ids,
invalid_req_indices=invalid_req_indices,
async_output_copy_stream=self.async_output_copy_stream,
)
return async_output
def _merge_additional_information_update(self, req_id: str, upd: dict) -> None:
req_state = self.requests.get(req_id)
@@ -365,13 +462,9 @@ class GPUARModelRunner(OmniGPUModelRunner):
if isinstance(v, torch.Tensor):
merged[k] = v.detach().to("cpu").contiguous()
elif isinstance(v, list):
new_list = []
for item in v:
if isinstance(item, torch.Tensor):
new_list.append(item.detach().to("cpu").contiguous())
else:
new_list.append(item)
merged[k] = new_list
merged[k] = [
(item.detach().to("cpu").contiguous() if isinstance(item, torch.Tensor) else item) for item in v
]
else:
merged[k] = v
setattr(req_state, "additional_information_cpu", merged)
+37 -18
View File
@@ -4,7 +4,8 @@ import os
import torch
from vllm.model_executor import set_random_seed
from vllm.platforms import current_platform
from vllm.utils import GiB_bytes, MemorySnapshot
from vllm.utils.mem_constants import GiB_bytes
from vllm.utils.mem_utils import MemorySnapshot
from vllm.v1.utils import report_usage_stats
from vllm.v1.worker.gpu_worker import Worker as GPUWorker
from vllm.v1.worker.gpu_worker import init_worker_distributed_environment
@@ -20,19 +21,37 @@ class GPUARWorker(GPUWorker):
"""
def init_device(self):
"""Initialize the GPU device and autoregressive model runner.
Sets up CUDA device, initializes distributed environment,
sets random seed, and creates a GPUARModelRunner instance.
Raises:
ValueError: If free GPU memory is insufficient for the
requested memory utilization
RuntimeError: If device type is not supported
"""
if self.device_config.device.type == "cuda":
device = self.device_config.device
if isinstance(device, torch.device) and device.type == "cuda":
# This env var set by Ray causes exceptions with graph building.
os.environ.pop("NCCL_ASYNC_ERROR_HANDLING", None)
if (
self.parallel_config.data_parallel_size > 1
and self.parallel_config.data_parallel_size_local > 0
and self.parallel_config.distributed_executor_backend not in ["ray", "external_launcher"]
and self.vllm_config.parallel_config.data_parallel_backend != "ray"
and self.vllm_config.parallel_config.nnodes_within_dp == 1
):
# Use local DP rank if available, otherwise use global DP rank.
dp_local_rank = self.parallel_config.data_parallel_rank_local
if dp_local_rank is None:
dp_local_rank = self.parallel_config.data_parallel_rank
tp_pp_world_size = (
self.parallel_config.pipeline_parallel_size * self.parallel_config.tensor_parallel_size
)
# DP_LOCAL_RANK * TP_PP_WORLD_SIZE + TP_LOCAL_RANK
self.local_rank += dp_local_rank * tp_pp_world_size
assert self.local_rank < torch.cuda.device_count(), (
f"DP adjusted local rank {self.local_rank} is out of bounds. "
)
visible_device_count = torch.cuda.device_count() if torch.cuda.is_available() else 0
assert self.parallel_config.local_world_size <= visible_device_count, (
f"local_world_size ({self.parallel_config.local_world_size}) must "
f"be less than or equal to the number of visible devices "
f"({visible_device_count})."
)
self.device = torch.device(f"cuda:{self.local_rank}")
current_platform.set_device(self.device)
@@ -62,23 +81,23 @@ class GPUARWorker(GPUWorker):
self.requested_memory = self.init_snapshot.total_memory * self.cache_config.gpu_memory_utilization
if self.init_snapshot.free_memory < self.requested_memory:
def GiB(b):
return round(b / GiB_bytes, 2)
def gib(bytes_val: float) -> float:
return round(bytes_val / GiB_bytes, 2)
raise ValueError(
f"Free memory on device "
f"({GiB(self.init_snapshot.free_memory)}/"
f"{GiB(self.init_snapshot.total_memory)} GiB) on startup "
f"({gib(self.init_snapshot.free_memory)}/"
f"{gib(self.init_snapshot.total_memory)} GiB) on startup "
f"is less than desired GPU memory utilization "
f"({self.cache_config.gpu_memory_utilization}, "
f"{GiB(self.requested_memory)} GiB). Decrease GPU memory "
f"{gib(self.requested_memory)} GiB). Decrease GPU memory "
f"utilization or reduce GPU memory used by other processes."
)
else:
raise RuntimeError(f"Not support device type: {self.device_config.device}")
# Construct the model runner
self.model_runner: GPUARModelRunner = GPUARModelRunner(self.vllm_config, self.device)
self.model_runner = GPUARModelRunner(self.vllm_config, self.device)
if self.rank == 0:
# If usage stat is enabled, collect relevant info.
+203 -232
View File
@@ -8,14 +8,12 @@ from __future__ import annotations
import gc
import logging
from typing import Any
import numpy as np
import torch
from vllm.config import CUDAGraphMode
from vllm.forward_context import BatchDescriptor
from vllm.multimodal.inputs import MultiModalKwargs
from vllm.v1.attention.backends.utils import CommonAttentionMetadata
from vllm.utils.math_utils import cdiv
from vllm.v1.core.sched.output import SchedulerOutput
from vllm.v1.spec_decode.eagle import EagleProposer
from vllm.v1.utils import record_function_or_nullcontext
@@ -27,7 +25,6 @@ from vllm.v1.worker.gpu_model_runner import (
get_pp_group,
set_forward_context,
)
from vllm.v1.worker.ubatch_utils import UBatchSlices
from vllm.v1.worker.utils import sanity_check_mm_encoder_outputs
from vllm_omni.outputs import OmniModelRunnerOutput
@@ -44,110 +41,6 @@ class GPUGenerationModelRunner(OmniGPUModelRunner):
- Executes generation process and returns tensors via `pooler_output`.
"""
def _preprocess(
self,
scheduler_output: SchedulerOutput,
intermediate_tensors: IntermediateTensors | None = None,
ubatch_slices: UBatchSlices | None = None,
num_tokens_after_padding: torch.Tensor | None = None,
) -> tuple[
int,
int,
torch.Tensor | None,
torch.Tensor | None,
torch.Tensor | None,
torch.Tensor,
IntermediateTensors | None,
dict[str, Any],
]:
# Input token count for this iteration (not used by diffusion, but
# retained to keep DP padding/ordering consistent)
num_input_tokens = scheduler_output.total_num_scheduled_tokens
num_pad, num_tokens_after_padding = self.get_dp_padding(num_input_tokens)
num_input_tokens += num_pad
# _prepare_inputs may reorder the batch, so we must gather multi
# modal outputs after that to ensure the correct order
if self.supports_mm_inputs and get_pp_group().is_first_rank and not self.model_config.is_encoder_decoder:
# Run the multimodal encoder if any.
self._execute_mm_encoder(scheduler_output)
mm_embeds = self._gather_mm_embeddings(scheduler_output)
# NOTE(woosuk): To unify token ids and soft tokens (vision
# embeddings), we always use embeddings (rather than token ids)
# as input to the multimodal model, even when the input is text.
inputs_embeds_scheduled = self.model.get_input_embeddings(
input_ids=self.input_ids.gpu[:num_input_tokens],
multimodal_embeddings=mm_embeds or None,
)
# TODO(woosuk): Avoid the copy. Optimize.
self.inputs_embeds.gpu[:num_input_tokens].copy_(inputs_embeds_scheduled)
input_ids = self.input_ids.gpu[:num_input_tokens]
inputs_embeds = self.inputs_embeds.gpu[:num_input_tokens]
model_kwargs = {
**self._init_model_kwargs(num_input_tokens),
**self._extract_mm_kwargs(scheduler_output),
}
elif self.enable_prompt_embeds and get_pp_group().is_first_rank:
# Get the input embeddings for the tokens that are not input embeds,
# then put them into the appropriate positions.
# TODO(qthequartermasterman): Since even when prompt embeds are
# enabled, (a) not all requests will use prompt embeds, and (b)
# after the initial prompt is processed, the rest of the generated
# tokens will be token ids, it is not desirable to have the
# embedding layer outside of the CUDA graph all the time. The v0
# engine avoids this by "double compiling" the CUDA graph, once
# with input_ids and again with inputs_embeds, for all num_tokens.
# If a batch only has token ids, then including the embedding layer
# in the CUDA graph will be more performant (like in the else case
# below).
token_ids_idx = self.is_token_ids.gpu[:num_input_tokens].nonzero(as_tuple=False).squeeze(1)
# Some tokens ids may need to become embeds
if token_ids_idx.numel() > 0:
token_ids = self.input_ids.gpu[token_ids_idx]
tokens_to_embeds = self.model.get_input_embeddings(input_ids=token_ids)
self.inputs_embeds.gpu[token_ids_idx] = tokens_to_embeds
inputs_embeds = self.inputs_embeds.gpu[:num_input_tokens]
model_kwargs = self._init_model_kwargs(num_input_tokens)
input_ids = self.input_ids.gpu[:num_input_tokens]
else:
# For text-only models, we use token ids as input.
# While it is possible to use embeddings as input just like the
# multimodal models, it is not desirable for performance since
# then the embedding layer is not included in the CUDA graph.
input_ids = self.input_ids.gpu[:num_input_tokens]
inputs_embeds = None
model_kwargs = self._init_model_kwargs(num_input_tokens)
if self.uses_mrope:
positions = self.mrope_positions.gpu[:, :num_input_tokens]
else:
positions = self.positions.gpu[:num_input_tokens]
if get_pp_group().is_first_rank:
intermediate_tensors = None
else:
intermediate_tensors = self.sync_and_slice_intermediate_tensors(
num_input_tokens, intermediate_tensors, True
)
if self.model_config.is_encoder_decoder and scheduler_output.scheduled_encoder_inputs:
encoder_inputs = self._extract_encoder_inputs(scheduler_output)
model_kwargs.update(encoder_inputs)
return (
num_input_tokens,
num_input_tokens,
num_tokens_after_padding,
input_ids,
inputs_embeds,
positions,
intermediate_tensors,
model_kwargs,
)
@torch.inference_mode()
def execute_model(
self,
@@ -156,52 +49,81 @@ class GPUGenerationModelRunner(OmniGPUModelRunner):
) -> OmniModelRunnerOutput | IntermediateTensors:
with record_function_or_nullcontext("Preprocess"):
with self.synchronize_input_prep():
# Update persistent batch states.
self._update_states(scheduler_output)
if not scheduler_output.total_num_scheduled_tokens:
return EMPTY_MODEL_RUNNER_OUTPUT
# Prepare the decoder inputs.
num_reqs = self.input_batch.num_reqs
req_ids = self.input_batch.req_ids
tokens = [scheduler_output.num_scheduled_tokens[i] for i in req_ids]
num_scheduled_tokens_np = np.array(tokens, dtype=np.int32)
max_num_scheduled_tokens = int(num_scheduled_tokens_np.max())
num_tokens_unpadded = scheduler_output.total_num_scheduled_tokens
logits_indices, spec_decode_metadata = self._prepare_inputs(
scheduler_output,
num_scheduled_tokens_np,
)
(
cudagraph_mode,
batch_desc,
ubatch_slices,
num_tokens_across_dp,
) = self._determine_batch_execution_and_padding(
num_tokens=num_tokens_unpadded,
num_reqs=num_reqs,
num_scheduled_tokens_np=num_scheduled_tokens_np,
max_num_scheduled_tokens=max_num_scheduled_tokens,
use_cascade_attn=False,
)
num_tokens_padded = batch_desc.num_tokens
num_reqs_padded = batch_desc.num_reqs if batch_desc.num_reqs is not None else num_reqs
use_spec_decode = len(scheduler_output.scheduled_spec_decode_tokens) > 0
pad_attn = cudagraph_mode == CUDAGraphMode.FULL
(
attn_metadata,
logits_indices,
spec_decode_metadata,
num_scheduled_tokens_np,
spec_decode_common_attn_metadata,
max_query_len,
ubatch_slices,
num_tokens_after_padding,
) = self._prepare_inputs(scheduler_output)
) = self._build_attention_metadata(
num_tokens=num_tokens_unpadded,
num_tokens_padded=num_tokens_padded if pad_attn else None,
num_reqs=num_reqs,
num_reqs_padded=num_reqs_padded if pad_attn else None,
max_query_len=max_num_scheduled_tokens,
ubatch_slices=ubatch_slices,
logits_indices=logits_indices,
use_spec_decode=use_spec_decode,
num_scheduled_tokens=scheduler_output.num_scheduled_tokens,
cascade_attn_prefix_lens=None,
)
# Input token count for this iteration (not used by diffusion, but
# retained to keep DP padding/ordering consistent)
(
num_scheduled_tokens,
num_input_tokens,
num_tokens_across_dp,
input_ids,
inputs_embeds,
positions,
intermediate_tensors,
model_kwargs,
ec_connector_output,
) = self._preprocess(
scheduler_output,
num_tokens_padded,
intermediate_tensors,
ubatch_slices,
num_tokens_after_padding,
)
cudagraph_runtime_mode = CUDAGraphMode.NONE
# Run the model.
# Use persistent buffers for CUDA graphs.
if self.calculate_kv_scales:
cudagraph_mode = CUDAGraphMode.NONE
self.calculate_kv_scales = False
with (
set_forward_context(
attn_metadata,
self.vllm_config,
num_tokens=num_input_tokens,
num_tokens=num_tokens_padded,
num_tokens_across_dp=num_tokens_across_dp,
cudagraph_runtime_mode=cudagraph_runtime_mode,
batch_descriptor=None,
cudagraph_runtime_mode=cudagraph_mode,
batch_descriptor=batch_desc,
ubatch_slices=ubatch_slices,
),
record_function_or_nullcontext("Forward"),
@@ -212,27 +134,32 @@ class GPUGenerationModelRunner(OmniGPUModelRunner):
positions=positions,
intermediate_tensors=intermediate_tensors,
inputs_embeds=inputs_embeds,
multimodal_kwargs=model_kwargs,
model_kwargs=model_kwargs,
logits_indices=logits_indices,
)
_, multimodal_outputs = self.extract_multimodal_outputs(outputs)
# Ensure one tensor per request, map to CPU for output struct
pooler_output: list[torch.Tensor | None] = []
pooler_output: list[object] = []
if isinstance(multimodal_outputs, torch.Tensor):
# If model returned a single stacked tensor, split by requests
assert multimodal_outputs.shape[0] == 1, (
"model should return a single tensor, to return multiple tensors, use a dict"
)
assert multimodal_outputs.shape[0] == self.input_batch.num_reqs
for i in range(self.input_batch.num_reqs):
pooler_output.append(multimodal_outputs[i].detach().to("cpu").contiguous())
pooler_output.append({"model_outputs": multimodal_outputs[i].detach().to("cpu").contiguous()})
elif isinstance(multimodal_outputs, list):
assert len(multimodal_outputs) == 1, (
"model should return a single list, to return multiple lists, use a dict"
)
for out in multimodal_outputs:
pooler_output.append(out.detach().to("cpu").contiguous() if out is not None else None)
pooler_output.append(
{"model_outputs": out.detach().to("cpu").contiguous() if out is not None else None}
)
elif isinstance(multimodal_outputs, dict):
for out in multimodal_outputs.values():
pooler_output.append(out.detach().to("cpu").contiguous() if out is not None else None)
for key, out in multimodal_outputs.items():
pooler_output.append({key: out.detach().to("cpu").contiguous() if out is not None else None})
else:
raise RuntimeError("Unsupported diffusion output type")
output = OmniModelRunnerOutput(
req_ids=self.input_batch.req_ids,
req_id_to_index=self.input_batch.req_id_to_index,
@@ -257,11 +184,11 @@ class GPUGenerationModelRunner(OmniGPUModelRunner):
def _run_generation_model(
self,
*,
input_ids: torch.Tensor,
positions: torch.Tensor,
input_ids: torch.Tensor | None,
positions: torch.Tensor | None,
intermediate_tensors: IntermediateTensors | None,
inputs_embeds: torch.Tensor | None,
multimodal_kwargs: dict,
model_kwargs: dict,
logits_indices: torch.Tensor,
) -> torch.Tensor | list[torch.Tensor]:
"""Run generation from codec codes to waveforms.
@@ -279,7 +206,7 @@ class GPUGenerationModelRunner(OmniGPUModelRunner):
positions=positions,
intermediate_tensors=intermediate_tensors,
inputs_embeds=inputs_embeds,
**MultiModalKwargs.as_kwargs(multimodal_kwargs, device=self.device),
**MultiModalKwargs.as_kwargs(model_kwargs, device=self.device),
sampling_metadata=self.input_batch.sampling_metadata,
logits_index=logits_indices,
sampler=self.sampler,
@@ -305,10 +232,13 @@ class GPUGenerationModelRunner(OmniGPUModelRunner):
cudagraph_runtime_mode: CUDAGraphMode | None = None,
force_attention: bool = False,
uniform_decode: bool = False,
allow_microbatching: bool = True,
skip_eplb: bool = False,
is_profile: bool = False,
create_mixed_batch: bool = False,
remove_lora: bool = True,
activate_lora: bool = False,
is_graph_capturing: bool = False,
) -> tuple[torch.Tensor, torch.Tensor]:
"""
Run a dummy forward pass to warm up/profile run or capture the
@@ -331,12 +261,9 @@ class GPUGenerationModelRunner(OmniGPUModelRunner):
create_mixed_batch: If True, create a mixed batch with both decode
(1 token) and prefill (multiple tokens) requests.
remove_lora: If False, dummy LoRAs are not destroyed after the run
activate_lora: If False, dummy_run is performed without LoRAs.
"""
assert cudagraph_runtime_mode is None or cudagraph_runtime_mode in {
CUDAGraphMode.NONE,
CUDAGraphMode.PIECEWISE,
CUDAGraphMode.FULL,
}
assert cudagraph_runtime_mode is None or cudagraph_runtime_mode.valid_runtime_modes()
# If cudagraph_mode.decode_mode() == FULL and
# cudagraph_mode.separate_routine(). This means that we are using
@@ -358,37 +285,79 @@ class GPUGenerationModelRunner(OmniGPUModelRunner):
# has num_tokens in total.
assert num_tokens <= self.scheduler_config.max_num_batched_tokens
max_num_reqs = self.scheduler_config.max_num_seqs
if create_mixed_batch:
assert not uniform_decode
# Create mixed batch:
# first half decode tokens, second half one prefill
num_decode_tokens = min(max_num_reqs - 1, num_tokens // 2)
num_prefill_tokens = num_tokens - num_decode_tokens
num_reqs = num_decode_tokens + 1
num_reqs = min(num_tokens, max_num_reqs)
min_tokens_per_req = num_tokens // num_reqs
num_scheduled_tokens_list = [min_tokens_per_req] * num_reqs
num_scheduled_tokens_list[-1] += num_tokens % num_reqs
# Create decode requests (1 token each) followed by prefill request
num_scheduled_tokens_list = [1] * num_decode_tokens + [num_prefill_tokens]
# Note: Overriding max_query_len to be the prefill tokens
max_query_len = num_prefill_tokens
elif uniform_decode:
assert not create_mixed_batch
num_reqs = min(max_num_reqs, cdiv(num_tokens, max_query_len))
num_scheduled_tokens_list = [max_query_len] * num_reqs
if num_tokens % max_query_len != 0:
num_scheduled_tokens_list[-1] = num_tokens % max_query_len
else:
num_reqs = min(num_tokens, max_num_reqs)
min_tokens_per_req = num_tokens // num_reqs
num_scheduled_tokens_list = [min_tokens_per_req] * num_reqs
num_scheduled_tokens_list[-1] += num_tokens % num_reqs
assert sum(num_scheduled_tokens_list) == num_tokens
assert len(num_scheduled_tokens_list) == num_reqs
num_scheduled_tokens = np.array(num_scheduled_tokens_list, dtype=np.int32)
num_tokens_unpadded = int(num_scheduled_tokens.sum())
num_tokens_after_padding = None
num_sampled_tokens = np.ones(num_reqs, dtype=np.int32)
# If we failed to microbatch, currently need to resynchronize
# TODO(lucas,sage): we should be able to avoid this second sync by
# refactoring `get_dp_padding_ubatch` and `get_dp_padding` into
# a single `coordinate_batch_across_dp` function.
if num_tokens_after_padding is None:
num_pad, num_tokens_across_dp = self.get_dp_padding(num_tokens)
num_tokens_after_padding = num_tokens + num_pad
_cudagraph_mode, batch_desc, ubatch_slices, num_tokens_across_dp = self._determine_batch_execution_and_padding(
num_tokens=num_tokens_unpadded,
num_reqs=num_reqs,
num_scheduled_tokens_np=num_scheduled_tokens,
max_num_scheduled_tokens=max_query_len,
use_cascade_attn=False,
allow_microbatching=allow_microbatching,
force_eager=is_profile or (cudagraph_runtime_mode == CUDAGraphMode.NONE),
# `force_uniform_decode` is used for cudagraph capture; because for
# capturing mixed prefill-decode batches, we sometimes use
# num_tokens == num_reqs which looks like a uniform decode batch to the
# dispatcher; but we actually want to capture a piecewise cudagraph
force_uniform_decode=uniform_decode,
# `force_has_lora` is used for cudagraph capture; because LoRA is
# activated later in the context manager, but we need to know the
# LoRA state when determining the batch descriptor for capture
force_has_lora=activate_lora,
)
if cudagraph_runtime_mode is None:
cudagraph_runtime_mode = _cudagraph_mode
else:
num_tokens_across_dp = num_tokens_after_padding
num_tokens_after_padding = int(num_tokens_after_padding[0].item())
assert cudagraph_runtime_mode == _cudagraph_mode, (
f"Cudagraph runtime mode mismatch in dummy_run. "
f"Expected {_cudagraph_mode}, but got {cudagraph_runtime_mode}."
)
num_tokens_padded = batch_desc.num_tokens
num_reqs_padded = batch_desc.num_reqs if batch_desc.num_reqs is not None else num_reqs
attn_metadata: PerLayerAttnMetadata | None = None
# If force_attention is True, we always capture attention. Otherwise,
# it only happens for cudagraph_runtime_mode=FULL.
if force_attention or cudagraph_runtime_mode == CUDAGraphMode.FULL:
attn_metadata = {}
seq_lens = max_query_len
if create_mixed_batch:
# In the mixed batch mode (used for FI warmup), we use
# shorter sequence lengths to run faster.
# TODO(luka) better system for describing dummy batches
seq_lens = [1] * num_decode_tokens + [num_prefill_tokens + 1]
else:
seq_lens = max_query_len # type: ignore[assignment]
self.seq_lens.np[:num_reqs] = seq_lens
self.seq_lens.np[num_reqs:] = 0
self.seq_lens.copy_to_gpu()
@@ -397,50 +366,45 @@ class GPUGenerationModelRunner(OmniGPUModelRunner):
self.query_start_loc.np[1 : num_reqs + 1] = cum_num_tokens
self.query_start_loc.copy_to_gpu()
for kv_cache_group_id, kv_cache_group_spec in enumerate(self.kv_cache_config.kv_cache_groups):
common_attn_metadata = CommonAttentionMetadata(
query_start_loc=self.query_start_loc.gpu[: num_reqs + 1],
query_start_loc_cpu=self.query_start_loc.cpu[: num_reqs + 1],
seq_lens=self.seq_lens.gpu[:num_reqs],
seq_lens_cpu=self.seq_lens.cpu[:num_reqs],
num_computed_tokens_cpu=self.input_batch.num_computed_tokens_cpu_tensor[:num_reqs], # noqa: E501
num_reqs=num_reqs,
num_actual_tokens=num_tokens,
max_query_len=max_query_len,
max_seq_len=self.max_model_len,
block_table_tensor=self.input_batch.block_table[kv_cache_group_id].get_device_tensor(num_reqs),
slot_mapping=self.input_batch.block_table[kv_cache_group_id].slot_mapping.gpu[:num_tokens],
causal=True,
)
for attn_group in self.attn_groups[kv_cache_group_id]:
assert type(attn_metadata) is dict
attn_metadata_i = attn_group.get_metadata_builder().build_for_cudagraph_capture(
common_attn_metadata
) # noqa: E501
for layer_name in attn_group.layer_names:
attn_metadata[layer_name] = attn_metadata_i
attn_metadata, _ = self._build_attention_metadata(
num_tokens=num_tokens_unpadded,
num_reqs=num_reqs_padded,
max_query_len=max_query_len,
ubatch_slices=ubatch_slices,
for_cudagraph_capture=is_graph_capturing,
)
with self.maybe_dummy_run_with_lora(self.lora_config, num_scheduled_tokens, remove_lora):
model_kwargs = self._init_model_kwargs(num_tokens)
with self.maybe_dummy_run_with_lora(
self.lora_config,
num_scheduled_tokens,
num_sampled_tokens,
activate_lora,
remove_lora,
):
# Make sure padding doesn't exceed max_num_tokens
assert num_tokens_padded <= self.max_num_tokens
model_kwargs = self._init_model_kwargs(num_tokens_padded)
if self.supports_mm_inputs and not self.model_config.is_encoder_decoder:
input_ids = None
inputs_embeds = self.inputs_embeds.gpu[:num_tokens]
inputs_embeds = self.inputs_embeds.gpu[:num_tokens_padded]
model_kwargs = {
**model_kwargs,
**self._dummy_mm_kwargs(num_reqs),
}
elif self.enable_prompt_embeds:
input_ids = None
inputs_embeds = self.inputs_embeds.gpu[:num_tokens]
model_kwargs = self._init_model_kwargs(num_tokens)
inputs_embeds = self.inputs_embeds.gpu[:num_tokens_padded]
model_kwargs = self._init_model_kwargs(num_tokens_padded)
else:
input_ids = self.input_ids.gpu[:num_tokens]
input_ids = self.input_ids.gpu[:num_tokens_padded]
inputs_embeds = None
if self.uses_mrope:
positions = self.mrope_positions.gpu[:, :num_tokens]
positions = self.mrope_positions.gpu[:, :num_tokens_padded]
elif self.uses_xdrope_dim > 0:
positions = self.xdrope_positions.gpu[:, :num_tokens_padded]
else:
positions = self.positions.gpu[:num_tokens]
positions = self.positions.gpu[:num_tokens_padded]
if get_pp_group().is_first_rank:
intermediate_tensors = None
@@ -452,38 +416,26 @@ class GPUGenerationModelRunner(OmniGPUModelRunner):
device=self.device,
)
intermediate_tensors = self.sync_and_slice_intermediate_tensors(num_tokens, None, False)
intermediate_tensors = self.sync_and_slice_intermediate_tensors(num_tokens_padded, None, False)
# filter out the valid batch descriptor
_cg_mode, batch_descriptor = (
self.cudagraph_dispatcher.dispatch(
BatchDescriptor(
num_tokens=num_tokens_after_padding,
uniform_decode=uniform_decode,
)
)
if not is_profile
else (CUDAGraphMode.NONE, None)
)
if cudagraph_runtime_mode is not None:
# we allow forcing NONE when the dispatcher disagrees to support
# warm ups for cudagraph capture
assert cudagraph_runtime_mode == CUDAGraphMode.NONE or cudagraph_runtime_mode == _cg_mode, (
f"Cudagraph runtime mode mismatch at dummy_run. "
f"Expected {_cg_mode}, but got {cudagraph_runtime_mode}."
)
else:
cudagraph_runtime_mode = _cg_mode
if ubatch_slices is not None:
# Adjust values to reflect a single ubatch.
# TODO(sage,lucas): this is cruft that should be addressed in
# the padding refactor.
num_tokens_padded = ubatch_slices[0].num_tokens
if num_tokens_across_dp is not None:
num_tokens_across_dp[:] = num_tokens_padded
with (
self.maybe_randomize_inputs(input_ids),
set_forward_context(
attn_metadata,
self.vllm_config,
num_tokens=num_tokens_after_padding,
num_tokens=num_tokens_padded,
num_tokens_across_dp=num_tokens_across_dp,
cudagraph_runtime_mode=cudagraph_runtime_mode,
batch_descriptor=batch_descriptor,
batch_descriptor=batch_desc,
ubatch_slices=ubatch_slices,
),
):
outputs = self.model(
@@ -498,10 +450,26 @@ class GPUGenerationModelRunner(OmniGPUModelRunner):
hidden_states, _ = outputs
else:
hidden_states = outputs
hidden_states, multimodal_outputs = self.extract_multimodal_outputs(hidden_states)
if self.speculative_config and self.speculative_config.use_eagle():
assert isinstance(self.drafter, EagleProposer)
self.drafter.dummy_run(num_tokens)
use_cudagraphs = (
cudagraph_runtime_mode.has_mode(CUDAGraphMode.PIECEWISE)
and not self.speculative_config.enforce_eager
)
# Note(gnovack) - We need to disable cudagraphs for one of the two
# lora cases when cudagraph_specialize_lora is enabled. This is a
# short term mitigation for issue mentioned in
# https://github.com/vllm-project/vllm/issues/28334
if self.compilation_config.cudagraph_specialize_lora and activate_lora:
use_cudagraphs = False
self.drafter.dummy_run(
num_tokens,
use_cudagraphs=use_cudagraphs,
is_graph_capturing=is_graph_capturing,
)
# This is necessary to avoid blocking DP.
# For dummy runs, we typically skip EPLB since we don't have any real
@@ -510,15 +478,16 @@ class GPUGenerationModelRunner(OmniGPUModelRunner):
# not have any requests to process, so they're executing dummy batches.
# In such cases, we still have to trigger EPLB to make sure
# ranks execute the rearrangement in synchronization.
if not skip_eplb:
self.eplb_step(is_dummy=True, is_profile=is_profile)
# logit_indices = np.cumsum(num_scheduled_tokens) - 1
hidden_states, _ = self.extract_multimodal_outputs(hidden_states)
return hidden_states, None
def profile_run(self) -> None:
# Profile with multimodal encoder & encoder cache.
if self.supports_mm_inputs:
if self.model_config.multimodal_config.skip_mm_profiling:
mm_config = self.model_config.multimodal_config
if mm_config is not None and mm_config.skip_mm_profiling:
logger.info("Skipping memory profiling for multimodal encoder and encoder cache.")
else:
mm_budget = self.mm_budget
@@ -532,8 +501,9 @@ class GPUGenerationModelRunner(OmniGPUModelRunner):
max_mm_items_per_batch = mm_budget.max_items_per_batch_by_modality[dummy_modality]
logger.info(
"Encoder cache will be initialized with a budget "
"of %s tokens, and profiled with %s %s items of the maximum feature size.",
"Encoder cache will be initialized with a budget of "
"%s tokens, and profiled with %s %s items of the "
"maximum feature size.",
encoder_budget,
max_mm_items_per_batch,
dummy_modality,
@@ -546,7 +516,7 @@ class GPUGenerationModelRunner(OmniGPUModelRunner):
)
# Run multimodal encoder.
dummy_encoder_outputs = self.model.get_multimodal_embeddings(**batched_dummy_mm_inputs)
dummy_encoder_outputs = self.model.embed_multimodal(**batched_dummy_mm_inputs)
sanity_check_mm_encoder_outputs(
dummy_encoder_outputs,
@@ -556,13 +526,15 @@ class GPUGenerationModelRunner(OmniGPUModelRunner):
# NOTE: This happens when encoder cache needs to store
# the embeddings that encoder outputs are scattered onto.
# In this case we create dummy embeddings of size
# (encode_budget, hidden_size) and scatter encoder
# output into it.
# (max_tokens_for_modality, hidden_size) and scatter
# encoder output into it.
encoder_output_shape = dummy_encoder_outputs[0].shape
if encoder_output_shape[0] < encoder_budget:
max_mm_tokens_per_item = mm_budget.max_tokens_by_modality[dummy_modality]
if encoder_output_shape[0] < max_mm_tokens_per_item:
encoder_hidden_size = encoder_output_shape[-1]
expanded_outputs = []
for output in dummy_encoder_outputs:
expanded = output.new_zeros((encoder_budget, encoder_output_shape[-1]))
expanded = output.new_zeros((max_mm_tokens_per_item, encoder_hidden_size))
num_tokens = output.shape[0]
expanded[:num_tokens].copy_(output)
expanded_outputs.append(expanded)
@@ -574,8 +546,7 @@ class GPUGenerationModelRunner(OmniGPUModelRunner):
# Add `is_profile` here to pre-allocate communication buffers
hidden_states, _ = self._dummy_run(self.max_num_tokens, is_profile=True)
if get_pp_group().is_last_rank:
pass
output = None
self._sync_device()
del hidden_states
self.encoder_cache.clear()
+37 -17
View File
@@ -4,7 +4,8 @@ import os
import torch
from vllm.model_executor import set_random_seed
from vllm.platforms import current_platform
from vllm.utils import GiB_bytes, MemorySnapshot
from vllm.utils.mem_constants import GiB_bytes
from vllm.utils.mem_utils import MemorySnapshot
from vllm.v1.utils import report_usage_stats
from vllm.v1.worker.gpu_worker import Worker as GPUWorker
from vllm.v1.worker.gpu_worker import init_worker_distributed_environment
@@ -20,18 +21,37 @@ class GPUGenerationWorker(GPUWorker):
"""
def init_device(self):
"""Initialize CUDA device and distributed environment."""
if self.device_config.device.type == "cuda":
# torch.distributed.all_reduce does not free the input tensor until
# the synchronization point. This causes the memory usage to grow
# as the number of all_reduce calls increases. This env var disables
# this behavior.
# Related issue:
# https://discuss.pytorch.org/t/cuda-allocation-lifetime-for-inputs-to-distributed-all-reduce/191573
os.environ["TORCH_NCCL_AVOID_RECORD_STREAMS"] = "1"
device = self.device_config.device
if isinstance(device, torch.device) and device.type == "cuda":
# This env var set by Ray causes exceptions with graph building.
os.environ.pop("NCCL_ASYNC_ERROR_HANDLING", None)
if (
self.parallel_config.data_parallel_size > 1
and self.parallel_config.data_parallel_size_local > 0
and self.parallel_config.distributed_executor_backend not in ["ray", "external_launcher"]
and self.vllm_config.parallel_config.data_parallel_backend != "ray"
and self.vllm_config.parallel_config.nnodes_within_dp == 1
):
# Use local DP rank if available, otherwise use global DP rank.
dp_local_rank = self.parallel_config.data_parallel_rank_local
if dp_local_rank is None:
dp_local_rank = self.parallel_config.data_parallel_rank
tp_pp_world_size = (
self.parallel_config.pipeline_parallel_size * self.parallel_config.tensor_parallel_size
)
# DP_LOCAL_RANK * TP_PP_WORLD_SIZE + TP_LOCAL_RANK
self.local_rank += dp_local_rank * tp_pp_world_size
assert self.local_rank < torch.cuda.device_count(), (
f"DP adjusted local rank {self.local_rank} is out of bounds. "
)
visible_device_count = torch.cuda.device_count() if torch.cuda.is_available() else 0
assert self.parallel_config.local_world_size <= visible_device_count, (
f"local_world_size ({self.parallel_config.local_world_size}) must "
f"be less than or equal to the number of visible devices "
f"({visible_device_count})."
)
self.device = torch.device(f"cuda:{self.local_rank}")
current_platform.set_device(self.device)
@@ -61,23 +81,23 @@ class GPUGenerationWorker(GPUWorker):
self.requested_memory = self.init_snapshot.total_memory * self.cache_config.gpu_memory_utilization
if self.init_snapshot.free_memory < self.requested_memory:
def GiB(b):
return round(b / GiB_bytes, 2)
def gib(bytes_val: float) -> float:
return round(bytes_val / GiB_bytes, 2)
raise ValueError(
f"Free memory on device "
f"({GiB(self.init_snapshot.free_memory)}/"
f"{GiB(self.init_snapshot.total_memory)} GiB) on startup "
f"({gib(self.init_snapshot.free_memory)}/"
f"{gib(self.init_snapshot.total_memory)} GiB) on startup "
f"is less than desired GPU memory utilization "
f"({self.cache_config.gpu_memory_utilization}, "
f"{GiB(self.requested_memory)} GiB). Decrease GPU memory "
f"{gib(self.requested_memory)} GiB). Decrease GPU memory "
f"utilization or reduce GPU memory used by other processes."
)
else:
raise RuntimeError(f"Not support device type: {self.device_config.device}")
# Construct the model runner
self.model_runner: GPUGenerationModelRunner = GPUGenerationModelRunner(self.vllm_config, self.device)
self.model_runner = GPUGenerationModelRunner(self.vllm_config, self.device)
if self.rank == 0:
# If usage stat is enabled, collect relevant info.
+293 -227
View File
@@ -4,22 +4,17 @@ import numpy as np
import torch
from vllm.config import CUDAGraphMode
from vllm.distributed.parallel_state import get_pp_group
from vllm.forward_context import BatchDescriptor, set_forward_context
from vllm.forward_context import set_forward_context
from vllm.logger import init_logger
from vllm.model_executor.layers.rotary_embedding import MRotaryEmbedding
from vllm.model_executor.models.interfaces import supports_mrope
from vllm.model_executor.models.interfaces_base import VllmModelForPooling
from vllm.sampling_params import SamplingType
from vllm.utils import LazyLoader, cdiv
from vllm.v1.attention.backends.utils import (
CommonAttentionMetadata,
split_attn_metadata,
)
from vllm.utils.import_utils import LazyLoader
from vllm.utils.math_utils import cdiv
from vllm.v1.spec_decode.eagle import EagleProposer
from vllm.v1.worker.gpu_input_batch import CachedRequestState
from vllm.v1.worker.gpu_model_runner import GPUModelRunner, IntermediateTensors, PerLayerAttnMetadata
from vllm.v1.worker.ubatch_splitting import ubatch_split
from vllm.v1.worker.ubatch_utils import UBatchSlices
if TYPE_CHECKING:
from vllm.v1.core.sched.output import SchedulerOutput
@@ -35,6 +30,12 @@ logger = init_logger(__name__)
class OmniGPUModelRunner(GPUModelRunner):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._omni_per_req_additional_information: dict[str, dict] | None = None
self._omni_num_scheduled_tokens_np: np.ndarray | None = None
self._omni_last_model_output: object | None = None
def _init_mrope_positions(self, req_state: CachedRequestState):
"""Initialize M-RoPE positions for multimodal inputs.
@@ -334,6 +335,8 @@ class OmniGPUModelRunner(GPUModelRunner):
is_profile: bool = False,
create_mixed_batch: bool = False,
remove_lora: bool = True,
activate_lora: bool = False,
is_graph_capturing: bool = False,
) -> tuple[torch.Tensor, torch.Tensor]:
"""
Run a dummy forward pass to warm up/profile run or capture the
@@ -356,12 +359,9 @@ class OmniGPUModelRunner(GPUModelRunner):
create_mixed_batch: If True, create a mixed batch with both decode
(1 token) and prefill (multiple tokens) requests.
remove_lora: If False, dummy LoRAs are not destroyed after the run
activate_lora: If False, dummy_run is performed without LoRAs.
"""
assert cudagraph_runtime_mode is None or cudagraph_runtime_mode in {
CUDAGraphMode.NONE,
CUDAGraphMode.PIECEWISE,
CUDAGraphMode.FULL,
}
assert cudagraph_runtime_mode is None or cudagraph_runtime_mode.valid_runtime_modes()
# If cudagraph_mode.decode_mode() == FULL and
# cudagraph_mode.separate_routine(). This means that we are using
@@ -387,7 +387,7 @@ class OmniGPUModelRunner(GPUModelRunner):
assert not uniform_decode
# Create mixed batch:
# first half decode tokens, second half one prefill
num_decode_tokens = num_tokens // 2
num_decode_tokens = min(max_num_reqs - 1, num_tokens // 2)
num_prefill_tokens = num_tokens - num_decode_tokens
num_reqs = num_decode_tokens + 1
@@ -397,7 +397,7 @@ class OmniGPUModelRunner(GPUModelRunner):
max_query_len = num_prefill_tokens
elif uniform_decode:
assert not create_mixed_batch
num_reqs = cdiv(num_tokens, max_query_len)
num_reqs = min(max_num_reqs, cdiv(num_tokens, max_query_len))
num_scheduled_tokens_list = [max_query_len] * num_reqs
if num_tokens % max_query_len != 0:
num_scheduled_tokens_list[-1] = num_tokens % max_query_len
@@ -410,56 +410,52 @@ class OmniGPUModelRunner(GPUModelRunner):
assert sum(num_scheduled_tokens_list) == num_tokens
assert len(num_scheduled_tokens_list) == num_reqs
num_scheduled_tokens = np.array(num_scheduled_tokens_list, dtype=np.int32)
num_tokens_unpadded = int(num_scheduled_tokens.sum())
total_num_scheduled_tokens = int(num_scheduled_tokens.sum())
num_sampled_tokens = np.ones(num_reqs, dtype=np.int32)
ubatch_slices = None
num_tokens_after_padding = None
_cudagraph_mode, batch_desc, ubatch_slices, num_tokens_across_dp = self._determine_batch_execution_and_padding(
num_tokens=num_tokens_unpadded,
num_reqs=num_reqs,
num_scheduled_tokens_np=num_scheduled_tokens,
max_num_scheduled_tokens=max_query_len,
use_cascade_attn=False,
allow_microbatching=allow_microbatching,
force_eager=is_profile or (cudagraph_runtime_mode == CUDAGraphMode.NONE),
# `force_uniform_decode` is used for cudagraph capture; because for
# capturing mixed prefill-decode batches, we sometimes use
# num_tokens == num_reqs which looks like a uniform decode batch to the
# dispatcher; but we actually want to capture a piecewise cudagraph
force_uniform_decode=uniform_decode,
# `force_has_lora` is used for cudagraph capture; because LoRA is
# activated later in the context manager, but we need to know the
# LoRA state when determining the batch descriptor for capture
force_has_lora=activate_lora,
)
# We currently only microbatch if the number of tokens is
# over a certain threshold.
if self.parallel_config.enable_dbo and allow_microbatching:
ubatch_slices, ubatch_num_tokens_after_padding = ubatch_split(
num_scheduled_tokens,
total_num_scheduled_tokens,
total_num_scheduled_tokens,
uniform_decode=uniform_decode,
vllm_config=self.vllm_config,
)
# Currently when DBO is enabled `ubatch_split` returns
# the num_tokens_after_padding for a single ubatch, but we have 2
# TODO(sage,lucas): this is cruft that should be addressed in the
# padding refactor.
if ubatch_num_tokens_after_padding is not None:
num_tokens_after_padding = ubatch_num_tokens_after_padding * 2
# If we failed to microbatch, currently need to resynchronize
# TODO(lucas,sage): we should be able to avoid this second sync by
# refactoring `get_dp_padding_ubatch` and `get_dp_padding` into
# a single `coordinate_batch_across_dp` function.
if num_tokens_after_padding is None:
num_pad, num_tokens_across_dp = self.get_dp_padding(num_tokens)
num_tokens_after_padding = num_tokens + num_pad
if cudagraph_runtime_mode is None:
cudagraph_runtime_mode = _cudagraph_mode
else:
num_tokens_across_dp = num_tokens_after_padding
num_tokens_after_padding = int(num_tokens_after_padding[0].item())
assert cudagraph_runtime_mode == _cudagraph_mode, (
f"Cudagraph runtime mode mismatch in dummy_run. "
f"Expected {_cudagraph_mode}, but got {cudagraph_runtime_mode}."
)
num_tokens_padded = batch_desc.num_tokens
num_reqs_padded = batch_desc.num_reqs if batch_desc.num_reqs is not None else num_reqs
attn_metadata: PerLayerAttnMetadata | None = None
# If force_attention is True, we always capture attention. Otherwise,
# it only happens for cudagraph_runtime_mode=FULL.
if force_attention or cudagraph_runtime_mode == CUDAGraphMode.FULL:
attn_metadata = {}
if ubatch_slices is not None:
attn_metadata = [dict() for _ in range(len(ubatch_slices))]
if create_mixed_batch:
# In the mixed batch mode (used for FI warmup), we use
# shorter sequence lengths to run faster.
# TODO(luka) better system for describing dummy batches
seq_lens = [1] * num_decode_tokens + [num_prefill_tokens + 1]
else:
seq_lens = max_query_len
seq_lens = max_query_len # type: ignore[assignment]
self.seq_lens.np[:num_reqs] = seq_lens
self.seq_lens.np[num_reqs:] = 0
self.seq_lens.copy_to_gpu()
@@ -468,61 +464,45 @@ class OmniGPUModelRunner(GPUModelRunner):
self.query_start_loc.np[1 : num_reqs + 1] = cum_num_tokens
self.query_start_loc.copy_to_gpu()
for kv_cache_group_id, kv_cache_group_spec in enumerate(self.kv_cache_config.kv_cache_groups):
common_attn_metadata = CommonAttentionMetadata(
query_start_loc=self.query_start_loc.gpu[: num_reqs + 1],
query_start_loc_cpu=self.query_start_loc.cpu[: num_reqs + 1],
seq_lens=self.seq_lens.gpu[:num_reqs],
seq_lens_cpu=self.seq_lens.cpu[:num_reqs],
num_computed_tokens_cpu=self.input_batch.num_computed_tokens_cpu_tensor[:num_reqs], # noqa: E501
num_reqs=num_reqs,
num_actual_tokens=num_tokens,
max_query_len=max_query_len,
max_seq_len=self.max_model_len,
block_table_tensor=self.input_batch.block_table[kv_cache_group_id].get_device_tensor(num_reqs),
slot_mapping=self.input_batch.block_table[kv_cache_group_id].slot_mapping.gpu[:num_tokens],
causal=True,
)
for attn_group in self.attn_groups[kv_cache_group_id]:
if ubatch_slices is not None:
common_attn_metadata_list = split_attn_metadata(ubatch_slices, common_attn_metadata)
for ubid, common_attn_metadata in enumerate(common_attn_metadata_list):
assert common_attn_metadata.max_query_len == 1
attn_metadata_i = attn_group.get_metadata_builder(
ubatch_id=ubid
).build_for_cudagraph_capture(common_attn_metadata)
for layer_name in attn_group.layer_names:
assert type(attn_metadata) is list
attn_metadata[ubid][layer_name] = attn_metadata_i
else:
assert type(attn_metadata) is dict
attn_metadata_i = attn_group.get_metadata_builder().build_for_cudagraph_capture(
common_attn_metadata
) # noqa: E501
for layer_name in attn_group.layer_names:
attn_metadata[layer_name] = attn_metadata_i
attn_metadata, _ = self._build_attention_metadata(
num_tokens=num_tokens_unpadded,
num_reqs=num_reqs_padded,
max_query_len=max_query_len,
ubatch_slices=ubatch_slices,
for_cudagraph_capture=is_graph_capturing,
)
with self.maybe_dummy_run_with_lora(self.lora_config, num_scheduled_tokens, remove_lora):
model_kwargs = self._init_model_kwargs(num_tokens)
with self.maybe_dummy_run_with_lora(
self.lora_config,
num_scheduled_tokens,
num_sampled_tokens,
activate_lora,
remove_lora,
):
# Make sure padding doesn't exceed max_num_tokens
assert num_tokens_padded <= self.max_num_tokens
model_kwargs = self._init_model_kwargs(num_tokens_padded)
if self.supports_mm_inputs and not self.model_config.is_encoder_decoder:
input_ids = None
inputs_embeds = self.inputs_embeds.gpu[:num_tokens]
inputs_embeds = self.inputs_embeds.gpu[:num_tokens_padded]
model_kwargs = {
**model_kwargs,
**self._dummy_mm_kwargs(num_reqs),
}
elif self.enable_prompt_embeds:
input_ids = None
inputs_embeds = self.inputs_embeds.gpu[:num_tokens]
model_kwargs = self._init_model_kwargs(num_tokens)
inputs_embeds = self.inputs_embeds.gpu[:num_tokens_padded]
model_kwargs = self._init_model_kwargs(num_tokens_padded)
else:
input_ids = self.input_ids.gpu[:num_tokens]
input_ids = self.input_ids.gpu[:num_tokens_padded]
inputs_embeds = None
if self.uses_mrope:
positions = self.mrope_positions.gpu[:, :num_tokens]
positions = self.mrope_positions.gpu[:, :num_tokens_padded]
elif self.uses_xdrope_dim > 0:
positions = self.xdrope_positions.gpu[:, :num_tokens_padded]
else:
positions = self.positions.gpu[:num_tokens]
positions = self.positions.gpu[:num_tokens_padded]
if get_pp_group().is_first_rank:
intermediate_tensors = None
@@ -534,46 +514,25 @@ class OmniGPUModelRunner(GPUModelRunner):
device=self.device,
)
intermediate_tensors = self.sync_and_slice_intermediate_tensors(num_tokens, None, False)
# filter out the valid batch descriptor
_cg_mode, batch_descriptor = (
self.cudagraph_dispatcher.dispatch(
BatchDescriptor(
num_tokens=num_tokens_after_padding,
uniform_decode=uniform_decode,
)
)
if not is_profile
else (CUDAGraphMode.NONE, None)
)
if cudagraph_runtime_mode is not None:
# we allow forcing NONE when the dispatcher disagrees to support
# warm ups for cudagraph capture
assert cudagraph_runtime_mode == CUDAGraphMode.NONE or cudagraph_runtime_mode == _cg_mode, (
f"Cudagraph runtime mode mismatch at dummy_run. "
f"Expected {_cg_mode}, but got {cudagraph_runtime_mode}."
)
else:
cudagraph_runtime_mode = _cg_mode
intermediate_tensors = self.sync_and_slice_intermediate_tensors(num_tokens_padded, None, False)
if ubatch_slices is not None:
# Adjust values to reflect a single ubatch.
# TODO(sage,lucas): this is cruft that should be addressed in
# the padding refactor.
num_tokens_after_padding = ubatch_slices[0].num_tokens
num_tokens_padded = ubatch_slices[0].num_tokens
if num_tokens_across_dp is not None:
num_tokens_across_dp[:] = num_tokens_after_padding
num_tokens_across_dp[:] = num_tokens_padded
with (
self.maybe_randomize_inputs(input_ids),
set_forward_context(
attn_metadata,
self.vllm_config,
num_tokens=num_tokens_after_padding,
num_tokens=num_tokens_padded,
num_tokens_across_dp=num_tokens_across_dp,
cudagraph_runtime_mode=cudagraph_runtime_mode,
batch_descriptor=batch_descriptor,
batch_descriptor=batch_desc,
ubatch_slices=ubatch_slices,
),
):
@@ -589,10 +548,26 @@ class OmniGPUModelRunner(GPUModelRunner):
hidden_states, _ = outputs
else:
hidden_states = outputs
hidden_states, multimodal_outputs = self.extract_multimodal_outputs(hidden_states)
if self.speculative_config and self.speculative_config.use_eagle():
assert isinstance(self.drafter, EagleProposer)
self.drafter.dummy_run(num_tokens)
use_cudagraphs = (
cudagraph_runtime_mode.has_mode(CUDAGraphMode.PIECEWISE)
and not self.speculative_config.enforce_eager
)
# Note(gnovack) - We need to disable cudagraphs for one of the two
# lora cases when cudagraph_specialize_lora is enabled. This is a
# short term mitigation for issue mentioned in
# https://github.com/vllm-project/vllm/issues/28334
if self.compilation_config.cudagraph_specialize_lora and activate_lora:
use_cudagraphs = False
self.drafter.dummy_run(
num_tokens,
use_cudagraphs=use_cudagraphs,
is_graph_capturing=is_graph_capturing,
)
# This is necessary to avoid blocking DP.
# For dummy runs, we typically skip EPLB since we don't have any real
@@ -605,110 +580,8 @@ class OmniGPUModelRunner(GPUModelRunner):
self.eplb_step(is_dummy=True, is_profile=is_profile)
logit_indices = np.cumsum(num_scheduled_tokens) - 1
hidden_states, multimodal_outputs = self.extract_multimodal_outputs(hidden_states)
return hidden_states, hidden_states[logit_indices]
def _preprocess(
self,
scheduler_output: "SchedulerOutput",
num_scheduled_tokens_np: np.ndarray,
intermediate_tensors: IntermediateTensors | None = None,
ubatch_slices: UBatchSlices | None = None,
num_tokens_after_padding: torch.Tensor | None = None,
) -> tuple[
int,
int,
torch.Tensor | None,
torch.Tensor | None,
torch.Tensor | None,
torch.Tensor,
IntermediateTensors | None,
dict[str, Any],
dict[str, dict] | None,
]:
num_scheduled_tokens = scheduler_output.total_num_scheduled_tokens
if ubatch_slices:
assert num_tokens_after_padding is not None
num_input_tokens = int(num_tokens_after_padding[0].item() * 2)
self.pad_out_ubatch_slice(ubatch_slices, num_input_tokens)
elif ubatch_slices is None:
num_input_tokens = self._get_num_input_tokens(num_scheduled_tokens)
num_pad, num_tokens_after_padding = self.get_dp_padding(num_input_tokens)
num_input_tokens += num_pad
# _prepare_inputs may reorder the batch, so we must gather multi
# modal outputs after that to ensure the correct order
per_req_additional_information: dict[str, dict] | None = None
if self.supports_mm_inputs and get_pp_group().is_first_rank and not self.model_config.is_encoder_decoder:
# Build multimodal inputs and overlay prompt embeds; collect per-request info
per_req_additional_information = self._build_mm_inputs_and_overlays(
scheduler_output, num_scheduled_tokens, num_scheduled_tokens_np
)
input_ids = self.input_ids.gpu[:num_input_tokens]
inputs_embeds = self.inputs_embeds.gpu[:num_input_tokens]
model_kwargs = {
**self._init_model_kwargs(num_scheduled_tokens),
**self._extract_mm_kwargs(scheduler_output),
}
elif self.enable_prompt_embeds and get_pp_group().is_first_rank:
# Get the input embeddings for the tokens that are not input embeds,
# then put them into the appropriate positions.
# TODO(qthequartermasterman): Since even when prompt embeds are
# enabled, (a) not all requests will use prompt embeds, and (b)
# after the initial prompt is processed, the rest of the generated
# tokens will be token ids, it is not desirable to have the
# embedding layer outside of the CUDA graph all the time. The v0
# engine avoids this by "double compiling" the CUDA graph, once
# with input_ids and again with inputs_embeds, for all num_tokens.
# If a batch only has token ids, then including the embedding layer
# in the CUDA graph will be more performant (like in the else case
# below).
token_ids_idx = self.is_token_ids.gpu[:num_scheduled_tokens].nonzero(as_tuple=False).squeeze(1)
# Some tokens ids may need to become embeds
if token_ids_idx.numel() > 0:
token_ids = self.input_ids.gpu[token_ids_idx]
tokens_to_embeds = self.model.get_input_embeddings(input_ids=token_ids)
self.inputs_embeds.gpu[token_ids_idx] = tokens_to_embeds
inputs_embeds = self.inputs_embeds.gpu[:num_input_tokens]
model_kwargs = self._init_model_kwargs(num_input_tokens)
input_ids = None
else:
# For text-only models, we use token ids as input.
# While it is possible to use embeddings as input just like the
# multimodal models, it is not desirable for performance since
# then the embedding layer is not included in the CUDA graph.
input_ids = self.input_ids.gpu[:num_input_tokens]
inputs_embeds = None
model_kwargs = self._init_model_kwargs(num_input_tokens)
if self.uses_mrope:
positions = self.mrope_positions.gpu[:, :num_input_tokens]
else:
positions = self.positions.gpu[:num_input_tokens]
if get_pp_group().is_first_rank:
intermediate_tensors = None
else:
intermediate_tensors = self.sync_and_slice_intermediate_tensors(
num_input_tokens, intermediate_tensors, True
)
if self.model_config.is_encoder_decoder and scheduler_output.scheduled_encoder_inputs:
encoder_inputs = self._extract_encoder_inputs(scheduler_output)
model_kwargs.update(encoder_inputs)
return (
num_scheduled_tokens,
num_input_tokens,
num_tokens_after_padding,
input_ids,
inputs_embeds,
positions,
intermediate_tensors,
model_kwargs,
per_req_additional_information,
)
logit_indices_device = torch.from_numpy(logit_indices).to(self.device, non_blocking=True)
return hidden_states, hidden_states[logit_indices_device]
def _decode_and_store_request_payloads(self, scheduler_output: "SchedulerOutput") -> None:
"""Decode per-request prompt_embeds and additional_information for newly
@@ -770,7 +643,14 @@ class OmniGPUModelRunner(GPUModelRunner):
for req_id in self.input_batch.req_ids:
req_state = self.requests.get(req_id)
info = getattr(req_state, "additional_information_cpu", None) if req_state is not None else None
per_req_runtime_info.append(info if isinstance(info, dict) else {})
if info and isinstance(info, dict):
per_req_runtime_info.append(info)
if "thinker_reply_part_per_request" in info:
q = info["thinker_reply_part_per_request"]
if hasattr(q, "shape"):
logger.debug(f"[OMNI] req={req_id} has thinker_reply_part_per_request queue shape: {q.shape}")
else:
per_req_runtime_info.append({})
return per_req_runtime_info
def _compute_request_token_spans(self, num_scheduled_tokens_np) -> list[tuple[int, int]]:
@@ -798,24 +678,34 @@ class OmniGPUModelRunner(GPUModelRunner):
model_kwargs_extra["additional_information_by_req_id"] = per_req_additional_information
try:
model_kwargs_extra["runtime_additional_information"] = self._gather_runtime_additional_information()
model_kwargs_extra["request_ids"] = self.input_batch.req_ids
model_kwargs_extra["request_token_spans"] = self._compute_request_token_spans(num_scheduled_tokens_np)
except Exception:
pass
model_kwargs_extra["request_ids"] = list(self.input_batch.req_ids)
if num_scheduled_tokens_np is not None:
model_kwargs_extra["request_token_spans"] = self._compute_request_token_spans(num_scheduled_tokens_np)
except Exception as e:
logger.error(f"[OMNI DEBUG] Error building model_kwargs_extra: {e}")
import traceback
traceback.print_exc()
return model_kwargs_extra
def _process_additional_information_updates(self, multimodal_outputs: object) -> None:
"""Process model-provided per-request additional_information updates and merge into request state."""
try:
if multimodal_outputs is None:
logger.debug("[OMNI] multimodal_outputs is None, no updates to process")
return
if not isinstance(multimodal_outputs, dict):
logger.debug(f"[OMNI] multimodal_outputs is not dict: {type(multimodal_outputs)}")
return
if (
"additional_information_update" not in multimodal_outputs
and "additional_information_update_by_req_id" not in multimodal_outputs
):
logger.debug(f"[OMNI] multimodal_outputs has no update keys: {list(multimodal_outputs.keys())}")
return
updates_list = multimodal_outputs.get("additional_information_update")
if isinstance(updates_list, list):
logger.debug(f"[OMNI] Processing additional_information_update list with {len(updates_list)} items")
for idx, upd in enumerate(updates_list):
if not isinstance(upd, dict) or idx >= len(self.input_batch.req_ids):
continue
@@ -823,18 +713,33 @@ class OmniGPUModelRunner(GPUModelRunner):
self._merge_additional_information_update(req_id, upd)
updates_map = multimodal_outputs.get("additional_information_update_by_req_id")
if isinstance(updates_map, dict):
logger.debug(
f"[OMNI] Processing additional_information_update_by_req_id for {list(updates_map.keys())}"
)
for req_id, upd in updates_map.items():
if not isinstance(upd, dict):
continue
if req_id not in self.requests:
logger.warning(f"[OMNI] req_id {req_id} not found in requests")
continue
self._merge_additional_information_update(req_id, upd)
req_state = self.requests.get(req_id)
if req_state:
info = getattr(req_state, "additional_information_cpu", {})
if "thinker_reply_part_per_request" in info:
q = info["thinker_reply_part_per_request"]
if hasattr(q, "shape"):
logger.debug(f"[OMNI] After update, req={req_id} queue shape: {q.shape}")
except Exception as e:
logger.error(
f"Error merging for requests:{self.input_batch.req_ids} "
f"additional information update: {e}, with the multimodal_outputs "
f"as {multimodal_outputs}"
)
import traceback
traceback.print_exc()
def _build_mm_inputs_and_overlays(
self,
@@ -895,3 +800,164 @@ class OmniGPUModelRunner(GPUModelRunner):
if req_info:
per_req_additional_information[req_id] = req_info
return per_req_additional_information
def _preprocess(
self,
scheduler_output: "SchedulerOutput",
num_input_tokens: int,
intermediate_tensors: IntermediateTensors | None = None,
):
"""Align with v0.12 preprocess and omni's additional information handling."""
# Decode payload first, ensure request state has prompt_embeds / additional_information
self._decode_and_store_request_payloads(scheduler_output)
num_scheduled_tokens = scheduler_output.total_num_scheduled_tokens
is_first_rank = get_pp_group().is_first_rank
is_encoder_decoder = self.model_config.is_encoder_decoder
# _prepare_inputs may reorder the batch, so we must gather multi
# modal outputs after that to ensure the correct order
ec_connector_output = None
if self.supports_mm_inputs and is_first_rank and not is_encoder_decoder:
# Run the multimodal encoder if any.
with self.maybe_get_ec_connector_output(
scheduler_output,
encoder_cache=self.encoder_cache,
) as ec_connector_output:
self._execute_mm_encoder(scheduler_output)
mm_embeds, is_mm_embed = self._gather_mm_embeddings(scheduler_output)
# NOTE(woosuk): To unify token ids and soft tokens (vision
# embeddings), we always use embeddings (rather than token ids)
# as input to the multimodal model, even when the input is text.
inputs_embeds_scheduled = self.model.embed_input_ids(
self.input_ids.gpu[:num_scheduled_tokens],
multimodal_embeddings=mm_embeds,
is_multimodal=is_mm_embed,
)
# TODO(woosuk): Avoid the copy. Optimize.
self.inputs_embeds.gpu[:num_scheduled_tokens].copy_(inputs_embeds_scheduled)
input_ids = self.input_ids.gpu[:num_input_tokens]
inputs_embeds = self.inputs_embeds.gpu[:num_input_tokens]
model_kwargs = {
**self._init_model_kwargs(num_scheduled_tokens),
**self._extract_mm_kwargs(scheduler_output),
}
elif self.enable_prompt_embeds and is_first_rank:
# Get the input embeddings for the tokens that are not input embeds,
# then put them into the appropriate positions.
# TODO(qthequartermasterman): Since even when prompt embeds are
# enabled, (a) not all requests will use prompt embeds, and (b)
# after the initial prompt is processed, the rest of the generated
# tokens will be token ids, it is not desirable to have the
# embedding layer outside of the CUDA graph all the time. The v0
# engine avoids this by "double compiling" the CUDA graph, once
# with input_ids and again with inputs_embeds, for all num_tokens.
# If a batch only has token ids, then including the embedding layer
# in the CUDA graph will be more performant (like in the else case
# below).
token_ids_idx = self.is_token_ids.gpu[:num_scheduled_tokens].nonzero(as_tuple=False).squeeze(1)
# Some tokens ids may need to become embeds
if token_ids_idx.numel() > 0:
token_ids = self.input_ids.gpu[token_ids_idx]
tokens_to_embeds = self.model.embed_input_ids(input_ids=token_ids)
self.inputs_embeds.gpu[token_ids_idx] = tokens_to_embeds
inputs_embeds = self.inputs_embeds.gpu[:num_input_tokens]
model_kwargs = self._init_model_kwargs(num_input_tokens)
input_ids = self.input_ids.gpu[:num_input_tokens]
else:
# For text-only models, we use token ids as input.
# While it is possible to use embeddings as input just like the
# multimodal models, it is not desirable for performance since
# then the embedding layer is not included in the CUDA graph.
input_ids = self.input_ids.gpu[:num_input_tokens]
inputs_embeds = None
model_kwargs = self._init_model_kwargs(num_input_tokens)
if self.uses_mrope:
positions = self.mrope_positions.gpu[:, :num_input_tokens]
elif self.uses_xdrope_dim > 0:
positions = self.xdrope_positions.gpu[:, :num_input_tokens]
else:
positions = self.positions.gpu[:num_input_tokens]
if is_first_rank:
intermediate_tensors = None
else:
assert intermediate_tensors is not None
intermediate_tensors = self.sync_and_slice_intermediate_tensors(
num_input_tokens, intermediate_tensors, True
)
if is_encoder_decoder and scheduler_output.scheduled_encoder_inputs:
# Run the encoder, just like we do with other multimodal inputs.
# For an encoder-decoder model, our processing here is a bit
# simpler, because the outputs are just passed to the decoder.
# We are not doing any prompt replacement. We also will only
# ever have a single encoder input.
encoder_outputs = self._execute_mm_encoder(scheduler_output)
model_kwargs.update({"encoder_outputs": encoder_outputs})
req_ids = self.input_batch.req_ids
num_scheduled_tokens_np = np.array(
[scheduler_output.num_scheduled_tokens[rid] for rid in req_ids],
dtype=np.int32,
)
self._omni_num_scheduled_tokens_np = num_scheduled_tokens_np
# Always collect additional_information, not only in prefill.
# _collect_additional_information_for_prefill handles prompt_embeds overlay during prefill,
# but even in decode we need per_req_additional_information so that
# _build_model_kwargs_extra can pass runtime_additional_information correctly.
per_req_additional_information: dict[str, dict] = {}
if inputs_embeds is not None:
# Prefill: overlay prompt_embeds and collect additional_information
per_req_additional_information = self._collect_additional_information_for_prefill(num_scheduled_tokens_np)
# Save for _model_forward regardless of prefill; runtime_additional_information
# will be fetched from request state inside _build_model_kwargs_extra
self._omni_per_req_additional_information = per_req_additional_information
return (
input_ids,
inputs_embeds,
positions,
intermediate_tensors,
model_kwargs,
ec_connector_output,
)
def _model_forward(
self,
input_ids: torch.Tensor | None = None,
positions: torch.Tensor | None = None,
intermediate_tensors: IntermediateTensors | None = None,
inputs_embeds: torch.Tensor | None = None,
**model_kwargs: dict[str, Any],
):
"""Inject omni-specific kwargs into forward and cache model output"""
model_kwargs_extra = self._build_model_kwargs_extra(
self._omni_per_req_additional_information,
self._omni_num_scheduled_tokens_np,
)
runtime_info = model_kwargs_extra.get("runtime_additional_information", [])
if runtime_info:
for i, info in enumerate(runtime_info):
if info:
logger.debug(f"[OMNI] req[{i}] runtime_additional_information keys: {list(info.keys())}")
model_output = super()._model_forward(
input_ids=input_ids,
positions=positions,
intermediate_tensors=intermediate_tensors,
inputs_embeds=inputs_embeds,
**model_kwargs,
**model_kwargs_extra,
)
# Cache model output so later sample_tokens can consume multimodal results.
self._omni_last_model_output = model_output
return model_output