Skip to content

Commit f75c21b

Browse files
gcanlinIsotr0pyGaohan123
authored
[E2E] Add Qwen2.5-Omni model test with OmniRunner (#168)
Signed-off-by: gcanlin <canlinguosdu@gmail.com> Signed-off-by: Isotr0py <mozf@mail2.sysu.edu.cn> Co-authored-by: Isotr0py <mozf@mail2.sysu.edu.cn> Co-authored-by: Gao Han <gaohan19@huawei.com>
1 parent 98fe157 commit f75c21b

8 files changed

Lines changed: 659 additions & 0 deletions

File tree

.buildkite/pipeline.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ steps:
1414
- ".buildkite/scripts/simple_test.sh"
1515
agents:
1616
queue: "cpu_queue_premerge"
17+
1718
- label: "Diffusion Model Test"
19+
timeout_in_minutes: 15
1820
depends_on: image-build
1921
commands:
2022
- pytest -s -v tests/single_stage/test_diffusion_model.py
@@ -29,3 +31,22 @@ steps:
2931
- "HF_HOME=/fsx/hf_cache"
3032
volumes:
3133
- "/fsx/hf_cache:/fsx/hf_cache"
34+
35+
- label: "Omni Model Test"
36+
timeout_in_minutes: 15
37+
depends_on: image-build
38+
commands:
39+
- export VLLM_LOGGING_LEVEL=DEBUG
40+
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
41+
- pytest -s -v tests/multi_stages/
42+
agents:
43+
queue: "gpu_1_queue" # g6.4xlarge instance on AWS, has 1 L4 GPU
44+
plugins:
45+
- docker#v5.2.0:
46+
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
47+
always-pull: true
48+
propagate-environment: true
49+
environment:
50+
- "HF_HOME=/fsx/hf_cache"
51+
volumes:
52+
- "/fsx/hf_cache:/fsx/hf_cache"

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ markers = [
156156
"integration: Integration tests",
157157
"benchmark: Benchmark tests",
158158
"slow: Slow tests",
159+
"core_model: enable this model test in each PR instead of only nightly",
159160
]
160161

161162
[tool.typos.default]

tests/multi_stages/__init__.py

Whitespace-only changes.

tests/multi_stages/conftest.py

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3+
"""
4+
Pytest configuration and fixtures for vllm-omni tests.
5+
"""
6+
7+
from typing import Any
8+
9+
import pytest
10+
from vllm.distributed.parallel_state import cleanup_dist_env_and_memory
11+
from vllm.sampling_params import SamplingParams
12+
13+
from vllm_omni.entrypoints.omni import Omni
14+
15+
PromptAudioInput = list[tuple[Any, int]] | tuple[Any, int] | None
16+
PromptImageInput = list[Any] | Any | None
17+
PromptVideoInput = list[Any] | Any | None
18+
19+
20+
class OmniRunner:
21+
"""
22+
Test runner for Omni models.
23+
"""
24+
25+
def __init__(
26+
self,
27+
model_name: str,
28+
seed: int = 42,
29+
init_sleep_seconds: int = 20,
30+
batch_timeout: int = 10,
31+
init_timeout: int = 300,
32+
shm_threshold_bytes: int = 65536,
33+
log_stats: bool = False,
34+
stage_configs_path: str | None = None,
35+
**kwargs,
36+
) -> None:
37+
"""
38+
Initialize an OmniRunner for testing.
39+
40+
Args:
41+
model_name: The model name or path
42+
seed: Random seed for reproducibility
43+
init_sleep_seconds: Sleep time after starting each stage
44+
batch_timeout: Timeout for batching in seconds
45+
init_timeout: Timeout for initializing stages in seconds
46+
shm_threshold_bytes: Threshold for using shared memory
47+
log_stats: Enable detailed statistics logging
48+
stage_configs_path: Optional path to YAML stage config file
49+
**kwargs: Additional arguments passed to Omni
50+
"""
51+
self.model_name = model_name
52+
self.seed = seed
53+
54+
self.omni = Omni(
55+
model=model_name,
56+
log_stats=log_stats,
57+
init_sleep_seconds=init_sleep_seconds,
58+
batch_timeout=batch_timeout,
59+
init_timeout=init_timeout,
60+
shm_threshold_bytes=shm_threshold_bytes,
61+
stage_configs_path=stage_configs_path,
62+
**kwargs,
63+
)
64+
65+
def get_default_sampling_params_list(self) -> list[SamplingParams]:
66+
"""
67+
Get a list of default sampling parameters for all stages.
68+
69+
Returns:
70+
List of SamplingParams with default decoding for each stage
71+
"""
72+
return [st.default_sampling_params for st in self.omni.instance.stage_list]
73+
74+
def get_omni_inputs(
75+
self,
76+
prompts: list[str] | str,
77+
system_prompt: str | None = None,
78+
audios: PromptAudioInput = None,
79+
images: PromptImageInput = None,
80+
videos: PromptVideoInput = None,
81+
mm_processor_kwargs: dict[str, Any] | None = None,
82+
) -> list[dict[str, Any]]:
83+
"""
84+
Construct Omni input format from prompts and multimodal data.
85+
86+
Args:
87+
prompts: Text prompt(s) - either a single string or list of strings
88+
system_prompt: Optional system prompt (defaults to Qwen system prompt)
89+
audios: Audio input(s) - tuple of (audio_array, sample_rate) or list of tuples
90+
images: Image input(s) - PIL Image or list of PIL Images
91+
videos: Video input(s) - numpy array or list of numpy arrays
92+
mm_processor_kwargs: Optional processor kwargs (e.g., use_audio_in_video)
93+
94+
Returns:
95+
List of prompt dictionaries suitable for Omni.generate()
96+
"""
97+
if system_prompt is None:
98+
system_prompt = (
99+
"You are Qwen, a virtual human developed by the Qwen Team, Alibaba "
100+
"Group, capable of perceiving auditory and visual inputs, as well as "
101+
"generating text and speech."
102+
)
103+
104+
if isinstance(prompts, str):
105+
prompts = [prompts]
106+
107+
def _normalize_mm_input(mm_input, num_prompts):
108+
if mm_input is None:
109+
return [None] * num_prompts
110+
if isinstance(mm_input, list):
111+
if len(mm_input) != num_prompts:
112+
raise ValueError(
113+
f"Multimodal input list length ({len(mm_input)}) must match prompts length ({num_prompts})"
114+
)
115+
return mm_input
116+
return [mm_input] * num_prompts
117+
118+
num_prompts = len(prompts)
119+
audios_list = _normalize_mm_input(audios, num_prompts)
120+
images_list = _normalize_mm_input(images, num_prompts)
121+
videos_list = _normalize_mm_input(videos, num_prompts)
122+
123+
omni_inputs = []
124+
for i, prompt_text in enumerate(prompts):
125+
user_content = ""
126+
multi_modal_data = {}
127+
128+
audio = audios_list[i]
129+
if audio is not None:
130+
if isinstance(audio, list):
131+
for _ in audio:
132+
user_content += "<|audio_bos|><|AUDIO|><|audio_eos|>"
133+
multi_modal_data["audio"] = audio
134+
else:
135+
user_content += "<|audio_bos|><|AUDIO|><|audio_eos|>"
136+
multi_modal_data["audio"] = audio
137+
138+
image = images_list[i]
139+
if image is not None:
140+
if isinstance(image, list):
141+
for _ in image:
142+
user_content += "<|vision_bos|><|IMAGE|><|vision_eos|>"
143+
multi_modal_data["image"] = image
144+
else:
145+
user_content += "<|vision_bos|><|IMAGE|><|vision_eos|>"
146+
multi_modal_data["image"] = image
147+
148+
video = videos_list[i]
149+
if video is not None:
150+
if isinstance(video, list):
151+
for _ in video:
152+
user_content += "<|vision_bos|><|VIDEO|><|vision_eos|>"
153+
multi_modal_data["video"] = video
154+
else:
155+
user_content += "<|vision_bos|><|VIDEO|><|vision_eos|>"
156+
multi_modal_data["video"] = video
157+
158+
user_content += prompt_text
159+
160+
full_prompt = (
161+
f"<|im_start|>system\n{system_prompt}<|im_end|>\n"
162+
f"<|im_start|>user\n{user_content}<|im_end|>\n"
163+
f"<|im_start|>assistant\n"
164+
)
165+
166+
input_dict: dict[str, Any] = {"prompt": full_prompt}
167+
if multi_modal_data:
168+
input_dict["multi_modal_data"] = multi_modal_data
169+
if mm_processor_kwargs:
170+
input_dict["mm_processor_kwargs"] = mm_processor_kwargs
171+
172+
omni_inputs.append(input_dict)
173+
174+
return omni_inputs
175+
176+
def generate(
177+
self,
178+
prompts: list[dict[str, Any]],
179+
sampling_params_list: list[SamplingParams] | None = None,
180+
) -> list[Any]:
181+
"""
182+
Generate outputs for the given prompts.
183+
184+
Args:
185+
prompts: List of prompt dictionaries with 'prompt' and optionally
186+
'multi_modal_data' keys
187+
sampling_params_list: List of sampling parameters for each stage.
188+
If None, uses default parameters.
189+
190+
Returns:
191+
List of OmniRequestOutput objects from stages with final_output=True
192+
"""
193+
if sampling_params_list is None:
194+
sampling_params_list = self.get_default_sampling_params_list()
195+
196+
return self.omni.generate(prompts, sampling_params_list)
197+
198+
def generate_multimodal(
199+
self,
200+
prompts: list[str] | str,
201+
sampling_params_list: list[SamplingParams] | None = None,
202+
system_prompt: str | None = None,
203+
audios: PromptAudioInput = None,
204+
images: PromptImageInput = None,
205+
videos: PromptVideoInput = None,
206+
mm_processor_kwargs: dict[str, Any] | None = None,
207+
) -> list[Any]:
208+
"""
209+
Convenience method to generate with multimodal inputs.
210+
211+
Args:
212+
prompts: Text prompt(s)
213+
sampling_params_list: List of sampling parameters for each stage
214+
system_prompt: Optional system prompt
215+
audios: Audio input(s)
216+
images: Image input(s)
217+
videos: Video input(s)
218+
mm_processor_kwargs: Optional processor kwargs
219+
220+
Returns:
221+
List of OmniRequestOutput objects from stages with final_output=True
222+
"""
223+
omni_inputs = self.get_omni_inputs(
224+
prompts=prompts,
225+
system_prompt=system_prompt,
226+
audios=audios,
227+
images=images,
228+
videos=videos,
229+
mm_processor_kwargs=mm_processor_kwargs,
230+
)
231+
return self.generate(omni_inputs, sampling_params_list)
232+
233+
def __enter__(self):
234+
"""Context manager entry."""
235+
return self
236+
237+
def __exit__(self, exc_type, exc_val, exc_tb):
238+
"""Context manager exit - cleanup resources."""
239+
self.close()
240+
del self.omni
241+
cleanup_dist_env_and_memory()
242+
243+
def close(self):
244+
"""Close and cleanup the Omni instance."""
245+
if hasattr(self.omni.instance, "close"):
246+
self.omni.instance.close()
247+
248+
249+
@pytest.fixture(scope="session")
250+
def omni_runner():
251+
return OmniRunner
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# stage config for running qwen2.5-omni with architecture of OmniLLM.
2+
3+
# The following config has been verified on 1x 24GB GPU (L4/RTX3090).
4+
# This config is optimized for CI e2e tests.
5+
stage_args:
6+
- stage_id: 0
7+
runtime:
8+
process: true # Run this stage in a separate process
9+
devices: "0" # Visible devices for this stage (CUDA_VISIBLE_DEVICES/torch.cuda.set_device)
10+
max_batch_size: 1
11+
engine_args:
12+
model_stage: thinker
13+
model_arch: Qwen2_5OmniForConditionalGeneration
14+
worker_cls: vllm_omni.worker.gpu_ar_worker.GPUARWorker
15+
scheduler_cls: vllm_omni.core.sched.omni_ar_scheduler.OmniARScheduler
16+
max_model_len: 896
17+
max_num_batched_tokens: 896
18+
max_num_seqs: 1
19+
gpu_memory_utilization: 0.64
20+
skip_mm_profiling: true
21+
enforce_eager: true # Now we only support eager mode
22+
trust_remote_code: true
23+
engine_output_type: latent
24+
enable_prefix_caching: false
25+
is_comprehension: true
26+
final_output: true
27+
final_output_type: text
28+
default_sampling_params:
29+
temperature: 0.0
30+
top_p: 1.0
31+
top_k: -1
32+
max_tokens: 128
33+
seed: 42
34+
detokenize: True
35+
repetition_penalty: 1.1
36+
- stage_id: 1
37+
runtime:
38+
process: true
39+
devices: "0"
40+
max_batch_size: 1
41+
engine_args:
42+
model_stage: talker
43+
model_arch: Qwen2_5OmniForConditionalGeneration
44+
worker_cls: vllm_omni.worker.gpu_ar_worker.GPUARWorker
45+
scheduler_cls: vllm_omni.core.sched.omni_ar_scheduler.OmniARScheduler
46+
max_model_len: 896
47+
max_num_batched_tokens: 896
48+
max_num_seqs: 1
49+
gpu_memory_utilization: 0.28
50+
skip_mm_profiling: true
51+
enforce_eager: true
52+
trust_remote_code: true
53+
enable_prefix_caching: false
54+
engine_output_type: latent
55+
engine_input_source: [0]
56+
custom_process_input_func: vllm_omni.model_executor.stage_input_processors.qwen2_5_omni.thinker2talker
57+
default_sampling_params:
58+
temperature: 0.9
59+
top_p: 0.8
60+
top_k: 40
61+
max_tokens: 128
62+
seed: 42
63+
detokenize: True
64+
repetition_penalty: 1.05
65+
stop_token_ids: [8294]
66+
- stage_id: 2
67+
runtime:
68+
process: true
69+
devices: "0" # Example: use a different GPU than the previous stage; use "0" if single GPU
70+
max_batch_size: 1
71+
engine_args:
72+
model_stage: code2wav
73+
model_arch: Qwen2_5OmniForConditionalGeneration
74+
worker_cls: vllm_omni.worker.gpu_generation_worker.GPUGenerationWorker
75+
scheduler_cls: vllm_omni.core.sched.omni_generation_scheduler.OmniGenerationScheduler
76+
gpu_memory_utilization: 0.07
77+
enforce_eager: true
78+
trust_remote_code: true
79+
enable_prefix_caching: false
80+
engine_output_type: audio
81+
engine_input_source: [1]
82+
final_output: true
83+
final_output_type: audio
84+
default_sampling_params:
85+
temperature: 0.0
86+
top_p: 1.0
87+
top_k: -1
88+
max_tokens: 128
89+
seed: 42
90+
detokenize: True
91+
repetition_penalty: 1.1
92+
93+
# Top-level runtime config (concise): default windows and stage edges
94+
runtime:
95+
enabled: true
96+
defaults:
97+
window_size: -1 # Simplified: trigger downstream only after full upstream completion
98+
max_inflight: 1 # Simplified: process serially within each stage
99+
edges:
100+
- from: 0 # thinker → talker: trigger only after receiving full input (-1)
101+
to: 1
102+
window_size: -1
103+
- from: 1 # talker → code2wav: trigger only after receiving full input (-1)
104+
to: 2
105+
window_size: -1

0 commit comments

Comments
 (0)