[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
@@ -208,7 +208,10 @@ class PipelineConfig:
(target_width, target_height), PIL.Image.Resampling.LANCZOS
), (target_width, target_height)
def prepare_image_processor_kwargs(self, batch):
def prepare_calculated_size(self, image):
return self.calculate_condition_image_size(image, image.width, image.height)
def prepare_image_processor_kwargs(self, batch, neg=False):
return {}
def postprocess_image_latent(self, latent_condition, batch):
@@ -297,6 +300,9 @@ class PipelineConfig:
latents = sequence_model_parallel_all_gather(latents, dim=2)
return latents
def preprocess_vae_image(self, batch, vae_image_processor):
pass
def shard_latents_for_sp(self, batch, latents):
# general logic for video models
sp_world_size, rank_in_sp_group = get_sp_world_size(), get_sp_parallel_rank()
@@ -107,8 +107,9 @@ class QwenImagePipelineConfig(ImagePipelineConfig):
def prepare_sigmas(self, sigmas, num_inference_steps):
return self._prepare_sigmas(sigmas, num_inference_steps)
def prepare_image_processor_kwargs(self, batch):
if batch.prompt:
def prepare_image_processor_kwargs(self, batch, neg=False):
prompt = batch.prompt if not neg else batch.negative_prompt
if prompt:
prompt_template_encode = "<|im_start|>system\nDescribe the key features of the input image (color, shape, size, texture, objects, background), then explain how the user's text instruction should alter or modify the image. Generate a new image that meets the user's requirements while maintaining consistency with the original input where appropriate.<|im_end|>\n<|im_start|>user\n<|vision_start|><|image_pad|><|vision_end|>{}<|im_end|>\n<|im_start|>assistant\n"
txt = prompt_template_encode.format(batch.prompt)
return dict(text=[txt], padding=True)
@@ -328,3 +329,137 @@ class QwenImageEditPipelineConfig(QwenImagePipelineConfig):
# remove noise over input image
noise = noise[:, : latents.size(1)]
return noise
CONDITION_IMAGE_SIZE = 384 * 384
VAE_IMAGE_SIZE = 1024 * 1024
@dataclass
class QwenImageEditPlusPipelineConfig(QwenImageEditPipelineConfig):
task_type: ModelTaskType = ModelTaskType.I2I
def _get_condition_image_sizes(self, batch) -> list[tuple[int, int]]:
image = batch.condition_image
if not isinstance(image, list):
image = [image]
condition_image_sizes = []
for img in image:
image_width, image_height = img.size
edit_width, edit_height, _ = calculate_dimensions(
VAE_IMAGE_SIZE, image_width / image_height
)
condition_image_sizes.append((edit_width, edit_height))
return condition_image_sizes
def prepare_image_processor_kwargs(self, batch, neg=False) -> dict:
prompt = batch.prompt if not neg else batch.negative_prompt
prompt_list = [prompt] if isinstance(prompt, str) else prompt
image_list = batch.condition_image
prompt_template_encode = (
"<|im_start|>system\nDescribe the key features of the input image "
"(color, shape, size, texture, objects, background), then explain how "
"the user's text instruction should alter or modify the image. Generate "
"a new image that meets the user's requirements while maintaining "
"consistency with the original input where appropriate.<|im_end|>\n"
"<|im_start|>user\n{}<|im_end|>\n"
"<|im_start|>assistant\n"
)
img_prompt_template = "Picture {}: <|vision_start|><|image_pad|><|vision_end|>"
if isinstance(image_list, list):
base_img_prompt = ""
for i, img in enumerate(image_list):
base_img_prompt += img_prompt_template.format(i + 1)
txt = [prompt_template_encode.format(base_img_prompt + p) for p in prompt_list]
return dict(text=txt, padding=True)
def prepare_calculated_size(self, image):
return self.calculate_vae_image_size(image, image.width, image.height)
def resize_condition_image(self, images, target_width, target_height):
if not isinstance(images, list):
images = [images]
new_images = []
for img, width, height in zip(images, target_width, target_height):
new_images.append(resize(img, height, width, resize_mode="default"))
return new_images
def calculate_condition_image_size(self, image, width, height) -> tuple[int, int]:
calculated_width, calculated_height, _ = calculate_dimensions(
CONDITION_IMAGE_SIZE, width / height
)
return calculated_width, calculated_height
def calculate_vae_image_size(self, image, width, height) -> tuple[int, int]:
calculated_width, calculated_height, _ = calculate_dimensions(
VAE_IMAGE_SIZE, width / height
)
return calculated_width, calculated_height
def preprocess_vae_image(self, batch, vae_image_processor):
if not isinstance(batch.condition_image, list):
batch.condition_image = [batch.condition_image]
new_images = []
vae_image_sizes = []
for img in batch.condition_image:
width, height = self.calculate_vae_image_size(img, img.width, img.height)
new_images.append(vae_image_processor.preprocess(img, height, width))
vae_image_sizes.append((width, height))
batch.vae_image = new_images
batch.vae_image_sizes = vae_image_sizes
return batch
def _prepare_edit_cond_kwargs(
self, batch, prompt_embeds, rotary_emb, device, dtype
):
batch_size = batch.latents.shape[0]
assert batch_size == 1
height = batch.height
width = batch.width
image_size = batch.original_condition_image_size
vae_scale_factor = self.get_vae_scale_factor()
img_shapes = [
[
(1, height // vae_scale_factor // 2, width // vae_scale_factor // 2),
*[
(
1,
vae_height // vae_scale_factor // 2,
vae_width // vae_scale_factor // 2,
)
for vae_width, vae_height in batch.vae_image_sizes
],
],
] * batch_size
txt_seq_lens = [prompt_embeds[0].shape[1]]
(img_cos, img_sin), (txt_cos, txt_sin) = (
QwenImageEditPlusPipelineConfig.get_freqs_cis(
img_shapes, txt_seq_lens, rotary_emb, device, dtype
)
)
# perform sp shard on noisy image tokens
noisy_img_seq_len = (
1 * (height // vae_scale_factor // 2) * (width // vae_scale_factor // 2)
)
noisy_img_cos = shard_rotary_emb_for_sp(img_cos[:noisy_img_seq_len, :])
noisy_img_sin = shard_rotary_emb_for_sp(img_sin[:noisy_img_seq_len, :])
# concat back the img_cos for input image (since it is not sp-shared later)
img_cos = torch.cat([noisy_img_cos, img_cos[noisy_img_seq_len:, :]], dim=0).to(
device=device
)
img_sin = torch.cat([noisy_img_sin, img_sin[noisy_img_seq_len:, :]], dim=0).to(
device=device
)
return {
"txt_seq_lens": txt_seq_lens,
"freqs_cis": ((img_cos, img_sin), (txt_cos, txt_sin)),
}
@@ -13,3 +13,11 @@ class QwenImageSamplingParams(SamplingParams):
# Denoising stage
guidance_scale: float = 4.0
num_inference_steps: int = 50
@dataclass
class QwenImageEditPlusSamplingParams(QwenImageSamplingParams):
# Denoising stage
guidance_scale: float = 4.0
# true_cfg_scale: float = 4.0
num_inference_steps: int = 40