Signed-off-by: dengyunyang <584797741@qq.com> Signed-off-by: bjf-frz <frz123db@gmail.com> Signed-off-by: Didan Deng <33117903+wtomin@users.noreply.github.com> Co-authored-by: bjf-frz <frz123db@gmail.com> Co-authored-by: Didan Deng <33117903+wtomin@users.noreply.github.com>
685 lines
29 KiB
Python
685 lines
29 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||
|
||
from __future__ import annotations
|
||
|
||
import inspect
|
||
import json
|
||
import os
|
||
import re
|
||
from collections.abc import Iterable
|
||
from functools import partial
|
||
from typing import Any
|
||
|
||
import numpy as np
|
||
import torch
|
||
from diffusers.image_processor import VaeImageProcessor
|
||
from diffusers.models import AutoencoderKL
|
||
from diffusers.pipelines.longcat_image.system_messages import SYSTEM_PROMPT_EN, SYSTEM_PROMPT_ZH
|
||
from diffusers.schedulers import FlowMatchEulerDiscreteScheduler, SchedulerMixin
|
||
from diffusers.utils.torch_utils import randn_tensor
|
||
from torch import nn
|
||
from transformers import AutoTokenizer, Qwen2_5_VLForConditionalGeneration, Qwen2VLProcessor
|
||
from vllm.logger import init_logger
|
||
from vllm.model_executor.models.utils import AutoWeightsLoader
|
||
|
||
from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig
|
||
from vllm_omni.diffusion.distributed.cfg_parallel import CFGParallelMixin
|
||
from vllm_omni.diffusion.distributed.utils import get_local_device
|
||
from vllm_omni.diffusion.model_loader.diffusers_loader import DiffusersPipelineLoader
|
||
from vllm_omni.diffusion.models.longcat_image.longcat_image_transformer import LongCatImageTransformer2DModel
|
||
from vllm_omni.diffusion.profiler.diffusion_pipeline_profiler import DiffusionPipelineProfilerMixin
|
||
from vllm_omni.diffusion.request import OmniDiffusionRequest
|
||
from vllm_omni.model_executor.model_loader.weight_utils import (
|
||
download_weights_from_hf_specific,
|
||
)
|
||
|
||
logger = init_logger(__name__)
|
||
|
||
|
||
def get_longcat_image_post_process_func(
|
||
od_config: OmniDiffusionConfig,
|
||
):
|
||
model_name = od_config.model
|
||
if os.path.exists(model_name):
|
||
model_path = model_name
|
||
else:
|
||
model_path = download_weights_from_hf_specific(model_name, None, ["*"])
|
||
vae_config_path = os.path.join(model_path, "vae/config.json")
|
||
with open(vae_config_path) as f:
|
||
vae_config = json.load(f)
|
||
vae_scale_factor = 2 ** (len(vae_config["block_out_channels"]) - 1) if "block_out_channels" in vae_config else 8
|
||
|
||
image_processor = VaeImageProcessor(vae_scale_factor=vae_scale_factor * 2)
|
||
|
||
def post_process_func(
|
||
images: torch.Tensor,
|
||
):
|
||
return image_processor.postprocess(images)
|
||
|
||
return post_process_func
|
||
|
||
|
||
def calculate_shift(
|
||
image_seq_len,
|
||
base_seq_len: int = 256,
|
||
max_seq_len: int = 4096,
|
||
base_shift: float = 0.5,
|
||
max_shift: float = 1.15,
|
||
):
|
||
m = (max_shift - base_shift) / (max_seq_len - base_seq_len)
|
||
b = base_shift - m * base_seq_len
|
||
mu = image_seq_len * m + b
|
||
return mu
|
||
|
||
|
||
def split_quotation(prompt, quote_pairs=None):
|
||
"""
|
||
Implement a regex-based string splitting algorithm that identifies delimiters
|
||
defined by single or double quote pairs.
|
||
|
||
Examples::
|
||
>>> prompt_en = "Please write 'Hello' on the blackboard for me."
|
||
>>> print(split_quotation(prompt_en))
|
||
>>> # output: [('Please write ', False), ("'Hello'", True), (' on the blackboard for me.', False)]
|
||
"""
|
||
word_internal_quote_pattern = re.compile(r"[a-zA-Z]+'[a-zA-Z]+")
|
||
matches_word_internal_quote_pattern = word_internal_quote_pattern.findall(prompt)
|
||
mapping_word_internal_quote = []
|
||
|
||
for i, word_src in enumerate(set(matches_word_internal_quote_pattern)):
|
||
word_tgt = "longcat_$##$_longcat" * (i + 1)
|
||
prompt = prompt.replace(word_src, word_tgt)
|
||
mapping_word_internal_quote.append([word_src, word_tgt])
|
||
|
||
if quote_pairs is None:
|
||
quote_pairs = [("'", "'"), ('"', '"'), ("‘", "’"), ("“", "”")]
|
||
pattern = "|".join([re.escape(q1) + r"[^" + re.escape(q1 + q2) + r"]*?" + re.escape(q2) for q1, q2 in quote_pairs])
|
||
parts = re.split(f"({pattern})", prompt)
|
||
|
||
result = []
|
||
for part in parts:
|
||
for word_src, word_tgt in mapping_word_internal_quote:
|
||
part = part.replace(word_tgt, word_src)
|
||
if re.match(pattern, part):
|
||
if len(part):
|
||
result.append((part, True))
|
||
else:
|
||
if len(part):
|
||
result.append((part, False))
|
||
return result
|
||
|
||
|
||
def prepare_pos_ids(modality_id=0, type="text", start=(0, 0), num_token=None, height=None, width=None) -> torch.Tensor:
|
||
if type == "text":
|
||
assert num_token
|
||
if height or width:
|
||
logger.warning('Warning: The parameters of height and width will be ignored in "text" type.')
|
||
pos_ids = torch.zeros(num_token, 3)
|
||
pos_ids[..., 0] = modality_id
|
||
pos_ids[..., 1] = torch.arange(num_token) + start[0]
|
||
pos_ids[..., 2] = torch.arange(num_token) + start[1]
|
||
elif type == "image":
|
||
assert height and width
|
||
if num_token:
|
||
logger.warning('Warning: The parameter of num_token will be ignored in "image" type.')
|
||
pos_ids = torch.zeros(height, width, 3)
|
||
pos_ids[..., 0] = modality_id
|
||
pos_ids[..., 1] = pos_ids[..., 1] + torch.arange(height)[:, None] + start[0]
|
||
pos_ids[..., 2] = pos_ids[..., 2] + torch.arange(width)[None, :] + start[1]
|
||
pos_ids = pos_ids.reshape(height * width, 3)
|
||
else:
|
||
raise KeyError(f'Unknown type {type}, only support "text" or "image".')
|
||
# pos_ids = pos_ids[None, :].repeat(batch_size, 1, 1)
|
||
return pos_ids
|
||
|
||
|
||
def retrieve_timesteps(
|
||
scheduler: SchedulerMixin,
|
||
num_inference_steps: int | None = None,
|
||
device: str | torch.device | None = None,
|
||
timesteps: list[int] | None = None,
|
||
sigmas: list[float] | None = None,
|
||
**kwargs,
|
||
) -> tuple[torch.Tensor, int]:
|
||
r"""
|
||
Calls the scheduler's `set_timesteps` method and retrieves timesteps from the scheduler after the call. Handles
|
||
custom timesteps. Any kwargs will be supplied to `scheduler.set_timesteps`.
|
||
|
||
Args:
|
||
scheduler (`SchedulerMixin`):
|
||
The scheduler to get timesteps from.
|
||
num_inference_steps (`int`):
|
||
The number of diffusion steps used when generating samples with a pre-trained model. If used, `timesteps`
|
||
must be `None`.
|
||
device (`str` or `torch.device`, *optional*):
|
||
The device to which the timesteps should be moved to. If `None`, the timesteps are not moved.
|
||
timesteps (`list[int]`, *optional*):
|
||
Custom timesteps used to override the timestep spacing strategy of the scheduler. If `timesteps` is passed,
|
||
`num_inference_steps` and `sigmas` must be `None`.
|
||
sigmas (`list[float]`, *optional*):
|
||
Custom sigmas used to override the timestep spacing strategy of the scheduler. If `sigmas` is passed,
|
||
`num_inference_steps` and `timesteps` must be `None`.
|
||
|
||
Returns:
|
||
`tuple[torch.Tensor, int]`: A tuple where the first element is the timestep schedule from the scheduler and the
|
||
second element is the number of inference steps.
|
||
"""
|
||
if timesteps is not None and sigmas is not None:
|
||
raise ValueError("Only one of `timesteps` or `sigmas` can be passed. Please choose one to set custom values")
|
||
if timesteps is not None:
|
||
accepts_timesteps = "timesteps" in set(inspect.signature(scheduler.set_timesteps).parameters.keys())
|
||
if not accepts_timesteps:
|
||
raise ValueError(
|
||
f"The current scheduler class {scheduler.__class__}'s `set_timesteps` does not support custom"
|
||
f" timestep schedules. Please check whether you are using the correct scheduler."
|
||
)
|
||
scheduler.set_timesteps(timesteps=timesteps, device=device, **kwargs)
|
||
timesteps = scheduler.timesteps
|
||
num_inference_steps = len(timesteps)
|
||
elif sigmas is not None:
|
||
accept_sigmas = "sigmas" in set(inspect.signature(scheduler.set_timesteps).parameters.keys())
|
||
if not accept_sigmas:
|
||
raise ValueError(
|
||
f"The current scheduler class {scheduler.__class__}'s `set_timesteps` does not support custom"
|
||
f" sigmas schedules. Please check whether you are using the correct scheduler."
|
||
)
|
||
scheduler.set_timesteps(sigmas=sigmas, device=device, **kwargs)
|
||
timesteps = scheduler.timesteps
|
||
num_inference_steps = len(timesteps)
|
||
else:
|
||
scheduler.set_timesteps(num_inference_steps, device=device, **kwargs)
|
||
timesteps = scheduler.timesteps
|
||
return timesteps, num_inference_steps
|
||
|
||
|
||
def get_prompt_language(prompt):
|
||
pattern = re.compile(r"[\u4e00-\u9fff]")
|
||
if bool(pattern.search(prompt)):
|
||
return "zh"
|
||
return "en"
|
||
|
||
|
||
class LongCatImagePipeline(nn.Module, CFGParallelMixin, DiffusionPipelineProfilerMixin):
|
||
def __init__(
|
||
self,
|
||
*,
|
||
od_config: OmniDiffusionConfig,
|
||
prefix: str = "",
|
||
):
|
||
super().__init__()
|
||
self.od_config = od_config
|
||
self.weights_sources = [
|
||
DiffusersPipelineLoader.ComponentSource(
|
||
model_or_path=od_config.model,
|
||
subfolder="transformer",
|
||
revision=None,
|
||
prefix="transformer.",
|
||
fall_back_to_pt=True,
|
||
)
|
||
]
|
||
|
||
self.device = get_local_device()
|
||
model = od_config.model
|
||
local_files_only = os.path.exists(model)
|
||
|
||
self.scheduler = FlowMatchEulerDiscreteScheduler.from_pretrained(
|
||
model, subfolder="scheduler", local_files_only=local_files_only
|
||
)
|
||
|
||
self.text_encoder = Qwen2_5_VLForConditionalGeneration.from_pretrained(
|
||
model, subfolder="text_encoder", local_files_only=local_files_only
|
||
)
|
||
self.text_processor = Qwen2VLProcessor.from_pretrained(
|
||
model, subfolder="tokenizer", local_files_only=local_files_only
|
||
)
|
||
self.vae = AutoencoderKL.from_pretrained(model, subfolder="vae", local_files_only=local_files_only).to(
|
||
self.device
|
||
)
|
||
self.transformer = LongCatImageTransformer2DModel(od_config=od_config)
|
||
self.tokenizer = AutoTokenizer.from_pretrained(model, subfolder="tokenizer", local_files_only=local_files_only)
|
||
|
||
self.vae_scale_factor = 2 ** (len(self.vae.config.block_out_channels) - 1) if getattr(self, "vae", None) else 8
|
||
|
||
self.prompt_template_encode_prefix = (
|
||
"<|im_start|>system\n"
|
||
"As an image captioning expert, generate a descriptive text prompt based on an image content,"
|
||
" suitable for input to a text-to-image model.<|im_end|>\n"
|
||
"<|im_start|>user\n"
|
||
)
|
||
self.prompt_template_encode_suffix = "<|im_end|>\n<|im_start|>assistant\n"
|
||
|
||
self.default_sample_size = 128
|
||
self.tokenizer_max_length = 512
|
||
self.setup_diffusion_pipeline_profiler(
|
||
enable_diffusion_pipeline_profiler=self.od_config.enable_diffusion_pipeline_profiler
|
||
)
|
||
|
||
def rewire_prompt(self, prompt, device):
|
||
prompt = [prompt] if isinstance(prompt, str) else prompt
|
||
all_text = []
|
||
for each_prompt in prompt:
|
||
language = get_prompt_language(each_prompt)
|
||
if language == "zh":
|
||
question = SYSTEM_PROMPT_ZH + f"\n用户输入为:{each_prompt}\n改写后的prompt为:"
|
||
else:
|
||
question = SYSTEM_PROMPT_EN + f"\nUser Input: {each_prompt}\nRewritten prompt:"
|
||
message = [
|
||
{
|
||
"role": "user",
|
||
"content": [
|
||
{"type": "text", "text": question},
|
||
],
|
||
}
|
||
]
|
||
text = self.text_processor.apply_chat_template(message, tokenize=False, add_generation_prompt=True)
|
||
all_text.append(text)
|
||
|
||
inputs = self.text_processor(text=all_text, padding=True, return_tensors="pt").to(device)
|
||
|
||
self.text_encoder.to(device)
|
||
generated_ids = self.text_encoder.generate(**inputs, max_new_tokens=self.tokenizer_max_length)
|
||
generated_ids_trimmed = [out_ids[len(in_ids) :] for in_ids, out_ids in zip(inputs.input_ids, generated_ids)]
|
||
output_text = self.text_processor.batch_decode(
|
||
generated_ids_trimmed, skip_special_tokens=True, clean_up_tokenization_spaces=False
|
||
)
|
||
return output_text
|
||
|
||
def _encode_prompt(self, prompt: list[str]) -> torch.Tensor:
|
||
batch_all_tokens = []
|
||
|
||
for each_prompt in prompt:
|
||
all_tokens = []
|
||
for clean_prompt_sub, matched in split_quotation(each_prompt):
|
||
if matched:
|
||
for sub_word in clean_prompt_sub:
|
||
tokens = self.tokenizer(sub_word, add_special_tokens=False)["input_ids"]
|
||
all_tokens.extend(tokens)
|
||
else:
|
||
tokens = self.tokenizer(clean_prompt_sub, add_special_tokens=False)["input_ids"]
|
||
all_tokens.extend(tokens)
|
||
|
||
if len(all_tokens) > self.tokenizer_max_length:
|
||
logger.warning(
|
||
"Your input was truncated because `max_sequence_length` is set to "
|
||
f"{self.tokenizer_max_length} input token nums : {len(all_tokens)}"
|
||
)
|
||
all_tokens = all_tokens[: self.tokenizer_max_length]
|
||
batch_all_tokens.append(all_tokens)
|
||
|
||
text_tokens_and_mask = self.tokenizer.pad(
|
||
{"input_ids": batch_all_tokens},
|
||
max_length=self.tokenizer_max_length,
|
||
padding="max_length",
|
||
return_attention_mask=True,
|
||
return_tensors="pt",
|
||
)
|
||
|
||
prefix_tokens = self.tokenizer(self.prompt_template_encode_prefix, add_special_tokens=False)["input_ids"]
|
||
suffix_tokens = self.tokenizer(self.prompt_template_encode_suffix, add_special_tokens=False)["input_ids"]
|
||
prefix_len = len(prefix_tokens)
|
||
suffix_len = len(suffix_tokens)
|
||
|
||
prefix_tokens_mask = torch.tensor([1] * len(prefix_tokens), dtype=text_tokens_and_mask.attention_mask[0].dtype)
|
||
suffix_tokens_mask = torch.tensor([1] * len(suffix_tokens), dtype=text_tokens_and_mask.attention_mask[0].dtype)
|
||
|
||
prefix_tokens = torch.tensor(prefix_tokens, dtype=text_tokens_and_mask.input_ids.dtype)
|
||
suffix_tokens = torch.tensor(suffix_tokens, dtype=text_tokens_and_mask.input_ids.dtype)
|
||
|
||
batch_size = text_tokens_and_mask.input_ids.size(0)
|
||
prefix_tokens_batch = prefix_tokens.unsqueeze(0).expand(batch_size, -1)
|
||
suffix_tokens_batch = suffix_tokens.unsqueeze(0).expand(batch_size, -1)
|
||
prefix_mask_batch = prefix_tokens_mask.unsqueeze(0).expand(batch_size, -1)
|
||
suffix_mask_batch = suffix_tokens_mask.unsqueeze(0).expand(batch_size, -1)
|
||
|
||
input_ids = torch.cat((prefix_tokens_batch, text_tokens_and_mask.input_ids, suffix_tokens_batch), dim=-1)
|
||
attention_mask = torch.cat((prefix_mask_batch, text_tokens_and_mask.attention_mask, suffix_mask_batch), dim=-1)
|
||
|
||
input_ids = input_ids.to(self.device)
|
||
attention_mask = attention_mask.to(self.device)
|
||
|
||
text_output = self.text_encoder(input_ids=input_ids, attention_mask=attention_mask, output_hidden_states=True)
|
||
prompt_embeds = text_output.hidden_states[-1].detach()
|
||
prompt_embeds = prompt_embeds[:, prefix_len:-suffix_len, :]
|
||
return prompt_embeds
|
||
|
||
def encode_prompt(
|
||
self,
|
||
prompt: str | list[str] | None = None,
|
||
num_images_per_prompt: int | None = 1,
|
||
prompt_embeds: torch.Tensor | None = None,
|
||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||
if prompt_embeds is None and prompt is None:
|
||
raise ValueError("Provide either `prompt` or `prompt_embeds`.")
|
||
|
||
prompt = [prompt] if isinstance(prompt, str) else prompt
|
||
if prompt_embeds is None:
|
||
prompt_embeds = self._encode_prompt(prompt)
|
||
batch_size = len(prompt)
|
||
else:
|
||
batch_size = prompt_embeds.shape[0]
|
||
|
||
_, seq_len, _ = prompt_embeds.shape
|
||
prompt_embeds = prompt_embeds.repeat(1, num_images_per_prompt, 1)
|
||
prompt_embeds = prompt_embeds.view(batch_size * num_images_per_prompt, seq_len, -1)
|
||
|
||
text_ids = prepare_pos_ids(modality_id=0, type="text", start=(0, 0), num_token=prompt_embeds.shape[1]).to(
|
||
self.device
|
||
)
|
||
return prompt_embeds.to(self.device), text_ids
|
||
|
||
@staticmethod
|
||
def _pack_latents(latents, batch_size, num_channels_latents, height, width):
|
||
latents = latents.view(batch_size, num_channels_latents, height // 2, 2, width // 2, 2)
|
||
latents = latents.permute(0, 2, 4, 1, 3, 5)
|
||
latents = latents.reshape(batch_size, (height // 2) * (width // 2), num_channels_latents * 4)
|
||
|
||
return latents
|
||
|
||
@staticmethod
|
||
def _unpack_latents(latents, height, width, vae_scale_factor):
|
||
batch_size, num_patches, channels = latents.shape
|
||
|
||
# VAE applies 8x compression on images but we must also account for packing which requires
|
||
# latent height and width to be divisible by 2.
|
||
height = 2 * (int(height) // (vae_scale_factor * 2))
|
||
width = 2 * (int(width) // (vae_scale_factor * 2))
|
||
|
||
latents = latents.view(batch_size, height // 2, width // 2, channels // 4, 2, 2)
|
||
latents = latents.permute(0, 3, 1, 4, 2, 5)
|
||
|
||
latents = latents.reshape(batch_size, channels // (2 * 2), height, width)
|
||
|
||
return latents
|
||
|
||
@property
|
||
def do_classifier_free_guidance(self):
|
||
return self._guidance_scale > 1
|
||
|
||
def prepare_latents(
|
||
self,
|
||
batch_size,
|
||
num_channels_latents,
|
||
height,
|
||
width,
|
||
dtype,
|
||
device,
|
||
generator,
|
||
latents=None,
|
||
):
|
||
# VAE applies 8x compression on images but we must also account for packing which requires
|
||
# latent height and width to be divisible by 2.
|
||
height = 2 * (int(height) // (self.vae_scale_factor * 2))
|
||
width = 2 * (int(width) // (self.vae_scale_factor * 2))
|
||
|
||
shape = (batch_size, num_channels_latents, height, width)
|
||
latent_image_ids = prepare_pos_ids(
|
||
modality_id=1,
|
||
type="image",
|
||
start=(self.tokenizer_max_length, self.tokenizer_max_length),
|
||
height=height // 2,
|
||
width=width // 2,
|
||
).to(device)
|
||
|
||
if latents is not None:
|
||
return latents.to(device=device, dtype=dtype), latent_image_ids
|
||
|
||
if isinstance(generator, list) and len(generator) != batch_size:
|
||
raise ValueError(
|
||
f"You have passed a list of generators of length {len(generator)}, but requested an effective batch"
|
||
f" size of {batch_size}. Make sure the batch size matches the length of the generators."
|
||
)
|
||
|
||
latents = randn_tensor(shape, generator=generator, device=device)
|
||
latents = latents.to(dtype=dtype)
|
||
latents = self._pack_latents(latents, batch_size, num_channels_latents, height, width)
|
||
|
||
return latents, latent_image_ids
|
||
|
||
def check_inputs(
|
||
self, prompt, height, width, negative_prompt=None, prompt_embeds=None, negative_prompt_embeds=None
|
||
):
|
||
if height % (self.vae_scale_factor * 2) != 0 or width % (self.vae_scale_factor * 2) != 0:
|
||
logger.warning(
|
||
"`height` and `width` have to be divisible by "
|
||
f"{self.vae_scale_factor * 2} but are {height} and {width}. "
|
||
"Dimensions will be resized accordingly"
|
||
)
|
||
|
||
if prompt is not None and prompt_embeds is not None:
|
||
raise ValueError(
|
||
f"Cannot forward both `prompt`: {prompt} and `prompt_embeds`: {prompt_embeds}. Please make sure to"
|
||
" only forward one of the two."
|
||
)
|
||
elif prompt is None and prompt_embeds is None:
|
||
raise ValueError(
|
||
"Provide either `prompt` or `prompt_embeds`. Cannot leave both `prompt` and `prompt_embeds` undefined."
|
||
)
|
||
elif prompt is not None and not isinstance(prompt, (str, list)):
|
||
raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}")
|
||
|
||
if negative_prompt is not None and negative_prompt_embeds is not None:
|
||
raise ValueError(
|
||
f"Cannot forward both `negative_prompt`: {negative_prompt} and `negative_prompt_embeds`:"
|
||
f" {negative_prompt_embeds}. Please make sure to only forward one of the two."
|
||
)
|
||
|
||
def cfg_normalize_function(self, noise_pred, comb_pred, cfg_renorm_min=0.0):
|
||
"""
|
||
Normalize the combined noise prediction.
|
||
"""
|
||
cond_norm = torch.norm(noise_pred, dim=-1, keepdim=True)
|
||
noise_norm = torch.norm(comb_pred, dim=-1, keepdim=True)
|
||
scale = (cond_norm / (noise_norm + 1e-8)).clamp(min=cfg_renorm_min, max=1.0)
|
||
noise_pred = comb_pred * scale
|
||
return noise_pred
|
||
|
||
def forward(
|
||
self,
|
||
req: OmniDiffusionRequest,
|
||
prompt: str | list[str] | None = None,
|
||
negative_prompt: str | list[str] | None = None,
|
||
height: int | None = None,
|
||
width: int | None = None,
|
||
num_inference_steps: int = 50,
|
||
sigmas: list[float] | None = None,
|
||
guidance_scale: float = 4.5,
|
||
num_images_per_prompt: int = 1,
|
||
generator: torch.Generator | list[torch.Generator] | None = None,
|
||
latents: torch.FloatTensor | None = None,
|
||
prompt_embeds: torch.Tensor | None = None,
|
||
negative_prompt_embeds: torch.Tensor | None = None,
|
||
output_type: str | None = "pil",
|
||
return_dict: bool = True,
|
||
joint_attention_kwargs: dict[str, Any] | None = None,
|
||
enable_cfg_renorm: bool | None = True,
|
||
cfg_renorm_min: float | None = 0.0,
|
||
enable_prompt_rewrite: bool | None = True,
|
||
) -> DiffusionOutput:
|
||
# TODO: In online mode, sometimes it receives [{"negative_prompt": None}, {...}], so cannot use .get("...", "")
|
||
# TODO: May be some data formatting operations on the API side. Hack for now.
|
||
prompt = [p if isinstance(p, str) else (p.get("prompt") or "") for p in req.prompts] or prompt
|
||
if all(isinstance(p, str) or p.get("negative_prompt") is None for p in req.prompts):
|
||
negative_prompt = None
|
||
elif req.prompts:
|
||
negative_prompt = ["" if isinstance(p, str) else (p.get("negative_prompt") or "") for p in req.prompts]
|
||
|
||
height = req.sampling_params.height or height or self.default_sample_size * self.vae_scale_factor
|
||
width = req.sampling_params.width or width or self.default_sample_size * self.vae_scale_factor
|
||
num_inference_steps = req.sampling_params.num_inference_steps or num_inference_steps
|
||
sigmas = req.sampling_params.sigmas or sigmas
|
||
generator = req.sampling_params.generator or generator
|
||
guidance_scale = (
|
||
req.sampling_params.guidance_scale if req.sampling_params.guidance_scale is not None else guidance_scale
|
||
)
|
||
num_images_per_prompt = (
|
||
req.sampling_params.num_outputs_per_prompt
|
||
if req.sampling_params.num_outputs_per_prompt is not None
|
||
else num_images_per_prompt
|
||
)
|
||
enable_prompt_rewrite = req.sampling_params.extra_args.get("enable_prompt_rewrite", enable_prompt_rewrite)
|
||
enable_cfg_renorm = req.sampling_params.extra_args.get("enable_cfg_renorm", enable_cfg_renorm)
|
||
cfg_renorm_min = req.sampling_params.extra_args.get("cfg_renorm_min", cfg_renorm_min)
|
||
|
||
req_prompt_embeds = [p.get("prompt_embeds") if not isinstance(p, str) else None for p in req.prompts]
|
||
if any(p is not None for p in req_prompt_embeds):
|
||
# If at list one prompt is provided as an embedding,
|
||
# Then assume that the user wants to provide embeddings for all prompts, and enter this if block
|
||
# If the user in fact provides mixed input format, req_prompt_embeds will have some None's
|
||
# And `torch.stack` automatically raises an exception for us
|
||
prompt_embeds = torch.stack(req_prompt_embeds) # type: ignore # intentionally expect TypeError
|
||
|
||
req_negative_prompt_embeds = [
|
||
p.get("negative_prompt_embeds") if not isinstance(p, str) else None for p in req.prompts
|
||
]
|
||
if any(p is not None for p in req_negative_prompt_embeds):
|
||
negative_prompt_embeds = torch.stack(req_negative_prompt_embeds) # type: ignore # intentionally expect TypeError
|
||
|
||
self.check_inputs(
|
||
prompt,
|
||
height,
|
||
width,
|
||
negative_prompt=negative_prompt,
|
||
prompt_embeds=prompt_embeds,
|
||
negative_prompt_embeds=negative_prompt_embeds,
|
||
)
|
||
|
||
self._guidance_scale = guidance_scale
|
||
self._joint_attention_kwargs = joint_attention_kwargs
|
||
self._current_timestep = None
|
||
self._interrupt = False
|
||
|
||
# 2. Define call parameters
|
||
if prompt is not None and isinstance(prompt, str):
|
||
batch_size = 1
|
||
elif prompt is not None and isinstance(prompt, list):
|
||
batch_size = len(prompt)
|
||
else:
|
||
batch_size = prompt_embeds.shape[0]
|
||
|
||
device = self.device
|
||
if enable_prompt_rewrite and prompt is not None:
|
||
prompt = self.rewire_prompt(prompt if isinstance(prompt, list) else [prompt], device)
|
||
|
||
negative_prompt = "" if negative_prompt is None else negative_prompt
|
||
|
||
(prompt_embeds, text_ids) = self.encode_prompt(
|
||
prompt=prompt,
|
||
prompt_embeds=prompt_embeds,
|
||
num_images_per_prompt=num_images_per_prompt,
|
||
)
|
||
if self.do_classifier_free_guidance:
|
||
(negative_prompt_embeds, negative_text_ids) = self.encode_prompt(
|
||
prompt=negative_prompt,
|
||
prompt_embeds=negative_prompt_embeds,
|
||
num_images_per_prompt=num_images_per_prompt,
|
||
)
|
||
|
||
# 4. Prepare latent variables
|
||
num_channels_latents = 16
|
||
latents, latent_image_ids = self.prepare_latents(
|
||
batch_size * num_images_per_prompt,
|
||
num_channels_latents,
|
||
height,
|
||
width,
|
||
prompt_embeds.dtype,
|
||
device,
|
||
generator,
|
||
latents,
|
||
)
|
||
|
||
# 5. Prepare timesteps
|
||
sigmas = np.linspace(1.0, 1.0 / num_inference_steps, num_inference_steps) if sigmas is None else sigmas
|
||
image_seq_len = latents.shape[1]
|
||
mu = calculate_shift(
|
||
image_seq_len,
|
||
self.scheduler.config.get("base_image_seq_len", 256),
|
||
self.scheduler.config.get("max_image_seq_len", 4096),
|
||
self.scheduler.config.get("base_shift", 0.5),
|
||
self.scheduler.config.get("max_shift", 1.15),
|
||
)
|
||
timesteps, num_inference_steps = retrieve_timesteps(
|
||
self.scheduler,
|
||
num_inference_steps,
|
||
device,
|
||
sigmas=sigmas,
|
||
mu=mu,
|
||
)
|
||
|
||
self._num_timesteps = len(timesteps)
|
||
|
||
# handle guidance
|
||
guidance = None
|
||
|
||
if self._joint_attention_kwargs is None:
|
||
self._joint_attention_kwargs = {}
|
||
|
||
prompt_embeds = prompt_embeds.to(device)
|
||
if self.do_classifier_free_guidance:
|
||
negative_prompt_embeds = negative_prompt_embeds.to(device)
|
||
|
||
# custom partial function with cfg_renorm_min
|
||
self.cfg_normalize_function = partial(self.cfg_normalize_function, cfg_renorm_min=cfg_renorm_min)
|
||
|
||
# 6. Denoising loop
|
||
for i, t in enumerate(timesteps):
|
||
if self._interrupt:
|
||
continue
|
||
|
||
self._current_timestep = t
|
||
timestep = t.expand(latents.shape[0]).to(latents.dtype)
|
||
|
||
positive_kwargs = {
|
||
"hidden_states": latents,
|
||
"timestep": timestep / 1000,
|
||
"guidance": guidance,
|
||
"encoder_hidden_states": prompt_embeds,
|
||
"txt_ids": text_ids,
|
||
"img_ids": latent_image_ids,
|
||
"return_dict": False,
|
||
}
|
||
if self.do_classifier_free_guidance:
|
||
negative_kwargs = {
|
||
"hidden_states": latents,
|
||
"timestep": timestep / 1000,
|
||
"encoder_hidden_states": negative_prompt_embeds,
|
||
"txt_ids": negative_text_ids,
|
||
"img_ids": latent_image_ids,
|
||
"return_dict": False,
|
||
}
|
||
else:
|
||
negative_kwargs = None
|
||
|
||
noise_pred = self.predict_noise_maybe_with_cfg(
|
||
do_true_cfg=self.do_classifier_free_guidance,
|
||
true_cfg_scale=guidance_scale,
|
||
positive_kwargs=positive_kwargs,
|
||
negative_kwargs=negative_kwargs,
|
||
cfg_normalize=enable_cfg_renorm,
|
||
)
|
||
|
||
# Compute the previous noisy sample x_t -> x_t-1 with automatic CFG sync
|
||
latents = self.scheduler_step_maybe_with_cfg(noise_pred, t, latents, self.do_classifier_free_guidance)
|
||
|
||
self._current_timestep = None
|
||
|
||
if output_type == "latent":
|
||
image = latents
|
||
else:
|
||
latents = self._unpack_latents(latents, height, width, self.vae_scale_factor)
|
||
latents = (latents / self.vae.config.scaling_factor) + self.vae.config.shift_factor
|
||
|
||
if latents.dtype != self.vae.dtype:
|
||
latents = latents.to(dtype=self.vae.dtype)
|
||
|
||
image = self.vae.decode(latents, return_dict=False)[0]
|
||
|
||
return DiffusionOutput(
|
||
output=image, stage_durations=self.stage_durations if hasattr(self, "stage_durations") else None
|
||
)
|
||
|
||
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
|
||
"""Load weights using AutoWeightsLoader for vLLM integration."""
|
||
loader = AutoWeightsLoader(self)
|
||
return loader.load_weights(weights)
|