Skip to content

Commit 284575a

Browse files
authored
[CI]: Add/Fix bagel e2e online/offline test (vllm-project#1895)
Signed-off-by: princepride <wangzhipeng628@gmail.com> Signed-off-by: 汪志鹏 <wangzhipeng628@gmail.com>
1 parent 2472c36 commit 284575a

File tree

5 files changed

+557
-52
lines changed

5 files changed

+557
-52
lines changed

.buildkite/test-ready.yml

Lines changed: 114 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -261,39 +261,117 @@ steps:
261261
# path: /mnt/hf-cache
262262
# type: DirectoryOrCreate
263263

264-
# - label: "Bagel Text2Img Model Test with H100"
265-
# depends_on: upload-ready-pipeline
266-
# commands:
267-
# - |
268-
# timeout 30m bash -c '
269-
# export VLLM_WORKER_MULTIPROC_METHOD=spawn
270-
# pytest -s -v tests/e2e/offline_inference/test_bagel_text2img.py
271-
# '
272-
# agents:
273-
# queue: "mithril-h100-pool"
274-
# plugins:
275-
# - kubernetes:
276-
# podSpec:
277-
# containers:
278-
# - image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
279-
# resources:
280-
# limits:
281-
# nvidia.com/gpu: 1
282-
# volumeMounts:
283-
# - name: devshm
284-
# mountPath: /dev/shm
285-
# - name: hf-cache
286-
# mountPath: /root/.cache/huggingface
287-
# env:
288-
# - name: HF_HOME
289-
# value: /root/.cache/huggingface
290-
# nodeSelector:
291-
# node.kubernetes.io/instance-type: gpu-h100-sxm
292-
# volumes:
293-
# - name: devshm
294-
# emptyDir:
295-
# medium: Memory
296-
# - name: hf-cache
297-
# hostPath:
298-
# path: /mnt/hf-cache
299-
# type: DirectoryOrCreate
264+
- label: "Bagel Text2Img Model Test with H100"
265+
depends_on: upload-ready-pipeline
266+
commands:
267+
- |
268+
timeout 30m bash -c '
269+
export VLLM_WORKER_MULTIPROC_METHOD=spawn
270+
export VLLM_TEST_CLEAN_GPU_MEMORY=1
271+
pytest -s -v tests/e2e/offline_inference/test_bagel_text2img.py
272+
'
273+
agents:
274+
queue: "mithril-h100-pool"
275+
plugins:
276+
- kubernetes:
277+
podSpec:
278+
containers:
279+
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
280+
resources:
281+
limits:
282+
nvidia.com/gpu: 1
283+
volumeMounts:
284+
- name: devshm
285+
mountPath: /dev/shm
286+
- name: hf-cache
287+
mountPath: /root/.cache/huggingface
288+
env:
289+
- name: HF_HOME
290+
value: /root/.cache/huggingface
291+
nodeSelector:
292+
node.kubernetes.io/instance-type: gpu-h100-sxm
293+
volumes:
294+
- name: devshm
295+
emptyDir:
296+
medium: Memory
297+
- name: hf-cache
298+
hostPath:
299+
path: /mnt/hf-cache
300+
type: DirectoryOrCreate
301+
302+
- label: "Bagel Img2Img Model Test with H100"
303+
depends_on: upload-ready-pipeline
304+
commands:
305+
- |
306+
timeout 30m bash -c '
307+
export VLLM_WORKER_MULTIPROC_METHOD=spawn
308+
export VLLM_TEST_CLEAN_GPU_MEMORY=1
309+
pytest -s -v tests/e2e/offline_inference/test_bagel_img2img.py
310+
'
311+
agents:
312+
queue: "mithril-h100-pool"
313+
plugins:
314+
- kubernetes:
315+
podSpec:
316+
containers:
317+
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
318+
resources:
319+
limits:
320+
nvidia.com/gpu: 1
321+
volumeMounts:
322+
- name: devshm
323+
mountPath: /dev/shm
324+
- name: hf-cache
325+
mountPath: /root/.cache/huggingface
326+
env:
327+
- name: HF_HOME
328+
value: /root/.cache/huggingface
329+
nodeSelector:
330+
node.kubernetes.io/instance-type: gpu-h100-sxm
331+
volumes:
332+
- name: devshm
333+
emptyDir:
334+
medium: Memory
335+
- name: hf-cache
336+
hostPath:
337+
path: /mnt/hf-cache
338+
type: DirectoryOrCreate
339+
340+
- label: "Bagel Online Serving Test with H100"
341+
depends_on: upload-ready-pipeline
342+
commands:
343+
- |
344+
timeout 40m bash -c '
345+
export VLLM_WORKER_MULTIPROC_METHOD=spawn
346+
export VLLM_TEST_CLEAN_GPU_MEMORY=1
347+
export VLLM_IMAGE_FETCH_TIMEOUT=60
348+
pytest -s -v tests/e2e/online_serving/test_bagel_online.py
349+
'
350+
agents:
351+
queue: "mithril-h100-pool"
352+
plugins:
353+
- kubernetes:
354+
podSpec:
355+
containers:
356+
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
357+
resources:
358+
limits:
359+
nvidia.com/gpu: 1
360+
volumeMounts:
361+
- name: devshm
362+
mountPath: /dev/shm
363+
- name: hf-cache
364+
mountPath: /root/.cache/huggingface
365+
env:
366+
- name: HF_HOME
367+
value: /root/.cache/huggingface
368+
nodeSelector:
369+
node.kubernetes.io/instance-type: gpu-h100-sxm
370+
volumes:
371+
- name: devshm
372+
emptyDir:
373+
medium: Memory
374+
- name: hf-cache
375+
hostPath:
376+
path: /mnt/hf-cache
377+
type: DirectoryOrCreate

tests/e2e/offline_inference/stage_configs/bagel_sharedmemory_ci.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ stage_args:
1010
max_batch_size: 1
1111
engine_args:
1212
model_stage: thinker
13-
model_arch: BagelForConditionalGeneration
13+
model_arch: OmniBagelForConditionalGeneration
1414
worker_type: ar
1515
scheduler_cls: vllm_omni.core.sched.omni_ar_scheduler.OmniARScheduler
1616
gpu_memory_utilization: 0.45
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3+
4+
"""
5+
End-to-end test for Bagel img2img generation.
6+
7+
This test validates that the Bagel model generates images from an input image
8+
and text prompt that match expected reference pixel values within a ±5 tolerance.
9+
10+
Equivalent to running:
11+
python3 examples/offline_inference/bagel/end2end.py \
12+
--prompts "Change the grass color to red" \
13+
--modality img2img --step 15 \
14+
--image-path 2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg
15+
"""
16+
17+
import socket
18+
from pathlib import Path
19+
from typing import Any
20+
21+
import pytest
22+
from PIL import Image
23+
from vllm.assets.image import ImageAsset
24+
25+
from tests.utils import hardware_test
26+
from vllm_omni.entrypoints.omni import Omni
27+
28+
# Reference pixel data extracted from the known-good output image
29+
# Generated with seed=52, num_inference_steps=15,
30+
# prompt='Change the grass color to red',
31+
# input image: 2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg
32+
REFERENCE_PIXELS = [
33+
{"position": (100, 100), "rgb": (157, 172, 217)},
34+
{"position": (400, 50), "rgb": (105, 144, 218)},
35+
{"position": (700, 100), "rgb": (118, 159, 233)},
36+
{"position": (150, 400), "rgb": (195, 34, 60)},
37+
{"position": (512, 336), "rgb": (222, 214, 193)},
38+
{"position": (700, 400), "rgb": (197, 15, 43)},
39+
{"position": (100, 600), "rgb": (105, 13, 18)},
40+
{"position": (400, 600), "rgb": (169, 33, 44)},
41+
{"position": (700, 600), "rgb": (101, 86, 93)},
42+
{"position": (256, 256), "rgb": (181, 202, 222)},
43+
]
44+
45+
PIXEL_TOLERANCE = 5
46+
47+
DEFAULT_PROMPT = "<|fim_middle|><|im_start|>Change the grass color to red<|im_end|>"
48+
49+
EXPECTED_OUTPUT_SIZE = (1024, 672)
50+
51+
52+
def _load_input_image() -> Image.Image:
53+
"""Load the test input image via vllm's ImageAsset."""
54+
return ImageAsset("2560px-Gfp-wisconsin-madison-the-nature-boardwalk").pil_image.convert("RGB")
55+
56+
57+
def _find_free_port() -> int:
58+
"""Find and return a free ephemeral port by binding to port 0."""
59+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
60+
s.bind(("127.0.0.1", 0))
61+
s.listen(1)
62+
port = s.getsockname()[1]
63+
return port
64+
65+
66+
def _configure_sampling_params(omni: Omni, max_tokens: int = 1, num_inference_steps: int = 15) -> list:
67+
"""Configure sampling parameters for Bagel img2img generation.
68+
69+
Args:
70+
omni: The Omni instance to get default params from.
71+
max_tokens: Maximum tokens for the first stage.
72+
num_inference_steps: Number of inference steps for the diffusion stage.
73+
74+
Returns:
75+
Configured sampling params list.
76+
"""
77+
params_list = omni.default_sampling_params_list
78+
params_list[0].max_tokens = max_tokens # type: ignore
79+
if len(params_list) > 1:
80+
params_list[1].num_inference_steps = num_inference_steps # type: ignore
81+
params_list[1].extra_args = { # type: ignore
82+
"cfg_text_scale": 4.0,
83+
"cfg_img_scale": 1.5,
84+
}
85+
return params_list
86+
87+
88+
def _extract_generated_image(omni_outputs: list) -> Image.Image | None:
89+
"""Extract the generated image from Omni outputs.
90+
91+
Args:
92+
omni_outputs: List of outputs from omni.generate().
93+
94+
Returns:
95+
The first generated PIL Image, or None if no image found.
96+
"""
97+
for req_output in omni_outputs:
98+
if images := getattr(req_output, "images", None):
99+
return images[0]
100+
if hasattr(req_output, "request_output") and req_output.request_output:
101+
for stage_out in req_output.request_output:
102+
if hasattr(stage_out, "images") and stage_out.images:
103+
return stage_out.images[0]
104+
return None
105+
106+
107+
def _validate_pixels(
108+
image: Image.Image,
109+
reference_pixels: list[dict[str, Any]] = REFERENCE_PIXELS,
110+
tolerance: int = PIXEL_TOLERANCE,
111+
) -> None:
112+
"""Validate that image pixels match expected reference values.
113+
114+
Args:
115+
image: The PIL Image to validate.
116+
reference_pixels: List of dicts with 'position' (x, y) and 'rgb' (R, G, B).
117+
tolerance: Maximum allowed difference per color channel.
118+
119+
Raises:
120+
AssertionError: If any pixel differs beyond tolerance.
121+
"""
122+
for ref in reference_pixels:
123+
x, y = ref["position"]
124+
expected = ref["rgb"]
125+
actual = image.getpixel((x, y))[:3]
126+
assert all(abs(a - e) <= tolerance for a, e in zip(actual, expected)), (
127+
f"Pixel mismatch at ({x}, {y}): expected {expected}, got {actual}"
128+
)
129+
130+
131+
def _generate_bagel_img2img(
132+
omni: Omni,
133+
input_image: Image.Image,
134+
prompt: str = DEFAULT_PROMPT,
135+
) -> Image.Image:
136+
"""Generate an image using Bagel model with img2img pipeline.
137+
138+
Args:
139+
omni: The Omni instance to use for generation.
140+
input_image: The input PIL Image for img2img.
141+
prompt: The text prompt for image editing.
142+
143+
Returns:
144+
The generated PIL Image.
145+
146+
Raises:
147+
AssertionError: If no image is generated or size is incorrect.
148+
"""
149+
params_list = _configure_sampling_params(omni)
150+
151+
omni_outputs = list(
152+
omni.generate(
153+
prompts=[
154+
{
155+
"prompt": prompt,
156+
"multi_modal_data": {"img2img": input_image},
157+
"modalities": ["img2img"],
158+
}
159+
],
160+
sampling_params_list=params_list,
161+
)
162+
)
163+
164+
generated_image = _extract_generated_image(omni_outputs)
165+
assert generated_image is not None, "No images generated"
166+
assert generated_image.size == EXPECTED_OUTPUT_SIZE, f"Expected {EXPECTED_OUTPUT_SIZE}, got {generated_image.size}"
167+
168+
return generated_image
169+
170+
171+
@pytest.mark.core_model
172+
@pytest.mark.diffusion
173+
@hardware_test(res={"cuda": "H100"})
174+
def test_bagel_img2img_shared_memory_connector():
175+
"""Test Bagel img2img with shared memory connector."""
176+
input_image = _load_input_image()
177+
config_path = str(Path(__file__).parent / "stage_configs" / "bagel_sharedmemory_ci.yaml")
178+
omni = Omni(model="ByteDance-Seed/BAGEL-7B-MoT", stage_configs_path=config_path, stage_init_timeout=300)
179+
180+
try:
181+
generated_image = _generate_bagel_img2img(omni, input_image)
182+
_validate_pixels(generated_image)
183+
finally:
184+
omni.close()

tests/e2e/offline_inference/test_bagel_text2img.py

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,6 @@
1414
"""
1515

1616
import os
17-
18-
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
19-
os.environ["VLLM_TEST_CLEAN_GPU_MEMORY"] = "1"
20-
2117
import signal
2218
import socket
2319
import subprocess
@@ -37,23 +33,23 @@
3733
# "Generated with seed=52, num_inference_steps=15,
3834
# prompt='A futuristic city skyline at twilight, cyberpunk style'"
3935
REFERENCE_PIXELS = [
40-
{"position": (100, 100), "rgb": (49, 96, 134)},
41-
{"position": (400, 50), "rgb": (63, 127, 167)},
42-
{"position": (700, 100), "rgb": (70, 101, 141)},
43-
{"position": (150, 400), "rgb": (115, 90, 150)},
44-
{"position": (512, 512), "rgb": (98, 86, 119)},
45-
{"position": (700, 400), "rgb": (29, 42, 91)},
46-
{"position": (100, 700), "rgb": (47, 50, 88)},
47-
{"position": (400, 700), "rgb": (36, 52, 91)},
48-
{"position": (700, 700), "rgb": (45, 58, 99)},
49-
{"position": (256, 256), "rgb": (62, 94, 135)},
36+
{"position": (100, 100), "rgb": (121, 118, 100)},
37+
{"position": (400, 50), "rgb": (163, 162, 143)},
38+
{"position": (700, 100), "rgb": (170, 156, 127)},
39+
{"position": (150, 400), "rgb": (129, 127, 112)},
40+
{"position": (512, 512), "rgb": (135, 61, 59)},
41+
{"position": (700, 400), "rgb": (205, 107, 43)},
42+
{"position": (100, 700), "rgb": (197, 177, 157)},
43+
{"position": (400, 700), "rgb": (139, 107, 86)},
44+
{"position": (700, 700), "rgb": (247, 205, 146)},
45+
{"position": (256, 256), "rgb": (171, 160, 153)},
5046
]
5147

5248
# Maximum allowed difference per color channel
5349
PIXEL_TOLERANCE = 5
5450

5551
# Default test prompt
56-
DEFAULT_PROMPT = "<|im_start|>A futuristic city skyline at twilight, cyberpunk style<|im_end|>"
52+
DEFAULT_PROMPT = "<|im_start|>A cute cat<|im_end|>"
5753

5854

5955
def _find_free_port() -> int:

0 commit comments

Comments
 (0)