Files
vllm-omni/vllm_omni/diffusion/models/ernie_image/pipeline_ernie_image.py
T

563 lines
22 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
# Adapted from: https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/ernie_image/pipeline_ernie_image.py
import json
import os
from collections.abc import Callable, Iterable
import torch
import torch.nn as nn
from diffusers.image_processor import VaeImageProcessor
from diffusers.models.autoencoders.autoencoder_kl_flux2 import AutoencoderKLFlux2
from diffusers.schedulers.scheduling_flow_match_euler_discrete import (
FlowMatchEulerDiscreteScheduler,
)
from diffusers.utils.torch_utils import randn_tensor
from transformers import AutoModel, AutoModelForCausalLM, AutoTokenizer
from vllm.logger import init_logger
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.ernie_image.ernie_image_transformer import ErnieImageTransformer2DModel
from vllm_omni.diffusion.models.interface import SupportImageInput
from vllm_omni.diffusion.models.progress_bar import ProgressBarMixin
from vllm_omni.diffusion.profiler.diffusion_pipeline_profiler import DiffusionPipelineProfilerMixin
from vllm_omni.diffusion.request import OmniDiffusionRequest
from vllm_omni.diffusion.utils.tf_utils import get_transformer_config_kwargs
from vllm_omni.model_executor.model_loader.weight_utils import download_weights_from_hf_specific
logger = init_logger(__name__)
def _resolve_model_path_for_optional_pe(model: str, revision: str | None) -> str:
if os.path.exists(model):
return model
try:
return download_weights_from_hf_specific(model, revision, ["pe/*", "pe_tokenizer/*"])
except Exception as e:
logger.debug("Failed to resolve ERNIE-Image PE files for %s: %s", model, e)
return model
def get_ernie_image_post_process_func(od_config: OmniDiffusionConfig):
if od_config.output_type == "latent":
return lambda x: x
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, do_normalize=False)
def post_process_func(images: torch.Tensor):
images = (images.clamp(-1, 1) + 1) / 2
return image_processor.postprocess(images)
return post_process_func
class ErnieImagePipeline(
nn.Module, CFGParallelMixin, SupportImageInput, ProgressBarMixin, DiffusionPipelineProfilerMixin
):
support_image_input = False
def __init__(
self,
*,
od_config: OmniDiffusionConfig,
prefix: str = "",
is_distilled: bool = False,
):
super().__init__()
self.od_config = od_config
self.is_distilled = is_distilled
self.weights_sources = [
DiffusersPipelineLoader.ComponentSource(
model_or_path=od_config.model,
subfolder="transformer",
revision=None,
prefix="transformer.",
fall_back_to_pt=True,
)
]
self._execution_device = get_local_device()
model = od_config.model
logger.info("Model path for initialization: %s", model)
local_files_only = os.path.exists(model)
logger.info("Local files only: %s", local_files_only)
self.scheduler = FlowMatchEulerDiscreteScheduler.from_pretrained(
model,
subfolder="scheduler",
local_files_only=local_files_only,
)
self.text_encoder = AutoModel.from_pretrained(
model,
subfolder="text_encoder",
torch_dtype=od_config.dtype,
local_files_only=local_files_only,
).to(self._execution_device)
self.tokenizer = AutoTokenizer.from_pretrained(
model,
subfolder="tokenizer",
local_files_only=local_files_only,
)
self.vae = AutoencoderKLFlux2.from_pretrained(
model,
subfolder="vae",
torch_dtype=od_config.dtype,
local_files_only=local_files_only,
).to(self._execution_device)
# Load PE (Prompt Enhancement) model if available. For repo IDs,
# resolve/download only PE files first so the existence check works.
pe_base_path = _resolve_model_path_for_optional_pe(model, getattr(od_config, "revision", None))
pe_model_path = os.path.join(pe_base_path, "pe")
if os.path.exists(pe_model_path):
try:
self.pe_model = AutoModelForCausalLM.from_pretrained(
pe_model_path,
torch_dtype=od_config.dtype,
local_files_only=True,
trust_remote_code=True,
).to(self._execution_device)
self.pe_tokenizer = AutoTokenizer.from_pretrained(
pe_base_path,
subfolder="pe_tokenizer",
local_files_only=True,
trust_remote_code=True,
use_fast=False,
)
self.use_pe = True
logger.info("Loaded PE model from %s", pe_model_path)
except Exception as e:
logger.warning("Failed to load PE model: %s", e)
self.pe_model = None
self.pe_tokenizer = None
self.use_pe = False
else:
self.pe_model = None
self.pe_tokenizer = None
self.use_pe = False
transformer_kwargs = get_transformer_config_kwargs(od_config.tf_model_config, ErnieImageTransformer2DModel)
self.transformer = ErnieImageTransformer2DModel(
quant_config=od_config.quantization_config, **transformer_kwargs
)
self.vae_scale_factor = 2 ** (len(self.vae.config.block_out_channels)) if getattr(self, "vae", None) else 16
self.image_processor = VaeImageProcessor(vae_scale_factor=self.vae_scale_factor)
self.tokenizer_max_length = 512
self.default_sample_size = 128
self._guidance_scale = None
self._attention_kwargs = None
self._num_timesteps = None
self._current_timestep = None
self._interrupt = False
self.setup_diffusion_pipeline_profiler(
enable_diffusion_pipeline_profiler=self.od_config.enable_diffusion_pipeline_profiler
)
@staticmethod
def _distributed_prompt_sync_state() -> tuple[bool, int]:
try:
if not torch.distributed.is_available() or not torch.distributed.is_initialized():
return False, 0
if torch.distributed.get_world_size() <= 1:
return False, 0
return True, torch.distributed.get_rank()
except Exception:
return False, 0
@staticmethod
def _broadcast_enhanced_prompt(prompt: str | None) -> str | None:
values = [prompt]
torch.distributed.broadcast_object_list(values, src=0)
return values[0]
def _enhance_prompt(
self,
prompt: str,
device: torch.device,
width: int = 1024,
height: int = 1024,
temperature: float = 0.6,
top_p: float = 0.95,
) -> str:
if not self.use_pe or self.pe_model is None:
return prompt
sync_prompt, rank = self._distributed_prompt_sync_state()
if sync_prompt and rank != 0:
try:
enhanced = self._broadcast_enhanced_prompt(None)
return enhanced if enhanced else prompt
except Exception as e:
logger.warning("PE enhancement broadcast failed: %s", e)
return prompt
try:
user_content = json.dumps(
{"prompt": prompt, "width": width, "height": height},
ensure_ascii=False,
)
messages = [{"role": "user", "content": user_content}]
input_text = self.pe_tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=False,
)
inputs = self.pe_tokenizer(input_text, return_tensors="pt").to(device)
output_ids = self.pe_model.generate(
**inputs,
max_new_tokens=512,
do_sample=temperature != 1.0 or top_p != 1.0,
temperature=temperature,
top_p=top_p,
pad_token_id=self.pe_tokenizer.pad_token_id,
eos_token_id=self.pe_tokenizer.eos_token_id,
)
output_ids = output_ids[0][inputs.input_ids.shape[1] :]
result = self.pe_tokenizer.decode(output_ids, skip_special_tokens=True).strip()
enhanced = result if result else prompt
except Exception as e:
logger.warning("PE enhancement failed: %s", e)
enhanced = prompt
if sync_prompt:
try:
synced = self._broadcast_enhanced_prompt(enhanced)
return synced if synced else prompt
except Exception as e:
logger.warning("PE enhancement broadcast failed: %s", e)
return enhanced
@staticmethod
def _is_warmup_request(req: OmniDiffusionRequest) -> bool:
is_dummy_run = getattr(req, "is_dummy_run", None)
if callable(is_dummy_run):
return bool(is_dummy_run())
return OmniDiffusionRequest.is_dummy_run_request_id(getattr(req, "request_id", None))
@staticmethod
def _should_apply_pe(req: OmniDiffusionRequest) -> bool:
if ErnieImagePipeline._is_warmup_request(req):
return False
extra_args = getattr(req.sampling_params, "extra_args", {}) or {}
return bool(extra_args.get("apply_pe", True))
def encode_prompt(
self,
prompt: str | list[str],
device: torch.device,
num_images_per_prompt: int = 1,
width: int = 1024,
height: int = 1024,
apply_pe: bool = True,
) -> list[torch.Tensor]:
if isinstance(prompt, str):
prompt = [prompt]
text_hiddens = []
for p in prompt:
if apply_pe and self.use_pe and self.pe_model is not None:
enhanced = self._enhance_prompt(p, device, width=width, height=height)
logger.info("PE: original='%s...' enhanced='%s...'", p[:50], enhanced[:50])
p = enhanced
ids = self.tokenizer(
p,
add_special_tokens=True,
truncation=True,
padding=False,
)["input_ids"]
if len(ids) == 0:
if self.tokenizer.bos_token_id is not None:
ids = [self.tokenizer.bos_token_id]
else:
ids = [0]
input_ids = torch.tensor([ids], device=device)
with torch.no_grad():
outputs = self.text_encoder(input_ids=input_ids, output_hidden_states=True)
hidden = outputs.hidden_states[-2][0]
for _ in range(num_images_per_prompt):
text_hiddens.append(hidden)
return text_hiddens
@staticmethod
def _patchify_latents(latents: torch.Tensor) -> torch.Tensor:
b, c, h, w = latents.shape
latents = latents.view(b, c, h // 2, 2, w // 2, 2)
latents = latents.permute(0, 1, 3, 5, 2, 4)
return latents.reshape(b, c * 4, h // 2, w // 2)
@staticmethod
def _unpatchify_latents(latents: torch.Tensor) -> torch.Tensor:
b, c, h, w = latents.shape
latents = latents.reshape(b, c // 4, 2, 2, h, w)
latents = latents.permute(0, 1, 4, 2, 5, 3)
return latents.reshape(b, c // 4, h * 2, w * 2)
@staticmethod
def _pad_text(text_hiddens: list[torch.Tensor], device: torch.device, dtype: torch.dtype, text_in_dim: int):
B = len(text_hiddens)
if B == 0:
return torch.zeros((0, 0, text_in_dim), device=device, dtype=dtype), torch.zeros(
(0,), device=device, dtype=torch.long
)
normalized = [
th.squeeze(1).to(device).to(dtype) if th.dim() == 3 else th.to(device).to(dtype) for th in text_hiddens
]
lens = torch.tensor([t.shape[0] for t in normalized], device=device, dtype=torch.long)
Tmax = int(lens.max().item())
text_bth = torch.zeros((B, Tmax, text_in_dim), device=device, dtype=dtype)
for i, t in enumerate(normalized):
text_bth[i, : t.shape[0], :] = t
return text_bth, lens
@property
def guidance_scale(self):
return self._guidance_scale
@property
def do_classifier_free_guidance(self):
return self._guidance_scale is not None and self._guidance_scale > 1 and not self.is_distilled
@property
def num_timesteps(self):
return self._num_timesteps
@property
def current_timestep(self):
return self._current_timestep
@property
def interrupt(self):
return self._interrupt
def _resize_dimensions(self, height: int, width: int) -> tuple[int, int]:
resized_height = height - height % self.vae_scale_factor
resized_width = width - width % self.vae_scale_factor
return resized_height, resized_width
def check_inputs(
self,
prompt,
height,
width,
prompt_embeds=None,
callback_on_step_end_tensor_inputs=None,
guidance_scale=None,
):
if (height is not None and height % self.vae_scale_factor != 0) or (
width is not None and width % self.vae_scale_factor != 0
):
resized_height, resized_width = self._resize_dimensions(height, width)
logger.warning(
"`height` and `width` have to be divisible by %s "
"but are %s and %s. Dimensions will be resized to %s and %s accordingly",
self.vae_scale_factor,
height,
width,
resized_height,
resized_width,
)
if callback_on_step_end_tensor_inputs is not None and not all(
k in ["latents", "prompt_embeds"] for k in callback_on_step_end_tensor_inputs
):
raise ValueError("`callback_on_step_end_tensor_inputs` must be a subset of ['latents', 'prompt_embeds'].")
if prompt is not None and prompt_embeds is not None:
raise ValueError("Cannot forward both `prompt` and `prompt_embeds`. Please forward only one.")
if prompt is None and prompt_embeds is None:
raise ValueError("Must provide either `prompt` or `prompt_embeds`.")
def forward(
self,
req: OmniDiffusionRequest,
prompt: str | list[str] | None = None,
negative_prompt: str | list[str] | None = "",
height: int = 1024,
width: int = 1024,
num_inference_steps: int = 50,
guidance_scale: float = 4.0,
num_images_per_prompt: int = 1,
generator: torch.Generator | None = None,
latents: torch.Tensor | None = None,
prompt_embeds: list[torch.FloatTensor] | None = None,
negative_prompt_embeds: list[torch.FloatTensor] | None = None,
output_type: str = "pil",
return_dict: bool = True,
callback_on_step_end: Callable[[int, int, dict], None] | None = None,
callback_on_step_end_tensor_inputs: list[str] = ["latents"],
) -> DiffusionOutput:
if len(req.prompts) > 1:
logger.warning("This model only supports a single prompt. Taking only the first.")
first_prompt = req.prompts[0]
prompt = first_prompt if isinstance(first_prompt, str) else (first_prompt.get("prompt") or "")
height = req.sampling_params.height or height
width = req.sampling_params.width or width
num_inference_steps = req.sampling_params.num_inference_steps or num_inference_steps
guidance_scale = (
req.sampling_params.guidance_scale if req.sampling_params.guidance_scale is not None else guidance_scale
)
generator = req.sampling_params.generator or generator
num_images_per_prompt = (
req.sampling_params.num_outputs_per_prompt
if req.sampling_params.num_outputs_per_prompt > 0
else num_images_per_prompt
)
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._execution_device
dtype = self.transformer.dtype
self._guidance_scale = guidance_scale
self._current_timestep = None
self._interrupt = False
self.check_inputs(prompt=prompt, height=height, width=width, prompt_embeds=prompt_embeds)
height, width = self._resize_dimensions(height, width)
if prompt is not None:
if isinstance(prompt, str):
prompt = [prompt]
if prompt_embeds is not None:
text_hiddens = prompt_embeds
else:
text_hiddens = self.encode_prompt(
prompt,
device,
num_images_per_prompt,
width=width,
height=height,
apply_pe=self._should_apply_pe(req),
)
if self.do_classifier_free_guidance:
if negative_prompt_embeds is not None:
uncond_text_hiddens = negative_prompt_embeds
else:
if negative_prompt is None:
negative_prompt = ""
if isinstance(negative_prompt, str):
negative_prompt = [negative_prompt] * batch_size
uncond_text_hiddens = self.encode_prompt(
negative_prompt, device, num_images_per_prompt, width=width, height=height, apply_pe=False
)
latent_h = height // self.vae_scale_factor
latent_w = width // self.vae_scale_factor
latent_channels = self.transformer.config.in_channels
if latents is None:
latents = randn_tensor(
(batch_size * num_images_per_prompt, latent_channels, latent_h, latent_w),
generator=generator,
device=device,
dtype=dtype,
)
sigmas = torch.linspace(1.0, 0.0, num_inference_steps + 1)
self.scheduler.set_timesteps(sigmas=sigmas[:-1], device=device)
if self.do_classifier_free_guidance:
cfg_text_hiddens = list(uncond_text_hiddens) + list(text_hiddens)
else:
cfg_text_hiddens = text_hiddens
text_bth, text_lens = self._pad_text(
text_hiddens=cfg_text_hiddens, device=device, dtype=dtype, text_in_dim=self.transformer.config.text_in_dim
)
with self.progress_bar(total=num_inference_steps) as progress_bar:
for i, t in enumerate(self.scheduler.timesteps):
if self.interrupt:
continue
self._current_timestep = t
if self.do_classifier_free_guidance:
latent_model_input = torch.cat([latents, latents], dim=0)
t_batch = torch.full(
(batch_size * num_images_per_prompt * 2,), t.item(), device=device, dtype=dtype
)
else:
latent_model_input = latents
t_batch = torch.full((batch_size * num_images_per_prompt,), t.item(), device=device, dtype=dtype)
pred = self.transformer(
hidden_states=latent_model_input,
timestep=t_batch,
text_bth=text_bth,
text_lens=text_lens,
return_dict=False,
)[0]
if self.do_classifier_free_guidance:
pred_uncond, pred_cond = pred.chunk(2, dim=0)
pred = pred_uncond + guidance_scale * (pred_cond - pred_uncond)
latents = self.scheduler.step(pred, t, latents, return_dict=False)[0]
if callback_on_step_end is not None:
callback_kwargs = {k: locals()[k] for k in callback_on_step_end_tensor_inputs}
callback_outputs = callback_on_step_end(self, i, t, callback_kwargs)
latents = callback_outputs.pop("latents", latents)
progress_bar.update()
self._current_timestep = None
if output_type == "latent":
return DiffusionOutput(output=latents)
bn_mean = self.vae.bn.running_mean.view(1, -1, 1, 1).to(device)
bn_std = torch.sqrt(self.vae.bn.running_var.view(1, -1, 1, 1) + self.vae.config.batch_norm_eps).to(device)
latents = latents * bn_std + bn_mean
latents = self._unpatchify_latents(latents)
images = self.vae.decode(latents, return_dict=False)[0]
return DiffusionOutput(
output=images, stage_durations=self.stage_durations if hasattr(self, "stage_durations") else None
)
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
loaded_weights = set()
transformer_weights = (
(name.replace("transformer.", "", 1), weight) for name, weight in weights if name.startswith("transformer.")
)
loaded_weights |= {f"transformer.{name}" for name in self.transformer.load_weights(transformer_weights)}
loaded_weights |= {f"vae.{name}" for name, _ in self.vae.named_parameters()}
loaded_weights |= {f"text_encoder.{name}" for name, _ in self.text_encoder.named_parameters()}
return loaded_weights