[diffusion] model: support mutli-image input and qwen-image-edit-2509 (#15005)

This commit is contained in:
Yuhao Yang
2025-12-15 16:17:10 +08:00
committed by GitHub
parent 16e6bc20b0
commit 01b955ac3d
15 changed files with 577 additions and 221 deletions

View File

@@ -454,72 +454,71 @@
"expected_median_denoise_ms": 102.03
},
"qwen_image_edit_ti2i": {
"notes": "single uploaded reference image, Qwen/Qwen-Image-Edit",
"expected_e2e_ms": 138500.0,
"expected_avg_denoise_ms": 720.0,
"expected_median_denoise_ms": 718.0,
"stages_ms": {
"InputValidationStage": 23,
"ImageEncodingStage": 1485.0,
"ImageVAEEncodingStage": 400.0,
"ConditioningStage": 0.13,
"TimestepPreparationStage": 13.78,
"LatentPreparationStage": 15.0,
"DenoisingStage": 36000.0,
"DecodingStage": 850.0
"InputValidationStage": 38.62,
"ImageEncodingStage": 1174.26,
"ImageVAEEncodingStage": 233.71,
"TimestepPreparationStage": 3.0,
"LatentPreparationStage": 0.17,
"ConditioningStage": 0.01,
"DenoisingStage": 42542.67,
"DecodingStage": 508.39
},
"denoise_step_ms": {
"0": 720.0,
"1": 720.0,
"2": 720.0,
"3": 720.0,
"4": 720.0,
"5": 720.0,
"6": 720.0,
"7": 720.0,
"8": 720.0,
"9": 720.0,
"10": 720.0,
"11": 720.0,
"12": 720.0,
"13": 720.0,
"14": 720.0,
"15": 720.0,
"16": 720.0,
"17": 720.0,
"18": 720.0,
"19": 720.0,
"20": 720.0,
"21": 720.0,
"22": 720.0,
"23": 720.0,
"24": 720.0,
"25": 720.0,
"26": 720.0,
"27": 720.0,
"28": 720.0,
"29": 720.0,
"30": 720.0,
"31": 720.0,
"32": 720.0,
"33": 720.0,
"34": 720.0,
"35": 720.0,
"36": 720.0,
"37": 720.0,
"38": 720.0,
"39": 720.0,
"40": 720.0,
"41": 720.0,
"42": 720.0,
"43": 720.0,
"44": 720.0,
"45": 720.0,
"46": 720.0,
"47": 720.0,
"48": 720.0,
"49": 720.0
}
"0": 705.2,
"1": 854.4,
"2": 853.02,
"3": 852.77,
"4": 850.58,
"5": 851.46,
"6": 850.78,
"7": 851.67,
"8": 852.81,
"9": 853.98,
"10": 853.68,
"11": 852.62,
"12": 853.78,
"13": 854.42,
"14": 853.59,
"15": 853.28,
"16": 853.58,
"17": 854.02,
"18": 854.42,
"19": 854.46,
"20": 853.6,
"21": 854.18,
"22": 854.05,
"23": 854.45,
"24": 855.4,
"25": 851.82,
"26": 855.31,
"27": 854.42,
"28": 854.2,
"29": 854.43,
"30": 855.49,
"31": 854.51,
"32": 855.26,
"33": 852.42,
"34": 853.82,
"35": 856.22,
"36": 854.53,
"37": 854.44,
"38": 854.07,
"39": 852.74,
"40": 854.56,
"41": 854.24,
"42": 853.56,
"43": 854.74,
"44": 855.34,
"45": 853.93,
"46": 854.36,
"47": 852.65,
"48": 851.19,
"49": 851.89
},
"expected_e2e_ms": 44503.12,
"expected_avg_denoise_ms": 850.73,
"expected_median_denoise_ms": 854.0
},
"wan2_1_t2v_1.3b": {
"stages_ms": {
@@ -655,6 +654,63 @@
"expected_avg_denoise_ms": 2608.84,
"expected_median_denoise_ms": 2601.59
},
"qwen_image_edit_2509_ti2i": {
"stages_ms": {
"InputValidationStage": 213.24,
"ImageEncodingStage": 1089.12,
"ImageVAEEncodingStage": 304.56,
"TimestepPreparationStage": 2.94,
"LatentPreparationStage": 0.2,
"ConditioningStage": 0.01,
"DenoisingStage": 50724.5,
"DecodingStage": 601.02
},
"denoise_step_ms": {
"0": 1057.09,
"1": 1267.06,
"2": 1268.33,
"3": 1268.94,
"4": 1270.36,
"5": 1270.44,
"6": 1268.61,
"7": 1270.21,
"8": 1274.98,
"9": 1271.57,
"10": 1273.15,
"11": 1271.56,
"12": 1272.69,
"13": 1271.62,
"14": 1274.04,
"15": 1276.81,
"16": 1272.2,
"17": 1269.33,
"18": 1275.96,
"19": 1274.43,
"20": 1272.57,
"21": 1275.28,
"22": 1273.63,
"23": 1275.06,
"24": 1277.39,
"25": 1277.27,
"26": 1274.74,
"27": 1273.38,
"28": 1276.77,
"29": 1275.59,
"30": 1275.51,
"31": 1274.9,
"32": 1274.8,
"33": 1279.03,
"34": 1272.9,
"35": 1274.67,
"36": 1272.61,
"37": 1272.82,
"38": 1276.41,
"39": 1273.55
},
"expected_e2e_ms": 52938.04,
"expected_avg_denoise_ms": 1267.96,
"expected_median_denoise_ms": 1273.46
},
"fastwan2_2_ti2v_5b": {
"stages_ms": {
"InputValidationStage": 88.86,

View File

@@ -90,15 +90,25 @@ def diffusion_server(case: DiffusionTestCase) -> ServerContext:
and sampling_params.image_path
):
# Handle URL or local path
if is_image_url(sampling_params.image_path):
image_path = download_image_from_url(str(sampling_params.image_path))
else:
image_path = Path(sampling_params.image_path)
image_path_list = sampling_params.image_path
if not isinstance(image_path_list, list):
image_path_list = [image_path_list]
new_image_path_list = []
for image_path in image_path_list:
if is_image_url(image_path):
new_image_path_list.append(download_image_from_url(str(image_path)))
else:
new_image_path_list.append(Path(image_path))
if not image_path.exists():
pytest.skip(f"{case.id}: file missing: {image_path}")
image_path_list = new_image_path_list
warmup.run_edit_warmups(
count=server_args.warmup_edit,
edit_prompt=sampling_params.prompt,
image_path=image_path,
image_path=image_path_list,
)
except Exception as exc:
logger.error("Warm-up failed for %s: %s", case.id, exc)

View File

@@ -278,23 +278,31 @@ class WarmupRunner:
if count <= 0:
return
if not image_path.exists():
logger.warning(
"[server-test] Skipping edit warmup: image missing at %s", image_path
)
return
if not isinstance(image_path, list):
image_path = [image_path]
for image in image_path:
if not image.exists():
logger.warning(
"[server-test] Skipping edit warmup: image missing at %s", image
)
return
logger.info("[server-test] Running %s edit warm-up(s)", count)
for _ in range(count):
with image_path.open("rb") as fh:
images = [open(image, "rb") for image in image_path]
try:
result = self.client.images.edit(
model=self.model,
image=fh,
image=images,
prompt=edit_prompt,
n=1,
size=self.output_size,
response_format="b64_json",
)
finally:
for img in images:
img.close()
validate_image(result.data[0].b64_json)
@@ -585,22 +593,36 @@ def get_generate_fn(
if not sampling_params.prompt or not sampling_params.image_path:
pytest.skip(f"{id}: no edit config")
if is_image_url(sampling_params.image_path):
image_path = download_image_from_url(str(sampling_params.image_path))
else:
image_path = Path(sampling_params.image_path)
if not image_path.exists():
pytest.skip(f"{id}: file missing: {image_path}")
image_paths = sampling_params.image_path
with image_path.open("rb") as fh:
if not isinstance(image_paths, list):
image_paths = [image_paths]
new_image_paths = []
for image_path in image_paths:
if is_image_url(image_path):
new_image_paths.append(download_image_from_url(str(image_path)))
else:
new_image_paths.append(Path(image_path))
if not image_path.exists():
pytest.skip(f"{id}: file missing: {image_path}")
image_paths = new_image_paths
images = [open(image_path, "rb") for image_path in image_paths]
try:
response = client.images.with_raw_response.edit(
model=model_path,
image=fh,
image=images,
prompt=sampling_params.prompt,
n=1,
size=sampling_params.output_size,
response_format="b64_json",
)
finally:
for img in images:
img.close()
rid = response.headers.get("x-request-id", "")
result = response.parse()

View File

@@ -226,6 +226,14 @@ TI2I_sampling_params = DiffusionSamplingParams(
image_path="https://github.com/lm-sys/lm-sys.github.io/releases/download/test/TI2I_Qwen_Image_Edit_Input.jpg",
)
MULTI_IMAGE_TI2I_sampling_params = DiffusionSamplingParams(
prompt="The magician bear is on the left, the alchemist bear is on the right, facing each other in the central park square.",
image_path=[
"https://qianwen-res.oss-cn-beijing.aliyuncs.com/Qwen-Image/edit2509/edit2509_1.jpg",
"https://qianwen-res.oss-cn-beijing.aliyuncs.com/Qwen-Image/edit2509/edit2509_2.jpg",
],
)
T2V_PROMPT = "A curious raccoon"
TI2V_sampling_params = DiffusionSamplingParams(
@@ -288,6 +296,16 @@ ONE_GPU_CASES_A: list[DiffusionTestCase] = [
),
TI2I_sampling_params,
),
DiffusionTestCase(
"qwen_image_edit_2509_ti2i",
DiffusionServerArgs(
model_path="Qwen/Qwen-Image-Edit-2509",
modality="image",
warmup_text=0,
warmup_edit=1,
),
MULTI_IMAGE_TI2I_sampling_params,
),
]
ONE_GPU_CASES_B: list[DiffusionTestCase] = [

View File

@@ -6,6 +6,7 @@ import logging
import os
import tempfile
from datetime import datetime
from typing import List, Union
from urllib.parse import urlparse
from urllib.request import urlopen
@@ -100,9 +101,9 @@ def upload_file_to_slack(
model: str = None,
prompt: str = None,
file_path: str = None,
origin_file_path: str = None,
origin_file_path: Union[str, List[str]] = None,
) -> bool:
temp_path = None
temp_paths = []
try:
from slack_sdk import WebClient
@@ -117,17 +118,39 @@ def upload_file_to_slack(
logger.info(f"Slack upload failed: no file path")
return False
if origin_file_path and origin_file_path.startswith(("http", "https")):
suffix = os.path.splitext(urlparse(origin_file_path).path)[1] or ".tmp"
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tf:
with urlopen(origin_file_path) as response:
tf.write(response.read())
temp_path = tf.name
origin_file_path = temp_path
origin_paths = []
if isinstance(origin_file_path, str):
if origin_file_path:
origin_paths.append(origin_file_path)
elif isinstance(origin_file_path, list):
origin_paths = [p for p in origin_file_path if p]
uploads = [{"file": file_path, "title": "Generated Image"}]
if origin_file_path and os.path.exists(origin_file_path):
uploads.insert(0, {"file": origin_file_path, "title": "Original Image"})
final_origin_paths = []
for path in origin_paths:
if path.startswith(("http", "https")):
try:
suffix = os.path.splitext(urlparse(path).path)[1] or ".tmp"
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tf:
with urlopen(path) as response:
tf.write(response.read())
temp_paths.append(tf.name)
final_origin_paths.append(tf.name)
except Exception as e:
logger.warning(f"Failed to download {path}: {e}")
else:
final_origin_paths.append(path)
uploads = []
for i, path in enumerate(final_origin_paths):
if os.path.exists(path):
title = (
"Original Image"
if len(final_origin_paths) == 1
else f"Original Image {i+1}"
)
uploads.append({"file": path, "title": title})
uploads.append({"file": file_path, "title": "Generated Image"})
message = (
f"*Case ID:* `{case_id}`\n" f"*Model:* `{model}`\n" f"*Prompt:* {prompt}"
@@ -189,5 +212,6 @@ def upload_file_to_slack(
logger.info(f"Slack upload failed: {e}")
return False
finally:
if temp_path and os.path.exists(temp_path):
os.remove(temp_path)
for p in temp_paths:
if os.path.exists(p):
os.remove(p)