[Feature] Support online inference (#64)
Signed-off-by: Gaohan123 <gaohan19@huawei.com>
This commit is contained in:
1 parent
bf82e18374
commit
c5a89302f1
52 files changed
+3666
-685
No files matched your search
@@ -1,51 +0,0 @@
|
||||
name: Deploy Documentation
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- 'mkdocs.yml'
|
||||
- 'vllm_omni/**'
|
||||
- '.github/workflows/docs.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.10'
|
||||
cache: 'pip'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install -e ".[docs]"
|
||||
|
||||
- name: Build documentation
|
||||
run: mkdocs build
|
||||
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v4
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: ./site
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
|
||||
@@ -26,4 +26,4 @@ repos:
|
||||
rev: 7.1.1
|
||||
hooks:
|
||||
- id: flake8
|
||||
args: ["--max-line-length=88", "--extend-ignore=E203,W503"]
|
||||
args: ["--max-line-length=160", "--extend-ignore=E203,W503"]
|
||||
@@ -100,15 +100,7 @@ uv pip install -e .
|
||||
|
||||
## Run examples (Qwen2.5-omni)
|
||||
|
||||
Get into the example folder
|
||||
```bash
|
||||
cd examples/offline_inference/qwen_2_5_omni
|
||||
```
|
||||
Modify PYTHONPATH in run.sh as your path of vllm_omni. Then run.
|
||||
```bash
|
||||
bash run.sh
|
||||
```
|
||||
The output audio is saved in ./output_audio
|
||||
Please check the folder of [examples](examples)
|
||||
|
||||
## Further details
|
||||
|
||||
|
||||
+3
-3
@@ -40,15 +40,15 @@ Example docstring:
|
||||
```python
|
||||
class OmniLLM:
|
||||
"""Main entry point for vLLM-omni inference.
|
||||
|
||||
|
||||
This class provides a high-level interface for running multi-modal
|
||||
inference with non-autoregressive models.
|
||||
|
||||
|
||||
Args:
|
||||
model: Model name or path
|
||||
stage_configs: Optional stage configurations
|
||||
**kwargs: Additional arguments passed to the engine
|
||||
|
||||
|
||||
Example:
|
||||
>>> llm = OmniLLM(model="Qwen/Qwen2.5-Omni")
|
||||
>>> outputs = llm.generate(prompts="Hello")
|
||||
|
||||
@@ -66,4 +66,3 @@ Worker classes and model runners for distributed inference.
|
||||
- [vllm_omni.worker.gpu_model_runner.OmniGPUModelRunner][]
|
||||
- [vllm_omni.worker.gpu_ar_model_runner.GPUARModelRunner][]
|
||||
- [vllm_omni.worker.gpu_diffusion_model_runner.GPUDiffusionModelRunner][]
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
This document contains comprehensive API design documentation for all core modules in vLLM-omni. These templates provide a standardized structure for designing and implementing the core, engine, executor, and worker modules.
|
||||
|
||||
## 📋 Module API
|
||||
## 📋 Module API
|
||||
|
||||
### Core Module API
|
||||
**Core module** provides fundamental scheduling, caching, and resource management functionality.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Architecture Overview
|
||||
# Architecture Overview
|
||||
|
||||
# Introduction
|
||||
|
||||
|
||||
@@ -10,4 +10,3 @@ This section contains design documents and architecture specifications for vLLM-
|
||||
## API Design Documentation
|
||||
|
||||
- [vLLM-omni API Documentation](api_design_doc.md)
|
||||
|
||||
+1
-2
@@ -28,6 +28,5 @@
|
||||
|
||||
## <span class="twemoji">📚</span> Documentation Navigation
|
||||
|
||||
- To run open-source models on vLLM-Omni, we recommend starting with the [:material-code-tags: User Quide](user_guide/getting_started/quickstart.md)
|
||||
- To run open-source models on vLLM-Omni, we recommend starting with the [:material-code-tags: User Quide](user_guide/getting_started/quickstart.md)
|
||||
- To develop and contribute to vLLM-Omni, we recommend starting with the [:material-tools: Developer Guide](contributing/README.md)
|
||||
|
||||
@@ -103,11 +103,20 @@ class Example:
|
||||
if self.path.is_file():
|
||||
return []
|
||||
# Text file extensions that can be safely included
|
||||
text_extensions = {".py", ".md", ".sh", ".yaml", ".yml", ".json", ".txt", ".toml", ".cfg", ".ini"}
|
||||
is_other_file = lambda file: (
|
||||
file.is_file()
|
||||
and file != self.main_file
|
||||
and file.suffix in text_extensions
|
||||
text_extensions = {
|
||||
".py",
|
||||
".md",
|
||||
".sh",
|
||||
".yaml",
|
||||
".yml",
|
||||
".json",
|
||||
".txt",
|
||||
".toml",
|
||||
".cfg",
|
||||
".ini",
|
||||
}
|
||||
is_other_file = lambda file: ( # noqa: E731
|
||||
file.is_file() and file != self.main_file and file.suffix in text_extensions
|
||||
)
|
||||
return [file for file in self.path.rglob("*") if is_other_file(file)]
|
||||
|
||||
@@ -172,7 +181,7 @@ class Example:
|
||||
f"{code_fence}\n"
|
||||
)
|
||||
else:
|
||||
with open(self.main_file) as f:
|
||||
with open(self.main_file, encoding="utf-8") as f:
|
||||
# Skip the title from md snippets as it's been included above
|
||||
main_content = f.readlines()[1:]
|
||||
content += self.fix_relative_links("".join(main_content))
|
||||
|
||||
@@ -53,4 +53,3 @@ body[data-md-color-scheme="slate"] .md-nav__item--section > label.md-nav__link .
|
||||
.md-nav__item--section:has([href*="api/vllm_omni/index"]) > .md-nav > .md-nav__list > .md-nav__item--nested > .md-nav > .md-nav__list > .md-nav__item--nested.md-nav__item--active > .md-nav {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -6,4 +6,3 @@ vLLM-omni's examples are split into two categories:
|
||||
- If you are using vLLM-omni from an HTTP application or client, see the *Online Serving* section.
|
||||
|
||||
For detailed example documentation, check the [examples directory](https://github.com/vllm-project/vllm-omni/tree/main/examples) in the repository.
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
# Offline Example of vLLM-omni for Qwen2.5-omni
|
||||
|
||||
Source <https://github.com/vllm-project/vllm-omni/tree/main/examples\offline_inference\qwen2_5_omni>.
|
||||
|
||||
|
||||
## 🛠️ Installation
|
||||
|
||||
Please refer to [README.md](https://github.com/vllm-project/vllm-omni/tree/main/README.md)
|
||||
|
||||
## Run examples (Qwen2.5-omni)
|
||||
### Multiple Prompts
|
||||
Download dataset from [seed_tts](https://drive.google.com/file/d/1GlSjVfSHkW3-leKKBlfrjuuTGqQ_xaLP/edit). To get the prompt, you can:
|
||||
```bash
|
||||
tar -xf <Your Download Path>/seedtts_testset.tar
|
||||
cp seedtts_testset/en/meta.lst examples/offline_inference/qwen2_5_omni/meta.lst
|
||||
python3 examples/offline_inference/qwen2_5_omni/extract_prompts.py \
|
||||
--input examples/offline_inference/qwen2_5_omni/meta.lst \
|
||||
--output examples/offline_inference/qwen2_5_omni/top100.txt \
|
||||
--topk 100
|
||||
```
|
||||
Get into the example folder
|
||||
```bash
|
||||
cd examples/offline_inference/qwen2_5_omni
|
||||
```
|
||||
Then run the command below.
|
||||
```bash
|
||||
bash run_multiple_prompts.sh
|
||||
```
|
||||
### Single Prompts
|
||||
Get into the example folder
|
||||
```bash
|
||||
cd examples/offline_inference/qwen2_5_omni
|
||||
```
|
||||
Then run the command below.
|
||||
```bash
|
||||
bash run_single_prompt.sh
|
||||
```
|
||||
|
||||
## Example materials
|
||||
|
||||
??? abstract "end2end.py"
|
||||
``````py
|
||||
--8<-- "examples\offline_inference\qwen2_5_omni\end2end.py"
|
||||
``````
|
||||
??? abstract "extract_prompts.py"
|
||||
``````py
|
||||
--8<-- "examples\offline_inference\qwen2_5_omni\extract_prompts.py"
|
||||
``````
|
||||
??? abstract "processing_omni.py"
|
||||
``````py
|
||||
--8<-- "examples\offline_inference\qwen2_5_omni\processing_omni.py"
|
||||
``````
|
||||
??? abstract "run_multiple_prompts.sh"
|
||||
``````sh
|
||||
--8<-- "examples\offline_inference\qwen2_5_omni\run_multiple_prompts.sh"
|
||||
``````
|
||||
??? abstract "run_single_prompt.sh"
|
||||
``````sh
|
||||
--8<-- "examples\offline_inference\qwen2_5_omni\run_single_prompt.sh"
|
||||
``````
|
||||
??? abstract "utils.py"
|
||||
``````py
|
||||
--8<-- "examples\offline_inference\qwen2_5_omni\utils.py"
|
||||
``````
|
||||
@@ -0,0 +1,35 @@
|
||||
# Online serving Example of vLLM-omni for Qwen2.5-omni
|
||||
|
||||
Source <https://github.com/vllm-project/vllm-omni/blob/main/examples\online_serving\README.md>.
|
||||
|
||||
|
||||
## 🛠️ Installation
|
||||
|
||||
Please refer to [README.md](https://github.com/vllm-project/vllm-omni/blob/main/README.md)
|
||||
|
||||
## Run examples (Qwen2.5-omni)
|
||||
|
||||
Launch the server
|
||||
```bash
|
||||
vllm serve Qwen/Qwen2.5-Omni-7B --omni --port 8091
|
||||
```
|
||||
|
||||
If you have custom stage configs file, launch the server with command below
|
||||
```bash
|
||||
vllm serve Qwen/Qwen2.5-Omni-7B --omni --port 8091 --stage-configs-path /path/to/stage_configs_file
|
||||
```
|
||||
|
||||
Get into the example folder
|
||||
```bash
|
||||
cd examples/online_serving
|
||||
```
|
||||
|
||||
Send request via python
|
||||
```bash
|
||||
python openai_chat_completion_client_for_multimodal_generation.py
|
||||
```
|
||||
|
||||
Send request via curl
|
||||
```bash
|
||||
bash run_curl_multimodal_generation.sh
|
||||
```
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
# OpenAI Chat Completion Client For Multimodal Generation
|
||||
|
||||
Source <https://github.com/vllm-project/vllm-omni/blob/main/examples\online_serving\openai_chat_completion_client_for_multimodal_generation.py>.
|
||||
|
||||
``````py
|
||||
--8<-- "examples\online_serving\openai_chat_completion_client_for_multimodal_generation.py"
|
||||
``````
|
||||
@@ -53,4 +53,3 @@ The output audio is saved in ./output_audio
|
||||
- Read the [architecture documentation](../../contributing/design_documents/vllm_omni_design.md)
|
||||
- Check out the [API reference](../../api/overview.md)
|
||||
- Explore the [examples](../examples/index.md)
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# Offline Example of vLLM-omni for Qwen2.5-omni
|
||||
|
||||
## 🛠️ Installation
|
||||
|
||||
Please refer to [README.md](../../../README.md)
|
||||
|
||||
## Run examples (Qwen2.5-omni)
|
||||
### Multiple Prompts
|
||||
Download dataset from [seed_tts](https://drive.google.com/file/d/1GlSjVfSHkW3-leKKBlfrjuuTGqQ_xaLP/edit). To get the prompt, you can:
|
||||
```bash
|
||||
tar -xf <Your Download Path>/seedtts_testset.tar
|
||||
cp seedtts_testset/en/meta.lst examples/offline_inference/qwen2_5_omni/meta.lst
|
||||
python3 examples/offline_inference/qwen2_5_omni/extract_prompts.py \
|
||||
--input examples/offline_inference/qwen2_5_omni/meta.lst \
|
||||
--output examples/offline_inference/qwen2_5_omni/top100.txt \
|
||||
--topk 100
|
||||
```
|
||||
Get into the example folder
|
||||
```bash
|
||||
cd examples/offline_inference/qwen2_5_omni
|
||||
```
|
||||
Then run the command below.
|
||||
```bash
|
||||
bash run_multiple_prompts.sh
|
||||
```
|
||||
### Single Prompts
|
||||
Get into the example folder
|
||||
```bash
|
||||
cd examples/offline_inference/qwen2_5_omni
|
||||
```
|
||||
Then run the command below.
|
||||
```bash
|
||||
bash run_single_prompt.sh
|
||||
```
|
||||
+13
-5
@@ -7,8 +7,8 @@ import numpy as np
|
||||
import soundfile as sf
|
||||
import torch
|
||||
from utils import make_omni_prompt
|
||||
from vllm.sampling_params import SamplingParams
|
||||
|
||||
from vllm.sampling_params import SamplingParams
|
||||
from vllm_omni.entrypoints.omni_llm import OmniLLM
|
||||
|
||||
_os_env_toggle.environ["VLLM_USE_V1"] = "1"
|
||||
@@ -109,10 +109,14 @@ def parse_args():
|
||||
parser.add_argument("--use-torchvision", action="store_true")
|
||||
parser.add_argument("--tokenize", action="store_true")
|
||||
parser.add_argument(
|
||||
"--output-wav", default="output.wav", help="[Deprecated] Output wav directory (use --output-dir)."
|
||||
"--output-wav",
|
||||
default="output.wav",
|
||||
help="[Deprecated] Output wav directory (use --output-dir).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output-dir", default="outputs", help="Output directory to save text and wav files together."
|
||||
"--output-dir",
|
||||
default="outputs",
|
||||
help="Output directory to save text and wav files together.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--thinker-hidden-states-dir",
|
||||
@@ -168,7 +172,9 @@ def main():
|
||||
raise
|
||||
|
||||
if args.prompts is None:
|
||||
raise ValueError("No prompts provided. Use --prompts ... or --txt-prompts <file.txt> (with --prompt_type text)")
|
||||
raise ValueError(
|
||||
"No prompts provided. Use --prompts ... or --txt-prompts <file.txt> (with --prompt_type text)"
|
||||
)
|
||||
omni_llm = OmniLLM(
|
||||
model=model_name,
|
||||
log_stats=args.enable_stats,
|
||||
@@ -217,7 +223,9 @@ def main():
|
||||
omni_outputs = omni_llm.generate(prompt, sampling_params_list)
|
||||
|
||||
# Determine output directory: prefer --output-dir; fallback to --output-wav
|
||||
output_dir = args.output_dir if getattr(args, "output_dir", None) else args.output_wav
|
||||
output_dir = (
|
||||
args.output_dir if getattr(args, "output_dir", None) else args.output_wav
|
||||
)
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
for stage_outputs in omni_outputs:
|
||||
if stage_outputs.final_output_type == "text":
|
||||
+10
-8
@@ -1,26 +1,30 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
from typing import Optional
|
||||
import os
|
||||
import torch
|
||||
|
||||
|
||||
def extract_prompt(line: str) -> Optional[str]:
|
||||
# 提取第一个 '|' 与第二个 '|' 之间的内容
|
||||
i = line.find('|')
|
||||
i = line.find("|")
|
||||
if i == -1:
|
||||
return None
|
||||
j = line.find('|', i + 1)
|
||||
j = line.find("|", i + 1)
|
||||
if j == -1:
|
||||
return None
|
||||
return line[i + 1:j].strip()
|
||||
return line[i + 1 : j].strip()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--input", "-i", required=True, help="Input .lst file path")
|
||||
parser.add_argument("--output", "-o", required=True, help="Output file path")
|
||||
parser.add_argument("--topk", "-k", type=int, default=100, help="Extract the top K prompts (default: 100)")
|
||||
parser.add_argument(
|
||||
"--topk",
|
||||
"-k",
|
||||
type=int,
|
||||
default=100,
|
||||
help="Extract the top K prompts (default: 100)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
prompts = []
|
||||
@@ -39,5 +43,3 @@ def main() -> None:
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
|
||||
File renamed without changes.
-1
@@ -1,4 +1,3 @@
|
||||
export PYTHONPATH=/path/to/vllm-omni:$PYTHONPATH
|
||||
python end2end.py --model Qwen/Qwen2.5-Omni-7B \
|
||||
--voice-type "m02" \
|
||||
--dit-ckpt none \
|
||||
-1
@@ -1,4 +1,3 @@
|
||||
export PYTHONPATH=/path/to/vllm-omni:$PYTHONPATH
|
||||
python end2end.py --model Qwen/Qwen2.5-Omni-7B \
|
||||
--voice-type "m02" \
|
||||
--dit-ckpt none \
|
||||
+1
@@ -9,6 +9,7 @@ import soundfile as sf
|
||||
import torch
|
||||
import torchvision.io
|
||||
from processing_omni import fetch_image, fetch_video
|
||||
from transformers import AutoConfig, AutoProcessor
|
||||
|
||||
from vllm.inputs import TextPrompt
|
||||
from vllm_omni.inputs.data import OmniTokensPrompt
|
||||
@@ -1,92 +0,0 @@
|
||||
# Offline Example of vLLM-omni for Qwen2.5-omni
|
||||
|
||||
## 🛠️ Installation
|
||||
|
||||
### Environment setup
|
||||
|
||||
Use Docker to keep consistent basic environment (Optional, Recommended)
|
||||
```bash
|
||||
docker run --gpus all --ipc=host --network=host -v $source_dir:$container_dir --name $container_name -it nvcr.io/nvidia/pytorch:25.01-py3 bash
|
||||
```
|
||||
|
||||
Set up basic uv environment
|
||||
```bash
|
||||
pip install uv
|
||||
uv venv --python 3.12 --seed
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
### Installation of vLLM (for users)
|
||||
|
||||
Now we build it based on vLLM v0.11.0. Please install it with command below.
|
||||
```bash
|
||||
uv pip install vllm==0.11.0 --torch-backend=auto
|
||||
```
|
||||
|
||||
### Installation of vLLM (for developers)
|
||||
|
||||
If you want to check or debug with source code of vLLM, install stable release version of vllm with 0.11.0 from source with pre-built wheel file.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/vllm-project/vllm.git
|
||||
cd vllm
|
||||
git checkout v0.11.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
|
||||
```
|
||||
Install vllm with command below (If you have no existing PyTorch).
|
||||
```bash
|
||||
uv pip install --editable .
|
||||
```
|
||||
|
||||
Install vllm with command below (If you already have PyTorch).
|
||||
```bash
|
||||
python use_existing_torch.py
|
||||
uv pip install -r requirements/build.txt
|
||||
uv pip install --no-build-isolation --editable .
|
||||
```
|
||||
|
||||
### Verification for successful installation of vLLM
|
||||
Just run the command below. If no error, it demonstrates that the installation is successfull.
|
||||
```bash
|
||||
python -c "import vllm._C"
|
||||
```
|
||||
|
||||
### Installation of vLLM-omni
|
||||
Install additional requirements for vllm-omni
|
||||
```bash
|
||||
git clone https://github.com/vllm-project/vllm-omni.git
|
||||
cd vllm_omni
|
||||
uv pip install -e .
|
||||
```
|
||||
|
||||
## Run examples (Qwen2.5-omni)
|
||||
### Multiple Prompts
|
||||
Download dataset from [seed_tts](https://drive.google.com/file/d/1GlSjVfSHkW3-leKKBlfrjuuTGqQ_xaLP/edit). To get the prompt, you can:
|
||||
```bash
|
||||
tar -xf <Your Download Path>/seedtts_testset.tar
|
||||
cp seedtts_testset/en/meta.lst examples/offline_inference/qwen_2_5_omni/meta.lst
|
||||
python3 examples/offline_inference/qwen_2_5_omni/extract_prompts.py \
|
||||
--input examples/offline_inference/qwen_2_5_omni/meta.lst \
|
||||
--output examples/offline_inference/qwen_2_5_omni/top100.txt \
|
||||
--topk 100
|
||||
```
|
||||
Get into the example folder
|
||||
```bash
|
||||
cd examples/offline_inference/qwen_2_5_omni
|
||||
```
|
||||
Modify PYTHONPATH in run_multiple_prompts.sh as your path of vllm_omni. Then run.
|
||||
```bash
|
||||
bash run_multiple_prompts.sh
|
||||
```
|
||||
### Single Prompts
|
||||
Get into the example folder
|
||||
```bash
|
||||
cd examples/offline_inference/qwen_2_5_omni
|
||||
```
|
||||
Modify PYTHONPATH in run_single_prompt.sh as your path of vllm_omni. Then run.
|
||||
```bash
|
||||
bash run_single_prompt.sh
|
||||
```
|
||||
@@ -0,0 +1,32 @@
|
||||
# Online serving Example of vLLM-omni for Qwen2.5-omni
|
||||
|
||||
## 🛠️ Installation
|
||||
|
||||
Please refer to [README.md](../../README.md)
|
||||
|
||||
## Run examples (Qwen2.5-omni)
|
||||
|
||||
Launch the server
|
||||
```bash
|
||||
vllm serve Qwen/Qwen2.5-Omni-7B --omni --port 8091
|
||||
```
|
||||
|
||||
If you have custom stage configs file, launch the server with command below
|
||||
```bash
|
||||
vllm serve Qwen/Qwen2.5-Omni-7B --omni --port 8091 --stage-configs-path /path/to/stage_configs_file
|
||||
```
|
||||
|
||||
Get into the example folder
|
||||
```bash
|
||||
cd examples/online_serving
|
||||
```
|
||||
|
||||
Send request via python
|
||||
```bash
|
||||
python openai_chat_completion_client_for_multimodal_generation.py
|
||||
```
|
||||
|
||||
Send request via curl
|
||||
```bash
|
||||
bash run_curl_multimodal_generation.sh
|
||||
```
|
||||
@@ -0,0 +1,104 @@
|
||||
import base64
|
||||
|
||||
from openai import OpenAI
|
||||
|
||||
# Modify OpenAI's API key and API base to use vLLM's API server.
|
||||
openai_api_key = "EMPTY"
|
||||
openai_api_base = "http://localhost:8091/v1"
|
||||
|
||||
client = OpenAI(
|
||||
# defaults to os.environ.get("OPENAI_API_KEY")
|
||||
api_key=openai_api_key,
|
||||
base_url=openai_api_base,
|
||||
)
|
||||
|
||||
SEED = 42
|
||||
|
||||
|
||||
def get_system_prompt():
|
||||
|
||||
return {
|
||||
"role": "system",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": (
|
||||
"You are Qwen, a virtual human developed by the Qwen Team, "
|
||||
"Alibaba Group, capable of perceiving auditory and visual inputs, "
|
||||
"as well as generating text and speech."
|
||||
),
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def run_text_to_audio(model: str) -> None:
|
||||
thinker_sampling_params = {
|
||||
"temperature": 0.0, # Deterministic - no randomness
|
||||
"top_p": 1.0, # Disable nucleus sampling
|
||||
"top_k": -1, # Disable top-k sampling
|
||||
"max_tokens": 2048,
|
||||
"seed": SEED, # Fixed seed for sampling
|
||||
"detokenize": True,
|
||||
"repetition_penalty": 1.1,
|
||||
}
|
||||
talker_sampling_params = {
|
||||
"temperature": 0.9,
|
||||
"top_p": 0.8,
|
||||
"top_k": 40,
|
||||
"max_tokens": 2048,
|
||||
"seed": SEED, # Fixed seed for sampling
|
||||
"detokenize": True,
|
||||
"repetition_penalty": 1.05,
|
||||
"stop_token_ids": [8294],
|
||||
}
|
||||
code2wav_sampling_params = {
|
||||
"temperature": 0.0, # Deterministic - no randomness
|
||||
"top_p": 1.0, # Disable nucleus sampling
|
||||
"top_k": -1, # Disable top-k sampling
|
||||
"max_tokens": 2048,
|
||||
"seed": SEED, # Fixed seed for sampling
|
||||
"detokenize": True,
|
||||
"repetition_penalty": 1.1,
|
||||
}
|
||||
|
||||
sampling_params_list = [
|
||||
thinker_sampling_params,
|
||||
talker_sampling_params,
|
||||
code2wav_sampling_params,
|
||||
]
|
||||
|
||||
chat_completion = client.chat.completions.create(
|
||||
messages=[
|
||||
get_system_prompt(),
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Explain the system architecture for a scalable audio generation pipeline. Answer in 15 words.",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
model=model,
|
||||
extra_body={
|
||||
"sampling_params_list": sampling_params_list
|
||||
}, # Optional, it has a default setting in stage_configs of the corresponding model.
|
||||
)
|
||||
|
||||
count = 0
|
||||
for choice in chat_completion.choices:
|
||||
if choice.message.audio:
|
||||
audio_data = base64.b64decode(choice.message.audio.data)
|
||||
audio_file_path = f"audio_{count}.wav"
|
||||
with open(audio_file_path, "wb") as f:
|
||||
f.write(audio_data)
|
||||
print(f"Audio saved to {audio_file_path}")
|
||||
count += 1
|
||||
elif choice.message.content:
|
||||
print("Chat completion output from text:", choice.message.content)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_text_to_audio("Qwen/Qwen2.5-Omni-7B")
|
||||
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SEED=42
|
||||
|
||||
thinker_sampling_params='{
|
||||
"temperature": 0.0,
|
||||
"top_p": 1.0,
|
||||
"top_k": -1,
|
||||
"max_tokens": 2048,
|
||||
"seed": 42,
|
||||
"detokenize": true,
|
||||
"repetition_penalty": 1.1
|
||||
}'
|
||||
|
||||
talker_sampling_params='{
|
||||
"temperature": 0.9,
|
||||
"top_p": 0.8,
|
||||
"top_k": 40,
|
||||
"max_tokens": 2048,
|
||||
"seed": 42,
|
||||
"detokenize": true,
|
||||
"repetition_penalty": 1.05,
|
||||
"stop_token_ids": [8294]
|
||||
}'
|
||||
|
||||
code2wav_sampling_params='{
|
||||
"temperature": 0.0,
|
||||
"top_p": 1.0,
|
||||
"top_k": -1,
|
||||
"max_tokens": 2048,
|
||||
"seed": 42,
|
||||
"detokenize": true,
|
||||
"repetition_penalty": 1.1
|
||||
}'
|
||||
# Above is optional, it has a default setting in stage_configs of the corresponding model.
|
||||
|
||||
output=$(curl -sS -X POST http://localhost:8091/v1/chat/completions \
|
||||
-H "Content-Type: application/json" \
|
||||
-d @- <<EOF
|
||||
{
|
||||
"model": "Qwen/Qwen2.5-Omni-7B",
|
||||
"extra_body": {
|
||||
"sampling_params_list": [
|
||||
$thinker_sampling_params,
|
||||
$talker_sampling_params,
|
||||
$code2wav_sampling_params
|
||||
]
|
||||
},
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "You are Qwen, a virtual human developed by the Qwen Team, Alibaba Group, capable of perceiving auditory and visual inputs, as well as generating text and speech."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Explain the system architecture for a scalable audio generation pipeline. Answer in 15 words."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
# Here it only shows the text content of the first choice. Audio content has many binaries, so it's not displayed here.
|
||||
echo "Output of request: $(echo "$output" | jq '.choices[0].message.content')"
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "vllm-omni"
|
||||
version = "0.1.0"
|
||||
version = "0.11.0rc1"
|
||||
description = "vLLM-omni: Multi-modality models inference and serving with non-autoregressive structures"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.9,<3.14" # Align with vLLM v0.11 supported Python versions
|
||||
|
||||
@@ -20,14 +20,20 @@ except ModuleNotFoundError as exc: # pragma: no cover - optional dependency
|
||||
# Allow importing vllm_omni without vllm (e.g., documentation builds)
|
||||
patch = None # type: ignore
|
||||
from .config import OmniModelConfig
|
||||
from .entrypoints.async_omni_llm import AsyncOmniLLM
|
||||
|
||||
# Main entry points
|
||||
from .entrypoints.omni_llm import OmniLLM
|
||||
|
||||
# from .version import __version__, __version_tuple__ # isort:skip
|
||||
|
||||
|
||||
__all__ = [
|
||||
# "__version__", # TODO: add __version__ back when it is implemented
|
||||
# "__version_tuple__", # TODO: add __version_tuple__ back when it is implemented
|
||||
# Main components
|
||||
"OmniLLM",
|
||||
# "AsyncOmniLLM", # TODO: add AsyncOmniLLM back when it is implemented
|
||||
"AsyncOmniLLM",
|
||||
# Configuration
|
||||
"OmniModelConfig",
|
||||
# All other components are available through their respective modules
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
from vllm.engine.arg_utils import EngineArgs
|
||||
from vllm.v1.engine.async_llm import AsyncEngineArgs
|
||||
from vllm_omni.config import OmniModelConfig
|
||||
|
||||
|
||||
@@ -31,3 +32,31 @@ class OmniEngineArgs(EngineArgs):
|
||||
omni_config.hf_config.architectures = omni_config.architectures
|
||||
|
||||
return omni_config
|
||||
|
||||
|
||||
@dataclass
|
||||
class AsyncOmniEngineArgs(AsyncEngineArgs):
|
||||
stage_id: int = 0
|
||||
model_stage: str = "thinker"
|
||||
model_arch: str = "Qwen2_5OmniForConditionalGeneration"
|
||||
engine_output_type: Optional[str] = None
|
||||
|
||||
def create_model_config(self) -> OmniModelConfig:
|
||||
# First, get the base ModelConfig from the parent class
|
||||
base_config = super().create_model_config()
|
||||
|
||||
# Create OmniModelConfig by copying all base config attributes
|
||||
# and adding the new omni-specific fields
|
||||
config_dict = base_config.__dict__.copy()
|
||||
|
||||
# Add the new omni-specific fields
|
||||
config_dict["stage_id"] = self.stage_id
|
||||
config_dict["model_stage"] = self.model_stage
|
||||
config_dict["model_arch"] = self.model_arch
|
||||
config_dict["engine_output_type"] = self.engine_output_type
|
||||
|
||||
# Create and return the OmniModelConfig instance
|
||||
omni_config = OmniModelConfig(**config_dict)
|
||||
omni_config.hf_config.architectures = omni_config.architectures
|
||||
|
||||
return omni_config
|
||||
@@ -7,6 +7,7 @@ import torch
|
||||
from vllm.config import VllmConfig
|
||||
from vllm.inputs import ProcessorInputs, PromptType
|
||||
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
|
||||
@@ -25,6 +26,8 @@ from vllm_omni.engine import (
|
||||
)
|
||||
from vllm_omni.inputs.preprocess import OmniInputPreprocessor
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
class OmniProcessor(Processor):
|
||||
@staticmethod
|
||||
|
||||
@@ -0,0 +1,778 @@
|
||||
import asyncio
|
||||
import multiprocessing as mp
|
||||
import os
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
from collections.abc import AsyncGenerator, Iterable
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from typing import Any, Mapping, Optional, Sequence, Union
|
||||
|
||||
import torch
|
||||
|
||||
# External library imports (vLLM)
|
||||
import vllm.envs as envs
|
||||
from vllm.config import VllmConfig
|
||||
from vllm.engine.protocol import EngineClient
|
||||
from vllm.inputs import PromptType
|
||||
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.sampling_params import SamplingParams
|
||||
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.v1.engine.async_llm import AsyncLLM
|
||||
from vllm.v1.engine.core_client import EngineCoreClient
|
||||
from vllm.v1.engine.exceptions import EngineDeadError
|
||||
from vllm.v1.executor.abstract import Executor
|
||||
from vllm.v1.metrics.loggers import StatLoggerFactory, StatLoggerManager
|
||||
|
||||
# Internal imports (our code)
|
||||
from vllm_omni.config import OmniModelConfig
|
||||
from vllm_omni.engine.arg_utils import AsyncOmniEngineArgs
|
||||
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,
|
||||
init_stats_paths,
|
||||
remove_old_logs,
|
||||
)
|
||||
from vllm_omni.entrypoints.omni_stage import OmniStage
|
||||
from vllm_omni.entrypoints.stage_utils import encode_for_ipc as _encode
|
||||
from vllm_omni.entrypoints.stage_utils import maybe_load_from_ipc as _load
|
||||
from vllm_omni.entrypoints.stage_utils import serialize_obj as _ser
|
||||
from vllm_omni.entrypoints.utils import (
|
||||
load_stage_configs_from_model,
|
||||
load_stage_configs_from_yaml,
|
||||
)
|
||||
from vllm_omni.outputs import OmniRequestOutput
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
class AsyncOmniLLM(EngineClient):
|
||||
def __init__(
|
||||
self,
|
||||
model: str,
|
||||
stage_configs_path: Optional[str] = None,
|
||||
log_stats: bool = False,
|
||||
log_file: Optional[str] = None,
|
||||
init_sleep_seconds: int = 20,
|
||||
shm_threshold_bytes: int = 65536,
|
||||
batch_timeout: int = 10,
|
||||
init_timeout: int = 300,
|
||||
**kwargs,
|
||||
):
|
||||
|
||||
self.batch_timeout = batch_timeout
|
||||
self._enable_stats: bool = bool(log_stats)
|
||||
|
||||
if stage_configs_path is None:
|
||||
self.stage_configs = load_stage_configs_from_model(model)
|
||||
else:
|
||||
self.stage_configs = load_stage_configs_from_yaml(stage_configs_path)
|
||||
|
||||
self.stage_list: list[OmniStage] = []
|
||||
self.default_sampling_params_list: list[SamplingParams] = []
|
||||
# Optional file handler for orchestrator
|
||||
self._log_file = log_file
|
||||
if self._log_file:
|
||||
remove_old_logs(self._log_file, len(self.stage_list))
|
||||
configure_orchestrator_logger(logger, self._log_file)
|
||||
|
||||
self._stats_file, self._overall_stats_file = init_stats_paths(
|
||||
self._enable_stats, self._log_file
|
||||
)
|
||||
self._initialize_stages(
|
||||
model, init_sleep_seconds, shm_threshold_bytes, init_timeout
|
||||
)
|
||||
|
||||
def _initialize_stages(
|
||||
self,
|
||||
model: str,
|
||||
init_sleep_seconds: int,
|
||||
shm_threshold_bytes: int,
|
||||
init_timeout: int,
|
||||
) -> None:
|
||||
self.stage_list: list[OmniStage] = []
|
||||
|
||||
# Build OmniStage instances in parallel, preserve original order
|
||||
def _build_stage(idx_cfg: tuple[int, Any]) -> tuple[int, OmniStage]:
|
||||
idx, cfg = idx_cfg
|
||||
return idx, OmniStage(cfg)
|
||||
|
||||
with ThreadPoolExecutor(
|
||||
max_workers=min(len(self.stage_configs), max(1, os.cpu_count() or 1))
|
||||
) as executor:
|
||||
futures = [
|
||||
executor.submit(_build_stage, (idx, cfg))
|
||||
for idx, cfg in enumerate(self.stage_configs)
|
||||
]
|
||||
results: list[tuple[int, OmniStage]] = []
|
||||
for fut in as_completed(futures):
|
||||
results.append(fut.result())
|
||||
results.sort(key=lambda x: x[0])
|
||||
self.stage_list = [st for _, st in results]
|
||||
self.default_sampling_params_list = [
|
||||
st.default_sampling_params for st in self.stage_list
|
||||
]
|
||||
logger.debug("[Orchestrator] Loaded %d stages", len(self.stage_list))
|
||||
|
||||
self._ctx = mp.get_context("spawn")
|
||||
self._stage_in_queues: list[mp.Queue] = []
|
||||
self._stage_out_queues: list[mp.Queue] = []
|
||||
self._init_sleep_seconds = max(0, int(init_sleep_seconds))
|
||||
self._shm_threshold_bytes = max(0, int(shm_threshold_bytes))
|
||||
self._start_stage_processes(model)
|
||||
# Wait for all stages to report readiness before seeding
|
||||
self._stages_ready: set[int] = set()
|
||||
self._wait_for_stages_ready(timeout=init_timeout)
|
||||
|
||||
def _start_stage_processes(self, model: str) -> None:
|
||||
for stage_id, stage in enumerate(self.stage_list):
|
||||
# Use unbounded queues to avoid deadlock when seeding many requests
|
||||
in_q: mp.Queue = self._ctx.Queue(maxsize=0)
|
||||
out_q: mp.Queue = self._ctx.Queue(maxsize=0)
|
||||
self._stage_in_queues.append(in_q)
|
||||
self._stage_out_queues.append(out_q)
|
||||
|
||||
# Attach queues and start Stage-owned worker process
|
||||
stage.attach_queues(in_q, out_q)
|
||||
stage.init_stage_worker(
|
||||
model,
|
||||
is_async=True,
|
||||
log_file=self._log_file,
|
||||
shm_threshold_bytes=self._shm_threshold_bytes,
|
||||
ctx=self._ctx,
|
||||
batch_timeout=self.batch_timeout,
|
||||
)
|
||||
logger.debug("[Orchestrator] Stage-%s process started", stage_id)
|
||||
time.sleep(self._init_sleep_seconds)
|
||||
|
||||
def close(self) -> None:
|
||||
for q in self._stage_in_queues:
|
||||
try:
|
||||
q.put_nowait(None)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"[Orchestrator] Failed to send shutdown signal to \
|
||||
stage input queue: %s",
|
||||
e,
|
||||
)
|
||||
for stage in self.stage_list:
|
||||
try:
|
||||
stage.stop_stage_worker()
|
||||
except Exception as e:
|
||||
logger.warning("[Orchestrator] Failed to stop stage worker: %s", e)
|
||||
|
||||
def __del__(self) -> None: # best-effort
|
||||
try:
|
||||
self.close()
|
||||
except Exception as e:
|
||||
logger.debug("[Orchestrator] __del__ close() raised: %s", e, exc_info=True)
|
||||
|
||||
def shutdown(self):
|
||||
"""Shutdown, cleaning up the background proc and IPC."""
|
||||
try:
|
||||
self.close()
|
||||
except Exception as e:
|
||||
logger.debug("[Orchestrator] __del__ close() raised: %s", e, exc_info=True)
|
||||
|
||||
async def generate(
|
||||
self,
|
||||
prompt: PromptType,
|
||||
request_id: str,
|
||||
sampling_params_list: Optional[
|
||||
Union[SamplingParams, Sequence[SamplingParams]]
|
||||
] = None,
|
||||
lora_request: Optional[LoRARequest] = None,
|
||||
trace_headers: Optional[Mapping[str, str]] = None,
|
||||
priority: int = 0,
|
||||
data_parallel_rank: Optional[int] = None,
|
||||
) -> AsyncGenerator[OmniRequestOutput, None]:
|
||||
logger.debug("[Orchestrator] generate() called")
|
||||
if sampling_params_list is None:
|
||||
sampling_params_list = self.default_sampling_params_list
|
||||
if len(sampling_params_list) != len(self.stage_list):
|
||||
raise ValueError(
|
||||
f"Expected {len(self.stage_list)} sampling params, \
|
||||
got {len(sampling_params_list)}"
|
||||
)
|
||||
|
||||
# Orchestrator keeps stage objects for input derivation
|
||||
num_stages = len(self.stage_list)
|
||||
# Track per-request start time for end-to-end timing
|
||||
_req_start_ts: dict[int, float] = {}
|
||||
_wall_start_ts: float = time.time()
|
||||
# _last_finish_ts: float = _wall_start_ts
|
||||
|
||||
# Determine the final stage for E2E stats (highest stage_id with
|
||||
# final_output=True; fallback to last stage)
|
||||
final_stage_id_for_e2e = -1
|
||||
try:
|
||||
for _sid, _st in enumerate(self.stage_list):
|
||||
if getattr(_st, "final_output", False):
|
||||
final_stage_id_for_e2e = max(final_stage_id_for_e2e, _sid)
|
||||
if final_stage_id_for_e2e < 0:
|
||||
final_stage_id_for_e2e = len(self.stage_list) - 1
|
||||
except Exception as e:
|
||||
logger.debug(
|
||||
"[Orchestrator] Failed to determine final stage for E2E; \
|
||||
falling back to last: %s",
|
||||
e,
|
||||
exc_info=True,
|
||||
)
|
||||
final_stage_id_for_e2e = len(self.stage_list) - 1
|
||||
# Metrics/aggregation helper
|
||||
metrics = OrchestratorMetrics(
|
||||
num_stages,
|
||||
self._enable_stats,
|
||||
self._stats_file,
|
||||
self._overall_stats_file,
|
||||
_wall_start_ts,
|
||||
)
|
||||
# Seed stage-0 queue with all requests
|
||||
logger.debug("[Orchestrator] Seeding request into stage-0")
|
||||
# Mark first input time for stage-0
|
||||
metrics.stage_first_ts[0] = metrics.stage_first_ts[0] or time.time()
|
||||
|
||||
sp0: SamplingParams = sampling_params_list[0] # type: ignore[index]
|
||||
task = {
|
||||
"request_id": request_id,
|
||||
"engine_inputs": prompt,
|
||||
"sampling_params": sp0,
|
||||
}
|
||||
self.stage_list[0].submit(task)
|
||||
_req_start_ts[request_id] = time.time()
|
||||
logger.debug("[Orchestrator] Enqueued request %s to stage-0", request_id)
|
||||
|
||||
logger.debug("[Orchestrator] Entering scheduling loop: stages=%d", num_stages)
|
||||
finished = False
|
||||
while not finished:
|
||||
made_progress = False
|
||||
for stage_id, stage in enumerate(self.stage_list):
|
||||
result = stage.try_collect()
|
||||
if result is None:
|
||||
continue
|
||||
|
||||
made_progress = True
|
||||
req_id = result.get("request_id")
|
||||
if "error" in result:
|
||||
logger.error(
|
||||
"Stage %s error on request %s: %s",
|
||||
stage_id,
|
||||
req_id,
|
||||
result["error"],
|
||||
)
|
||||
continue
|
||||
|
||||
if result.get("type") == "stage_ready":
|
||||
# Only happens when stage is initialized slower than expected,
|
||||
# so we wait for a short time and try again
|
||||
time.sleep(0.05)
|
||||
continue
|
||||
|
||||
engine_outputs = _load(
|
||||
result, obj_key="engine_outputs", shm_key="engine_outputs_shm"
|
||||
)
|
||||
# Mark last output time for this stage whenever we receive outputs
|
||||
metrics.stage_last_ts[stage_id] = max(
|
||||
metrics.stage_last_ts[stage_id] or 0.0, time.time()
|
||||
)
|
||||
try:
|
||||
_m = result.get("metrics")
|
||||
if _m is not None:
|
||||
metrics.on_stage_metrics(stage_id, req_id, _m)
|
||||
except Exception as e:
|
||||
logger.exception(
|
||||
"[Orchestrator] Failed to process metrics for stage %s, \
|
||||
req %s: %s",
|
||||
stage_id,
|
||||
req_id,
|
||||
e,
|
||||
)
|
||||
logger.debug(
|
||||
"[Orchestrator] Stage-%s completed request %s; \
|
||||
forwarding or finalizing",
|
||||
stage_id,
|
||||
req_id,
|
||||
)
|
||||
stage.set_engine_outputs(engine_outputs)
|
||||
|
||||
if getattr(stage, "final_output", False):
|
||||
logger.debug(
|
||||
"[Orchestrator] Request %s finalized at stage-%s",
|
||||
req_id,
|
||||
stage_id,
|
||||
)
|
||||
|
||||
# End-to-end timing and time-per-token for final output
|
||||
# (only once per request at the designated final stage)
|
||||
try:
|
||||
rid_int = (
|
||||
int(req_id)
|
||||
if isinstance(req_id, (int, str)) and str(req_id).isdigit()
|
||||
else req_id
|
||||
)
|
||||
if (
|
||||
stage_id == final_stage_id_for_e2e
|
||||
and rid_int not in metrics.e2e_done
|
||||
):
|
||||
metrics.on_finalize_request(
|
||||
stage_id,
|
||||
req_id,
|
||||
engine_outputs,
|
||||
_req_start_ts.get(req_id, _wall_start_ts),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception(
|
||||
"[Orchestrator] Finalize request handling error for \
|
||||
req %s at stage %s: %s",
|
||||
req_id,
|
||||
stage_id,
|
||||
e,
|
||||
)
|
||||
|
||||
if isinstance(engine_outputs, list):
|
||||
engine_outputs = engine_outputs[0]
|
||||
yield OmniRequestOutput(
|
||||
stage_id=stage_id,
|
||||
final_output_type=stage.final_output_type,
|
||||
request_output=engine_outputs,
|
||||
)
|
||||
|
||||
next_stage_id = stage_id + 1
|
||||
if next_stage_id < num_stages:
|
||||
next_stage: OmniStage = self.stage_list[next_stage_id]
|
||||
next_inputs = next_stage.process_engine_inputs(
|
||||
self.stage_list, prompt
|
||||
)
|
||||
sp_next: SamplingParams = sampling_params_list[next_stage_id]
|
||||
try:
|
||||
# Measure transfer size and time (encode + enqueue)
|
||||
size_bytes = 0
|
||||
try:
|
||||
size_bytes = len(_ser(next_inputs))
|
||||
except Exception:
|
||||
size_bytes = 0
|
||||
t0 = time.time()
|
||||
ipc_payload = _encode(
|
||||
next_inputs,
|
||||
getattr(self, "_shm_threshold_bytes", 65536),
|
||||
obj_key="engine_inputs",
|
||||
shm_key="engine_inputs_shm",
|
||||
)
|
||||
ipc_payload.update(
|
||||
{
|
||||
"request_id": req_id,
|
||||
"sampling_params": sp_next,
|
||||
"sent_ts": time.time(),
|
||||
}
|
||||
)
|
||||
self.stage_list[next_stage_id].submit(ipc_payload)
|
||||
t1 = time.time()
|
||||
tx_ms = (t1 - t0) * 1000.0
|
||||
metrics.on_forward(
|
||||
stage_id,
|
||||
next_stage_id,
|
||||
req_id,
|
||||
int(size_bytes),
|
||||
float(tx_ms),
|
||||
bool("engine_inputs_shm" in ipc_payload),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"[Orchestrator] IPC encode failed for req %s: %s; \
|
||||
falling back to inline payload",
|
||||
req_id,
|
||||
e,
|
||||
)
|
||||
self.stage_list[next_stage_id].submit(
|
||||
{
|
||||
"request_id": req_id,
|
||||
"engine_inputs": next_inputs,
|
||||
"sampling_params": sp_next,
|
||||
}
|
||||
)
|
||||
logger.debug(
|
||||
"[Orchestrator] Forwarded request %s to stage-%s",
|
||||
req_id,
|
||||
next_stage_id,
|
||||
)
|
||||
else:
|
||||
finished = True
|
||||
logger.debug("[Orchestrator] Request %s fully completed", req_id)
|
||||
|
||||
if not made_progress:
|
||||
time.sleep(0.005)
|
||||
|
||||
logger.debug("[Orchestrator] All requests completed")
|
||||
|
||||
# Summarize and print stats
|
||||
try:
|
||||
summary = metrics.build_and_log_summary(final_stage_id_for_e2e)
|
||||
logger.info("[Summary] %s", summary)
|
||||
except Exception as e:
|
||||
logger.exception("[Orchestrator] Failed to build/log summary: %s", e)
|
||||
|
||||
def _wait_for_stages_ready(self, timeout: int = 120) -> None:
|
||||
deadline = time.time() + max(0, int(timeout))
|
||||
num_stages = len(self.stage_list)
|
||||
while len(self._stages_ready) < num_stages and time.time() < deadline:
|
||||
progressed = False
|
||||
for stage_id, stage in enumerate(self.stage_list):
|
||||
if stage_id in self._stages_ready:
|
||||
continue
|
||||
result = stage.try_collect()
|
||||
if result is None:
|
||||
continue
|
||||
progressed = True
|
||||
if result.get("type") == "stage_ready":
|
||||
self._stages_ready.add(stage_id)
|
||||
# Store vllm_config received from worker process
|
||||
vllm_config = result.get("vllm_config")
|
||||
if vllm_config is not None:
|
||||
stage.set_vllm_config(vllm_config)
|
||||
tokenizer = result.get("tokenizer")
|
||||
if tokenizer is not None:
|
||||
stage.set_tokenizer(tokenizer)
|
||||
# input_preprocessor = result.get("input_preprocessor")
|
||||
# if input_preprocessor is not None:
|
||||
# stage.set_input_preprocessor(input_preprocessor)
|
||||
is_tracing_enabled = result.get("is_tracing_enabled")
|
||||
if is_tracing_enabled is not None:
|
||||
stage.set_is_tracing_enabled(is_tracing_enabled)
|
||||
logger.debug("[Orchestrator] Stage-%s reported ready", stage_id)
|
||||
else:
|
||||
# No user data should arrive before seeding; ignore other messages
|
||||
pass
|
||||
if not progressed:
|
||||
time.sleep(0.01)
|
||||
if len(self._stages_ready) < num_stages:
|
||||
not_ready = sorted(set(range(num_stages)) - set(self._stages_ready))
|
||||
logger.warning(
|
||||
"[Orchestrator] Initialization timeout: only %s/%s stages are \
|
||||
ready; not ready: %s",
|
||||
len(self._stages_ready),
|
||||
num_stages,
|
||||
not_ready,
|
||||
)
|
||||
# Provide actionable suggestions before shutdown
|
||||
try:
|
||||
suggestions = [
|
||||
"Verify GPU/device assignment in config (runtime.devices) is \
|
||||
correct.",
|
||||
"Check GPU/host memory availability; reduce model or batch size if needed.", # noqa: E501
|
||||
"Check model weights path and network reachability (if loading remotely).", # noqa: E501
|
||||
"Increase initialization wait time (init_sleep_seconds or \
|
||||
call-site timeout).",
|
||||
]
|
||||
if getattr(self, "_log_file", None):
|
||||
suggestions.append(
|
||||
f"Inspect per-stage log files for details: \
|
||||
{self._log_file}.stage<id>.log"
|
||||
)
|
||||
logger.error(
|
||||
"[Orchestrator] Stage initialization failed, shutting down. \
|
||||
Suggestions:\n- %s",
|
||||
"\n- ".join(suggestions),
|
||||
)
|
||||
except Exception:
|
||||
# Best-effort logging of suggestions
|
||||
logger.error(
|
||||
"[Orchestrator] Stage initialization failed and an error \
|
||||
occurred while logging suggestions",
|
||||
)
|
||||
|
||||
# Attempt graceful shutdown of all stages before exiting
|
||||
try:
|
||||
self.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Terminate the current process with non-zero exit code
|
||||
try:
|
||||
sys.exit(1)
|
||||
except SystemExit:
|
||||
raise
|
||||
except Exception:
|
||||
os._exit(1)
|
||||
|
||||
@property
|
||||
def is_running(self) -> bool:
|
||||
# Is None before the loop is started.
|
||||
return len(self._stage_in_queues) > 0
|
||||
|
||||
@property
|
||||
def is_stopped(self) -> bool:
|
||||
return self.errored
|
||||
|
||||
@property
|
||||
def errored(self) -> bool:
|
||||
return not self.is_running
|
||||
|
||||
@property
|
||||
def dead_error(self) -> BaseException:
|
||||
return EngineDeadError()
|
||||
|
||||
async def abort(self, request_id: Union[str, Iterable[str]]) -> None:
|
||||
pass
|
||||
|
||||
async def get_vllm_config(self) -> VllmConfig:
|
||||
for stage in self.stage_list:
|
||||
if stage.is_comprehension:
|
||||
# Use the vllm_config received from worker process
|
||||
if stage.vllm_config is not None:
|
||||
return stage.vllm_config
|
||||
return None
|
||||
|
||||
async def get_model_config(self) -> OmniModelConfig:
|
||||
for stage in self.stage_list:
|
||||
if stage.is_comprehension:
|
||||
# Use the vllm_config received from worker process
|
||||
if stage.vllm_config is not None:
|
||||
return stage.vllm_config.model_config
|
||||
return None
|
||||
|
||||
async def get_input_preprocessor(self) -> InputPreprocessor:
|
||||
# for stage in self.stage_list:
|
||||
# if stage.is_comprehension:
|
||||
# return stage.input_preprocessor
|
||||
return None
|
||||
|
||||
async def get_tokenizer(self) -> AnyTokenizer:
|
||||
for stage in self.stage_list:
|
||||
if stage.is_comprehension:
|
||||
return stage.tokenizer
|
||||
return None
|
||||
|
||||
async def is_tracing_enabled(self) -> bool:
|
||||
for stage in self.stage_list:
|
||||
if stage.is_comprehension:
|
||||
return stage.is_tracing_enabled
|
||||
return False
|
||||
|
||||
async def do_log_stats(self) -> None:
|
||||
pass
|
||||
|
||||
async def check_health(self) -> None:
|
||||
pass
|
||||
|
||||
async def reset_mm_cache(self) -> None:
|
||||
pass
|
||||
|
||||
async def reset_prefix_cache(self, device: Optional[Device] = None) -> None:
|
||||
pass
|
||||
|
||||
async def sleep(self, level: int = 1) -> None:
|
||||
pass
|
||||
|
||||
async def wake_up(self, tags: Optional[list[str]] = None) -> None:
|
||||
pass
|
||||
|
||||
async def is_sleeping(self) -> bool:
|
||||
"""Check whether the engine is sleeping"""
|
||||
return False
|
||||
|
||||
async def add_lora(self, lora_request: LoRARequest) -> bool:
|
||||
"""Load a new LoRA adapter into the engine for future requests."""
|
||||
return False
|
||||
|
||||
async def encode(
|
||||
self,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
"""Generate outputs for a request from a pooling model."""
|
||||
raise NotImplementedError("encode() is not implemented for AsyncOmniLLM")
|
||||
|
||||
async def start_profile(self) -> None:
|
||||
raise NotImplementedError("start_profile() is not implemented for AsyncOmniLLM")
|
||||
|
||||
async def stop_profile(self) -> None:
|
||||
raise NotImplementedError("stop_profile() is not implemented for AsyncOmniLLM")
|
||||
|
||||
|
||||
class AsyncOmniStageLLM(AsyncLLM):
|
||||
def __init__(
|
||||
self,
|
||||
engine_args: AsyncOmniEngineArgs,
|
||||
vllm_config: VllmConfig,
|
||||
executor_class: type[Executor],
|
||||
log_stats: bool,
|
||||
usage_context: UsageContext = UsageContext.ENGINE_CONTEXT,
|
||||
mm_registry: MultiModalRegistry = MULTIMODAL_REGISTRY,
|
||||
use_cached_outputs: bool = False,
|
||||
log_requests: bool = True,
|
||||
start_engine_loop: bool = True,
|
||||
stat_loggers: Optional[list[StatLoggerFactory]] = None,
|
||||
client_addresses: Optional[dict[str, str]] = None,
|
||||
client_count: int = 1,
|
||||
client_index: int = 0,
|
||||
) -> None:
|
||||
"""
|
||||
Create an AsyncLLM.
|
||||
|
||||
Args:
|
||||
vllm_config: global configuration.
|
||||
executor_class: an Executor impl, e.g. MultiprocExecutor.
|
||||
log_stats: Whether to log stats.
|
||||
usage_context: Usage context of the LLM.
|
||||
mm_registry: Multi-modal registry.
|
||||
use_cached_outputs: Whether to use cached outputs.
|
||||
log_requests: Whether to log requests.
|
||||
start_engine_loop: Whether to start the engine loop.
|
||||
stat_loggers: customized stat loggers for the engine.
|
||||
If not provided, default stat loggers will be used.
|
||||
PLEASE BE AWARE THAT STAT LOGGER IS NOT STABLE
|
||||
IN V1, AND ITS BASE CLASS INTERFACE MIGHT CHANGE.
|
||||
|
||||
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()
|
||||
|
||||
self.model_config = vllm_config.model_config
|
||||
self.vllm_config = vllm_config
|
||||
self.observability_config = vllm_config.observability_config
|
||||
self.log_requests = log_requests
|
||||
|
||||
self.log_stats = log_stats or (stat_loggers is not None)
|
||||
if not log_stats and stat_loggers is not None:
|
||||
logger.info(
|
||||
"AsyncLLM created with log_stats=False and non-empty custom "
|
||||
"logger list; enabling logging without default stat loggers"
|
||||
)
|
||||
|
||||
if self.model_config.skip_tokenizer_init:
|
||||
self.tokenizer = None
|
||||
else:
|
||||
# Tokenizer (+ ensure liveness if running in another process).
|
||||
self.tokenizer = init_tokenizer_from_configs(
|
||||
model_config=vllm_config.model_config
|
||||
)
|
||||
|
||||
# Processor (converts Inputs --> EngineCoreRequests).
|
||||
self.processor = OmniProcessor(
|
||||
vllm_config=vllm_config,
|
||||
tokenizer=self.tokenizer,
|
||||
mm_registry=mm_registry,
|
||||
)
|
||||
|
||||
# OutputProcessor (converts EngineCoreOutputs --> RequestOutput).
|
||||
self.output_processor = MultimodalOutputProcessor(
|
||||
tokenizer=self.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
|
||||
|
||||
# EngineCore (starts the engine in background process).
|
||||
self.engine_core = EngineCoreClient.make_async_mp_client(
|
||||
vllm_config=vllm_config,
|
||||
executor_class=executor_class,
|
||||
log_stats=self.log_stats,
|
||||
client_addresses=client_addresses,
|
||||
client_count=client_count,
|
||||
client_index=client_index,
|
||||
)
|
||||
|
||||
# Loggers.
|
||||
self.logger_manager: Optional[StatLoggerManager] = None
|
||||
if self.log_stats:
|
||||
self.logger_manager = StatLoggerManager(
|
||||
vllm_config=vllm_config,
|
||||
engine_idxs=self.engine_core.engine_ranks_managed,
|
||||
custom_stat_loggers=stat_loggers,
|
||||
enable_default_loggers=log_stats,
|
||||
client_count=client_count,
|
||||
)
|
||||
self.logger_manager.log_engine_initialized()
|
||||
|
||||
self.output_handler: Optional[asyncio.Task] = None
|
||||
try:
|
||||
# Start output handler eagerly if we are in the asyncio eventloop.
|
||||
asyncio.get_running_loop()
|
||||
self._run_output_handler()
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
if envs.VLLM_TORCH_PROFILER_DIR:
|
||||
logger.info(
|
||||
"Torch profiler enabled. AsyncLLM CPU traces will be collected under %s", # noqa: E501
|
||||
envs.VLLM_TORCH_PROFILER_DIR,
|
||||
)
|
||||
worker_name = f"{socket.gethostname()}_{os.getpid()}.async_llm"
|
||||
self.profiler = torch.profiler.profile(
|
||||
activities=[
|
||||
torch.profiler.ProfilerActivity.CPU,
|
||||
],
|
||||
with_stack=envs.VLLM_TORCH_PROFILER_WITH_STACK,
|
||||
on_trace_ready=torch.profiler.tensorboard_trace_handler(
|
||||
envs.VLLM_TORCH_PROFILER_DIR, worker_name=worker_name, use_gzip=True
|
||||
),
|
||||
)
|
||||
else:
|
||||
self.profiler = None
|
||||
|
||||
@classmethod
|
||||
@deprecate_kwargs(
|
||||
"disable_log_requests",
|
||||
additional_message=(
|
||||
"This argument will have no effect. " "Use `enable_log_requests` instead."
|
||||
),
|
||||
)
|
||||
def from_vllm_config(
|
||||
cls,
|
||||
vllm_config: VllmConfig,
|
||||
engine_args: AsyncOmniEngineArgs,
|
||||
start_engine_loop: bool = True,
|
||||
usage_context: UsageContext = UsageContext.ENGINE_CONTEXT,
|
||||
stat_loggers: Optional[list[StatLoggerFactory]] = None,
|
||||
enable_log_requests: bool = False,
|
||||
disable_log_stats: bool = False,
|
||||
client_addresses: Optional[dict[str, str]] = None,
|
||||
client_count: int = 1,
|
||||
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,
|
||||
executor_class=Executor.get_class(vllm_config),
|
||||
start_engine_loop=start_engine_loop,
|
||||
stat_loggers=stat_loggers,
|
||||
log_requests=enable_log_requests,
|
||||
log_stats=not disable_log_stats,
|
||||
usage_context=usage_context,
|
||||
client_addresses=client_addresses,
|
||||
client_count=client_count,
|
||||
client_index=client_index,
|
||||
engine_args=engine_args,
|
||||
)
|
||||
@@ -2,24 +2,54 @@
|
||||
CLI entry point for vLLM-omni that intercepts vLLM commands.
|
||||
"""
|
||||
|
||||
import importlib.metadata
|
||||
import sys
|
||||
|
||||
from vllm_omni.entrypoints.cli.serve import OmniServeCommand
|
||||
|
||||
|
||||
def main():
|
||||
"""Main CLI entry point that intercepts vLLM commands."""
|
||||
# Check if --omni flag is present
|
||||
if "--omni" in sys.argv:
|
||||
# Remove --omni flag and "serve" command, process with vLLM-omni
|
||||
omni_args = [arg for arg in sys.argv[1:] if arg not in ["--omni", "serve"]]
|
||||
omni_serve = OmniServeCommand()
|
||||
omni_serve.run(omni_args)
|
||||
else:
|
||||
# Forward to original vLLM CLI
|
||||
if "--omni" not in sys.argv:
|
||||
from vllm.entrypoints.cli.main import main as vllm_main
|
||||
|
||||
vllm_main()
|
||||
return
|
||||
else:
|
||||
import vllm_omni.entrypoints.cli.serve
|
||||
from vllm.entrypoints.utils import VLLM_SUBCMD_PARSER_EPILOG, cli_env_setup
|
||||
from vllm.utils import FlexibleArgumentParser
|
||||
|
||||
CMD_MODULES = [
|
||||
vllm_omni.entrypoints.cli.serve,
|
||||
]
|
||||
|
||||
cli_env_setup()
|
||||
|
||||
parser = FlexibleArgumentParser(
|
||||
description="vLLM OMNI CLI",
|
||||
epilog=VLLM_SUBCMD_PARSER_EPILOG.format(subcmd="[subcommand]"),
|
||||
)
|
||||
parser.add_argument(
|
||||
"-v",
|
||||
"--version",
|
||||
action="version",
|
||||
version=importlib.metadata.version("vllm_omni"),
|
||||
)
|
||||
subparsers = parser.add_subparsers(required=False, dest="subparser")
|
||||
cmds = {}
|
||||
for cmd_module in CMD_MODULES:
|
||||
new_cmds = cmd_module.cmd_init()
|
||||
for cmd in new_cmds:
|
||||
cmd.subparser_init(subparsers).set_defaults(dispatch_function=cmd.cmd)
|
||||
cmds[cmd.name] = cmd
|
||||
args = parser.parse_args()
|
||||
if args.subparser in cmds:
|
||||
cmds[args.subparser].validate(args)
|
||||
|
||||
if hasattr(args, "dispatch_function"):
|
||||
args.dispatch_function(args)
|
||||
else:
|
||||
parser.print_help()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -3,93 +3,67 @@ Omni serve command for vLLM-omni.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
from typing import List
|
||||
|
||||
import uvloop
|
||||
|
||||
from vllm.entrypoints.cli.types import CLISubcommand
|
||||
from vllm.entrypoints.openai.cli_args import make_arg_parser, validate_parsed_serve_args
|
||||
from vllm.entrypoints.utils import VLLM_SUBCMD_PARSER_EPILOG
|
||||
from vllm.logger import init_logger
|
||||
from vllm.utils import FlexibleArgumentParser
|
||||
from vllm_omni.entrypoints.openai.api_server import omni_run_server
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
DESCRIPTION = """Launch a local OpenAI-compatible API server to serve Omni LLM
|
||||
completions via HTTP. Defaults to Qwen/Qwen2.5-Omni-7B if no model is specified.
|
||||
|
||||
Search by using: `--help=<ConfigGroup>` to explore options by section (e.g.,
|
||||
--help=ModelConfig, --help=Frontend)
|
||||
Use `--help=all` to show all available flags at once.
|
||||
"""
|
||||
|
||||
|
||||
class OmniServeCommand:
|
||||
"""Command handler for vLLM-omni serve command."""
|
||||
class OmniServeCommand(CLISubcommand):
|
||||
"""The `serve` subcommand for the vLLM CLI."""
|
||||
|
||||
def __init__(self):
|
||||
self.parser = self._create_parser()
|
||||
name = "serve"
|
||||
|
||||
def _create_parser(self) -> argparse.ArgumentParser:
|
||||
"""Create argument parser for omni serve command."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="vLLM-omni: Multi-modality models inference and serving"
|
||||
@staticmethod
|
||||
def cmd(args: argparse.Namespace) -> None:
|
||||
# If model is specified in CLI (as positional arg), it takes precedence
|
||||
if hasattr(args, "model_tag") and args.model_tag is not None:
|
||||
args.model = args.model_tag
|
||||
|
||||
uvloop.run(omni_run_server(args))
|
||||
|
||||
def validate(self, args: argparse.Namespace) -> None:
|
||||
validate_parsed_serve_args(args)
|
||||
|
||||
def subparser_init(
|
||||
self, subparsers: argparse._SubParsersAction
|
||||
) -> FlexibleArgumentParser:
|
||||
serve_parser = subparsers.add_parser(
|
||||
self.name,
|
||||
description=DESCRIPTION,
|
||||
usage="vllm serve [model_tag] --omni [options]",
|
||||
)
|
||||
|
||||
# Model arguments - make it optional with default
|
||||
parser.add_argument(
|
||||
"model",
|
||||
nargs="?",
|
||||
default="Qwen/Qwen3-0.6B",
|
||||
help="Path to the model or model name (default: Qwen/Qwen3-0.6B)",
|
||||
)
|
||||
|
||||
# Server arguments
|
||||
parser.add_argument(
|
||||
"--port",
|
||||
type=int,
|
||||
default=8000,
|
||||
help="Port to run the server on",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--host",
|
||||
type=str,
|
||||
default="0.0.0.0",
|
||||
help="Host to run the server on",
|
||||
)
|
||||
|
||||
# Stage configuration arguments
|
||||
parser.add_argument("--ar-stage", type=str, help="AR stage model path")
|
||||
|
||||
parser.add_argument("--dit-stage", type=str, help="DiT stage model path")
|
||||
|
||||
parser.add_argument(
|
||||
"--dit-steps",
|
||||
type=int,
|
||||
default=50,
|
||||
help="Number of DiT inference steps",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--dit-guidance-scale",
|
||||
type=float,
|
||||
default=7.5,
|
||||
help="DiT guidance scale",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--use-diffusers",
|
||||
serve_parser = make_arg_parser(serve_parser)
|
||||
serve_parser.epilog = VLLM_SUBCMD_PARSER_EPILOG.format(subcmd=self.name)
|
||||
serve_parser.add_argument(
|
||||
"--omni",
|
||||
action="store_true",
|
||||
help="Use diffusers pipeline for DiT stage",
|
||||
help="Enable vLLM-omni mode",
|
||||
)
|
||||
|
||||
# Other arguments
|
||||
parser.add_argument(
|
||||
"--log-stats", action="store_true", help="Enable logging statistics"
|
||||
serve_parser.add_argument(
|
||||
"--stage-configs-path",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Path to the stage configs file. If not specified, the stage configs will be loaded from the model.",
|
||||
)
|
||||
return serve_parser
|
||||
|
||||
return parser
|
||||
|
||||
def run(self, args: List[str]) -> None:
|
||||
"""Run the omni serve command."""
|
||||
pass
|
||||
|
||||
def _create_stage_configs(self, args) -> List:
|
||||
"""Create stage configurations based on arguments."""
|
||||
pass
|
||||
|
||||
async def _start_server(self, omni_llm, args) -> None:
|
||||
"""Start the API server."""
|
||||
try:
|
||||
# Import here to avoid circular imports
|
||||
from vllm_omni.entrypoints.api_server import run_server
|
||||
|
||||
await run_server(omni_llm_instance=omni_llm, host=args.host, port=args.port)
|
||||
except KeyboardInterrupt:
|
||||
print("\nShutting down server...")
|
||||
except Exception as e:
|
||||
print(f"Error starting server: {e}")
|
||||
raise
|
||||
def cmd_init() -> list[CLISubcommand]:
|
||||
return [OmniServeCommand()]
|
||||
+368
-184
@@ -1,10 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from vllm_omni.entrypoints.stage_utils import append_jsonl as _append_jsonl
|
||||
|
||||
@@ -49,22 +48,32 @@ def remove_old_logs(log_file: Optional[str], num_stages: int) -> None:
|
||||
pass
|
||||
|
||||
|
||||
def configure_orchestrator_logger(logger: logging.Logger, log_file: Optional[str]) -> None:
|
||||
def configure_orchestrator_logger(
|
||||
logger: logging.Logger, log_file: Optional[str]
|
||||
) -> None:
|
||||
try:
|
||||
if not log_file:
|
||||
return
|
||||
has_file_handler = any(isinstance(h, logging.FileHandler) for h in logger.handlers)
|
||||
has_file_handler = any(
|
||||
isinstance(h, logging.FileHandler) for h in logger.handlers
|
||||
)
|
||||
if not has_file_handler:
|
||||
fh = logging.FileHandler(log_file)
|
||||
fh.setLevel(logging.DEBUG)
|
||||
fh.setFormatter(logging.Formatter("%(asctime)s [PID:%(process)d] %(levelname)s: %(message)s"))
|
||||
fh.setFormatter(
|
||||
logging.Formatter(
|
||||
"%(asctime)s [PID:%(process)d] %(levelname)s: %(message)s"
|
||||
)
|
||||
)
|
||||
logger.addHandler(fh)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def init_stats_paths(enable_stats: bool, log_file: Optional[str]) -> tuple[Optional[str], Optional[str]]:
|
||||
def init_stats_paths(
|
||||
enable_stats: bool, log_file: Optional[str]
|
||||
) -> tuple[Optional[str], Optional[str]]:
|
||||
stats_file: Optional[str] = None
|
||||
overall_file: Optional[str] = None
|
||||
try:
|
||||
@@ -86,122 +95,217 @@ def _safe_append_jsonl(path: Optional[str], record: Dict[str, Any]) -> None:
|
||||
pass
|
||||
|
||||
|
||||
def log_transfer_tx(stats_file: Optional[str], from_stage: int, to_stage: int, request_id: Any,
|
||||
size_bytes: int, tx_time_ms: float, used_shm: bool) -> None:
|
||||
_safe_append_jsonl(stats_file, {
|
||||
"type": "transfer_stats",
|
||||
"from_stage": from_stage,
|
||||
"to_stage": to_stage,
|
||||
"request_id": request_id,
|
||||
"size_bytes": int(size_bytes),
|
||||
"tx_time_ms": float(tx_time_ms),
|
||||
"tx_mbps": (float(size_bytes) * 8.0) / (max(tx_time_ms, 1e-6) * 1000.0),
|
||||
"used_shm": bool(used_shm),
|
||||
})
|
||||
def log_transfer_tx(
|
||||
stats_file: Optional[str],
|
||||
from_stage: int,
|
||||
to_stage: int,
|
||||
request_id: Any,
|
||||
size_bytes: int,
|
||||
tx_time_ms: float,
|
||||
used_shm: bool,
|
||||
) -> None:
|
||||
_safe_append_jsonl(
|
||||
stats_file,
|
||||
{
|
||||
"type": "transfer_stats",
|
||||
"from_stage": from_stage,
|
||||
"to_stage": to_stage,
|
||||
"request_id": request_id,
|
||||
"size_bytes": int(size_bytes),
|
||||
"tx_time_ms": float(tx_time_ms),
|
||||
"tx_mbps": (float(size_bytes) * 8.0) / (max(tx_time_ms, 1e-6) * 1000.0),
|
||||
"used_shm": bool(used_shm),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def log_transfer_rx(stats_file: Optional[str], from_stage: int, to_stage: int, request_id: Any,
|
||||
rx_bytes: int, rx_decode_time_ms: float, in_flight_time_ms: float) -> None:
|
||||
_safe_append_jsonl(stats_file, {
|
||||
"type": "transfer_rx_stats",
|
||||
"from_stage": from_stage,
|
||||
"to_stage": to_stage,
|
||||
"request_id": request_id,
|
||||
"rx_bytes": int(rx_bytes),
|
||||
"rx_decode_time_ms": float(rx_decode_time_ms),
|
||||
"in_flight_time_ms": float(in_flight_time_ms),
|
||||
"rx_time_per_kb_ms": (float(rx_decode_time_ms) / max(float(rx_bytes) / 1024.0, 1e-6)) if rx_bytes > 0 else 0.0,
|
||||
})
|
||||
def log_transfer_rx(
|
||||
stats_file: Optional[str],
|
||||
from_stage: int,
|
||||
to_stage: int,
|
||||
request_id: Any,
|
||||
rx_bytes: int,
|
||||
rx_decode_time_ms: float,
|
||||
in_flight_time_ms: float,
|
||||
) -> None:
|
||||
_safe_append_jsonl(
|
||||
stats_file,
|
||||
{
|
||||
"type": "transfer_rx_stats",
|
||||
"from_stage": from_stage,
|
||||
"to_stage": to_stage,
|
||||
"request_id": request_id,
|
||||
"rx_bytes": int(rx_bytes),
|
||||
"rx_decode_time_ms": float(rx_decode_time_ms),
|
||||
"in_flight_time_ms": float(in_flight_time_ms),
|
||||
"rx_time_per_kb_ms": (
|
||||
(float(rx_decode_time_ms) / max(float(rx_bytes) / 1024.0, 1e-6))
|
||||
if rx_bytes > 0
|
||||
else 0.0
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def log_transfer_total(stats_file: Optional[str], from_stage: int, to_stage: int, request_id: Any,
|
||||
size_bytes: int, tx_time_ms: float, in_flight_time_ms: float,
|
||||
rx_decode_time_ms: float, total_time_ms: float) -> None:
|
||||
_safe_append_jsonl(stats_file, {
|
||||
"type": "transfer_total_stats",
|
||||
"from_stage": from_stage,
|
||||
"to_stage": to_stage,
|
||||
"request_id": request_id,
|
||||
"size_bytes": int(size_bytes),
|
||||
"tx_time_ms": float(tx_time_ms),
|
||||
"in_flight_time_ms": float(in_flight_time_ms),
|
||||
"rx_decode_time_ms": float(rx_decode_time_ms),
|
||||
"total_time_ms": float(total_time_ms),
|
||||
"total_time_per_kb_ms": float(total_time_ms) / max(float(size_bytes) / 1024.0, 1e-6) if size_bytes > 0 else 0.0,
|
||||
})
|
||||
def log_transfer_total(
|
||||
stats_file: Optional[str],
|
||||
from_stage: int,
|
||||
to_stage: int,
|
||||
request_id: Any,
|
||||
size_bytes: int,
|
||||
tx_time_ms: float,
|
||||
in_flight_time_ms: float,
|
||||
rx_decode_time_ms: float,
|
||||
total_time_ms: float,
|
||||
) -> None:
|
||||
_safe_append_jsonl(
|
||||
stats_file,
|
||||
{
|
||||
"type": "transfer_total_stats",
|
||||
"from_stage": from_stage,
|
||||
"to_stage": to_stage,
|
||||
"request_id": request_id,
|
||||
"size_bytes": int(size_bytes),
|
||||
"tx_time_ms": float(tx_time_ms),
|
||||
"in_flight_time_ms": float(in_flight_time_ms),
|
||||
"rx_decode_time_ms": float(rx_decode_time_ms),
|
||||
"total_time_ms": float(total_time_ms),
|
||||
"total_time_per_kb_ms": (
|
||||
float(total_time_ms) / max(float(size_bytes) / 1024.0, 1e-6)
|
||||
if size_bytes > 0
|
||||
else 0.0
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def log_orchestrator_e2e(stats_file: Optional[str], request_id: Any, final_stage_id: int,
|
||||
e2e_time_ms: float, num_tokens_out: int, e2e_time_per_token_ms: float) -> None:
|
||||
_safe_append_jsonl(stats_file, {
|
||||
"type": "orchestrator_request_e2e",
|
||||
"request_id": request_id,
|
||||
"final_stage_id": final_stage_id,
|
||||
"e2e_time_ms": float(e2e_time_ms),
|
||||
"num_tokens_out": int(num_tokens_out),
|
||||
"e2e_time_per_token_ms": float(e2e_time_per_token_ms),
|
||||
})
|
||||
def log_orchestrator_e2e(
|
||||
stats_file: Optional[str],
|
||||
request_id: Any,
|
||||
final_stage_id: int,
|
||||
e2e_time_ms: float,
|
||||
num_tokens_out: int,
|
||||
e2e_time_per_token_ms: float,
|
||||
) -> None:
|
||||
_safe_append_jsonl(
|
||||
stats_file,
|
||||
{
|
||||
"type": "orchestrator_request_e2e",
|
||||
"request_id": request_id,
|
||||
"final_stage_id": final_stage_id,
|
||||
"e2e_time_ms": float(e2e_time_ms),
|
||||
"num_tokens_out": int(num_tokens_out),
|
||||
"e2e_time_per_token_ms": float(e2e_time_per_token_ms),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def log_orchestrator_summary(stats_file: Optional[str], summary: Dict[str, Any]) -> None:
|
||||
def log_orchestrator_summary(
|
||||
stats_file: Optional[str], summary: Dict[str, Any]
|
||||
) -> None:
|
||||
_safe_append_jsonl(stats_file, {"type": "orchestrator_summary", **summary})
|
||||
|
||||
|
||||
def log_overall_summary(overall_stats_file: Optional[str], summary: Dict[str, Any]) -> None:
|
||||
def log_overall_summary(
|
||||
overall_stats_file: Optional[str], summary: Dict[str, Any]
|
||||
) -> None:
|
||||
_safe_append_jsonl(overall_stats_file, {"type": "overall_summary", **summary})
|
||||
|
||||
|
||||
def log_overall_record(overall_stats_file: Optional[str], record: Dict[str, Any]) -> None:
|
||||
def log_overall_record(
|
||||
overall_stats_file: Optional[str], record: Dict[str, Any]
|
||||
) -> None:
|
||||
_safe_append_jsonl(overall_stats_file, record)
|
||||
|
||||
|
||||
def log_stage_request_stats(stats_file: Optional[str], stage_id: int, request_id: Any, batch_size: int,
|
||||
num_tokens_out: int, stage_gen_time_ms: float, tokens_per_s: float,
|
||||
rx_transfer_bytes: int, rx_decode_time_ms: float, rx_mbps: float) -> None:
|
||||
_safe_append_jsonl(stats_file, {
|
||||
"type": "stage_request_stats",
|
||||
"stage_id": stage_id,
|
||||
"request_id": request_id,
|
||||
"batch_size": int(batch_size),
|
||||
"num_tokens_out": int(num_tokens_out),
|
||||
"stage_gen_time_ms": float(stage_gen_time_ms),
|
||||
"tokens_per_s": float(tokens_per_s),
|
||||
"rx_transfer_bytes": int(rx_transfer_bytes),
|
||||
"rx_decode_time_ms": float(rx_decode_time_ms),
|
||||
"rx_mbps": float(rx_mbps),
|
||||
})
|
||||
def log_stage_request_stats(
|
||||
stats_file: Optional[str],
|
||||
stage_id: int,
|
||||
request_id: Any,
|
||||
batch_size: int,
|
||||
num_tokens_out: int,
|
||||
stage_gen_time_ms: float,
|
||||
tokens_per_s: float,
|
||||
rx_transfer_bytes: int,
|
||||
rx_decode_time_ms: float,
|
||||
rx_mbps: float,
|
||||
) -> None:
|
||||
_safe_append_jsonl(
|
||||
stats_file,
|
||||
{
|
||||
"type": "stage_request_stats",
|
||||
"stage_id": stage_id,
|
||||
"request_id": request_id,
|
||||
"batch_size": int(batch_size),
|
||||
"num_tokens_out": int(num_tokens_out),
|
||||
"stage_gen_time_ms": float(stage_gen_time_ms),
|
||||
"tokens_per_s": float(tokens_per_s),
|
||||
"rx_transfer_bytes": int(rx_transfer_bytes),
|
||||
"rx_decode_time_ms": float(rx_decode_time_ms),
|
||||
"rx_mbps": float(rx_mbps),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def log_stage_running_avg(stats_file: Optional[str], stage_id: int, total_tokens: int,
|
||||
total_gen_time_ms: float, avg_tokens_per_s: float) -> None:
|
||||
_safe_append_jsonl(stats_file, {
|
||||
"type": "stage_running_avg",
|
||||
"stage_id": stage_id,
|
||||
"total_tokens": int(total_tokens),
|
||||
"total_gen_time_ms": float(total_gen_time_ms),
|
||||
"avg_tokens_per_s": float(avg_tokens_per_s),
|
||||
})
|
||||
def log_stage_running_avg(
|
||||
stats_file: Optional[str],
|
||||
stage_id: int,
|
||||
total_tokens: int,
|
||||
total_gen_time_ms: float,
|
||||
avg_tokens_per_s: float,
|
||||
) -> None:
|
||||
_safe_append_jsonl(
|
||||
stats_file,
|
||||
{
|
||||
"type": "stage_running_avg",
|
||||
"stage_id": stage_id,
|
||||
"total_tokens": int(total_tokens),
|
||||
"total_gen_time_ms": float(total_gen_time_ms),
|
||||
"avg_tokens_per_s": float(avg_tokens_per_s),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def log_stage_batch_stats(stats_file: Optional[str], stage_id: int, batch_size: int,
|
||||
batch_gen_time_ms: float, request_ids: List[Any]) -> None:
|
||||
_safe_append_jsonl(stats_file, {
|
||||
"type": "stage_batch_stats",
|
||||
"stage_id": stage_id,
|
||||
"batch_size": int(batch_size),
|
||||
"batch_gen_time_ms": float(batch_gen_time_ms),
|
||||
"request_ids": list(request_ids),
|
||||
})
|
||||
def log_stage_batch_stats(
|
||||
stats_file: Optional[str],
|
||||
stage_id: int,
|
||||
batch_size: int,
|
||||
batch_gen_time_ms: float,
|
||||
request_ids: List[Any],
|
||||
) -> None:
|
||||
_safe_append_jsonl(
|
||||
stats_file,
|
||||
{
|
||||
"type": "stage_batch_stats",
|
||||
"stage_id": stage_id,
|
||||
"batch_size": int(batch_size),
|
||||
"batch_gen_time_ms": float(batch_gen_time_ms),
|
||||
"request_ids": list(request_ids),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def compute_and_log_stage_request_stats(stats_file: Optional[str], stage_id: int, request_id: Any,
|
||||
batch_size: int, engine_outputs: List[Any],
|
||||
stage_gen_time_ms: float, rx_transfer_bytes: int,
|
||||
rx_decode_time_ms: float) -> None:
|
||||
def compute_and_log_stage_request_stats(
|
||||
stats_file: Optional[str],
|
||||
stage_id: int,
|
||||
request_id: Any,
|
||||
batch_size: int,
|
||||
engine_outputs: List[Any],
|
||||
stage_gen_time_ms: float,
|
||||
rx_transfer_bytes: int,
|
||||
rx_decode_time_ms: float,
|
||||
) -> None:
|
||||
"""Compute per-request metrics and log them in one call."""
|
||||
num_tokens = count_tokens_from_outputs(engine_outputs)
|
||||
tokens_per_s = (num_tokens * 1000.0 / stage_gen_time_ms) if stage_gen_time_ms > 0 else 0.0
|
||||
rx_mbps = (float(rx_transfer_bytes) * 8.0) / (max(float(rx_decode_time_ms), 1e-6) * 1000.0) if rx_transfer_bytes > 0 else 0.0
|
||||
tokens_per_s = (
|
||||
(num_tokens * 1000.0 / stage_gen_time_ms) if stage_gen_time_ms > 0 else 0.0
|
||||
)
|
||||
rx_mbps = (
|
||||
(float(rx_transfer_bytes) * 8.0)
|
||||
/ (max(float(rx_decode_time_ms), 1e-6) * 1000.0)
|
||||
if rx_transfer_bytes > 0
|
||||
else 0.0
|
||||
)
|
||||
log_stage_request_stats(
|
||||
stats_file,
|
||||
stage_id,
|
||||
@@ -218,18 +322,23 @@ def compute_and_log_stage_request_stats(stats_file: Optional[str], stage_id: int
|
||||
|
||||
# ----------------- Aggregation helpers for orchestrator -----------------
|
||||
|
||||
def record_stage_metrics(per_request: Dict[int, Dict[str, Any]],
|
||||
stage_req_counts: List[int],
|
||||
stage_total_time_ms: List[float],
|
||||
stage_total_tokens: List[int],
|
||||
stage_id: int,
|
||||
req_id: Any,
|
||||
metrics: Dict[str, Any]) -> None:
|
||||
|
||||
def record_stage_metrics(
|
||||
per_request: Dict[int, Dict[str, Any]],
|
||||
stage_req_counts: List[int],
|
||||
stage_total_time_ms: List[float],
|
||||
stage_total_tokens: List[int],
|
||||
stage_id: int,
|
||||
req_id: Any,
|
||||
metrics: Dict[str, Any],
|
||||
) -> None:
|
||||
try:
|
||||
stage_req_counts[stage_id] += 1
|
||||
stage_total_tokens[stage_id] += int(metrics.get("num_tokens_out", 0))
|
||||
rid_int = int(req_id)
|
||||
pr = per_request.setdefault(rid_int, {"stages": {}, "transfers_ms": 0.0, "transfers_bytes": 0})
|
||||
pr = per_request.setdefault(
|
||||
rid_int, {"stages": {}, "transfers_ms": 0.0, "transfers_bytes": 0}
|
||||
)
|
||||
pr_stages = pr["stages"] # type: ignore[index]
|
||||
pr_stages[stage_id] = {
|
||||
"stage_gen_time_ms": float(metrics.get("stage_gen_time_ms", 0.0)),
|
||||
@@ -239,23 +348,32 @@ def record_stage_metrics(per_request: Dict[int, Dict[str, Any]],
|
||||
pass
|
||||
|
||||
|
||||
def aggregate_rx_and_maybe_total(transfer_edge_req: Dict[tuple[int, int, int], Dict[str, float]],
|
||||
transfer_agg: Dict[tuple[int, int], Dict[str, float]],
|
||||
per_request: Dict[int, Dict[str, Any]],
|
||||
stage_id: int,
|
||||
req_id: Any,
|
||||
rx_bytes: float,
|
||||
rx_ms: float,
|
||||
in_flight_ms: float) -> Optional[tuple[int, float, float]]:
|
||||
def aggregate_rx_and_maybe_total(
|
||||
transfer_edge_req: Dict[tuple[int, int, int], Dict[str, float]],
|
||||
transfer_agg: Dict[tuple[int, int], Dict[str, float]],
|
||||
per_request: Dict[int, Dict[str, Any]],
|
||||
stage_id: int,
|
||||
req_id: Any,
|
||||
rx_bytes: float,
|
||||
rx_ms: float,
|
||||
in_flight_ms: float,
|
||||
) -> Optional[tuple[int, float, float]]:
|
||||
try:
|
||||
# Update RX aggregates for (stage_id-1 -> stage_id)
|
||||
if stage_id > 0:
|
||||
key = (stage_id - 1, stage_id)
|
||||
agg = transfer_agg.get(key)
|
||||
if agg is None:
|
||||
agg = {"sum_bytes": 0.0, "sum_ms": 0.0, "count": 0.0,
|
||||
"sum_rx_bytes": 0.0, "sum_rx_ms": 0.0, "rx_count": 0.0,
|
||||
"sum_total_ms": 0.0, "total_count": 0.0}
|
||||
agg = {
|
||||
"sum_bytes": 0.0,
|
||||
"sum_ms": 0.0,
|
||||
"count": 0.0,
|
||||
"sum_rx_bytes": 0.0,
|
||||
"sum_rx_ms": 0.0,
|
||||
"rx_count": 0.0,
|
||||
"sum_total_ms": 0.0,
|
||||
"total_count": 0.0,
|
||||
}
|
||||
transfer_agg[key] = agg
|
||||
agg["sum_rx_bytes"] += float(rx_bytes)
|
||||
agg["sum_rx_ms"] += float(rx_ms)
|
||||
@@ -276,7 +394,9 @@ def aggregate_rx_and_maybe_total(transfer_edge_req: Dict[tuple[int, int, int], D
|
||||
agg["total_count"] += 1.0
|
||||
# accumulate per-request transfer totals
|
||||
try:
|
||||
pr = per_request.setdefault(rid_int, {"stages": {}, "transfers_ms": 0.0, "transfers_bytes": 0})
|
||||
pr = per_request.setdefault(
|
||||
rid_int, {"stages": {}, "transfers_ms": 0.0, "transfers_bytes": 0}
|
||||
)
|
||||
pr["transfers_ms"] = float(pr.get("transfers_ms", 0.0)) + total_ms # type: ignore[index]
|
||||
pr["transfers_bytes"] = int(pr.get("transfers_bytes", 0)) + int(rx_bytes) # type: ignore[index]
|
||||
except Exception:
|
||||
@@ -287,20 +407,29 @@ def aggregate_rx_and_maybe_total(transfer_edge_req: Dict[tuple[int, int, int], D
|
||||
return None
|
||||
|
||||
|
||||
def record_sender_transfer_agg(transfer_agg: Dict[tuple[int, int], Dict[str, float]],
|
||||
transfer_edge_req: Dict[tuple[int, int, int], Dict[str, float]],
|
||||
from_stage: int,
|
||||
to_stage: int,
|
||||
req_id: Any,
|
||||
size_bytes: int,
|
||||
tx_ms: float) -> None:
|
||||
def record_sender_transfer_agg(
|
||||
transfer_agg: Dict[tuple[int, int], Dict[str, float]],
|
||||
transfer_edge_req: Dict[tuple[int, int, int], Dict[str, float]],
|
||||
from_stage: int,
|
||||
to_stage: int,
|
||||
req_id: Any,
|
||||
size_bytes: int,
|
||||
tx_ms: float,
|
||||
) -> None:
|
||||
try:
|
||||
key = (from_stage, to_stage)
|
||||
agg = transfer_agg.get(key)
|
||||
if agg is None:
|
||||
agg = {"sum_bytes": 0.0, "sum_ms": 0.0, "count": 0.0,
|
||||
"sum_rx_bytes": 0.0, "sum_rx_ms": 0.0, "rx_count": 0.0,
|
||||
"sum_total_ms": 0.0, "total_count": 0.0}
|
||||
agg = {
|
||||
"sum_bytes": 0.0,
|
||||
"sum_ms": 0.0,
|
||||
"count": 0.0,
|
||||
"sum_rx_bytes": 0.0,
|
||||
"sum_rx_ms": 0.0,
|
||||
"rx_count": 0.0,
|
||||
"sum_total_ms": 0.0,
|
||||
"total_count": 0.0,
|
||||
}
|
||||
transfer_agg[key] = agg
|
||||
agg["sum_bytes"] += float(size_bytes)
|
||||
agg["sum_ms"] += float(tx_ms)
|
||||
@@ -332,9 +461,11 @@ def count_tokens_from_outputs(engine_outputs: List[Any]) -> int:
|
||||
return total
|
||||
|
||||
|
||||
def build_stage_summary(stage_req_counts: List[int],
|
||||
stage_total_tokens: List[int],
|
||||
stage_total_time_ms: List[float]) -> List[Dict[str, Any]]:
|
||||
def build_stage_summary(
|
||||
stage_req_counts: List[int],
|
||||
stage_total_tokens: List[int],
|
||||
stage_total_time_ms: List[float],
|
||||
) -> List[Dict[str, Any]]:
|
||||
summary: List[Dict[str, Any]] = []
|
||||
for sid in range(len(stage_req_counts)):
|
||||
reqs = stage_req_counts[sid]
|
||||
@@ -342,53 +473,74 @@ def build_stage_summary(stage_req_counts: List[int],
|
||||
total_ms = float(stage_total_time_ms[sid])
|
||||
avg_req = (total_ms / reqs) if reqs > 0 else 0.0
|
||||
avg_tok = (tokens * 1000.0 / total_ms) if total_ms > 0 else 0.0
|
||||
summary.append({
|
||||
"stage_id": sid,
|
||||
"requests": int(reqs),
|
||||
"tokens": int(tokens),
|
||||
"total_time_ms": total_ms,
|
||||
"avg_time_per_request_ms": avg_req,
|
||||
"avg_tokens_per_s": avg_tok,
|
||||
})
|
||||
summary.append(
|
||||
{
|
||||
"stage_id": sid,
|
||||
"requests": int(reqs),
|
||||
"tokens": int(tokens),
|
||||
"total_time_ms": total_ms,
|
||||
"avg_time_per_request_ms": avg_req,
|
||||
"avg_tokens_per_s": avg_tok,
|
||||
}
|
||||
)
|
||||
return summary
|
||||
|
||||
|
||||
def build_transfer_summary(transfer_agg: Dict[tuple[int, int], Dict[str, float]]) -> List[Dict[str, Any]]:
|
||||
def build_transfer_summary(
|
||||
transfer_agg: Dict[tuple[int, int], Dict[str, float]]
|
||||
) -> List[Dict[str, Any]]:
|
||||
summary: List[Dict[str, Any]] = []
|
||||
for (src, dst), agg in transfer_agg.items():
|
||||
sum_bytes = float(agg.get("sum_bytes", 0.0))
|
||||
sum_ms = float(agg.get("sum_ms", 0.0))
|
||||
samples = int(agg.get("count", 0.0))
|
||||
tx_mbps = (sum_bytes * 8.0) / (max(sum_ms, 1e-6) * 1000.0) if sum_bytes > 0 else 0.0
|
||||
tx_mbps = (
|
||||
(sum_bytes * 8.0) / (max(sum_ms, 1e-6) * 1000.0) if sum_bytes > 0 else 0.0
|
||||
)
|
||||
sum_rx_bytes = float(agg.get("sum_rx_bytes", 0.0))
|
||||
sum_rx_ms = float(agg.get("sum_rx_ms", 0.0))
|
||||
samples_rx = int(agg.get("rx_count", 0.0))
|
||||
rx_mbps = (sum_rx_bytes * 8.0) / (max(sum_rx_ms, 1e-6) * 1000.0) if sum_rx_bytes > 0 else 0.0
|
||||
rx_mbps = (
|
||||
(sum_rx_bytes * 8.0) / (max(sum_rx_ms, 1e-6) * 1000.0)
|
||||
if sum_rx_bytes > 0
|
||||
else 0.0
|
||||
)
|
||||
sum_total_ms = float(agg.get("sum_total_ms", 0.0))
|
||||
samples_total = int(agg.get("total_count", 0.0))
|
||||
total_mbps = (sum_bytes * 8.0) / (max(sum_total_ms, 1e-6) * 1000.0) if sum_bytes > 0 else 0.0
|
||||
summary.append({
|
||||
"from_stage": src,
|
||||
"to_stage": dst,
|
||||
"samples": samples,
|
||||
"total_bytes": int(sum_bytes),
|
||||
"total_time_ms": sum_ms,
|
||||
"tx_mbps": tx_mbps,
|
||||
"rx_samples": samples_rx,
|
||||
"rx_total_bytes": int(sum_rx_bytes),
|
||||
"rx_total_time_ms": sum_rx_ms,
|
||||
"rx_mbps": rx_mbps,
|
||||
"total_samples": samples_total,
|
||||
"total_transfer_time_ms": sum_total_ms,
|
||||
"total_mbps": total_mbps,
|
||||
})
|
||||
total_mbps = (
|
||||
(sum_bytes * 8.0) / (max(sum_total_ms, 1e-6) * 1000.0)
|
||||
if sum_bytes > 0
|
||||
else 0.0
|
||||
)
|
||||
summary.append(
|
||||
{
|
||||
"from_stage": src,
|
||||
"to_stage": dst,
|
||||
"samples": samples,
|
||||
"total_bytes": int(sum_bytes),
|
||||
"total_time_ms": sum_ms,
|
||||
"tx_mbps": tx_mbps,
|
||||
"rx_samples": samples_rx,
|
||||
"rx_total_bytes": int(sum_rx_bytes),
|
||||
"rx_total_time_ms": sum_rx_ms,
|
||||
"rx_mbps": rx_mbps,
|
||||
"total_samples": samples_total,
|
||||
"total_transfer_time_ms": sum_total_ms,
|
||||
"total_mbps": total_mbps,
|
||||
}
|
||||
)
|
||||
return summary
|
||||
|
||||
|
||||
class OrchestratorMetrics:
|
||||
def __init__(self, num_stages: int, enable_stats: bool,
|
||||
stats_file: Optional[str], overall_stats_file: Optional[str],
|
||||
wall_start_ts: float) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
num_stages: int,
|
||||
enable_stats: bool,
|
||||
stats_file: Optional[str],
|
||||
overall_stats_file: Optional[str],
|
||||
wall_start_ts: float,
|
||||
) -> None:
|
||||
self.num_stages = int(num_stages)
|
||||
self.enable_stats = bool(enable_stats)
|
||||
self.stats_file = stats_file
|
||||
@@ -406,11 +558,19 @@ class OrchestratorMetrics:
|
||||
self.sum_per_request_transfer_ms: float = 0.0
|
||||
self.wall_start_ts: float = float(wall_start_ts)
|
||||
self.last_finish_ts: float = float(wall_start_ts)
|
||||
self.stage_seen_batches: Dict[int, set] = {sid: set() for sid in range(self.num_stages)}
|
||||
self.stage_first_ts: List[Optional[float]] = [None for _ in range(self.num_stages)]
|
||||
self.stage_last_ts: List[Optional[float]] = [None for _ in range(self.num_stages)]
|
||||
self.stage_seen_batches: Dict[int, set] = {
|
||||
sid: set() for sid in range(self.num_stages)
|
||||
}
|
||||
self.stage_first_ts: List[Optional[float]] = [
|
||||
None for _ in range(self.num_stages)
|
||||
]
|
||||
self.stage_last_ts: List[Optional[float]] = [
|
||||
None for _ in range(self.num_stages)
|
||||
]
|
||||
|
||||
def on_stage_metrics(self, stage_id: int, req_id: Any, metrics: Dict[str, Any]) -> None:
|
||||
def on_stage_metrics(
|
||||
self, stage_id: int, req_id: Any, metrics: Dict[str, Any]
|
||||
) -> None:
|
||||
record_stage_metrics(
|
||||
self.per_request,
|
||||
self.stage_req_counts,
|
||||
@@ -425,7 +585,9 @@ class OrchestratorMetrics:
|
||||
if batch_id_raw is not None:
|
||||
batch_id = int(batch_id_raw)
|
||||
if batch_id not in self.stage_seen_batches[stage_id]:
|
||||
self.stage_total_time_ms[stage_id] += float(metrics.get("stage_gen_time_ms", 0.0))
|
||||
self.stage_total_time_ms[stage_id] += float(
|
||||
metrics.get("stage_gen_time_ms", 0.0)
|
||||
)
|
||||
self.stage_seen_batches[stage_id].add(batch_id)
|
||||
except Exception:
|
||||
pass
|
||||
@@ -466,8 +628,15 @@ class OrchestratorMetrics:
|
||||
float(total_ms_c),
|
||||
)
|
||||
|
||||
def on_forward(self, from_stage: int, to_stage: int, req_id: Any,
|
||||
size_bytes: int, tx_ms: float, used_shm: bool) -> None:
|
||||
def on_forward(
|
||||
self,
|
||||
from_stage: int,
|
||||
to_stage: int,
|
||||
req_id: Any,
|
||||
size_bytes: int,
|
||||
tx_ms: float,
|
||||
used_shm: bool,
|
||||
) -> None:
|
||||
# Mark first input time for the destination stage if not set
|
||||
if self.stage_first_ts[to_stage] is None:
|
||||
self.stage_first_ts[to_stage] = time.time()
|
||||
@@ -491,8 +660,9 @@ class OrchestratorMetrics:
|
||||
float(tx_ms),
|
||||
)
|
||||
|
||||
def on_finalize_request(self, stage_id: int, req_id: Any, engine_outputs: List[Any],
|
||||
req_start_ts: float) -> None:
|
||||
def on_finalize_request(
|
||||
self, stage_id: int, req_id: Any, engine_outputs: List[Any], req_start_ts: float
|
||||
) -> None:
|
||||
try:
|
||||
rid_int = int(req_id)
|
||||
except Exception:
|
||||
@@ -509,7 +679,9 @@ class OrchestratorMetrics:
|
||||
self.e2e_total_tokens += int(num_tokens)
|
||||
self.e2e_count += 1
|
||||
self.e2e_done.add(rid_int)
|
||||
pr = self.per_request.setdefault(rid_int, {"stages": {}, "transfers_ms": 0.0, "transfers_bytes": 0})
|
||||
pr = self.per_request.setdefault(
|
||||
rid_int, {"stages": {}, "transfers_ms": 0.0, "transfers_bytes": 0}
|
||||
)
|
||||
per_req_record = {
|
||||
"type": "overall_request",
|
||||
"request_id": rid_int,
|
||||
@@ -524,7 +696,9 @@ class OrchestratorMetrics:
|
||||
log_overall_record(self.overall_stats_file, per_req_record)
|
||||
if self.enable_stats and self.stats_file:
|
||||
e2e_tpt = (e2e_ms / num_tokens) if num_tokens > 0 else 0.0
|
||||
log_orchestrator_e2e(self.stats_file, req_id, stage_id, e2e_ms, int(num_tokens), e2e_tpt)
|
||||
log_orchestrator_e2e(
|
||||
self.stats_file, req_id, stage_id, e2e_ms, int(num_tokens), e2e_tpt
|
||||
)
|
||||
|
||||
def build_and_log_summary(self, final_stage_id_for_e2e: int) -> Dict[str, Any]:
|
||||
# Compute stage summary using wall time between first input and last output per stage
|
||||
@@ -532,22 +706,34 @@ class OrchestratorMetrics:
|
||||
for sid in range(self.num_stages):
|
||||
first_ts = self.stage_first_ts[sid]
|
||||
last_ts = self.stage_last_ts[sid]
|
||||
total_ms = (max(0.0, (last_ts - first_ts)) * 1000.0) if (first_ts is not None and last_ts is not None) else 0.0
|
||||
total_ms = (
|
||||
(max(0.0, (last_ts - first_ts)) * 1000.0)
|
||||
if (first_ts is not None and last_ts is not None)
|
||||
else 0.0
|
||||
)
|
||||
reqs = self.stage_req_counts[sid]
|
||||
tokens = self.stage_total_tokens[sid]
|
||||
avg_req = (total_ms / reqs) if reqs > 0 else 0.0
|
||||
avg_tok = (tokens * 1000.0 / total_ms) if total_ms > 0 else 0.0
|
||||
stage_summary.append({
|
||||
"stage_id": sid,
|
||||
"requests": int(reqs),
|
||||
"tokens": int(tokens),
|
||||
"total_time_ms": float(total_ms),
|
||||
"avg_time_per_request_ms": float(avg_req),
|
||||
"avg_tokens_per_s": float(avg_tok),
|
||||
})
|
||||
stage_summary.append(
|
||||
{
|
||||
"stage_id": sid,
|
||||
"requests": int(reqs),
|
||||
"tokens": int(tokens),
|
||||
"total_time_ms": float(total_ms),
|
||||
"avg_time_per_request_ms": float(avg_req),
|
||||
"avg_tokens_per_s": float(avg_tok),
|
||||
}
|
||||
)
|
||||
transfer_summary = build_transfer_summary(self.transfer_agg)
|
||||
e2e_avg_req = (self.e2e_total_ms / self.e2e_count) if self.e2e_count > 0 else 0.0
|
||||
e2e_avg_tok = (self.e2e_total_tokens * 1000.0 / self.e2e_total_ms) if self.e2e_total_ms > 0 else 0.0
|
||||
e2e_avg_req = (
|
||||
(self.e2e_total_ms / self.e2e_count) if self.e2e_count > 0 else 0.0
|
||||
)
|
||||
e2e_avg_tok = (
|
||||
(self.e2e_total_tokens * 1000.0 / self.e2e_total_ms)
|
||||
if self.e2e_total_ms > 0
|
||||
else 0.0
|
||||
)
|
||||
wall_time_ms = max(0.0, (self.last_finish_ts - self.wall_start_ts) * 1000.0)
|
||||
summary: Dict[str, Any] = {
|
||||
"e2e_requests": int(self.e2e_count),
|
||||
@@ -566,5 +752,3 @@ class OrchestratorMetrics:
|
||||
if self.enable_stats and self.overall_stats_file:
|
||||
log_overall_summary(self.overall_stats_file, summary)
|
||||
return summary
|
||||
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
from typing import Any, Optional, Sequence, Union
|
||||
import logging
|
||||
import multiprocessing as mp
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
import os
|
||||
import queue
|
||||
import sys
|
||||
import time
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from typing import Any, Optional, Sequence, Union
|
||||
|
||||
import cloudpickle
|
||||
from pydantic import ValidationError
|
||||
@@ -26,18 +24,19 @@ from vllm.v1.engine.llm_engine import LLMEngine
|
||||
from vllm_omni.engine.arg_utils import OmniEngineArgs
|
||||
from vllm_omni.engine.output_processor import MultimodalOutputProcessor
|
||||
from vllm_omni.engine.processor import OmniProcessor
|
||||
from vllm_omni.entrypoints.omni_stage import OmniStage
|
||||
from vllm_omni.entrypoints.utils import load_stage_configs_from_model
|
||||
from vllm_omni.entrypoints.log_utils import (
|
||||
remove_old_logs,
|
||||
OrchestratorMetrics,
|
||||
configure_orchestrator_logger,
|
||||
init_stats_paths,
|
||||
OrchestratorMetrics,
|
||||
remove_old_logs,
|
||||
)
|
||||
from vllm_omni.entrypoints.stage_utils import (
|
||||
maybe_load_from_ipc as _load,
|
||||
encode_for_ipc as _encode,
|
||||
serialize_obj as _ser,
|
||||
from vllm_omni.entrypoints.omni_stage import OmniStage
|
||||
from vllm_omni.entrypoints.stage_utils import encode_for_ipc as _encode
|
||||
from vllm_omni.entrypoints.stage_utils import maybe_load_from_ipc as _load
|
||||
from vllm_omni.entrypoints.stage_utils import serialize_obj as _ser
|
||||
from vllm_omni.entrypoints.utils import (
|
||||
load_stage_configs_from_model,
|
||||
load_stage_configs_from_yaml,
|
||||
)
|
||||
from vllm_omni.outputs import OmniRequestOutput
|
||||
|
||||
@@ -52,23 +51,25 @@ class OmniLLM:
|
||||
- max_inflight=1 per stage (serial within a stage), but pipeline across stages
|
||||
"""
|
||||
|
||||
def __init__(self, model: str,
|
||||
stage_configs=None,
|
||||
log_stats: bool = False,
|
||||
log_file: Optional[str] = None,
|
||||
init_sleep_seconds: int = 20,
|
||||
shm_threshold_bytes: int = 65536,
|
||||
batch_timeout: int = 10,
|
||||
init_timeout: int = 300,
|
||||
**kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
model: str,
|
||||
stage_configs_path: Optional[str] = None,
|
||||
log_stats: bool = False,
|
||||
log_file: Optional[str] = None,
|
||||
init_sleep_seconds: int = 20,
|
||||
shm_threshold_bytes: int = 65536,
|
||||
batch_timeout: int = 10,
|
||||
init_timeout: int = 300,
|
||||
**kwargs,
|
||||
):
|
||||
self.batch_timeout = batch_timeout
|
||||
self._enable_stats: bool = bool(log_stats)
|
||||
# Do NOT call super().__init__ to avoid creating OmniStageLLM instances in parent.
|
||||
if stage_configs is None:
|
||||
if stage_configs_path is None:
|
||||
self.stage_configs = load_stage_configs_from_model(model)
|
||||
else:
|
||||
self.stage_configs = stage_configs
|
||||
|
||||
self.stage_configs = load_stage_configs_from_yaml(stage_configs_path)
|
||||
|
||||
# Optional file handler for orchestrator
|
||||
self._log_file = log_file
|
||||
@@ -76,18 +77,34 @@ class OmniLLM:
|
||||
remove_old_logs(self._log_file, len(self.stage_list))
|
||||
configure_orchestrator_logger(logger, self._log_file)
|
||||
|
||||
self._stats_file, self._overall_stats_file = init_stats_paths(self._enable_stats, self._log_file)
|
||||
self._initialize_stages(model, init_sleep_seconds, shm_threshold_bytes, init_timeout)
|
||||
self._stats_file, self._overall_stats_file = init_stats_paths(
|
||||
self._enable_stats, self._log_file
|
||||
)
|
||||
self._initialize_stages(
|
||||
model, init_sleep_seconds, shm_threshold_bytes, init_timeout
|
||||
)
|
||||
|
||||
def _initialize_stages(self, model: str, init_sleep_seconds: int, shm_threshold_bytes: int, init_timeout: int) -> None:
|
||||
def _initialize_stages(
|
||||
self,
|
||||
model: str,
|
||||
init_sleep_seconds: int,
|
||||
shm_threshold_bytes: int,
|
||||
init_timeout: int,
|
||||
) -> None:
|
||||
self.stage_list: list[OmniStage] = []
|
||||
|
||||
# Build OmniStage instances in parallel, preserve original order
|
||||
def _build_stage(idx_cfg: tuple[int, Any]) -> tuple[int, OmniStage]:
|
||||
idx, cfg = idx_cfg
|
||||
return idx, OmniStage(cfg)
|
||||
|
||||
with ThreadPoolExecutor(max_workers=min(len(self.stage_configs), max(1, os.cpu_count() or 1))) as executor:
|
||||
futures = [executor.submit(_build_stage, (idx, cfg)) for idx, cfg in enumerate(self.stage_configs)]
|
||||
with ThreadPoolExecutor(
|
||||
max_workers=min(len(self.stage_configs), max(1, os.cpu_count() or 1))
|
||||
) as executor:
|
||||
futures = [
|
||||
executor.submit(_build_stage, (idx, cfg))
|
||||
for idx, cfg in enumerate(self.stage_configs)
|
||||
]
|
||||
results: list[tuple[int, OmniStage]] = []
|
||||
for fut in as_completed(futures):
|
||||
results.append(fut.result())
|
||||
@@ -104,7 +121,6 @@ class OmniLLM:
|
||||
# Wait for all stages to report readiness before seeding
|
||||
self._stages_ready: set[int] = set()
|
||||
self._wait_for_stages_ready(timeout=init_timeout)
|
||||
|
||||
|
||||
def _start_stage_processes(self, model: str) -> None:
|
||||
for stage_id, stage in enumerate(self.stage_list):
|
||||
@@ -131,7 +147,10 @@ class OmniLLM:
|
||||
try:
|
||||
q.put_nowait(None)
|
||||
except Exception as e:
|
||||
logger.warning("[Orchestrator] Failed to send shutdown signal to stage input queue: %s", e)
|
||||
logger.warning(
|
||||
"[Orchestrator] Failed to send shutdown signal to stage input queue: %s",
|
||||
e,
|
||||
)
|
||||
for stage in self.stage_list:
|
||||
try:
|
||||
stage.stop_stage_worker()
|
||||
@@ -150,10 +169,27 @@ class OmniLLM:
|
||||
sampling_params_list: Optional[
|
||||
Union[SamplingParams, Sequence[SamplingParams]]
|
||||
] = None,
|
||||
) -> list[OmniRequestOutput]:
|
||||
try:
|
||||
return self._run_generation(prompts, sampling_params_list)
|
||||
except Exception as e:
|
||||
logger.exception("[Orchestrator] Failed to run generation: %s", e)
|
||||
raise e
|
||||
finally:
|
||||
self.close()
|
||||
|
||||
def _run_generation(
|
||||
self,
|
||||
prompts: Union[PromptType, Sequence[PromptType]],
|
||||
sampling_params_list: Optional[
|
||||
Union[SamplingParams, Sequence[SamplingParams]]
|
||||
] = None,
|
||||
) -> list[OmniRequestOutput]:
|
||||
logger.debug("[Orchestrator] generate() called")
|
||||
if sampling_params_list is None:
|
||||
raise ValueError("sampling_params_list is required for pipelined generation")
|
||||
raise ValueError(
|
||||
"sampling_params_list is required for pipelined generation"
|
||||
)
|
||||
if len(sampling_params_list) != len(self.stage_list):
|
||||
raise ValueError(
|
||||
f"Expected {len(self.stage_list)} sampling params, got {len(sampling_params_list)}"
|
||||
@@ -171,12 +207,13 @@ class OmniLLM:
|
||||
num_stages = len(self.stage_list)
|
||||
|
||||
# Map from request_id to original prompt
|
||||
request_id_to_prompt: dict[int, PromptType] = {i: p for i, p in enumerate(request_prompts)}
|
||||
request_id_to_prompt: dict[int, PromptType] = {
|
||||
i: p for i, p in enumerate(request_prompts)
|
||||
}
|
||||
|
||||
# Track per-request start time for end-to-end timing
|
||||
_req_start_ts: dict[int, float] = {}
|
||||
_wall_start_ts: float = time.time()
|
||||
_last_finish_ts: float = _wall_start_ts
|
||||
|
||||
# Determine the final stage for E2E stats (highest stage_id with final_output=True; fallback to last stage)
|
||||
final_stage_id_for_e2e = -1
|
||||
@@ -187,13 +224,25 @@ class OmniLLM:
|
||||
if final_stage_id_for_e2e < 0:
|
||||
final_stage_id_for_e2e = len(self.stage_list) - 1
|
||||
except Exception as e:
|
||||
logger.debug("[Orchestrator] Failed to determine final stage for E2E; falling back to last: %s", e, exc_info=True)
|
||||
logger.debug(
|
||||
"[Orchestrator] Failed to determine final stage for E2E; falling back to last: %s",
|
||||
e,
|
||||
exc_info=True,
|
||||
)
|
||||
final_stage_id_for_e2e = len(self.stage_list) - 1
|
||||
# Metrics/aggregation helper
|
||||
metrics = OrchestratorMetrics(num_stages, self._enable_stats, self._stats_file, self._overall_stats_file, _wall_start_ts)
|
||||
metrics = OrchestratorMetrics(
|
||||
num_stages,
|
||||
self._enable_stats,
|
||||
self._stats_file,
|
||||
self._overall_stats_file,
|
||||
_wall_start_ts,
|
||||
)
|
||||
|
||||
# Seed stage-0 queue with all requests
|
||||
logger.debug("[Orchestrator] Seeding %d requests into stage-0", len(request_prompts))
|
||||
logger.debug(
|
||||
"[Orchestrator] Seeding %d requests into stage-0", len(request_prompts)
|
||||
)
|
||||
# Mark first input time for stage-0
|
||||
metrics.stage_first_ts[0] = metrics.stage_first_ts[0] or time.time()
|
||||
|
||||
@@ -214,7 +263,11 @@ class OmniLLM:
|
||||
completed_requests = 0
|
||||
total_requests = len(request_prompts)
|
||||
|
||||
logger.debug("[Orchestrator] Entering scheduling loop: total_requests=%d, stages=%d", total_requests, num_stages)
|
||||
logger.debug(
|
||||
"[Orchestrator] Entering scheduling loop: total_requests=%d, stages=%d",
|
||||
total_requests,
|
||||
num_stages,
|
||||
)
|
||||
while completed_requests < total_requests:
|
||||
made_progress = False
|
||||
for stage_id, stage in enumerate(self.stage_list):
|
||||
@@ -225,24 +278,42 @@ class OmniLLM:
|
||||
made_progress = True
|
||||
req_id = result.get("request_id")
|
||||
if "error" in result:
|
||||
logger.error("Stage %s error on request %s: %s", stage_id, req_id, result["error"])
|
||||
logger.error(
|
||||
"Stage %s error on request %s: %s",
|
||||
stage_id,
|
||||
req_id,
|
||||
result["error"],
|
||||
)
|
||||
continue
|
||||
|
||||
|
||||
if result.get("type") == "stage_ready":
|
||||
#Only happens when stage is initialized slower than expected, so we wait for a short time and try again
|
||||
# Only happens when stage is initialized slower than expected, so we wait for a short time and try again
|
||||
time.sleep(0.05)
|
||||
continue
|
||||
|
||||
engine_outputs = _load(result, obj_key="engine_outputs", shm_key="engine_outputs_shm")
|
||||
engine_outputs = _load(
|
||||
result, obj_key="engine_outputs", shm_key="engine_outputs_shm"
|
||||
)
|
||||
# Mark last output time for this stage whenever we receive outputs
|
||||
metrics.stage_last_ts[stage_id] = max(metrics.stage_last_ts[stage_id] or 0.0, time.time())
|
||||
metrics.stage_last_ts[stage_id] = max(
|
||||
metrics.stage_last_ts[stage_id] or 0.0, time.time()
|
||||
)
|
||||
try:
|
||||
_m = result.get("metrics")
|
||||
if _m is not None:
|
||||
metrics.on_stage_metrics(stage_id, req_id, _m)
|
||||
except Exception as e:
|
||||
logger.exception("[Orchestrator] Failed to process metrics for stage %s, req %s: %s", stage_id, req_id, e)
|
||||
logger.debug("[Orchestrator] Stage-%s completed request %s; forwarding or finalizing", stage_id, req_id)
|
||||
logger.exception(
|
||||
"[Orchestrator] Failed to process metrics for stage %s, req %s: %s",
|
||||
stage_id,
|
||||
req_id,
|
||||
e,
|
||||
)
|
||||
logger.debug(
|
||||
"[Orchestrator] Stage-%s completed request %s; forwarding or finalizing",
|
||||
stage_id,
|
||||
req_id,
|
||||
)
|
||||
stage.set_engine_outputs(engine_outputs)
|
||||
|
||||
if getattr(stage, "final_output", False):
|
||||
@@ -253,20 +324,43 @@ class OmniLLM:
|
||||
request_output=engine_outputs,
|
||||
)
|
||||
)
|
||||
logger.debug("[Orchestrator] Request %s finalized at stage-%s", req_id, stage_id)
|
||||
logger.debug(
|
||||
"[Orchestrator] Request %s finalized at stage-%s",
|
||||
req_id,
|
||||
stage_id,
|
||||
)
|
||||
|
||||
# End-to-end timing and time-per-token for final output (only once per request at the designated final stage)
|
||||
try:
|
||||
rid_int = int(req_id) if isinstance(req_id, (int, str)) and str(req_id).isdigit() else req_id
|
||||
if stage_id == final_stage_id_for_e2e and rid_int not in metrics.e2e_done:
|
||||
metrics.on_finalize_request(stage_id, req_id, engine_outputs, _req_start_ts.get(req_id, _wall_start_ts))
|
||||
rid_int = (
|
||||
int(req_id)
|
||||
if isinstance(req_id, (int, str)) and str(req_id).isdigit()
|
||||
else req_id
|
||||
)
|
||||
if (
|
||||
stage_id == final_stage_id_for_e2e
|
||||
and rid_int not in metrics.e2e_done
|
||||
):
|
||||
metrics.on_finalize_request(
|
||||
stage_id,
|
||||
req_id,
|
||||
engine_outputs,
|
||||
_req_start_ts.get(req_id, _wall_start_ts),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception("[Orchestrator] Finalize request handling error for req %s at stage %s: %s", req_id, stage_id, e)
|
||||
logger.exception(
|
||||
"[Orchestrator] Finalize request handling error for req %s at stage %s: %s",
|
||||
req_id,
|
||||
stage_id,
|
||||
e,
|
||||
)
|
||||
|
||||
next_stage_id = stage_id + 1
|
||||
if next_stage_id < num_stages:
|
||||
next_stage: OmniStage = self.stage_list[next_stage_id]
|
||||
next_inputs = next_stage.process_engine_inputs(self.stage_list, [request_id_to_prompt[req_id]])
|
||||
next_inputs = next_stage.process_engine_inputs(
|
||||
self.stage_list, [request_id_to_prompt[req_id]]
|
||||
)
|
||||
sp_next: SamplingParams = sampling_params_list[next_stage_id] # type: ignore[index]
|
||||
try:
|
||||
# Measure transfer size and time (encode + enqueue)
|
||||
@@ -282,27 +376,51 @@ class OmniLLM:
|
||||
obj_key="engine_inputs",
|
||||
shm_key="engine_inputs_shm",
|
||||
)
|
||||
ipc_payload.update({
|
||||
"request_id": req_id,
|
||||
"sampling_params": sp_next,
|
||||
"sent_ts": time.time(),
|
||||
})
|
||||
ipc_payload.update(
|
||||
{
|
||||
"request_id": req_id,
|
||||
"sampling_params": sp_next,
|
||||
"sent_ts": time.time(),
|
||||
}
|
||||
)
|
||||
self.stage_list[next_stage_id].submit(ipc_payload)
|
||||
t1 = time.time()
|
||||
tx_ms = (t1 - t0) * 1000.0
|
||||
metrics.on_forward(stage_id, next_stage_id, req_id, int(size_bytes), float(tx_ms), bool("engine_inputs_shm" in ipc_payload))
|
||||
metrics.on_forward(
|
||||
stage_id,
|
||||
next_stage_id,
|
||||
req_id,
|
||||
int(size_bytes),
|
||||
float(tx_ms),
|
||||
bool("engine_inputs_shm" in ipc_payload),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning("[Orchestrator] IPC encode failed for req %s: %s; falling back to inline payload", req_id, e)
|
||||
self.stage_list[next_stage_id].submit({
|
||||
"request_id": req_id,
|
||||
"engine_inputs": next_inputs,
|
||||
"sampling_params": sp_next,
|
||||
})
|
||||
logger.debug("[Orchestrator] Forwarded request %s to stage-%s", req_id, next_stage_id)
|
||||
logger.warning(
|
||||
"[Orchestrator] IPC encode failed for req %s: %s; falling back to inline payload",
|
||||
req_id,
|
||||
e,
|
||||
)
|
||||
self.stage_list[next_stage_id].submit(
|
||||
{
|
||||
"request_id": req_id,
|
||||
"engine_inputs": next_inputs,
|
||||
"sampling_params": sp_next,
|
||||
}
|
||||
)
|
||||
logger.debug(
|
||||
"[Orchestrator] Forwarded request %s to stage-%s",
|
||||
req_id,
|
||||
next_stage_id,
|
||||
)
|
||||
remaining_by_stage[next_stage_id] += 1
|
||||
else:
|
||||
completed_requests += 1
|
||||
logger.debug("[Orchestrator] Request %s fully completed (%d/%d)", req_id, completed_requests, total_requests)
|
||||
logger.debug(
|
||||
"[Orchestrator] Request %s fully completed (%d/%d)",
|
||||
req_id,
|
||||
completed_requests,
|
||||
total_requests,
|
||||
)
|
||||
|
||||
if not made_progress:
|
||||
time.sleep(0.005)
|
||||
@@ -341,7 +459,9 @@ class OmniLLM:
|
||||
not_ready = sorted(set(range(num_stages)) - set(self._stages_ready))
|
||||
logger.warning(
|
||||
"[Orchestrator] Initialization timeout: only %s/%s stages are ready; not ready: %s",
|
||||
len(self._stages_ready), num_stages, not_ready,
|
||||
len(self._stages_ready),
|
||||
num_stages,
|
||||
not_ready,
|
||||
)
|
||||
# Provide actionable suggestions before shutdown
|
||||
try:
|
||||
|
||||
@@ -1,44 +1,58 @@
|
||||
"""
|
||||
Stage manager for orchestrating multiple engines in vLLM-omni.
|
||||
|
||||
Enhanced to encapsulate per-stage process lifecycle and worker logic
|
||||
(device setup, LLM init, batching, shared-memory IPC), while preserving
|
||||
the original input processing utilities for cross-stage data wiring.
|
||||
|
||||
Enhanced to encapsulate per-stage process lifecycle and worker logic
|
||||
(device setup, LLM init, batching, shared-memory IPC), while preserving
|
||||
the original input processing utilities for cross-stage data wiring.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import importlib
|
||||
import logging
|
||||
import multiprocessing as mp
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
from vllm_omni.entrypoints.stage_utils import (
|
||||
_to_dict,
|
||||
set_stage_gpu_devices,
|
||||
maybe_load_from_ipc_with_metrics,
|
||||
maybe_dump_to_shm,
|
||||
)
|
||||
|
||||
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.usage.usage_lib import UsageContext
|
||||
from vllm.v1.engine import EngineCoreOutput
|
||||
from vllm.v1.engine.async_llm import AsyncLLM
|
||||
from vllm.v1.engine.llm_engine import LLMEngine
|
||||
from vllm_omni.engine.arg_utils import AsyncOmniEngineArgs
|
||||
from vllm_omni.entrypoints.stage_utils import (
|
||||
_to_dict,
|
||||
maybe_dump_to_shm,
|
||||
maybe_load_from_ipc_with_metrics,
|
||||
set_stage_gpu_devices,
|
||||
)
|
||||
from vllm_omni.inputs.data import OmniTokensPrompt
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
class OmniStage:
|
||||
def __init__(self, stage_config):
|
||||
self.stage_config = stage_config
|
||||
self.engine = None
|
||||
self.async_engine = None
|
||||
self.vllm_config = None
|
||||
self.tokenizer = None
|
||||
self.input_preprocessor = None
|
||||
self.is_tracing_enabled = False
|
||||
self.stage_id = stage_config.stage_id
|
||||
self.engine_args = stage_config.engine_args
|
||||
self.model_stage = stage_config.engine_args.model_stage
|
||||
if hasattr(stage_config, "engine_input_source"):
|
||||
self.engine_input_source = stage_config.engine_input_source
|
||||
else:
|
||||
self.engine_input_source = []
|
||||
self.engine_input_source = getattr(stage_config, "engine_input_source", [])
|
||||
self.engine_output_type = stage_config.engine_args.engine_output_type
|
||||
self.engine_outputs = None
|
||||
self.is_comprehension = getattr(stage_config, "is_comprehension", False)
|
||||
if hasattr(stage_config, "custom_process_input_func"):
|
||||
# Import the module specified in the config (already a full module path)
|
||||
module_path, func_name = stage_config.custom_process_input_func.rsplit(
|
||||
@@ -49,16 +63,12 @@ class OmniStage:
|
||||
else:
|
||||
self.custom_process_input_func = None
|
||||
|
||||
if hasattr(stage_config, "final_output"):
|
||||
self.final_output = stage_config.final_output
|
||||
else:
|
||||
self.final_output = False
|
||||
|
||||
if hasattr(stage_config, "final_output_type"):
|
||||
self.final_output_type = stage_config.final_output_type
|
||||
else:
|
||||
self.final_output_type = None
|
||||
|
||||
self.final_output = getattr(stage_config, "final_output", False)
|
||||
self.final_output_type = getattr(stage_config, "final_output_type", None)
|
||||
default_sampling_params = getattr(stage_config, "default_sampling_params", {})
|
||||
self.default_sampling_params = SamplingParams(
|
||||
**_to_dict(default_sampling_params)
|
||||
)
|
||||
# Runtime orchestration state (added)
|
||||
self._in_q: Optional[mp.Queue] = None
|
||||
self._out_q: Optional[mp.Queue] = None
|
||||
@@ -75,6 +85,22 @@ class OmniStage:
|
||||
"""Initialize the async engine for the stage."""
|
||||
self.async_engine = async_engine
|
||||
|
||||
def set_vllm_config(self, vllm_config) -> None:
|
||||
"""Set the vllm_config for the stage (received from worker process)."""
|
||||
self.vllm_config = vllm_config
|
||||
|
||||
def set_tokenizer(self, tokenizer: AnyTokenizer) -> None:
|
||||
"""Set the tokenizer for the stage (received from worker process)."""
|
||||
self.tokenizer = tokenizer
|
||||
|
||||
def set_input_preprocessor(self, input_preprocessor: InputPreprocessor) -> None:
|
||||
"""Set the input preprocessor for the stage (received from worker process)."""
|
||||
self.input_preprocessor = input_preprocessor
|
||||
|
||||
def set_is_tracing_enabled(self, is_tracing_enabled: bool) -> None:
|
||||
"""Set the is_tracing_enabled for the stage (received from worker process)."""
|
||||
self.is_tracing_enabled = is_tracing_enabled
|
||||
|
||||
def set_engine_outputs(self, engine_outputs: EngineCoreOutput) -> None:
|
||||
"""Set the engine output for the stage."""
|
||||
self.engine_outputs = engine_outputs
|
||||
@@ -88,12 +114,15 @@ class OmniStage:
|
||||
self,
|
||||
model: str,
|
||||
*,
|
||||
is_async: bool = False,
|
||||
log_file: Optional[str] = None,
|
||||
shm_threshold_bytes: int = 65536,
|
||||
ctx: Optional[mp.context.BaseContext] = None,
|
||||
batch_timeout: int = 10,
|
||||
) -> None:
|
||||
assert self._in_q is not None and self._out_q is not None, "Queues must be attached before start_process"
|
||||
assert (
|
||||
self._in_q is not None and self._out_q is not None
|
||||
), "Queues must be attached before start_process"
|
||||
self._log_file = log_file
|
||||
self._shm_threshold_bytes = shm_threshold_bytes
|
||||
ctx = ctx or mp.get_context("spawn")
|
||||
@@ -106,10 +135,31 @@ class OmniStage:
|
||||
"runtime": runtime_cfg,
|
||||
"shm_threshold_bytes": self._shm_threshold_bytes,
|
||||
}
|
||||
self._proc = ctx.Process(
|
||||
target=_stage_worker,
|
||||
args=(model, stage_payload, self._in_q, self._out_q, self._log_file, batch_timeout),
|
||||
)
|
||||
if is_async:
|
||||
self._proc = ctx.Process(
|
||||
target=_stage_worker_async_entry,
|
||||
args=(
|
||||
self,
|
||||
model,
|
||||
stage_payload,
|
||||
self._in_q,
|
||||
self._out_q,
|
||||
self._log_file,
|
||||
batch_timeout,
|
||||
),
|
||||
)
|
||||
else:
|
||||
self._proc = ctx.Process(
|
||||
target=_stage_worker,
|
||||
args=(
|
||||
model,
|
||||
stage_payload,
|
||||
self._in_q,
|
||||
self._out_q,
|
||||
self._log_file,
|
||||
batch_timeout,
|
||||
),
|
||||
)
|
||||
self._proc.start()
|
||||
|
||||
def stop_stage_worker(self) -> None:
|
||||
@@ -117,17 +167,23 @@ class OmniStage:
|
||||
try:
|
||||
self._in_q.put_nowait(None)
|
||||
except Exception as e:
|
||||
self._logger.warning("[Stage-%s] Failed to send shutdown to in_q: %s", self.stage_id, e)
|
||||
self._logger.warning(
|
||||
"[Stage-%s] Failed to send shutdown to in_q: %s", self.stage_id, e
|
||||
)
|
||||
if self._proc is not None:
|
||||
try:
|
||||
self._proc.join(timeout=5)
|
||||
except Exception as e:
|
||||
self._logger.debug("[Stage-%s] join() failed: %s", self.stage_id, e, exc_info=True)
|
||||
self._logger.debug(
|
||||
"[Stage-%s] join() failed: %s", self.stage_id, e, exc_info=True
|
||||
)
|
||||
if self._proc.is_alive():
|
||||
try:
|
||||
self._proc.terminate()
|
||||
except Exception as e:
|
||||
self._logger.warning("[Stage-%s] terminate() failed: %s", self.stage_id, e)
|
||||
self._logger.warning(
|
||||
"[Stage-%s] terminate() failed: %s", self.stage_id, e
|
||||
)
|
||||
|
||||
def submit(self, payload: Dict[str, Any]) -> None:
|
||||
assert self._in_q is not None
|
||||
@@ -150,9 +206,11 @@ class OmniStage:
|
||||
raise ValueError("engine_input_source is empty")
|
||||
source_stage_id = self.engine_input_source[0]
|
||||
source_outputs = stage_list[source_stage_id].engine_outputs
|
||||
if not isinstance(prompt, list):
|
||||
prompt = [prompt]
|
||||
multi_modal_data = {
|
||||
source_output.request_id: prompt.get("multi_modal_data", None)
|
||||
for source_output, prompt in zip(source_outputs, prompt)
|
||||
source_output.request_id: p.get("multi_modal_data", None)
|
||||
for source_output, p in zip(source_outputs, prompt)
|
||||
}
|
||||
|
||||
for source_output in source_outputs:
|
||||
@@ -184,15 +242,16 @@ def _stage_worker(
|
||||
) -> None:
|
||||
"""Stage worker entry: device setup, LLM init, batching, SHM IPC."""
|
||||
import logging as _logging
|
||||
from vllm_omni.entrypoints.omni_llm import OmniStageLLM # noqa: WPS433
|
||||
import time as _time
|
||||
|
||||
from vllm_omni.entrypoints.log_utils import ( # noqa: WPS433
|
||||
log_stage_running_avg,
|
||||
log_stage_batch_stats,
|
||||
compute_and_log_stage_request_stats,
|
||||
count_tokens_from_outputs,
|
||||
log_stage_batch_stats,
|
||||
log_stage_running_avg,
|
||||
)
|
||||
import queue as _queue
|
||||
import time as _time
|
||||
from vllm_omni.entrypoints.omni_llm import OmniStageLLM # noqa: WPS433
|
||||
|
||||
# no inline JSONL/serialization imports; logging handled by utilities
|
||||
|
||||
stage_id = stage_payload["stage_id"]
|
||||
@@ -207,11 +266,17 @@ def _stage_worker(
|
||||
stage_log.setLevel(_logging.DEBUG)
|
||||
fh = _logging.FileHandler(f"{log_file}.stage{stage_id}.log")
|
||||
fh.setLevel(_logging.DEBUG)
|
||||
fh.setFormatter(_logging.Formatter("%(asctime)s [PID:%(process)d] [Stage-%(stage)s] %(levelname)s: %(message)s"))
|
||||
fh.setFormatter(
|
||||
_logging.Formatter(
|
||||
"%(asctime)s [PID:%(process)d] [Stage-%(stage)s] %(levelname)s: %(message)s"
|
||||
)
|
||||
)
|
||||
|
||||
class _StageFilter(_logging.Filter):
|
||||
def filter(self, record: _logging.LogRecord) -> bool:
|
||||
setattr(record, "stage", stage_id)
|
||||
return True
|
||||
|
||||
fh.addFilter(_StageFilter())
|
||||
stage_log.addHandler(fh)
|
||||
except Exception:
|
||||
@@ -230,10 +295,16 @@ def _stage_worker(
|
||||
try:
|
||||
set_stage_gpu_devices(stage_id, runtime_cfg.get("devices"))
|
||||
except Exception as e:
|
||||
_logging.getLogger(__name__).warning("[Stage-%s] Device setup failed: %s", stage_id, e)
|
||||
_logging.getLogger(__name__).warning(
|
||||
"[Stage-%s] Device setup failed: %s", stage_id, e
|
||||
)
|
||||
|
||||
# Init LLM
|
||||
_logging.getLogger(__name__).debug("[Stage-%s] Initializing engine with args keys=%s", stage_id, list(engine_args.keys()))
|
||||
_logging.getLogger(__name__).debug(
|
||||
"[Stage-%s] Initializing engine with args keys=%s",
|
||||
stage_id,
|
||||
list(engine_args.keys()),
|
||||
)
|
||||
stage_engine = OmniStageLLM(model=model, **engine_args)
|
||||
_logging.getLogger(__name__).debug("[Stage-%s] Engine initialized", stage_id)
|
||||
# Signal readiness to orchestrator
|
||||
@@ -247,7 +318,9 @@ def _stage_worker(
|
||||
task = in_q.get()
|
||||
_recv_dequeue_ts = _time.time()
|
||||
if task is None:
|
||||
_logging.getLogger(__name__).debug("[Stage-%s] Received shutdown signal", stage_id)
|
||||
_logging.getLogger(__name__).debug(
|
||||
"[Stage-%s] Received shutdown signal", stage_id
|
||||
)
|
||||
break
|
||||
|
||||
max_batch_size = int(runtime_cfg.get("max_batch_size", 1) or 1)
|
||||
@@ -289,23 +362,37 @@ def _stage_worker(
|
||||
elif isinstance(ein, dict):
|
||||
batch_engine_inputs.append(ein)
|
||||
else:
|
||||
_logging.getLogger(__name__).exception("[Stage-%s] Invalid engine input type: %s", stage_id, type(ein))
|
||||
_logging.getLogger(__name__).exception(
|
||||
"[Stage-%s] Invalid engine input type: %s", stage_id, type(ein)
|
||||
)
|
||||
sampling_params = batch_tasks[0]["sampling_params"]
|
||||
_logging.getLogger(__name__).debug("[Stage-%s] Received batch size=%d, request_ids=%s", stage_id, len(batch_tasks), batch_request_ids)
|
||||
_logging.getLogger(__name__).debug(
|
||||
"[Stage-%s] Received batch size=%d, request_ids=%s",
|
||||
stage_id,
|
||||
len(batch_tasks),
|
||||
batch_request_ids,
|
||||
)
|
||||
print("--------------------------------", flush=True)
|
||||
print(f"[Stage-{stage_id}] Received batch size={len(batch_tasks)}, request_ids={batch_request_ids}", flush=True)
|
||||
print(
|
||||
f"[Stage-{stage_id}] Received batch size={len(batch_tasks)}, request_ids={batch_request_ids}",
|
||||
flush=True,
|
||||
)
|
||||
print("--------------------------------", flush=True)
|
||||
try:
|
||||
_batch_seq += 1
|
||||
gen_outputs: List[Any] = []
|
||||
_gen_t0 = _time.time()
|
||||
for ro in stage_engine.generate(batch_engine_inputs, sampling_params, use_tqdm=False):
|
||||
for ro in stage_engine.generate(
|
||||
batch_engine_inputs, sampling_params, use_tqdm=False
|
||||
):
|
||||
gen_outputs.append(ro)
|
||||
_gen_t1 = _time.time()
|
||||
_gen_ms = (_gen_t1 - _gen_t0) * 1000.0
|
||||
|
||||
# Group outputs per request id with fallback
|
||||
req_to_outputs: Dict[Any, List[Any]] = {rid: [] for rid in batch_request_ids}
|
||||
req_to_outputs: Dict[Any, List[Any]] = {
|
||||
rid: [] for rid in batch_request_ids
|
||||
}
|
||||
unmapped: List[Any] = []
|
||||
for ro in gen_outputs:
|
||||
rid = getattr(ro, "request_id", None)
|
||||
@@ -326,12 +413,27 @@ def _stage_worker(
|
||||
_num_tokens = count_tokens_from_outputs(_r_outputs)
|
||||
_agg_total_tokens += _num_tokens
|
||||
_agg_total_gen_time_ms += _gen_ms
|
||||
_tokens_per_s = (_num_tokens * 1000.0 / _gen_ms) if _gen_ms > 0 else 0.0
|
||||
|
||||
if _stats_file:
|
||||
_avg_tokens_per_s = (_agg_total_tokens * 1000.0 / _agg_total_gen_time_ms) if _agg_total_gen_time_ms > 0 else 0.0
|
||||
log_stage_running_avg(_stats_file, stage_id, int(_agg_total_tokens), float(_agg_total_gen_time_ms), float(_avg_tokens_per_s))
|
||||
log_stage_batch_stats(_stats_file, stage_id, len(batch_tasks), float(_gen_ms), list(batch_request_ids))
|
||||
_avg_tokens_per_s = (
|
||||
(_agg_total_tokens * 1000.0 / _agg_total_gen_time_ms)
|
||||
if _agg_total_gen_time_ms > 0
|
||||
else 0.0
|
||||
)
|
||||
log_stage_running_avg(
|
||||
_stats_file,
|
||||
stage_id,
|
||||
int(_agg_total_tokens),
|
||||
float(_agg_total_gen_time_ms),
|
||||
float(_avg_tokens_per_s),
|
||||
)
|
||||
log_stage_batch_stats(
|
||||
_stats_file,
|
||||
stage_id,
|
||||
len(batch_tasks),
|
||||
float(_gen_ms),
|
||||
list(batch_request_ids),
|
||||
)
|
||||
|
||||
# Emit per-request results
|
||||
for rid in batch_request_ids:
|
||||
@@ -344,7 +446,9 @@ def _stage_worker(
|
||||
"batch_id": int(_batch_seq),
|
||||
"rx_decode_time_ms": float(_rx_decode_ms_by_rid.get(rid, 0.0)),
|
||||
"rx_transfer_bytes": int(_rx_bytes_by_rid.get(rid, 0)),
|
||||
"rx_in_flight_time_ms": float(_in_flight_ms_by_rid.get(rid, 0.0)),
|
||||
"rx_in_flight_time_ms": float(
|
||||
_in_flight_ms_by_rid.get(rid, 0.0)
|
||||
),
|
||||
}
|
||||
if _stats_file:
|
||||
compute_and_log_stage_request_stats(
|
||||
@@ -354,42 +458,339 @@ def _stage_worker(
|
||||
len(batch_tasks),
|
||||
r_outputs,
|
||||
float(_gen_ms),
|
||||
int(_metrics["rx_transfer_bytes"]), # type: ignore[index]
|
||||
float(_metrics["rx_decode_time_ms"]), # type: ignore[index]
|
||||
int(_metrics["rx_transfer_bytes"]), # type: ignore[index]
|
||||
float(_metrics["rx_decode_time_ms"]), # type: ignore[index]
|
||||
)
|
||||
if use_shm:
|
||||
out_q.put({
|
||||
out_q.put(
|
||||
{
|
||||
"request_id": rid,
|
||||
"stage_id": stage_id,
|
||||
"engine_outputs_shm": payload,
|
||||
"metrics": _metrics,
|
||||
}
|
||||
)
|
||||
else:
|
||||
out_q.put(
|
||||
{
|
||||
"request_id": rid,
|
||||
"stage_id": stage_id,
|
||||
"engine_outputs": payload,
|
||||
"metrics": _metrics,
|
||||
}
|
||||
)
|
||||
except Exception:
|
||||
out_q.put(
|
||||
{
|
||||
"request_id": rid,
|
||||
"stage_id": stage_id,
|
||||
"engine_outputs": r_outputs,
|
||||
"metrics": {
|
||||
"num_tokens_out": int(
|
||||
count_tokens_from_outputs(r_outputs)
|
||||
),
|
||||
"stage_gen_time_ms": _gen_ms,
|
||||
"rx_decode_time_ms": float(
|
||||
_rx_decode_ms_by_rid.get(rid, 0.0)
|
||||
),
|
||||
"rx_transfer_bytes": int(_rx_bytes_by_rid.get(rid, 0)),
|
||||
"rx_in_flight_time_ms": float(
|
||||
_in_flight_ms_by_rid.get(rid, 0.0)
|
||||
),
|
||||
},
|
||||
}
|
||||
)
|
||||
_logging.getLogger(__name__).debug(
|
||||
"[Stage-%s] Enqueued result for request %s to downstream",
|
||||
stage_id,
|
||||
rid,
|
||||
)
|
||||
except Exception as e:
|
||||
_logging.getLogger(__name__).exception(
|
||||
"[Stage-%s] Failed on batch %s: %s", stage_id, batch_request_ids, e
|
||||
)
|
||||
for rid in batch_request_ids:
|
||||
out_q.put(
|
||||
{
|
||||
"request_id": rid,
|
||||
"stage_id": stage_id,
|
||||
"error": str(e),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _stage_worker_async_entry(
|
||||
omni_stage: OmniStage,
|
||||
model: str,
|
||||
stage_payload: Dict[str, Any],
|
||||
in_q: mp.Queue,
|
||||
out_q: mp.Queue,
|
||||
log_file: Optional[str] = None,
|
||||
batch_timeout: int = 10,
|
||||
) -> None:
|
||||
asyncio.run(
|
||||
_stage_worker_async(
|
||||
omni_stage, model, stage_payload, in_q, out_q, log_file, batch_timeout
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def _stage_worker_async(
|
||||
omni_stage: OmniStage,
|
||||
model: str,
|
||||
stage_payload: Dict[str, Any],
|
||||
in_q: mp.Queue,
|
||||
out_q: mp.Queue,
|
||||
log_file: Optional[str] = None,
|
||||
batch_timeout: int = 10,
|
||||
) -> None:
|
||||
"""Stage worker entry: device setup, LLM init, batching, SHM IPC."""
|
||||
import logging as _logging
|
||||
import time as _time
|
||||
|
||||
from vllm_omni.entrypoints.async_omni_llm import AsyncOmniStageLLM # noqa: WPS433
|
||||
from vllm_omni.entrypoints.log_utils import ( # noqa: WPS433
|
||||
compute_and_log_stage_request_stats,
|
||||
count_tokens_from_outputs,
|
||||
log_stage_batch_stats,
|
||||
log_stage_running_avg,
|
||||
)
|
||||
|
||||
# no inline JSONL/serialization imports; logging handled by utilities
|
||||
|
||||
stage_id = stage_payload["stage_id"]
|
||||
engine_args = stage_payload.get("engine_args", {})
|
||||
runtime_cfg = stage_payload.get("runtime", {})
|
||||
shm_threshold_bytes = int(stage_payload.get("shm_threshold_bytes", 65536))
|
||||
|
||||
# Per-stage file logger (optional)
|
||||
try:
|
||||
if log_file:
|
||||
stage_log = _logging.getLogger(__name__)
|
||||
stage_log.setLevel(_logging.DEBUG)
|
||||
fh = _logging.FileHandler(f"{log_file}.stage{stage_id}.log")
|
||||
fh.setLevel(_logging.DEBUG)
|
||||
fh.setFormatter(
|
||||
_logging.Formatter(
|
||||
"%(asctime)s [PID:%(process)d] [Stage-%(stage)s] %(levelname)s: %(message)s" # noqa: E501
|
||||
)
|
||||
)
|
||||
|
||||
class _StageFilter(_logging.Filter):
|
||||
def filter(self, record: _logging.LogRecord) -> bool:
|
||||
setattr(record, "stage", stage_id)
|
||||
return True
|
||||
|
||||
fh.addFilter(_StageFilter())
|
||||
stage_log.addHandler(fh)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Stage stats JSONL file
|
||||
_stats_file = f"{log_file}.stage{stage_id}.stats.jsonl" if log_file else None
|
||||
|
||||
# Aggregates for running average
|
||||
_agg_total_tokens = 0
|
||||
_agg_total_gen_time_ms = 0.0
|
||||
# Monotonic batch id per stage process for orchestrator dedup on time
|
||||
# aggregation
|
||||
_batch_seq = 0
|
||||
|
||||
# Device mapping
|
||||
try:
|
||||
set_stage_gpu_devices(stage_id, runtime_cfg.get("devices"))
|
||||
except Exception as e:
|
||||
_logging.getLogger(__name__).warning(
|
||||
"[Stage-%s] Device setup failed: %s", stage_id, e
|
||||
)
|
||||
|
||||
# Init LLM
|
||||
_logging.getLogger(__name__).debug(
|
||||
"[Stage-%s] Initializing engine with args keys=%s",
|
||||
stage_id,
|
||||
list(engine_args.keys()),
|
||||
)
|
||||
omni_engine_args = AsyncOmniEngineArgs(model=model, **engine_args)
|
||||
usage_context = UsageContext.OPENAI_API_SERVER
|
||||
vllm_config = omni_engine_args.create_engine_config(usage_context=usage_context)
|
||||
stage_engine = AsyncOmniStageLLM.from_vllm_config(
|
||||
vllm_config=vllm_config,
|
||||
usage_context=usage_context,
|
||||
engine_args=omni_engine_args,
|
||||
)
|
||||
omni_stage.set_async_engine(stage_engine)
|
||||
# Don't keep the dummy data in memory
|
||||
await stage_engine.reset_mm_cache()
|
||||
_logging.getLogger(__name__).debug("[Stage-%s] Engine initialized", stage_id)
|
||||
# Signal readiness to orchestrator and send vllm_config back to main process
|
||||
try:
|
||||
# Send vllm_config back to main process so it can be accessed via
|
||||
# get_vllm_config(). This is needed because async_engine is only available
|
||||
# in the worker process
|
||||
|
||||
# input_preprocessor = await stage_engine.get_input_preprocessor()
|
||||
out_q.put(
|
||||
{
|
||||
"type": "stage_ready",
|
||||
"stage_id": stage_id,
|
||||
"vllm_config": vllm_config,
|
||||
"tokenizer": getattr(stage_engine, "tokenizer", None),
|
||||
"is_tracing_enabled": await stage_engine.is_tracing_enabled(),
|
||||
# "input_preprocessor": input_preprocessor,
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
_logging.getLogger(__name__).warning(
|
||||
"[Stage-%s] Failed to send stage ready signal: %s", stage_id, e
|
||||
)
|
||||
|
||||
# Batch processing loop
|
||||
while True:
|
||||
task = in_q.get()
|
||||
_recv_dequeue_ts = _time.time()
|
||||
if task is None:
|
||||
_logging.getLogger(__name__).debug(
|
||||
"[Stage-%s] Received shutdown signal", stage_id
|
||||
)
|
||||
break
|
||||
|
||||
_rx_bytes_by_rid: Dict[Any, int] = {}
|
||||
_rx_decode_ms_by_rid: Dict[Any, float] = {}
|
||||
_in_flight_ms_by_rid: Dict[Any, float] = {}
|
||||
|
||||
rid = task["request_id"]
|
||||
try:
|
||||
sent_ts = (
|
||||
float(task.get("sent_ts", None)) if isinstance(task, dict) else None
|
||||
)
|
||||
if sent_ts is not None:
|
||||
_in_flight_ms_by_rid[rid] = (_recv_dequeue_ts - sent_ts) * 1000.0
|
||||
else:
|
||||
_in_flight_ms_by_rid[rid] = 0.0
|
||||
except Exception:
|
||||
_in_flight_ms_by_rid[rid] = 0.0
|
||||
ein, _rx_metrics = maybe_load_from_ipc_with_metrics(
|
||||
task, obj_key="engine_inputs", shm_key="engine_inputs_shm"
|
||||
)
|
||||
_rx_decode_ms_by_rid[rid] = float(_rx_metrics.get("rx_decode_time_ms", 0.0))
|
||||
_rx_bytes_by_rid[rid] = int(_rx_metrics.get("rx_transfer_bytes", 0))
|
||||
|
||||
sampling_params = task["sampling_params"]
|
||||
_logging.getLogger(__name__).debug(
|
||||
"[Stage-%s] Received batch size=1, request_ids=%d", stage_id, rid
|
||||
)
|
||||
print("--------------------------------", flush=True)
|
||||
print(
|
||||
f"[Stage-{stage_id}] Received batch size=1, request_ids={rid}", flush=True
|
||||
)
|
||||
print("--------------------------------", flush=True)
|
||||
try:
|
||||
_batch_seq += 1
|
||||
_gen_t0 = _time.time()
|
||||
if isinstance(ein, list):
|
||||
ein = ein[0]
|
||||
|
||||
async for res in stage_engine.generate(ein, sampling_params, rid):
|
||||
gen_output = res
|
||||
_gen_t1 = _time.time()
|
||||
_gen_ms = (_gen_t1 - _gen_t0) * 1000.0
|
||||
|
||||
r_outputs = [gen_output]
|
||||
_num_tokens = count_tokens_from_outputs(r_outputs)
|
||||
_agg_total_tokens += _num_tokens
|
||||
_agg_total_gen_time_ms += _gen_ms
|
||||
|
||||
if _stats_file:
|
||||
_avg_tokens_per_s = (
|
||||
(_agg_total_tokens * 1000.0 / _agg_total_gen_time_ms)
|
||||
if _agg_total_gen_time_ms > 0
|
||||
else 0.0
|
||||
)
|
||||
log_stage_running_avg(
|
||||
_stats_file,
|
||||
stage_id,
|
||||
int(_agg_total_tokens),
|
||||
float(_agg_total_gen_time_ms),
|
||||
float(_avg_tokens_per_s),
|
||||
)
|
||||
log_stage_batch_stats(_stats_file, stage_id, 1, float(_gen_ms), [rid])
|
||||
|
||||
try:
|
||||
use_shm, payload = maybe_dump_to_shm(r_outputs, shm_threshold_bytes)
|
||||
_metrics = {
|
||||
"num_tokens_out": int(count_tokens_from_outputs(r_outputs)),
|
||||
"stage_gen_time_ms": _gen_ms,
|
||||
"batch_id": int(_batch_seq),
|
||||
"rx_decode_time_ms": float(_rx_decode_ms_by_rid.get(rid, 0.0)),
|
||||
"rx_transfer_bytes": int(_rx_bytes_by_rid.get(rid, 0)),
|
||||
"rx_in_flight_time_ms": float(_in_flight_ms_by_rid.get(rid, 0.0)),
|
||||
}
|
||||
if _stats_file:
|
||||
compute_and_log_stage_request_stats(
|
||||
_stats_file,
|
||||
stage_id,
|
||||
rid,
|
||||
1,
|
||||
r_outputs,
|
||||
float(_gen_ms),
|
||||
int(_metrics["rx_transfer_bytes"]), # type: ignore[index]
|
||||
float(_metrics["rx_decode_time_ms"]), # type: ignore[index]
|
||||
)
|
||||
if use_shm:
|
||||
out_q.put(
|
||||
{
|
||||
"request_id": rid,
|
||||
"stage_id": stage_id,
|
||||
"engine_outputs_shm": payload,
|
||||
"metrics": _metrics,
|
||||
})
|
||||
else:
|
||||
out_q.put({
|
||||
}
|
||||
)
|
||||
else:
|
||||
out_q.put(
|
||||
{
|
||||
"request_id": rid,
|
||||
"stage_id": stage_id,
|
||||
"engine_outputs": payload,
|
||||
"metrics": _metrics,
|
||||
})
|
||||
except Exception:
|
||||
out_q.put({
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
_logging.getLogger(__name__).exception(
|
||||
"[Stage-%s] Failed to enqueue result for request %s: %s",
|
||||
stage_id,
|
||||
rid,
|
||||
e,
|
||||
)
|
||||
out_q.put(
|
||||
{
|
||||
"request_id": rid,
|
||||
"stage_id": stage_id,
|
||||
"engine_outputs": r_outputs,
|
||||
"metrics": {
|
||||
"num_tokens_out": int(count_tokens_from_outputs(r_outputs)),
|
||||
"stage_gen_time_ms": _gen_ms,
|
||||
"rx_decode_time_ms": float(_rx_decode_ms_by_rid.get(rid, 0.0)),
|
||||
"rx_decode_time_ms": float(
|
||||
_rx_decode_ms_by_rid.get(rid, 0.0)
|
||||
),
|
||||
"rx_transfer_bytes": int(_rx_bytes_by_rid.get(rid, 0)),
|
||||
"rx_in_flight_time_ms": float(_in_flight_ms_by_rid.get(rid, 0.0)),
|
||||
"rx_in_flight_time_ms": float(
|
||||
_in_flight_ms_by_rid.get(rid, 0.0)
|
||||
),
|
||||
},
|
||||
})
|
||||
_logging.getLogger(__name__).debug("[Stage-%s] Enqueued result for request %s to downstream", stage_id, rid)
|
||||
}
|
||||
)
|
||||
_logging.getLogger(__name__).debug(
|
||||
"[Stage-%s] Enqueued result for request %s to downstream", stage_id, rid
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
_logging.getLogger(__name__).exception("[Stage-%s] Failed on batch %s: %s", stage_id, batch_request_ids, e)
|
||||
for rid in batch_request_ids:
|
||||
out_q.put({
|
||||
_logging.getLogger(__name__).exception(
|
||||
"[Stage-%s] Failed on request %s: %s", stage_id, rid, e
|
||||
)
|
||||
out_q.put(
|
||||
{
|
||||
"request_id": rid,
|
||||
"stage_id": stage_id,
|
||||
"error": str(e),
|
||||
})
|
||||
}
|
||||
)
|
||||
Whitespace-only changes.
@@ -0,0 +1,346 @@
|
||||
import multiprocessing
|
||||
import multiprocessing.forkserver as forkserver
|
||||
import os
|
||||
from argparse import Namespace
|
||||
from contextlib import asynccontextmanager
|
||||
from http import HTTPStatus
|
||||
from typing import Any, AsyncIterator, Optional
|
||||
|
||||
from fastapi import Depends, HTTPException, Request
|
||||
from fastapi.responses import JSONResponse, StreamingResponse
|
||||
from starlette.datastructures import State
|
||||
|
||||
import vllm.envs as envs
|
||||
from vllm.config import VllmConfig
|
||||
from vllm.engine.protocol import EngineClient
|
||||
from vllm.entrypoints.chat_utils import (
|
||||
load_chat_template,
|
||||
resolve_hf_chat_template,
|
||||
resolve_mistral_chat_template,
|
||||
)
|
||||
from vllm.entrypoints.launcher import serve_http
|
||||
from vllm.entrypoints.logger import RequestLogger
|
||||
from vllm.entrypoints.openai.api_server import (
|
||||
base,
|
||||
build_app,
|
||||
load_log_config,
|
||||
maybe_register_tokenizer_info_endpoint,
|
||||
router,
|
||||
setup_server,
|
||||
validate_json_request,
|
||||
)
|
||||
from vllm.entrypoints.openai.protocol import (
|
||||
ChatCompletionRequest,
|
||||
ChatCompletionResponse,
|
||||
ErrorResponse,
|
||||
)
|
||||
from vllm.entrypoints.openai.serving_models import (
|
||||
BaseModelPath,
|
||||
LoRAModulePath,
|
||||
OpenAIServingModels,
|
||||
)
|
||||
from vllm.entrypoints.openai.tool_parsers import ToolParserManager
|
||||
|
||||
# yapf conflicts with isort for this block
|
||||
# yapf: disable
|
||||
# yapf: enable
|
||||
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_omni.entrypoints.async_omni_llm import AsyncOmniLLM
|
||||
from vllm_omni.entrypoints.openai.serving_chat import OmniOpenAIServingChat
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
async def omni_run_server(args, **uvicorn_kwargs) -> None:
|
||||
"""Run a single-worker API server."""
|
||||
|
||||
# Add process-specific prefix to stdout and stderr.
|
||||
decorate_logs("APIServer")
|
||||
|
||||
listen_address, sock = setup_server(args)
|
||||
await omni_run_server_worker(listen_address, sock, args, **uvicorn_kwargs)
|
||||
|
||||
|
||||
async def omni_run_server_worker(
|
||||
listen_address, sock, args, client_config=None, **uvicorn_kwargs
|
||||
) -> None:
|
||||
"""Run a single API server worker."""
|
||||
|
||||
if args.tool_parser_plugin and len(args.tool_parser_plugin) > 3:
|
||||
ToolParserManager.import_tool_parser(args.tool_parser_plugin)
|
||||
|
||||
# Load logging config for uvicorn if specified
|
||||
log_config = load_log_config(args.log_config_file)
|
||||
if log_config is not None:
|
||||
uvicorn_kwargs["log_config"] = log_config
|
||||
|
||||
async with build_async_omni_llm(
|
||||
args,
|
||||
client_config=client_config,
|
||||
) as engine_client:
|
||||
maybe_register_tokenizer_info_endpoint(args)
|
||||
app = build_app(args)
|
||||
|
||||
vllm_config = await engine_client.get_vllm_config()
|
||||
await omni_init_app_state(engine_client, vllm_config, app.state, args)
|
||||
|
||||
logger.info(
|
||||
"Starting vLLM API server %d on %s",
|
||||
vllm_config.parallel_config._api_process_rank,
|
||||
listen_address,
|
||||
)
|
||||
shutdown_task = await serve_http(
|
||||
app,
|
||||
sock=sock,
|
||||
enable_ssl_refresh=args.enable_ssl_refresh,
|
||||
host=args.host,
|
||||
port=args.port,
|
||||
log_level=args.uvicorn_log_level,
|
||||
# NOTE: When the 'disable_uvicorn_access_log' value is True,
|
||||
# no access log will be output.
|
||||
access_log=not args.disable_uvicorn_access_log,
|
||||
timeout_keep_alive=envs.VLLM_HTTP_TIMEOUT_KEEP_ALIVE,
|
||||
ssl_keyfile=args.ssl_keyfile,
|
||||
ssl_certfile=args.ssl_certfile,
|
||||
ssl_ca_certs=args.ssl_ca_certs,
|
||||
ssl_cert_reqs=args.ssl_cert_reqs,
|
||||
h11_max_incomplete_event_size=args.h11_max_incomplete_event_size,
|
||||
h11_max_header_count=args.h11_max_header_count,
|
||||
**uvicorn_kwargs,
|
||||
)
|
||||
|
||||
# NB: Await server shutdown only after the backend context is exited
|
||||
try:
|
||||
await shutdown_task
|
||||
finally:
|
||||
sock.close()
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def build_async_omni_llm(
|
||||
args: Namespace,
|
||||
*,
|
||||
disable_frontend_multiprocessing: Optional[bool] = None,
|
||||
client_config: Optional[dict[str, Any]] = None,
|
||||
) -> AsyncIterator[EngineClient]:
|
||||
|
||||
if os.getenv("VLLM_WORKER_MULTIPROC_METHOD") == "forkserver":
|
||||
# The executor is expected to be mp.
|
||||
# Pre-import heavy modules in the forkserver process
|
||||
logger.debug("Setup forkserver with pre-imports")
|
||||
multiprocessing.set_start_method("forkserver")
|
||||
multiprocessing.set_forkserver_preload(["vllm.v1.engine.async_llm"])
|
||||
forkserver.ensure_running()
|
||||
logger.debug("Forkserver setup complete!")
|
||||
|
||||
# Context manager to handle async_omni_llm lifecycle
|
||||
# Ensures everything is shutdown and cleaned up on error/exit
|
||||
async with build_async_omni_llm_from_stage_config(
|
||||
args,
|
||||
disable_frontend_multiprocessing=disable_frontend_multiprocessing,
|
||||
) as async_omni_llm:
|
||||
yield async_omni_llm
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def build_async_omni_llm_from_stage_config(
|
||||
args: Namespace,
|
||||
*,
|
||||
disable_frontend_multiprocessing: bool = False,
|
||||
client_config: Optional[dict[str, Any]] = None,
|
||||
) -> AsyncIterator[EngineClient]:
|
||||
"""
|
||||
Create AsyncOmniLLM, either:
|
||||
- in-process using the AsyncOmniLLM Directly
|
||||
- multiprocess using AsyncOmniLLM RPC
|
||||
|
||||
Returns the AsyncOmniLLM or None if the creation failed.
|
||||
"""
|
||||
|
||||
# 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."
|
||||
)
|
||||
|
||||
async_omni_llm: Optional[EngineClient] = None
|
||||
|
||||
try:
|
||||
if getattr(args, "stage_configs_path", None):
|
||||
async_omni_llm = AsyncOmniLLM(
|
||||
model=args.model, stage_configs_path=args.stage_configs_path
|
||||
)
|
||||
else:
|
||||
async_omni_llm = AsyncOmniLLM(model=args.model)
|
||||
|
||||
# # Don't keep the dummy data in memory
|
||||
# await async_llm.reset_mm_cache()
|
||||
|
||||
yield async_omni_llm
|
||||
finally:
|
||||
if async_omni_llm:
|
||||
async_omni_llm.shutdown()
|
||||
|
||||
|
||||
async def omni_init_app_state(
|
||||
engine_client: EngineClient,
|
||||
vllm_config: VllmConfig,
|
||||
state: State,
|
||||
args: Namespace,
|
||||
) -> None:
|
||||
|
||||
if args.served_model_name is not None:
|
||||
served_model_names = args.served_model_name
|
||||
else:
|
||||
served_model_names = [args.model]
|
||||
|
||||
if args.enable_log_requests:
|
||||
request_logger = RequestLogger(max_log_len=args.max_log_len)
|
||||
else:
|
||||
request_logger = None
|
||||
|
||||
base_model_paths = [
|
||||
BaseModelPath(name=name, model_path=args.model) for name in served_model_names
|
||||
]
|
||||
state.engine_client = engine_client
|
||||
state.log_stats = not args.disable_log_stats
|
||||
state.vllm_config = vllm_config
|
||||
model_config = vllm_config.model_config
|
||||
state.log_stats = not args.disable_log_stats
|
||||
|
||||
# For omni models
|
||||
state.stage_configs = engine_client.stage_configs
|
||||
|
||||
resolved_chat_template = load_chat_template(args.chat_template)
|
||||
if resolved_chat_template is not None:
|
||||
# Get the tokenizer to check official template
|
||||
tokenizer = await engine_client.get_tokenizer()
|
||||
|
||||
if isinstance(tokenizer, MistralTokenizer):
|
||||
# The warning is logged in resolve_mistral_chat_template.
|
||||
resolved_chat_template = resolve_mistral_chat_template(
|
||||
chat_template=resolved_chat_template
|
||||
)
|
||||
else:
|
||||
hf_chat_template = resolve_hf_chat_template(
|
||||
tokenizer=tokenizer,
|
||||
chat_template=None,
|
||||
tools=None,
|
||||
model_config=vllm_config.model_config,
|
||||
)
|
||||
|
||||
if hf_chat_template != resolved_chat_template:
|
||||
logger.warning(
|
||||
"Using supplied chat template: %s\n"
|
||||
"It is different from official chat template '%s'. "
|
||||
"This discrepancy may lead to performance degradation.",
|
||||
resolved_chat_template,
|
||||
args.model,
|
||||
)
|
||||
|
||||
if args.tool_server == "demo":
|
||||
tool_server: Optional[ToolServer] = DemoToolServer()
|
||||
assert isinstance(tool_server, DemoToolServer)
|
||||
await tool_server.init_and_validate()
|
||||
elif args.tool_server:
|
||||
tool_server = MCPToolServer()
|
||||
await tool_server.add_tool_server(args.tool_server)
|
||||
else:
|
||||
tool_server = None
|
||||
|
||||
# Merge default_mm_loras into the static lora_modules
|
||||
default_mm_loras = (
|
||||
vllm_config.lora_config.default_mm_loras
|
||||
if vllm_config.lora_config is not None
|
||||
else {}
|
||||
)
|
||||
|
||||
lora_modules = args.lora_modules
|
||||
if default_mm_loras:
|
||||
default_mm_lora_paths = [
|
||||
LoRAModulePath(
|
||||
name=modality,
|
||||
path=lora_path,
|
||||
)
|
||||
for modality, lora_path in default_mm_loras.items()
|
||||
]
|
||||
if args.lora_modules is None:
|
||||
lora_modules = default_mm_lora_paths
|
||||
else:
|
||||
lora_modules += default_mm_lora_paths
|
||||
|
||||
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,
|
||||
chat_template=resolved_chat_template,
|
||||
chat_template_content_format=args.chat_template_content_format,
|
||||
trust_request_chat_template=args.trust_request_chat_template,
|
||||
return_tokens_as_token_ids=args.return_tokens_as_token_ids,
|
||||
enable_auto_tools=args.enable_auto_tool_choice,
|
||||
exclude_tools_when_tool_choice_none=args.exclude_tools_when_tool_choice_none,
|
||||
tool_parser=args.tool_call_parser,
|
||||
reasoning_parser=args.structured_outputs_config.reasoning_parser,
|
||||
enable_prompt_tokens_details=args.enable_prompt_tokens_details,
|
||||
enable_force_include_usage=args.enable_force_include_usage,
|
||||
enable_log_outputs=args.enable_log_outputs,
|
||||
log_error_stack=args.log_error_stack,
|
||||
)
|
||||
|
||||
state.enable_server_load_tracking = args.enable_server_load_tracking
|
||||
state.server_load_metrics = 0
|
||||
|
||||
|
||||
def Omnichat(request: Request) -> Optional[OmniOpenAIServingChat]:
|
||||
return request.app.state.openai_serving_chat
|
||||
|
||||
|
||||
@router.post(
|
||||
"/v1/chat/completions",
|
||||
dependencies=[Depends(validate_json_request)],
|
||||
responses={
|
||||
HTTPStatus.OK.value: {"content": {"text/event-stream": {}}},
|
||||
HTTPStatus.BAD_REQUEST.value: {"model": ErrorResponse},
|
||||
HTTPStatus.NOT_FOUND.value: {"model": ErrorResponse},
|
||||
HTTPStatus.INTERNAL_SERVER_ERROR.value: {"model": ErrorResponse},
|
||||
},
|
||||
)
|
||||
@with_cancellation
|
||||
@load_aware_call
|
||||
async def create_chat_completion(request: ChatCompletionRequest, raw_request: Request):
|
||||
handler = Omnichat(raw_request)
|
||||
if handler is None:
|
||||
return base(raw_request).create_error_response(
|
||||
message="The model does not support Chat Completions API"
|
||||
)
|
||||
try:
|
||||
generator = await handler.create_chat_completion(request, raw_request)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR.value, detail=str(e)
|
||||
) from e
|
||||
if isinstance(generator, ErrorResponse):
|
||||
return JSONResponse(
|
||||
content=generator.model_dump(), status_code=generator.error.code
|
||||
)
|
||||
|
||||
elif isinstance(generator, ChatCompletionResponse):
|
||||
return JSONResponse(content=generator.model_dump())
|
||||
|
||||
return StreamingResponse(content=generator, media_type="text/event-stream")
|
||||
@@ -0,0 +1,727 @@
|
||||
import asyncio
|
||||
import base64
|
||||
import json
|
||||
import time
|
||||
import uuid
|
||||
from collections.abc import AsyncGenerator, AsyncIterator
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from io import BytesIO
|
||||
from typing import Optional, Union
|
||||
|
||||
import jinja2
|
||||
from fastapi import Request
|
||||
from pydantic import TypeAdapter
|
||||
|
||||
try:
|
||||
import soundfile
|
||||
except ImportError:
|
||||
soundfile = None
|
||||
|
||||
from openai.types.chat.chat_completion_audio import (
|
||||
ChatCompletionAudio as OpenAIChatCompletionAudio,
|
||||
)
|
||||
|
||||
from vllm.entrypoints.chat_utils import (
|
||||
ConversationMessage,
|
||||
get_history_tool_calls_cnt,
|
||||
make_tool_call_id,
|
||||
)
|
||||
from vllm.entrypoints.harmony_utils import parse_chat_output
|
||||
from vllm.entrypoints.openai.protocol import (
|
||||
ChatCompletionNamedToolChoiceParam,
|
||||
ChatCompletionRequest,
|
||||
ChatCompletionResponse,
|
||||
ChatCompletionResponseChoice,
|
||||
ChatMessage,
|
||||
ErrorResponse,
|
||||
FunctionCall,
|
||||
FunctionDefinition,
|
||||
PromptTokenUsageInfo,
|
||||
RequestResponseMetadata,
|
||||
ToolCall,
|
||||
UsageInfo,
|
||||
)
|
||||
from vllm.entrypoints.openai.serving_chat import OpenAIServingChat
|
||||
from vllm.entrypoints.openai.serving_engine import RequestPrompt, clamp_prompt_logprobs
|
||||
from vllm.entrypoints.openai.tool_parsers.mistral_tool_parser import MistralToolCall
|
||||
from vllm.inputs.data import PromptType
|
||||
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 (
|
||||
maybe_serialize_tool_calls,
|
||||
truncate_tool_call_ids,
|
||||
validate_request_params,
|
||||
)
|
||||
from vllm.utils import as_list
|
||||
from vllm_omni.outputs import OmniRequestOutput
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
class OmniOpenAIServingChat(OpenAIServingChat):
|
||||
async def create_chat_completion(
|
||||
self,
|
||||
request: ChatCompletionRequest,
|
||||
raw_request: Optional[Request] = None,
|
||||
) -> Union[AsyncGenerator[str, None], ChatCompletionResponse, ErrorResponse]:
|
||||
"""
|
||||
Chat Completion API similar to OpenAI's API.
|
||||
|
||||
See https://platform.openai.com/docs/api-reference/chat/create
|
||||
for the API specification. This API mimics the OpenAI
|
||||
Chat Completion API.
|
||||
"""
|
||||
error_check_ret = await self._check_model(request)
|
||||
if error_check_ret is not None:
|
||||
logger.error("Error with model %s", error_check_ret)
|
||||
return error_check_ret
|
||||
|
||||
# If the engine is dead, raise the engine's DEAD_ERROR.
|
||||
# This is required for the streaming case, where we return a
|
||||
# success status before we actually start generating text :).
|
||||
if self.engine_client.errored:
|
||||
raise self.engine_client.dead_error
|
||||
|
||||
try:
|
||||
lora_request = self._maybe_get_adapters(
|
||||
request, supports_default_mm_loras=True
|
||||
)
|
||||
|
||||
model_name = self.models.model_name(lora_request)
|
||||
|
||||
tokenizer = await self.engine_client.get_tokenizer()
|
||||
|
||||
tool_parser = self.tool_parser
|
||||
|
||||
if isinstance(tokenizer, MistralTokenizer):
|
||||
# because of issues with pydantic we need to potentially
|
||||
# re-serialize the tool_calls field of the request
|
||||
# for more info: see comment in `maybe_serialize_tool_calls`
|
||||
maybe_serialize_tool_calls(request)
|
||||
truncate_tool_call_ids(request)
|
||||
validate_request_params(request)
|
||||
|
||||
if (
|
||||
request.tool_choice == "auto"
|
||||
and not (self.enable_auto_tools and tool_parser is not None)
|
||||
and not isinstance(tokenizer, MistralTokenizer)
|
||||
and not self.use_harmony
|
||||
):
|
||||
# for hf tokenizers, "auto" tools requires
|
||||
# --enable-auto-tool-choice and --tool-call-parser
|
||||
return self.create_error_response(
|
||||
'"auto" tool choice requires '
|
||||
"--enable-auto-tool-choice and --tool-call-parser to be set"
|
||||
)
|
||||
|
||||
if request.tools is None or (
|
||||
request.tool_choice == "none"
|
||||
and self.exclude_tools_when_tool_choice_none
|
||||
):
|
||||
tool_dicts = None
|
||||
else:
|
||||
tool_dicts = [tool.model_dump() for tool in request.tools]
|
||||
|
||||
# Common case.
|
||||
request_chat_template = request.chat_template
|
||||
chat_template_kwargs = request.chat_template_kwargs
|
||||
if not self.trust_request_chat_template and (
|
||||
request_chat_template is not None
|
||||
or (
|
||||
chat_template_kwargs
|
||||
and chat_template_kwargs.get("chat_template") is not None
|
||||
)
|
||||
):
|
||||
return self.create_error_response(
|
||||
"Chat template is passed with request, but "
|
||||
"--trust-request-chat-template is not set. "
|
||||
"Refused request with untrusted chat template."
|
||||
)
|
||||
(
|
||||
conversation,
|
||||
request_prompts,
|
||||
engine_prompts,
|
||||
) = await self._preprocess_chat(
|
||||
request,
|
||||
tokenizer,
|
||||
request.messages,
|
||||
chat_template=request_chat_template or self.chat_template,
|
||||
chat_template_content_format=self.chat_template_content_format,
|
||||
add_generation_prompt=request.add_generation_prompt,
|
||||
continue_final_message=request.continue_final_message,
|
||||
tool_dicts=tool_dicts,
|
||||
documents=request.documents,
|
||||
chat_template_kwargs=request.chat_template_kwargs,
|
||||
tool_parser=tool_parser,
|
||||
add_special_tokens=request.add_special_tokens,
|
||||
)
|
||||
|
||||
except (ValueError, TypeError, RuntimeError, jinja2.TemplateError) as e:
|
||||
logger.exception("Error in preprocessing prompt inputs")
|
||||
return self.create_error_response(f"{e} {e.__cause__}")
|
||||
|
||||
request_id = (
|
||||
"chatcmpl-" f"{self._base_request_id(raw_request, request.request_id)}"
|
||||
)
|
||||
|
||||
request_metadata = RequestResponseMetadata(request_id=request_id)
|
||||
if raw_request:
|
||||
raw_request.state.request_metadata = request_metadata
|
||||
|
||||
# Schedule the request and get the result generator.
|
||||
generators: list[AsyncGenerator[RequestOutput, None]] = []
|
||||
try:
|
||||
for i, engine_prompt in enumerate(engine_prompts):
|
||||
|
||||
if hasattr(request, "sampling_params_list"):
|
||||
sampling_params_list = self._to_sampling_params_list(
|
||||
request.sampling_params_list
|
||||
)
|
||||
else:
|
||||
sampling_params_list = None
|
||||
|
||||
self._log_inputs(
|
||||
request_id,
|
||||
request_prompts[i],
|
||||
params_list=sampling_params_list,
|
||||
lora_request=lora_request,
|
||||
)
|
||||
|
||||
trace_headers = (
|
||||
None
|
||||
if raw_request is None
|
||||
else await self._get_trace_headers(raw_request.headers)
|
||||
)
|
||||
|
||||
generator = self.engine_client.generate(
|
||||
prompt=engine_prompt,
|
||||
request_id=request_id,
|
||||
sampling_params_list=sampling_params_list,
|
||||
lora_request=lora_request,
|
||||
trace_headers=trace_headers,
|
||||
priority=request.priority,
|
||||
)
|
||||
|
||||
generators.append(generator)
|
||||
except ValueError as e:
|
||||
# TODO: Use a vllm-specific Validation Error
|
||||
return self.create_error_response(str(e))
|
||||
|
||||
assert len(generators) == 1
|
||||
(result_generator,) = generators
|
||||
|
||||
# Streaming response
|
||||
if request.stream:
|
||||
return self.chat_completion_stream_generator(
|
||||
request,
|
||||
result_generator,
|
||||
request_id,
|
||||
model_name,
|
||||
conversation,
|
||||
tokenizer,
|
||||
request_metadata,
|
||||
enable_force_include_usage=self.enable_force_include_usage,
|
||||
)
|
||||
|
||||
try:
|
||||
return await self.chat_completion_full_generator(
|
||||
request,
|
||||
result_generator,
|
||||
request_id,
|
||||
model_name,
|
||||
conversation,
|
||||
tokenizer,
|
||||
request_metadata,
|
||||
)
|
||||
except ValueError as e:
|
||||
# TODO: Use a vllm-specific Validation Error
|
||||
return self.create_error_response(str(e))
|
||||
|
||||
def _to_sampling_params_list(
|
||||
self, sampling_params_list: list[dict]
|
||||
) -> list[SamplingParams]:
|
||||
|
||||
final_sampling_params_list = []
|
||||
for sampling_params in sampling_params_list:
|
||||
if isinstance(sampling_params, dict):
|
||||
final_sampling_params_list.append(SamplingParams(**sampling_params))
|
||||
elif isinstance(sampling_params, SamplingParams):
|
||||
final_sampling_params_list.append(sampling_params)
|
||||
else:
|
||||
raise ValueError(f"Invalid sampling params: {sampling_params}")
|
||||
return final_sampling_params_list
|
||||
|
||||
def _log_inputs(
|
||||
self,
|
||||
request_id: str,
|
||||
inputs: Union[RequestPrompt, PromptType],
|
||||
params_list: Optional[list[SamplingParams]],
|
||||
lora_request: Optional[LoRARequest],
|
||||
) -> None:
|
||||
if self.request_logger is None:
|
||||
return
|
||||
prompt, prompt_token_ids, prompt_embeds = None, None, None
|
||||
if isinstance(inputs, str):
|
||||
prompt = inputs
|
||||
elif isinstance(inputs, list):
|
||||
prompt_token_ids = inputs
|
||||
else:
|
||||
prompt = getattr(inputs, "prompt", None)
|
||||
prompt_token_ids = getattr(inputs, "prompt_token_ids", None)
|
||||
|
||||
logger.info(
|
||||
"Received request %s: prompt: %r, "
|
||||
"params_list: %s, prompt_token_ids: %s, "
|
||||
"prompt_embeds shape: %s, "
|
||||
"lora_request: %s.",
|
||||
request_id,
|
||||
prompt,
|
||||
params_list,
|
||||
prompt_token_ids,
|
||||
prompt_embeds.shape if prompt_embeds is not None else None,
|
||||
lora_request,
|
||||
)
|
||||
|
||||
async def chat_completion_full_generator(
|
||||
self,
|
||||
request: ChatCompletionRequest,
|
||||
result_generator: AsyncIterator[RequestOutput],
|
||||
request_id: str,
|
||||
model_name: str,
|
||||
conversation: list[ConversationMessage],
|
||||
tokenizer: AnyTokenizer,
|
||||
request_metadata: RequestResponseMetadata,
|
||||
) -> Union[ErrorResponse, ChatCompletionResponse]:
|
||||
|
||||
created_time = int(time.time())
|
||||
final_res: Optional[RequestOutput] = None
|
||||
|
||||
final_outputs: list[OmniRequestOutput] = []
|
||||
try:
|
||||
async for res in result_generator:
|
||||
final_outputs.append(res)
|
||||
except asyncio.CancelledError:
|
||||
return self.create_error_response("Client disconnected")
|
||||
except ValueError as e:
|
||||
# TODO: Use a vllm-specific Validation Error
|
||||
return self.create_error_response(str(e))
|
||||
|
||||
assert final_outputs is not None
|
||||
|
||||
choices: list[ChatCompletionResponseChoice] = []
|
||||
|
||||
usage = UsageInfo(prompt_tokens=0, completion_tokens=0, total_tokens=0)
|
||||
role = self.get_chat_request_role(request)
|
||||
prompt_logprobs = None
|
||||
prompt_token_ids = None
|
||||
kv_transfer_params = None
|
||||
|
||||
for omni_outputs in final_outputs:
|
||||
choices_data = []
|
||||
if omni_outputs.final_output_type == "text":
|
||||
(
|
||||
choices_data,
|
||||
usage,
|
||||
prompt_logprobs,
|
||||
prompt_token_ids,
|
||||
kv_transfer_params,
|
||||
) = self._create_text_choice(
|
||||
request, omni_outputs, tokenizer, conversation, role
|
||||
)
|
||||
elif omni_outputs.final_output_type == "audio":
|
||||
choices_data = self._create_audio_choice(omni_outputs, role)
|
||||
else:
|
||||
logger.warning(
|
||||
f"Unsupported final output type: {omni_outputs.final_output_type}"
|
||||
)
|
||||
continue
|
||||
choices.extend(choices_data)
|
||||
|
||||
response = ChatCompletionResponse(
|
||||
id=request_id,
|
||||
created=created_time,
|
||||
model=model_name,
|
||||
choices=choices,
|
||||
usage=usage,
|
||||
prompt_logprobs=prompt_logprobs,
|
||||
prompt_token_ids=prompt_token_ids,
|
||||
kv_transfer_params=kv_transfer_params,
|
||||
)
|
||||
|
||||
# Log complete response if output logging is enabled
|
||||
if self.enable_log_outputs and self.request_logger:
|
||||
for choice in choices:
|
||||
output_text = ""
|
||||
if choice.message.content:
|
||||
output_text = choice.message.content
|
||||
elif choice.message.tool_calls:
|
||||
# For tool calls, log the function name and arguments
|
||||
tool_call_descriptions = []
|
||||
for tc in choice.message.tool_calls:
|
||||
if hasattr(tc.function, "name") and hasattr(
|
||||
tc.function, "arguments"
|
||||
):
|
||||
tool_call_descriptions.append(
|
||||
f"{tc.function.name}({tc.function.arguments})"
|
||||
)
|
||||
tool_calls_str = ", ".join(tool_call_descriptions)
|
||||
output_text = f"[tool_calls: {tool_calls_str}]"
|
||||
|
||||
if output_text:
|
||||
# Get the corresponding output token IDs
|
||||
output_token_ids = None
|
||||
if choice.index < len(final_res.outputs):
|
||||
output_token_ids = final_res.outputs[choice.index].token_ids
|
||||
|
||||
self.request_logger.log_outputs(
|
||||
request_id=request_id,
|
||||
outputs=output_text,
|
||||
output_token_ids=output_token_ids,
|
||||
finish_reason=choice.finish_reason,
|
||||
is_streaming=False,
|
||||
delta=False,
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
def _create_text_choice(
|
||||
self,
|
||||
request: ChatCompletionRequest,
|
||||
omni_outputs: OmniRequestOutput,
|
||||
tokenizer: AnyTokenizer,
|
||||
conversation: list[ConversationMessage],
|
||||
role: str,
|
||||
):
|
||||
final_res = omni_outputs.request_output
|
||||
if self.tool_call_id_type == "kimi_k2":
|
||||
history_tool_call_cnt = get_history_tool_calls_cnt(conversation)
|
||||
else:
|
||||
history_tool_call_cnt = 0
|
||||
|
||||
choices: list[ChatCompletionResponseChoice] = []
|
||||
|
||||
for output in final_res.outputs:
|
||||
token_ids = output.token_ids
|
||||
out_logprobs = output.logprobs
|
||||
tool_call_info = None
|
||||
|
||||
if request.logprobs and request.top_logprobs is not None:
|
||||
assert out_logprobs is not None, "Did not output logprobs"
|
||||
logprobs = self._create_chat_logprobs(
|
||||
token_ids=token_ids,
|
||||
top_logprobs=out_logprobs,
|
||||
num_output_top_logprobs=request.top_logprobs,
|
||||
tokenizer=tokenizer,
|
||||
return_as_token_id=request.return_tokens_as_token_ids,
|
||||
)
|
||||
else:
|
||||
logprobs = None
|
||||
|
||||
if self.use_harmony:
|
||||
reasoning_content, content, _ = parse_chat_output(token_ids)
|
||||
if not request.include_reasoning:
|
||||
reasoning_content = None
|
||||
|
||||
if self.tool_parser is not None:
|
||||
tool_parser = self.tool_parser(tokenizer)
|
||||
# NOTE: We use token_ids for openai tool parser
|
||||
tool_call_info = tool_parser.extract_tool_calls(
|
||||
"",
|
||||
request=request,
|
||||
token_ids=token_ids, # type: ignore
|
||||
)
|
||||
content = tool_call_info.content
|
||||
message = ChatMessage(
|
||||
role=role,
|
||||
reasoning_content=reasoning_content,
|
||||
content=content,
|
||||
tool_calls=tool_call_info.tool_calls,
|
||||
)
|
||||
else:
|
||||
message = ChatMessage(
|
||||
role=role,
|
||||
reasoning_content=reasoning_content,
|
||||
content=content,
|
||||
)
|
||||
|
||||
choice_data = ChatCompletionResponseChoice(
|
||||
index=output.index,
|
||||
message=message,
|
||||
logprobs=logprobs,
|
||||
finish_reason=(
|
||||
"tool_calls"
|
||||
if (tool_call_info is not None and tool_call_info.tools_called)
|
||||
else (output.finish_reason if output.finish_reason else "stop")
|
||||
),
|
||||
stop_reason=output.stop_reason,
|
||||
)
|
||||
choices.append(choice_data)
|
||||
continue
|
||||
|
||||
if self.reasoning_parser:
|
||||
try:
|
||||
reasoning_parser = self.reasoning_parser(tokenizer)
|
||||
except RuntimeError as e:
|
||||
logger.exception("Error in reasoning parser creation.")
|
||||
return self.create_error_response(str(e))
|
||||
# If the reasoning parser is enabled,
|
||||
# tool calls are extracted exclusively from the content.
|
||||
reasoning_content, content = reasoning_parser.extract_reasoning_content(
|
||||
output.text, request=request
|
||||
)
|
||||
if not request.include_reasoning:
|
||||
reasoning_content = None
|
||||
else:
|
||||
reasoning_content = None
|
||||
content = output.text
|
||||
|
||||
auto_tools_called = False
|
||||
# if auto tools are not enabled, and a named tool choice using
|
||||
# outlines is not being used
|
||||
if (not self.enable_auto_tools or not self.tool_parser) and (
|
||||
not isinstance(request.tool_choice, ChatCompletionNamedToolChoiceParam)
|
||||
and request.tool_choice != "required"
|
||||
):
|
||||
message = ChatMessage(
|
||||
role=role, reasoning_content=reasoning_content, content=content
|
||||
)
|
||||
|
||||
# if the request uses tools and specified a tool choice
|
||||
elif (
|
||||
request.tool_choice
|
||||
and type(request.tool_choice) is ChatCompletionNamedToolChoiceParam
|
||||
):
|
||||
|
||||
tool_call_class = (
|
||||
MistralToolCall
|
||||
if isinstance(tokenizer, MistralTokenizer)
|
||||
else ToolCall
|
||||
)
|
||||
message = ChatMessage(
|
||||
role=role,
|
||||
reasoning_content=reasoning_content,
|
||||
content="",
|
||||
tool_calls=[
|
||||
tool_call_class(
|
||||
function=FunctionCall(
|
||||
name=request.tool_choice.function.name,
|
||||
arguments=content,
|
||||
)
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
elif request.tool_choice and request.tool_choice == "required":
|
||||
tool_call_class = (
|
||||
MistralToolCall
|
||||
if isinstance(tokenizer, MistralTokenizer)
|
||||
else ToolCall
|
||||
)
|
||||
|
||||
# the fields of FunctionDefinition are a superset of the
|
||||
# tool call outputs and can be used for parsing
|
||||
assert content is not None
|
||||
tool_calls = TypeAdapter(list[FunctionDefinition]).validate_json(
|
||||
content
|
||||
)
|
||||
tool_call_ids = []
|
||||
for tool_call in tool_calls:
|
||||
tool_call_ids.append(
|
||||
make_tool_call_id(
|
||||
id_type=self.tool_call_id_type,
|
||||
func_name=tool_call.name,
|
||||
idx=history_tool_call_cnt,
|
||||
)
|
||||
)
|
||||
history_tool_call_cnt += 1
|
||||
message = ChatMessage(
|
||||
role=role,
|
||||
content="",
|
||||
tool_calls=[
|
||||
tool_call_class(
|
||||
id=tool_call_ids[i],
|
||||
function=FunctionCall(
|
||||
name=tool_call.name,
|
||||
arguments=json.dumps(
|
||||
tool_call.parameters, ensure_ascii=False
|
||||
),
|
||||
),
|
||||
)
|
||||
for i, tool_call in enumerate(tool_calls)
|
||||
],
|
||||
reasoning_content=reasoning_content,
|
||||
)
|
||||
|
||||
# if the request doesn't use tool choice
|
||||
# OR specifies to not use a tool
|
||||
elif not request.tool_choice or request.tool_choice == "none":
|
||||
|
||||
message = ChatMessage(
|
||||
role=role, reasoning_content=reasoning_content, content=content
|
||||
)
|
||||
|
||||
# handle when there are tools and tool choice is auto
|
||||
elif (
|
||||
request.tools
|
||||
and (request.tool_choice == "auto" or request.tool_choice is None)
|
||||
and self.enable_auto_tools
|
||||
and self.tool_parser
|
||||
):
|
||||
|
||||
try:
|
||||
tool_parser = self.tool_parser(tokenizer)
|
||||
except RuntimeError as e:
|
||||
logger.exception("Error in tool parser creation.")
|
||||
return self.create_error_response(str(e))
|
||||
|
||||
tool_call_info = tool_parser.extract_tool_calls(
|
||||
content if content is not None else "", request=request
|
||||
)
|
||||
# In the OpenAI API the finish_reason is "tools_called"
|
||||
# if the tool choice is auto and the model produced a tool
|
||||
# call. The same is not true for named function calls
|
||||
auto_tools_called = tool_call_info.tools_called
|
||||
if tool_call_info.tools_called:
|
||||
message = ChatMessage(
|
||||
role=role,
|
||||
reasoning_content=reasoning_content,
|
||||
content=tool_call_info.content,
|
||||
tool_calls=tool_call_info.tool_calls,
|
||||
)
|
||||
|
||||
else:
|
||||
# FOR NOW make it a chat message; we will have to detect
|
||||
# the type to make it later.
|
||||
ret_content = content
|
||||
|
||||
# try to use content return from tool parser first,
|
||||
# tool parser may do some modify for the content.
|
||||
if tool_call_info.content and len(tool_call_info.content) > 0:
|
||||
ret_content = tool_call_info.content
|
||||
message = ChatMessage(
|
||||
role=role,
|
||||
reasoning_content=reasoning_content,
|
||||
content=ret_content,
|
||||
)
|
||||
|
||||
# undetermined case that is still important to handle
|
||||
else:
|
||||
logger.error(
|
||||
"Error in chat_completion_full_generator - cannot determine"
|
||||
" if tools should be extracted. Returning a standard chat "
|
||||
"completion."
|
||||
)
|
||||
message = ChatMessage(
|
||||
role=role, reasoning_content=reasoning_content, content=content
|
||||
)
|
||||
|
||||
choice_data = ChatCompletionResponseChoice(
|
||||
index=output.index,
|
||||
message=message,
|
||||
logprobs=logprobs,
|
||||
finish_reason=(
|
||||
"tool_calls"
|
||||
if auto_tools_called
|
||||
else output.finish_reason if output.finish_reason else "stop"
|
||||
),
|
||||
stop_reason=output.stop_reason,
|
||||
token_ids=(
|
||||
as_list(output.token_ids) if request.return_token_ids else None
|
||||
),
|
||||
)
|
||||
choices.append(choice_data)
|
||||
|
||||
if request.echo:
|
||||
last_msg_content: Union[str, list[dict[str, str]]] = ""
|
||||
if (
|
||||
conversation
|
||||
and "content" in conversation[-1]
|
||||
and conversation[-1].get("role") == role
|
||||
):
|
||||
last_msg_content = conversation[-1]["content"] or ""
|
||||
if isinstance(last_msg_content, list):
|
||||
last_msg_content = "\n".join(msg["text"] for msg in last_msg_content)
|
||||
|
||||
for choice in choices:
|
||||
full_message = last_msg_content + (choice.message.content or "")
|
||||
choice.message.content = full_message
|
||||
|
||||
assert final_res.prompt_token_ids is not None
|
||||
num_prompt_tokens = len(final_res.prompt_token_ids)
|
||||
if final_res.encoder_prompt_token_ids is not None:
|
||||
num_prompt_tokens += len(final_res.encoder_prompt_token_ids)
|
||||
num_generated_tokens = sum(
|
||||
len(output.token_ids) for output in final_res.outputs
|
||||
)
|
||||
usage = UsageInfo(
|
||||
prompt_tokens=num_prompt_tokens,
|
||||
completion_tokens=num_generated_tokens,
|
||||
total_tokens=num_prompt_tokens + num_generated_tokens,
|
||||
)
|
||||
if self.enable_prompt_tokens_details and final_res.num_cached_tokens:
|
||||
usage.prompt_tokens_details = PromptTokenUsageInfo(
|
||||
cached_tokens=final_res.num_cached_tokens
|
||||
)
|
||||
|
||||
prompt_logprobs = clamp_prompt_logprobs(final_res.prompt_logprobs)
|
||||
prompt_token_ids = (
|
||||
final_res.prompt_token_ids if request.return_token_ids else None
|
||||
)
|
||||
kv_transfer_params = final_res.kv_transfer_params
|
||||
|
||||
return choices, usage, prompt_logprobs, prompt_token_ids, kv_transfer_params
|
||||
|
||||
def _create_audio_choice(self, omni_outputs: OmniRequestOutput, role: str):
|
||||
choices: list[ChatCompletionResponseChoice] = []
|
||||
final_res = omni_outputs.request_output
|
||||
audio_tensor = final_res.multimodal_output["audio"].detach().cpu().numpy()
|
||||
|
||||
# Convert numpy array to WAV bytes and encode as base64
|
||||
if soundfile is None:
|
||||
raise ImportError(
|
||||
"soundfile is required for audio generation. "
|
||||
"Please install it with: pip install soundfile"
|
||||
)
|
||||
|
||||
# Default sample rate for TTS models (typically 24000 Hz)
|
||||
# You may need to adjust this based on your model's configuration
|
||||
sample_rate = 24000
|
||||
|
||||
# Ensure audio is 1D (flatten if needed)
|
||||
if audio_tensor.ndim > 1:
|
||||
audio_tensor = audio_tensor.flatten()
|
||||
|
||||
# Convert to WAV format and encode as base64
|
||||
with BytesIO() as buffer:
|
||||
soundfile.write(buffer, audio_tensor, sample_rate, format="WAV")
|
||||
wav_bytes = buffer.getvalue()
|
||||
|
||||
audio_base64 = base64.b64encode(wav_bytes).decode("utf-8")
|
||||
|
||||
# Generate unique ID for the audio
|
||||
audio_id = f"audio-{uuid.uuid4().hex[:16]}"
|
||||
|
||||
# Set expiration time (e.g., 24 hours from now) as Unix timestamp
|
||||
expires_at = int((datetime.now(timezone.utc) + timedelta(hours=24)).timestamp())
|
||||
|
||||
# Create OpenAIChatCompletionAudio object with all required fields
|
||||
audio_obj = OpenAIChatCompletionAudio(
|
||||
id=audio_id,
|
||||
data=audio_base64,
|
||||
expires_at=expires_at,
|
||||
transcript="", # Empty transcript if not available
|
||||
)
|
||||
|
||||
for output in final_res.outputs:
|
||||
choice_data = ChatCompletionResponseChoice(
|
||||
index=output.index,
|
||||
message=ChatMessage(role=role, audio=audio_obj),
|
||||
logprobs=None,
|
||||
finish_reason="stop",
|
||||
stop_reason=None,
|
||||
)
|
||||
choices.append(choice_data)
|
||||
return choices
|
||||
@@ -1,14 +1,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, Optional, Tuple, Union
|
||||
|
||||
import logging
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import pickle
|
||||
from multiprocessing import shared_memory as _shm
|
||||
from typing import Any, Dict, Optional, Tuple, Union
|
||||
|
||||
import cloudpickle
|
||||
from omegaconf import OmegaConf
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -37,12 +37,18 @@ def set_stage_gpu_devices(stage_id: int, devices: Optional[Union[str, int]]) ->
|
||||
selected_physical = int(toks[0])
|
||||
logger.debug(
|
||||
"[Stage-%s] Set CUDA_VISIBLE_DEVICES to %s; logical 0 -> physical %s",
|
||||
stage_id, devices, selected_physical,
|
||||
stage_id,
|
||||
devices,
|
||||
selected_physical,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.debug("[Stage-%s] Failed to parse first CUDA device: %s", stage_id, e)
|
||||
logger.debug(
|
||||
"[Stage-%s] Failed to parse first CUDA device: %s", stage_id, e
|
||||
)
|
||||
selected_physical = None
|
||||
elif isinstance(devices, (int, str)) and (isinstance(devices, int) or str(devices).isdigit()):
|
||||
elif isinstance(devices, (int, str)) and (
|
||||
isinstance(devices, int) or str(devices).isdigit()
|
||||
):
|
||||
logical_idx = max(0, int(devices))
|
||||
vis = os.environ.get("CUDA_VISIBLE_DEVICES")
|
||||
if vis:
|
||||
@@ -51,43 +57,72 @@ def set_stage_gpu_devices(stage_id: int, devices: Optional[Union[str, int]]) ->
|
||||
if 0 <= logical_idx < len(mapping):
|
||||
selected_physical = mapping[logical_idx]
|
||||
except Exception as e:
|
||||
logger.debug("[Stage-%s] Failed to map logical index via CUDA_VISIBLE_DEVICES: %s", stage_id, e)
|
||||
logger.debug(
|
||||
"[Stage-%s] Failed to map logical index via CUDA_VISIBLE_DEVICES: %s",
|
||||
stage_id,
|
||||
e,
|
||||
)
|
||||
selected_physical = None
|
||||
if selected_physical is None:
|
||||
selected_physical = int(logical_idx)
|
||||
os.environ["CUDA_VISIBLE_DEVICES"] = str(selected_physical)
|
||||
logger.debug(
|
||||
"[Stage-%s] Logical index %d -> physical %s; set CUDA_VISIBLE_DEVICES to single device",
|
||||
stage_id, logical_idx + 1, selected_physical,
|
||||
stage_id,
|
||||
logical_idx + 1,
|
||||
selected_physical,
|
||||
)
|
||||
elif devices in (None, "cpu"):
|
||||
logger.debug("[Stage-%s] Using default device visibility (devices=%s)", stage_id, devices)
|
||||
logger.debug(
|
||||
"[Stage-%s] Using default device visibility (devices=%s)",
|
||||
stage_id,
|
||||
devices,
|
||||
)
|
||||
else:
|
||||
selected_physical = int(str(devices))
|
||||
os.environ["CUDA_VISIBLE_DEVICES"] = str(selected_physical)
|
||||
logger.debug("[Stage-%s] Set CUDA_VISIBLE_DEVICES to single device %s (fallback)", stage_id, selected_physical)
|
||||
logger.debug(
|
||||
"[Stage-%s] Set CUDA_VISIBLE_DEVICES to single device %s (fallback)",
|
||||
stage_id,
|
||||
selected_physical,
|
||||
)
|
||||
|
||||
try:
|
||||
import torch # noqa: WPS433
|
||||
|
||||
if torch.cuda.is_available():
|
||||
try:
|
||||
torch.cuda.set_device(0)
|
||||
except Exception as e:
|
||||
logger.debug("[Stage-%s] torch.cuda.set_device(0) failed: %s", stage_id, e, exc_info=True)
|
||||
logger.debug(
|
||||
"[Stage-%s] torch.cuda.set_device(0) failed: %s",
|
||||
stage_id,
|
||||
e,
|
||||
exc_info=True,
|
||||
)
|
||||
num = torch.cuda.device_count()
|
||||
info = []
|
||||
for i in range(num):
|
||||
total = torch.cuda.get_device_properties(i).total_memory
|
||||
free, _ = torch.cuda.mem_get_info(i)
|
||||
info.append({
|
||||
"idx": i,
|
||||
"name": torch.cuda.get_device_name(i),
|
||||
"total": int(total),
|
||||
"free": int(free),
|
||||
})
|
||||
logger.debug("[Stage-%s] CUDA devices visible=%s info=%s", stage_id, num, info)
|
||||
info.append(
|
||||
{
|
||||
"idx": i,
|
||||
"name": torch.cuda.get_device_name(i),
|
||||
"total": int(total),
|
||||
"free": int(free),
|
||||
}
|
||||
)
|
||||
logger.debug(
|
||||
"[Stage-%s] CUDA devices visible=%s info=%s", stage_id, num, info
|
||||
)
|
||||
except Exception as e:
|
||||
logger.debug("[Stage-%s] Failed to query CUDA devices: %s", stage_id, e, exc_info=True)
|
||||
logger.debug(
|
||||
"[Stage-%s] Failed to query CUDA devices: %s",
|
||||
stage_id,
|
||||
e,
|
||||
exc_info=True,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning("Failed to interpret devices for stage %s: %s", stage_id, e)
|
||||
|
||||
@@ -110,7 +145,7 @@ def shm_write_bytes(payload: bytes) -> Dict[str, Any]:
|
||||
try:
|
||||
shm.close()
|
||||
except Exception as e:
|
||||
logger.debug("Ensure parent dir failed for %s: %s", path, e)
|
||||
logger.debug("Failed to close shared memory: %s", e)
|
||||
return meta
|
||||
|
||||
|
||||
@@ -180,7 +215,9 @@ def maybe_load_from_ipc(container: Dict[str, Any], obj_key: str, shm_key: str) -
|
||||
return container[obj_key]
|
||||
|
||||
|
||||
def maybe_load_from_ipc_with_metrics(container: Dict[str, Any], obj_key: str, shm_key: str) -> tuple[Any, Dict[str, float]]:
|
||||
def maybe_load_from_ipc_with_metrics(
|
||||
container: Dict[str, Any], obj_key: str, shm_key: str
|
||||
) -> tuple[Any, Dict[str, float]]:
|
||||
"""Load object and return (object, metrics) with RX bytes and decode time.
|
||||
|
||||
Metrics keys:
|
||||
@@ -188,6 +225,7 @@ def maybe_load_from_ipc_with_metrics(container: Dict[str, Any], obj_key: str, sh
|
||||
- rx_decode_time_ms: float
|
||||
"""
|
||||
import time as _time # local import to avoid overhead at module import
|
||||
|
||||
t0 = _time.time()
|
||||
if shm_key in container:
|
||||
meta = container[shm_key] # type: ignore[index]
|
||||
@@ -211,7 +249,9 @@ def maybe_load_from_ipc_with_metrics(container: Dict[str, Any], obj_key: str, sh
|
||||
}
|
||||
|
||||
|
||||
def encode_for_ipc(obj: Any, threshold: int, obj_key: str, shm_key: str) -> Dict[str, Any]:
|
||||
def encode_for_ipc(
|
||||
obj: Any, threshold: int, obj_key: str, shm_key: str
|
||||
) -> Dict[str, Any]:
|
||||
"""Return a dict payload for IPC: inline (obj_key) or SHM (shm_key).
|
||||
|
||||
When serialized size exceeds threshold, returns {shm_key: {name,size}};
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
@@ -5,6 +8,8 @@ from omegaconf import OmegaConf
|
||||
|
||||
from vllm.transformers_utils.config import get_config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Get the project root directory (2 levels up from this file)
|
||||
PROJECT_ROOT = Path(__file__).parent.parent.parent
|
||||
|
||||
|
||||
@@ -4,10 +4,13 @@ from typing_extensions import assert_never
|
||||
|
||||
from vllm.inputs.data import SingletonInputs, SingletonPrompt, TextPrompt, TokensPrompt
|
||||
from vllm.inputs.preprocess import InputPreprocessor
|
||||
from vllm.logger import init_logger
|
||||
from vllm.multimodal.inputs import MultiModalInputs, MultiModalUUIDDict
|
||||
from vllm_omni.inputs.data import OmniTokenInputs, token_inputs_omni
|
||||
from vllm_omni.inputs.parse import parse_singleton_prompt_omni
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
class OmniInputPreprocessor(InputPreprocessor):
|
||||
def _process_tokens(
|
||||
|
||||
@@ -279,7 +279,9 @@ class Qwen2_5OmniForConditionalGeneration(
|
||||
|
||||
if input_ids is None and additional_information is None:
|
||||
input_ids = torch.zeros(
|
||||
inputs_embeds.shape[0], dtype=torch.long, device=inputs_embeds.device
|
||||
inputs_embeds.shape[0],
|
||||
dtype=torch.long,
|
||||
device=inputs_embeds.device,
|
||||
)
|
||||
additional_information = {}
|
||||
self.thinker_reply_part = torch.zeros_like(inputs_embeds)
|
||||
@@ -293,13 +295,19 @@ class Qwen2_5OmniForConditionalGeneration(
|
||||
|
||||
# ------- Request-scoped additional information (no cross-request concat) -------
|
||||
request_ids: Optional[list[str]] = kwargs.get("request_ids") # ordered
|
||||
request_token_spans: Optional[list[tuple[int,int]]] = kwargs.get("request_token_spans")
|
||||
request_token_spans: Optional[list[tuple[int, int]]] = kwargs.get(
|
||||
"request_token_spans"
|
||||
)
|
||||
addi_by_req: Optional[dict] = kwargs.get("additional_information_by_req_id")
|
||||
runtime_addi = kwargs.get("runtime_additional_information")
|
||||
|
||||
# Normalize runtime_addi into a mapping by request_id for convenience
|
||||
runtime_addi_by_req: dict[str, dict] = {}
|
||||
if isinstance(request_ids, list) and isinstance(runtime_addi, list) and len(runtime_addi) == len(request_ids):
|
||||
if (
|
||||
isinstance(request_ids, list)
|
||||
and isinstance(runtime_addi, list)
|
||||
and len(runtime_addi) == len(request_ids)
|
||||
):
|
||||
for i, rid in enumerate(request_ids):
|
||||
if isinstance(rid, str) and isinstance(runtime_addi[i], dict):
|
||||
runtime_addi_by_req[rid] = runtime_addi[i]
|
||||
@@ -332,9 +340,19 @@ class Qwen2_5OmniForConditionalGeneration(
|
||||
otoks = info.get("thinker_output_token_ids") # list[int]
|
||||
|
||||
if not isinstance(pe, torch.Tensor):
|
||||
pe = torch.zeros(0, self.talker.config.hidden_size, dtype=inputs_embeds.dtype, device=self._module_device(self.model))
|
||||
pe = torch.zeros(
|
||||
0,
|
||||
self.talker.config.hidden_size,
|
||||
dtype=inputs_embeds.dtype,
|
||||
device=self._module_device(self.model),
|
||||
)
|
||||
if not isinstance(tr, torch.Tensor):
|
||||
tr = torch.zeros(0, self.talker.config.hidden_size, dtype=inputs_embeds.dtype, device=self._module_device(self.model))
|
||||
tr = torch.zeros(
|
||||
0,
|
||||
self.talker.config.hidden_size,
|
||||
dtype=inputs_embeds.dtype,
|
||||
device=self._module_device(self.model),
|
||||
)
|
||||
if not isinstance(ptoks, (list, torch.Tensor)):
|
||||
ptoks = []
|
||||
if not isinstance(otoks, (list, torch.Tensor)):
|
||||
@@ -342,22 +360,35 @@ class Qwen2_5OmniForConditionalGeneration(
|
||||
|
||||
req_input_ids, req_embeds = self._thinker_to_talker_prefill(
|
||||
voice_type=voice_type,
|
||||
output_prompt_embeds=tr.to(inputs_embeds.dtype).to(self._module_device(self.model)),
|
||||
output_prompt_embeds=tr.to(inputs_embeds.dtype).to(
|
||||
self._module_device(self.model)
|
||||
),
|
||||
output_token_ids=otoks,
|
||||
thinker_prompt_embeds=pe.to(inputs_embeds.dtype).to(self._module_device(self.model)),
|
||||
thinker_prompt_embeds=pe.to(inputs_embeds.dtype).to(
|
||||
self._module_device(self.model)
|
||||
),
|
||||
prompt_token_ids=ptoks,
|
||||
)
|
||||
seg_len = min(span_len, req_embeds.shape[0])
|
||||
inputs_embeds[s : s + seg_len] = req_embeds[:seg_len]
|
||||
if isinstance(req_input_ids, torch.Tensor) and req_input_ids.numel() == seg_len:
|
||||
if (
|
||||
isinstance(req_input_ids, torch.Tensor)
|
||||
and req_input_ids.numel() == seg_len
|
||||
):
|
||||
input_ids[s : s + seg_len] = req_input_ids
|
||||
|
||||
# Prepare per-request reply queue for subsequent decode: drop first row
|
||||
if tr.ndim == 2 and tr.shape[0] > 0:
|
||||
update_by_req_id.setdefault(rid, {})["thinker_reply_part_per_request"] = tr[1:].detach().to("cpu").contiguous()
|
||||
update_by_req_id.setdefault(rid, {})[
|
||||
"thinker_reply_part_per_request"
|
||||
] = (tr[1:].detach().to("cpu").contiguous())
|
||||
|
||||
# ------- Decode: span_len == 1 -------
|
||||
if not is_profile and isinstance(request_ids, list) and isinstance(request_token_spans, list):
|
||||
if (
|
||||
not is_profile
|
||||
and isinstance(request_ids, list)
|
||||
and isinstance(request_token_spans, list)
|
||||
):
|
||||
for idx_req, rid in enumerate(request_ids):
|
||||
s, e = request_token_spans[idx_req]
|
||||
if (int(e) - int(s)) != 1:
|
||||
@@ -367,18 +398,34 @@ class Qwen2_5OmniForConditionalGeneration(
|
||||
# A) runtime queue
|
||||
q = None
|
||||
if isinstance(rid, str):
|
||||
q = runtime_addi_by_req.get(rid, {}).get("thinker_reply_part_per_request")
|
||||
q = runtime_addi_by_req.get(rid, {}).get(
|
||||
"thinker_reply_part_per_request"
|
||||
)
|
||||
if isinstance(q, torch.Tensor) and q.numel() > 0:
|
||||
step_vec = q[0:1]
|
||||
new_q = q[1:].detach().to("cpu").contiguous()
|
||||
update_by_req_id.setdefault(rid, {})["thinker_reply_part_per_request"] = new_q
|
||||
update_by_req_id.setdefault(rid, {})[
|
||||
"thinker_reply_part_per_request"
|
||||
] = new_q
|
||||
else:
|
||||
# B) per-request provided decode vector (optional)
|
||||
info = addi_by_req.get(rid, {}) if isinstance(addi_by_req, dict) else {}
|
||||
dv = info.get("decode_output_prompt_embeds") if isinstance(info, dict) else None
|
||||
info = (
|
||||
addi_by_req.get(rid, {})
|
||||
if isinstance(addi_by_req, dict)
|
||||
else {}
|
||||
)
|
||||
dv = (
|
||||
info.get("decode_output_prompt_embeds")
|
||||
if isinstance(info, dict)
|
||||
else None
|
||||
)
|
||||
if isinstance(dv, torch.Tensor) and dv.numel() > 0:
|
||||
step_vec = dv[0:1] if dv.ndim == 2 else dv.view(1, -1)
|
||||
elif hasattr(self, "thinker_reply_part") and isinstance(self.thinker_reply_part, torch.Tensor) and self.thinker_reply_part.numel() > 0:
|
||||
elif (
|
||||
hasattr(self, "thinker_reply_part")
|
||||
and isinstance(self.thinker_reply_part, torch.Tensor)
|
||||
and self.thinker_reply_part.numel() > 0
|
||||
):
|
||||
# C) fallback shared pool
|
||||
step_vec = self.thinker_reply_part[0:1]
|
||||
self.thinker_reply_part = self.thinker_reply_part[1:]
|
||||
@@ -386,7 +433,9 @@ class Qwen2_5OmniForConditionalGeneration(
|
||||
if isinstance(step_vec, torch.Tensor) and step_vec.numel() > 0:
|
||||
one_id = input_ids[s : s + 1]
|
||||
_, one_embed = self._thinker_to_talker_decode_one_step(
|
||||
output_prompt_embeds=step_vec.to(inputs_embeds.dtype).to(self._module_device(self.model)),
|
||||
output_prompt_embeds=step_vec.to(inputs_embeds.dtype).to(
|
||||
self._module_device(self.model)
|
||||
),
|
||||
output_token_ids=one_id,
|
||||
)
|
||||
inputs_embeds[s] = one_embed[0]
|
||||
@@ -400,11 +449,15 @@ class Qwen2_5OmniForConditionalGeneration(
|
||||
multimodal_outputs: dict = None
|
||||
# Return updates if any
|
||||
if update_by_req_id:
|
||||
multimodal_outputs = {"additional_information_update_by_req_id": update_by_req_id}
|
||||
multimodal_outputs = {
|
||||
"additional_information_update_by_req_id": update_by_req_id
|
||||
}
|
||||
|
||||
if sampling_metadata is not None:
|
||||
# the padding token id is set to text model's pad token id, which do not match with the talker model's word embedding size
|
||||
sampling_metadata.prompt_token_ids[sampling_metadata.prompt_token_ids == 152064] = 8448
|
||||
sampling_metadata.prompt_token_ids[
|
||||
sampling_metadata.prompt_token_ids == 152064
|
||||
] = 8448
|
||||
|
||||
return OmniOutput(
|
||||
text_hidden_states=talker_hidden,
|
||||
@@ -421,13 +474,11 @@ class Qwen2_5OmniForConditionalGeneration(
|
||||
device=inputs_embeds.device,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
code = code[:-1] if code[-1] == TALKER_CODEC_EOS_TOKEN_ID else code
|
||||
code = code[1:] if code[0] == TALKER_CODEC_BOS_TOKEN_ID else code
|
||||
|
||||
audio_tensor = self.generate_audio(
|
||||
code, voice_type
|
||||
)
|
||||
|
||||
audio_tensor = self.generate_audio(code, voice_type)
|
||||
return OmniOutput(
|
||||
text_hidden_states=None, multimodal_outputs={"audio": audio_tensor}
|
||||
)
|
||||
|
||||
@@ -166,8 +166,16 @@ class Qwen2_5OmniTalkerForConditionalGeneration(
|
||||
)
|
||||
return hidden_states
|
||||
|
||||
def bad_word_processor(self, logits: torch.Tensor) -> torch.Tensor:
|
||||
if hasattr(self.config, "tts_codec_start_token_id"):
|
||||
bos_id = int(getattr(self.config, "tts_codec_start_token_id"))
|
||||
logits[..., bos_id] = -1e9
|
||||
return logits
|
||||
|
||||
def compute_logits(self, hidden_states: torch.Tensor) -> Optional[torch.Tensor]:
|
||||
return self.language_model.compute_logits(hidden_states)
|
||||
logits = self.language_model.compute_logits(hidden_states)
|
||||
logits = self.bad_word_processor(logits)
|
||||
return logits
|
||||
|
||||
def sample(
|
||||
self,
|
||||
|
||||
@@ -15,8 +15,17 @@ stage_args:
|
||||
trust_remote_code: true
|
||||
engine_output_type: latent # change the param name,such as pooling_output
|
||||
enable_prefix_caching: false
|
||||
is_comprehension: true
|
||||
final_output: true
|
||||
final_output_type: text
|
||||
default_sampling_params:
|
||||
temperature: 0.0
|
||||
top_p: 1.0
|
||||
top_k: -1
|
||||
max_tokens: 2048
|
||||
seed: 42
|
||||
detokenize: True
|
||||
repetition_penalty: 1.1
|
||||
- stage_id: 1
|
||||
runtime:
|
||||
process: true
|
||||
@@ -34,6 +43,15 @@ stage_args:
|
||||
engine_output_type: latent
|
||||
engine_input_source: [0]
|
||||
custom_process_input_func: vllm_omni.model_executor.stage_input_processors.qwen2_5_omni.thinker2talker
|
||||
default_sampling_params:
|
||||
temperature: 0.9
|
||||
top_p: 0.8
|
||||
top_k: 40
|
||||
max_tokens: 2048
|
||||
seed: 42
|
||||
detokenize: True
|
||||
repetition_penalty: 1.05
|
||||
stop_token_ids: [8294]
|
||||
- stage_id: 2
|
||||
runtime:
|
||||
process: true
|
||||
@@ -52,6 +70,14 @@ stage_args:
|
||||
engine_input_source: [1]
|
||||
final_output: true
|
||||
final_output_type: audio
|
||||
default_sampling_params:
|
||||
temperature: 0.0
|
||||
top_p: 1.0
|
||||
top_k: -1
|
||||
max_tokens: 2048
|
||||
seed: 42
|
||||
detokenize: True
|
||||
repetition_penalty: 1.1
|
||||
|
||||
# Top-level runtime config (concise): default windows and stage edges
|
||||
runtime:
|
||||
|
||||
@@ -4,9 +4,12 @@ import torch
|
||||
|
||||
from vllm.inputs import TextPrompt
|
||||
from vllm_omni.inputs.data import OmniTokensPrompt
|
||||
|
||||
TALKER_CODEC_PAD_TOKEN_ID = 8292
|
||||
TALKER_CODEC_START_TOKEN_ID = 8293
|
||||
TALKER_CODEC_END_TOKEN_ID = 8294
|
||||
|
||||
|
||||
def thinker2talker(
|
||||
stage_list, engine_input_source, prompt: Union[OmniTokensPrompt, TextPrompt] = None
|
||||
):
|
||||
@@ -19,9 +22,11 @@ def thinker2talker(
|
||||
raise RuntimeError(f"Stage {source_stage_id} has no outputs yet")
|
||||
thinker_outputs = stage_list[source_stage_id].engine_outputs
|
||||
talker_inputs = []
|
||||
if not isinstance(prompt, list):
|
||||
prompt = [prompt]
|
||||
multi_modal_data = {
|
||||
thinker_output.request_id: prompt.get("multi_modal_data", None)
|
||||
for thinker_output, prompt in zip(thinker_outputs, prompt)
|
||||
thinker_output.request_id: p.get("multi_modal_data", None)
|
||||
for thinker_output, p in zip(thinker_outputs, prompt)
|
||||
}
|
||||
|
||||
for i, thinker_output in enumerate(thinker_outputs):
|
||||
@@ -31,21 +36,28 @@ def thinker2talker(
|
||||
prompt_token_ids_len = len(prompt_token_ids)
|
||||
latent = output.multimodal_output["latent"]
|
||||
thinker_hidden_states = latent.clone().detach().to(latent.device)
|
||||
addtional_information = {
|
||||
"thinker_result": thinker_hidden_states[prompt_token_ids_len:].to(
|
||||
torch.float32
|
||||
),
|
||||
"prompt_embeds": thinker_hidden_states[:prompt_token_ids_len].to(
|
||||
torch.float32
|
||||
),
|
||||
"prompt_token_ids": prompt_token_ids,
|
||||
"thinker_output_token_ids": thinker_output_ids,
|
||||
"thinker_result_shape": list(
|
||||
thinker_hidden_states[prompt_token_ids_len:].shape
|
||||
),
|
||||
"prompt_embeds_shape": list(
|
||||
thinker_hidden_states[:prompt_token_ids_len].shape
|
||||
),
|
||||
}
|
||||
talker_inputs.append(
|
||||
OmniTokensPrompt(
|
||||
prompt_token_ids=[TALKER_CODEC_START_TOKEN_ID] + [TALKER_CODEC_PAD_TOKEN_ID] * (len(prompt_token_ids)) + [TALKER_CODEC_END_TOKEN_ID],
|
||||
additional_information={
|
||||
"thinker_result": thinker_hidden_states[prompt_token_ids_len:].to(
|
||||
torch.float32
|
||||
),
|
||||
"prompt_embeds": thinker_hidden_states[:prompt_token_ids_len].to(
|
||||
torch.float32
|
||||
),
|
||||
"prompt_token_ids": prompt_token_ids,
|
||||
"thinker_output_token_ids": thinker_output_ids,
|
||||
"thinker_result_shape": list(thinker_hidden_states[prompt_token_ids_len:].shape),
|
||||
"prompt_embeds_shape": list(thinker_hidden_states[:prompt_token_ids_len].shape),
|
||||
},
|
||||
prompt_token_ids=[TALKER_CODEC_START_TOKEN_ID]
|
||||
+ [TALKER_CODEC_PAD_TOKEN_ID] * (len(prompt_token_ids))
|
||||
+ [TALKER_CODEC_END_TOKEN_ID],
|
||||
additional_information=addtional_information,
|
||||
multi_modal_data=(
|
||||
multi_modal_data[thinker_output.request_id]
|
||||
if multi_modal_data is not None
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
try:
|
||||
from ._version import __version__, __version_tuple__
|
||||
except Exception as e:
|
||||
import warnings
|
||||
|
||||
warnings.warn(f"Failed to read commit hash:\n{e}", RuntimeWarning, stacklevel=2)
|
||||
|
||||
__version__ = "dev"
|
||||
__version_tuple__ = (0, 0, __version__)
|
||||
@@ -138,9 +138,15 @@ class GPUARModelRunner(OmniGPUModelRunner):
|
||||
# For prefill tokens, pass only the scheduled slice; for decode or no scheduled tokens, pass whole tensor
|
||||
if overlay_len > 0:
|
||||
try:
|
||||
seg = v[
|
||||
num_computed_tokens : num_computed_tokens + overlay_len
|
||||
].detach().to("cpu").contiguous()
|
||||
seg = (
|
||||
v[
|
||||
num_computed_tokens : num_computed_tokens
|
||||
+ overlay_len
|
||||
]
|
||||
.detach()
|
||||
.to("cpu")
|
||||
.contiguous()
|
||||
)
|
||||
except Exception:
|
||||
seg = v.detach().to("cpu").contiguous()
|
||||
req_info[k] = seg
|
||||
@@ -380,7 +386,9 @@ class GPUARModelRunner(OmniGPUModelRunner):
|
||||
model_kwargs_extra = {}
|
||||
# Pass per-request additional information map for this step (no concat)
|
||||
if per_req_additional_information:
|
||||
model_kwargs_extra["additional_information_by_req_id"] = per_req_additional_information
|
||||
model_kwargs_extra["additional_information_by_req_id"] = (
|
||||
per_req_additional_information
|
||||
)
|
||||
# Always pass per-request runtime additional_information (persisted in request state)
|
||||
try:
|
||||
per_req_runtime_info = []
|
||||
@@ -433,12 +441,9 @@ class GPUARModelRunner(OmniGPUModelRunner):
|
||||
# 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.
|
||||
try:
|
||||
if (
|
||||
isinstance(multimodal_outputs, dict)
|
||||
and (
|
||||
"additional_information_update" in multimodal_outputs
|
||||
or "additional_information_update_by_req_id" in multimodal_outputs
|
||||
)
|
||||
if isinstance(multimodal_outputs, dict) and (
|
||||
"additional_information_update" in multimodal_outputs
|
||||
or "additional_information_update_by_req_id" in multimodal_outputs
|
||||
):
|
||||
# Option A: list[dict] in batch order
|
||||
updates_list = multimodal_outputs.get(
|
||||
@@ -446,7 +451,9 @@ class GPUARModelRunner(OmniGPUModelRunner):
|
||||
)
|
||||
if isinstance(updates_list, list):
|
||||
for idx, upd in enumerate(updates_list):
|
||||
if not isinstance(upd, dict) or idx >= len(self.input_batch.req_ids):
|
||||
if not isinstance(upd, dict) or idx >= len(
|
||||
self.input_batch.req_ids
|
||||
):
|
||||
continue
|
||||
req_id = self.input_batch.req_ids[idx]
|
||||
self._merge_additional_information_update(req_id, upd)
|
||||
@@ -462,7 +469,10 @@ class GPUARModelRunner(OmniGPUModelRunner):
|
||||
continue
|
||||
self._merge_additional_information_update(req_id, upd)
|
||||
except Exception as e:
|
||||
logger.error(f"Error merging for requests:{self.input_batch.req_ids} additional information update: {e}, with the multimodal_outputs as {multimodal_outputs}")
|
||||
logger.error(
|
||||
f"Error merging for requests:{self.input_batch.req_ids} additional \
|
||||
information update: {e}, with the multimodal_outputs as {multimodal_outputs}"
|
||||
)
|
||||
if not self.broadcast_pp_output:
|
||||
|
||||
# Common case.
|
||||
@@ -624,7 +634,7 @@ class GPUARModelRunner(OmniGPUModelRunner):
|
||||
invalid_req_indices=invalid_req_indices,
|
||||
async_output_copy_stream=self.async_output_copy_stream,
|
||||
)
|
||||
|
||||
|
||||
def _merge_additional_information_update(self, req_id: str, upd: dict) -> None:
|
||||
req_state = self.requests.get(req_id)
|
||||
if req_state is None:
|
||||
@@ -646,4 +656,4 @@ class GPUARModelRunner(OmniGPUModelRunner):
|
||||
merged[k] = new_list
|
||||
else:
|
||||
merged[k] = v
|
||||
setattr(req_state, "additional_information_cpu", merged)
|
||||
setattr(req_state, "additional_information_cpu", merged)
|
||||
@@ -501,13 +501,16 @@ class GPUDiffusionModelRunner(OmniGPUModelRunner):
|
||||
else:
|
||||
cudagraph_runtime_mode = _cg_mode
|
||||
|
||||
with self.maybe_randomize_inputs(input_ids), set_forward_context(
|
||||
attn_metadata,
|
||||
self.vllm_config,
|
||||
num_tokens=num_tokens_after_padding,
|
||||
num_tokens_across_dp=num_tokens_across_dp,
|
||||
cudagraph_runtime_mode=cudagraph_runtime_mode,
|
||||
batch_descriptor=batch_descriptor,
|
||||
with (
|
||||
self.maybe_randomize_inputs(input_ids),
|
||||
set_forward_context(
|
||||
attn_metadata,
|
||||
self.vllm_config,
|
||||
num_tokens=num_tokens_after_padding,
|
||||
num_tokens_across_dp=num_tokens_across_dp,
|
||||
cudagraph_runtime_mode=cudagraph_runtime_mode,
|
||||
batch_descriptor=batch_descriptor,
|
||||
),
|
||||
):
|
||||
outputs = self.model(
|
||||
input_ids=input_ids,
|
||||
|
||||
@@ -545,14 +545,17 @@ class OmniGPUModelRunner(GPUModelRunner):
|
||||
if num_tokens_across_dp is not None:
|
||||
num_tokens_across_dp[:] = num_tokens_after_padding
|
||||
|
||||
with self.maybe_randomize_inputs(input_ids), set_forward_context(
|
||||
attn_metadata,
|
||||
self.vllm_config,
|
||||
num_tokens=num_tokens_after_padding,
|
||||
num_tokens_across_dp=num_tokens_across_dp,
|
||||
cudagraph_runtime_mode=cudagraph_runtime_mode,
|
||||
batch_descriptor=batch_descriptor,
|
||||
ubatch_slices=ubatch_slices,
|
||||
with (
|
||||
self.maybe_randomize_inputs(input_ids),
|
||||
set_forward_context(
|
||||
attn_metadata,
|
||||
self.vllm_config,
|
||||
num_tokens=num_tokens_after_padding,
|
||||
num_tokens_across_dp=num_tokens_across_dp,
|
||||
cudagraph_runtime_mode=cudagraph_runtime_mode,
|
||||
batch_descriptor=batch_descriptor,
|
||||
ubatch_slices=ubatch_slices,
|
||||
),
|
||||
):
|
||||
outputs = self.model(
|
||||
input_ids=input_ids,
|
||||
|
||||
Reference in new issue
Block a user