2025-12-18 23:35:27 +08:00
|
|
|
|
# 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
|
2026-01-31 00:23:44 +08:00
|
|
|
|
from functools import partial
|
2025-12-18 23:35:27 +08:00
|
|
|
|
from typing import Any
|
|
|
|
|
|
|
|
|
|
|
|
import numpy as np
|
|
|
|
|
|
import torch
|
|
|
|
|
|
from diffusers.image_processor import VaeImageProcessor
|
|
|
|
|
|
from diffusers.models import AutoencoderKL
|
2025-12-22 21:31:50 +08:00
|
|
|
|
from diffusers.pipelines.longcat_image.system_messages import SYSTEM_PROMPT_EN, SYSTEM_PROMPT_ZH
|
2025-12-18 23:35:27 +08:00
|
|
|
|
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
|
2026-01-31 00:23:44 +08:00
|
|
|
|
from vllm_omni.diffusion.distributed.cfg_parallel import CFGParallelMixin
|
2025-12-18 23:35:27 +08:00
|
|
|
|
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
|
2026-03-19 12:26:12 +08:00
|
|
|
|
from vllm_omni.diffusion.profiler.diffusion_pipeline_profiler import DiffusionPipelineProfilerMixin
|
2025-12-18 23:35:27 +08:00
|
|
|
|
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"
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-19 12:26:12 +08:00
|
|
|
|
class LongCatImagePipeline(nn.Module, CFGParallelMixin, DiffusionPipelineProfilerMixin):
|
2025-12-18 23:35:27 +08:00
|
|
|
|
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
|
2026-03-19 12:26:12 +08:00
|
|
|
|
self.setup_diffusion_pipeline_profiler(
|
|
|
|
|
|
enable_diffusion_pipeline_profiler=self.od_config.enable_diffusion_pipeline_profiler
|
|
|
|
|
|
)
|
2025-12-18 23:35:27 +08:00
|
|
|
|
|
|
|
|
|
|
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."
|
|
|
|
|
|
)
|
2026-01-28 16:07:42 +08:00
|
|
|
|
elif prompt is not None and not isinstance(prompt, (str, list)):
|
2025-12-18 23:35:27 +08:00
|
|
|
|
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."
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-01-31 00:23:44 +08:00
|
|
|
|
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
|
|
|
|
|
|
|
2025-12-18 23:35:27 +08:00
|
|
|
|
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,
|
2026-01-28 16:07:42 +08:00
|
|
|
|
num_images_per_prompt: int = 1,
|
2025-12-18 23:35:27 +08:00
|
|
|
|
generator: torch.Generator | list[torch.Generator] | None = None,
|
|
|
|
|
|
latents: torch.FloatTensor | None = None,
|
2026-01-28 16:07:42 +08:00
|
|
|
|
prompt_embeds: torch.Tensor | None = None,
|
|
|
|
|
|
negative_prompt_embeds: torch.Tensor | None = None,
|
2025-12-18 23:35:27 +08:00
|
|
|
|
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:
|
2026-01-28 16:07:42 +08:00
|
|
|
|
# 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]
|
2025-12-18 23:35:27 +08:00
|
|
|
|
|
2026-01-28 16:07:42 +08:00
|
|
|
|
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
|
2025-12-18 23:35:27 +08:00
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
2026-01-31 00:23:44 +08:00
|
|
|
|
# custom partial function with cfg_renorm_min
|
|
|
|
|
|
self.cfg_normalize_function = partial(self.cfg_normalize_function, cfg_renorm_min=cfg_renorm_min)
|
|
|
|
|
|
|
2025-12-18 23:35:27 +08:00
|
|
|
|
# 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)
|
|
|
|
|
|
|
2026-01-31 00:23:44 +08:00
|
|
|
|
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,
|
|
|
|
|
|
}
|
2025-12-18 23:35:27 +08:00
|
|
|
|
if self.do_classifier_free_guidance:
|
2026-01-31 00:23:44 +08:00
|
|
|
|
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,
|
|
|
|
|
|
}
|
2025-12-18 23:35:27 +08:00
|
|
|
|
else:
|
2026-01-31 00:23:44 +08:00
|
|
|
|
negative_kwargs = None
|
2025-12-18 23:35:27 +08:00
|
|
|
|
|
2026-01-31 00:23:44 +08:00
|
|
|
|
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,
|
|
|
|
|
|
)
|
2025-12-18 23:35:27 +08:00
|
|
|
|
|
2026-01-31 00:23:44 +08:00
|
|
|
|
# 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)
|
2025-12-18 23:35:27 +08:00
|
|
|
|
|
|
|
|
|
|
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]
|
|
|
|
|
|
|
2026-03-19 12:26:12 +08:00
|
|
|
|
return DiffusionOutput(
|
|
|
|
|
|
output=image, stage_durations=self.stage_durations if hasattr(self, "stage_durations") else None
|
|
|
|
|
|
)
|
2025-12-18 23:35:27 +08:00
|
|
|
|
|
|
|
|
|
|
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)
|