diffusion: support Qwen-Image-Layered (#15817)

Co-authored-by: yhyang201 <yhyang201@gmail.com>
This commit is contained in:
chhnb
2025-12-26 14:57:01 +08:00
committed by GitHub
parent e75657c839
commit a1e9b4edfa
12 changed files with 962 additions and 24 deletions

View File

@@ -20,7 +20,7 @@ class QwenImageVAEArchConfig(VAEArchConfig):
dropout: float = 0.0
is_residual: bool = False
in_channels: int = 3
input_channels: int = 3
out_channels: int = 3
patch_size: int | None = None
scale_factor_temporal: int = 4

View File

@@ -299,6 +299,9 @@ class PipelineConfig:
return shape
def allow_set_num_frames(self):
return False
def get_decode_scale_and_shift(self, device, dtype, vae):
vae_arch_config = self.vae_config.arch_config
scaling_factor = getattr(vae_arch_config, "scaling_factor", None)

View File

@@ -12,6 +12,7 @@ from sglang.multimodal_gen.configs.models.vaes.qwenimage import QwenImageVAEConf
from sglang.multimodal_gen.configs.pipeline_configs.base import (
ImagePipelineConfig,
ModelTaskType,
maybe_unpad_latents,
shard_rotary_emb_for_sp,
)
from sglang.multimodal_gen.runtime.models.vision_utils import resize
@@ -466,3 +467,90 @@ class QwenImageEditPlusPipelineConfig(QwenImageEditPipelineConfig):
"freqs_cis": ((img_cos, img_sin), (txt_cos, txt_sin)),
"img_shapes": img_shapes,
}
@dataclass
class QwenImageLayeredPipelineConfig(QwenImageEditPipelineConfig):
resolution: int = 640 # TODO: allow user to set resolution
vae_precision: str = "bf16"
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 = batch.img_shapes
txt_seq_lens = batch.txt_seq_lens
(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,
"img_shapes": img_shapes,
"freqs_cis": ((img_cos, img_sin), (txt_cos, txt_sin)),
"additional_t_cond": torch.tensor([0], device=device, dtype=torch.long),
}
def _unpad_and_unpack_latents(self, latents, batch):
vae_scale_factor = self.vae_config.arch_config.vae_scale_factor
channels = self.dit_config.arch_config.in_channels
batch_size = latents.shape[0]
layers = batch.num_frames
height = 2 * (int(batch.height) // (vae_scale_factor * 2))
width = 2 * (int(batch.width) // (vae_scale_factor * 2))
latents = maybe_unpad_latents(latents, batch)
latents = latents.view(
batch_size, layers + 1, height // 2, width // 2, channels // 4, 2, 2
)
latents = latents.permute(0, 1, 4, 2, 5, 3, 6)
latents = latents.reshape(
batch_size, layers + 1, channels // (2 * 2), height, width
)
latents = latents.permute(0, 2, 1, 3, 4) # (b, c, f, h, w)
return latents, batch_size, channels, height, width
def allow_set_num_frames(self):
return True
def post_denoising_loop(self, latents, batch):
# unpack latents for qwen-image
(
latents,
batch_size,
channels,
height,
width,
) = self._unpad_and_unpack_latents(latents, batch)
b, c, f, h, w = latents.shape
latents = latents[:, :, 1:] # remove the first frame as it is the origin input
latents = latents.permute(0, 2, 1, 3, 4).view(-1, c, 1, h, w)
# latents = latents.reshape(batch_size, channels // (2 * 2), 1, height, width)
return latents

View File

@@ -21,3 +21,17 @@ class QwenImageEditPlusSamplingParams(QwenImageSamplingParams):
guidance_scale: float = 4.0
# true_cfg_scale: float = 4.0
num_inference_steps: int = 40
@dataclass
class QwenImageLayeredSamplingParams(QwenImageSamplingParams):
# num_frames: int = 4
height: int = 640
width: int = 640
prompt: str = " "
negative_prompt: str = " "
guidance_scale: float = 4.0
num_inference_steps: int = 50
cfg_normalize: bool = True
use_en_prompt: bool = True

View File

@@ -69,7 +69,7 @@ class DataType(Enum):
def get_default_extension(self) -> str:
if self == DataType.IMAGE:
return "jpg"
return "png"
else:
return "mp4"
@@ -279,8 +279,10 @@ class SamplingParams:
if pipeline_config.task_type.is_image_gen():
# settle num_frames
logger.debug(f"num_frames set to 1 for image generation model")
self.num_frames = 1
if not server_args.pipeline_config.allow_set_num_frames():
logger.debug(f"num_frames set to 1 for image generation model")
self.num_frames = 1
elif self.adjust_frames:
# NOTE: We must apply adjust_num_frames BEFORE the SP alignment logic below.
# If we apply it after, adjust_num_frames might modify the frame count

View File

@@ -30,6 +30,7 @@ from sglang.multimodal_gen.configs.pipeline_configs.flux import Flux2PipelineCon
from sglang.multimodal_gen.configs.pipeline_configs.qwen_image import (
QwenImageEditPipelineConfig,
QwenImageEditPlusPipelineConfig,
QwenImageLayeredPipelineConfig,
QwenImagePipelineConfig,
)
from sglang.multimodal_gen.configs.pipeline_configs.wan import (
@@ -46,6 +47,7 @@ from sglang.multimodal_gen.configs.sample.hunyuan import (
)
from sglang.multimodal_gen.configs.sample.qwenimage import (
QwenImageEditPlusSamplingParams,
QwenImageLayeredSamplingParams,
QwenImageSamplingParams,
)
from sglang.multimodal_gen.configs.sample.stepvideo import StepVideoT2VSamplingParams
@@ -443,5 +445,11 @@ def _register_configs():
hf_model_paths=["Qwen/Qwen-Image-Edit-2511"],
)
register_configs(
sampling_param_cls=QwenImageLayeredSamplingParams,
pipeline_config_cls=QwenImageLayeredPipelineConfig,
hf_model_paths=["Qwen/Qwen-Image-Layered"],
)
_register_configs()

View File

@@ -183,14 +183,13 @@ class DiffGenerator:
raise ValueError(f"No prompts found in file: {prompt_txt_path}")
logger.info("Found %d prompts in %s", len(prompts), prompt_txt_path)
elif prompt is not None:
else:
if prompt is None:
prompt = " "
if isinstance(prompt, str):
prompts.append(prompt)
elif isinstance(prompt, list):
prompts.extend(prompt)
else:
raise ValueError("Either prompt or prompt_txt must be provided")
sampling_params = SamplingParams.from_user_sampling_params_args(
self.server_args.model_path,
server_args=self.server_args,

View File

@@ -93,7 +93,16 @@ def post_process_sample(
)
else:
quality = 75
imageio.imwrite(save_file_path, frames[0], quality=quality)
if len(frames) > 1:
for i, image in enumerate(frames):
parts = save_file_path.rsplit(".", 1)
if len(parts) == 2:
indexed_path = f"{parts[0]}_{i}.{parts[1]}"
else:
indexed_path = f"{save_file_path}_{i}"
imageio.imwrite(indexed_path, image, quality=quality)
else:
imageio.imwrite(save_file_path, frames[0], quality=quality)
logger.info(f"Output saved to {CYAN}{save_file_path}{RESET}")
else:
logger.info(f"No output path provided, output not saved")

View File

@@ -44,7 +44,7 @@ def _get_qkv_projections(
class QwenTimestepProjEmbeddings(nn.Module):
def __init__(self, embedding_dim):
def __init__(self, embedding_dim, use_additional_t_cond=False):
super().__init__()
self.time_proj = Timesteps(
@@ -53,14 +53,25 @@ class QwenTimestepProjEmbeddings(nn.Module):
self.timestep_embedder = TimestepEmbedding(
in_channels=256, time_embed_dim=embedding_dim
)
self.use_additional_t_cond = use_additional_t_cond
if use_additional_t_cond:
self.addition_t_embedding = nn.Embedding(2, embedding_dim)
def forward(self, timestep, hidden_states):
def forward(self, timestep, hidden_states, addition_t_cond=None):
timesteps_proj = self.time_proj(timestep)
timesteps_emb = self.timestep_embedder(
timesteps_proj.to(dtype=hidden_states.dtype)
) # (N, D)
conditioning = timesteps_emb
if self.use_additional_t_cond:
if addition_t_cond is None:
raise ValueError(
"When additional_t_cond is True, addition_t_cond must be provided."
)
addition_t_emb = self.addition_t_embedding(addition_t_cond)
addition_t_emb = addition_t_emb.to(dtype=hidden_states.dtype)
conditioning = conditioning + addition_t_emb
return conditioning
@@ -232,6 +243,200 @@ class QwenEmbedRope(nn.Module):
return freqs.clone().contiguous()
class QwenEmbedLayer3DRope(nn.Module):
def __init__(self, theta: int, axes_dim: List[int], scale_rope=False):
super().__init__()
self.theta = theta
self.axes_dim = axes_dim
pos_index = torch.arange(4096)
neg_index = torch.arange(4096).flip(0) * -1 - 1
self.pos_freqs = torch.cat(
[
self.rope_params(pos_index, self.axes_dim[0], self.theta),
self.rope_params(pos_index, self.axes_dim[1], self.theta),
self.rope_params(pos_index, self.axes_dim[2], self.theta),
],
dim=1,
)
self.neg_freqs = torch.cat(
[
self.rope_params(neg_index, self.axes_dim[0], self.theta),
self.rope_params(neg_index, self.axes_dim[1], self.theta),
self.rope_params(neg_index, self.axes_dim[2], self.theta),
],
dim=1,
)
self.scale_rope = scale_rope
def rope_params(self, index, dim, theta=10000):
"""
Args:
index: [0, 1, 2, 3] 1D Tensor representing the position index of the token
"""
device = index.device
assert dim % 2 == 0
freqs = torch.outer(
index,
(
1.0
/ torch.pow(
theta,
torch.arange(0, dim, 2, device=device).to(torch.float32).div(dim),
)
).to(device=device),
)
freqs = torch.polar(torch.ones_like(freqs), freqs)
return freqs
def forward(self, video_fhw, txt_seq_lens, device):
"""
Args: video_fhw: [frame, height, width] a list of 3 integers representing the shape of the video Args:
txt_length: [bs] a list of 1 integers representing the length of the text
"""
# When models are initialized under a "meta" device context (e.g. init_empty_weights),
# tensors created during __init__ become meta tensors. Calling .to(...) on a meta tensor
# raises "Cannot copy out of meta tensor". Rebuild the frequencies on the target device
# in that case; otherwise move them if just on a different device.
if getattr(self.pos_freqs, "device", torch.device("meta")).type == "meta":
pos_index = torch.arange(4096, device=device)
neg_index = torch.arange(4096, device=device).flip(0) * -1 - 1
self.pos_freqs = torch.cat(
[
self.rope_params(pos_index, self.axes_dim[0], self.theta),
self.rope_params(pos_index, self.axes_dim[1], self.theta),
self.rope_params(pos_index, self.axes_dim[2], self.theta),
],
dim=1,
).to(device=device)
self.neg_freqs = torch.cat(
[
self.rope_params(neg_index, self.axes_dim[0], self.theta),
self.rope_params(neg_index, self.axes_dim[1], self.theta),
self.rope_params(neg_index, self.axes_dim[2], self.theta),
],
dim=1,
).to(device=device)
elif self.pos_freqs.device != device:
self.pos_freqs = self.pos_freqs.to(device)
self.neg_freqs = self.neg_freqs.to(device)
if isinstance(video_fhw, list):
video_fhw = video_fhw[0]
if not isinstance(video_fhw, list):
video_fhw = [video_fhw]
vid_freqs = []
max_vid_index = 0
layer_num = len(video_fhw) - 1
for idx, fhw in enumerate(video_fhw):
frame, height, width = fhw
if idx != layer_num:
video_freq = self._compute_video_freqs(frame, height, width, idx)
else:
### For the condition image, we set the layer index to -1
video_freq = self._compute_condition_freqs(frame, height, width)
video_freq = video_freq.to(device)
vid_freqs.append(video_freq)
if self.scale_rope:
max_vid_index = max(height // 2, width // 2, max_vid_index)
else:
max_vid_index = max(height, width, max_vid_index)
max_vid_index = max(max_vid_index, layer_num)
max_len = max(txt_seq_lens)
txt_freqs = self.pos_freqs[max_vid_index : max_vid_index + max_len, ...]
vid_freqs = torch.cat(vid_freqs, dim=0)
return vid_freqs, txt_freqs
@functools.lru_cache(maxsize=None)
def _compute_video_freqs(self, frame, height, width, idx=0):
seq_lens = frame * height * width
freqs_pos = self.pos_freqs.split([x // 2 for x in self.axes_dim], dim=1)
freqs_neg = self.neg_freqs.split([x // 2 for x in self.axes_dim], dim=1)
freqs_frame = (
freqs_pos[0][idx : idx + frame]
.view(frame, 1, 1, -1)
.expand(frame, height, width, -1)
)
if self.scale_rope:
freqs_height = torch.cat(
[freqs_neg[1][-(height - height // 2) :], freqs_pos[1][: height // 2]],
dim=0,
)
freqs_height = freqs_height.view(1, height, 1, -1).expand(
frame, height, width, -1
)
freqs_width = torch.cat(
[freqs_neg[2][-(width - width // 2) :], freqs_pos[2][: width // 2]],
dim=0,
)
freqs_width = freqs_width.view(1, 1, width, -1).expand(
frame, height, width, -1
)
else:
freqs_height = (
freqs_pos[1][:height]
.view(1, height, 1, -1)
.expand(frame, height, width, -1)
)
freqs_width = (
freqs_pos[2][:width]
.view(1, 1, width, -1)
.expand(frame, height, width, -1)
)
freqs = torch.cat([freqs_frame, freqs_height, freqs_width], dim=-1).reshape(
seq_lens, -1
)
return freqs.clone().contiguous()
@functools.lru_cache(maxsize=None)
def _compute_condition_freqs(self, frame, height, width):
seq_lens = frame * height * width
freqs_pos = self.pos_freqs.split([x // 2 for x in self.axes_dim], dim=1)
freqs_neg = self.neg_freqs.split([x // 2 for x in self.axes_dim], dim=1)
freqs_frame = (
freqs_neg[0][-1:].view(frame, 1, 1, -1).expand(frame, height, width, -1)
)
if self.scale_rope:
freqs_height = torch.cat(
[freqs_neg[1][-(height - height // 2) :], freqs_pos[1][: height // 2]],
dim=0,
)
freqs_height = freqs_height.view(1, height, 1, -1).expand(
frame, height, width, -1
)
freqs_width = torch.cat(
[freqs_neg[2][-(width - width // 2) :], freqs_pos[2][: width // 2]],
dim=0,
)
freqs_width = freqs_width.view(1, 1, width, -1).expand(
frame, height, width, -1
)
else:
freqs_height = (
freqs_pos[1][:height]
.view(1, height, 1, -1)
.expand(frame, height, width, -1)
)
freqs_width = (
freqs_pos[2][:width]
.view(1, 1, width, -1)
.expand(frame, height, width, -1)
)
freqs = torch.cat([freqs_frame, freqs_height, freqs_width], dim=-1).reshape(
seq_lens, -1
)
return freqs.clone().contiguous()
class QwenImageCrossAttention(nn.Module):
def __init__(
self,
@@ -553,17 +758,31 @@ class QwenImageTransformer2DModel(CachableDiT):
num_attention_heads = config.arch_config.num_attention_heads
joint_attention_dim = config.arch_config.joint_attention_dim
axes_dims_rope = config.arch_config.axes_dims_rope
zero_cond_t = getattr(config.arch_config, "zero_cond_t", False)
self.zero_cond_t = getattr(config.arch_config, "zero_cond_t", False)
self.out_channels = out_channels or in_channels
self.inner_dim = num_attention_heads * attention_head_dim
self.zero_cond_t = zero_cond_t
self.rotary_emb = QwenEmbedRope(
theta=10000, axes_dim=list(axes_dims_rope), scale_rope=True
self.use_additional_t_cond: bool = getattr(
config.arch_config, "use_additional_t_cond", False
) # For qwen-image-layered now
self.use_layer3d_rope: bool = getattr(
config.arch_config, "use_layer3d_rope", False
) # For qwen-image-layered now
if not self.use_layer3d_rope:
self.rotary_emb = QwenEmbedRope(
theta=10000, axes_dim=list(axes_dims_rope), scale_rope=True
)
else:
self.rotary_emb = QwenEmbedLayer3DRope(
theta=10000, axes_dim=list(axes_dims_rope), scale_rope=True
)
self.time_text_embed = QwenTimestepProjEmbeddings(
embedding_dim=self.inner_dim,
use_additional_t_cond=self.use_additional_t_cond,
)
self.time_text_embed = QwenTimestepProjEmbeddings(embedding_dim=self.inner_dim)
self.txt_norm = RMSNorm(joint_attention_dim, eps=1e-6)
self.img_in = nn.Linear(in_channels, self.inner_dim)
@@ -575,7 +794,7 @@ class QwenImageTransformer2DModel(CachableDiT):
dim=self.inner_dim,
num_attention_heads=num_attention_heads,
attention_head_dim=attention_head_dim,
zero_cond_t=zero_cond_t,
zero_cond_t=self.zero_cond_t,
)
for _ in range(num_layers)
]
@@ -597,6 +816,7 @@ class QwenImageTransformer2DModel(CachableDiT):
img_shapes: Optional[List[Tuple[int, int, int]]] = None,
txt_seq_lens: Optional[List[int]] = None,
freqs_cis: tuple[torch.Tensor, torch.Tensor] = None,
additional_t_cond: Optional[torch.Tensor] = None,
guidance: torch.Tensor = None, # TODO: this should probably be removed
attention_kwargs: Optional[Dict[str, Any]] = None,
controlnet_block_samples=None,
@@ -658,7 +878,7 @@ class QwenImageTransformer2DModel(CachableDiT):
encoder_hidden_states = self.txt_norm(encoder_hidden_states)
encoder_hidden_states = self.txt_in(encoder_hidden_states)
temb = self.time_text_embed(timestep, hidden_states)
temb = self.time_text_embed(timestep, hidden_states, additional_t_cond)
image_rotary_emb = freqs_cis
for index_block, block in enumerate(self.transformer_blocks):

View File

@@ -0,0 +1,528 @@
import inspect
import math
from typing import List, Optional, Union
import numpy as np
import torch
from diffusers.image_processor import VaeImageProcessor
from diffusers.utils.torch_utils import randn_tensor
from sglang.multimodal_gen.runtime.distributed import get_local_torch_device
from sglang.multimodal_gen.runtime.managers.forward_context import set_forward_context
from sglang.multimodal_gen.runtime.models.vision_utils import load_image
from sglang.multimodal_gen.runtime.pipelines_core.schedule_batch import Req
from sglang.multimodal_gen.runtime.pipelines_core.stages.base import PipelineStage
from sglang.multimodal_gen.runtime.server_args import ServerArgs
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__)
# Copied from diffusers.pipelines.qwenimage.pipeline_qwenimage_edit_plus.calculate_dimensions
def calculate_dimensions(target_area, ratio):
width = math.sqrt(target_area * ratio)
height = width / ratio
width = round(width / 32) * 32
height = round(height / 32) * 32
return width, height
# Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion_img2img.retrieve_latents
def retrieve_latents(
encoder_output: torch.Tensor,
generator: Optional[torch.Generator] = None,
sample_mode: str = "sample",
):
if sample_mode == "sample":
return encoder_output.sample(generator)
elif sample_mode == "argmax":
return encoder_output.mode()
else:
return encoder_output
# Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.retrieve_timesteps
def retrieve_timesteps(
scheduler,
num_inference_steps: Optional[int] = None,
device: Optional[Union[str, torch.device]] = None,
timesteps: Optional[List[int]] = None,
sigmas: Optional[List[float]] = None,
**kwargs,
):
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
class QwenImageLayeredBeforeDenoisingStage(PipelineStage):
def __init__(
self, vae, tokenizer, processor, transformer, scheduler, model_path
) -> None:
self.vae = vae.to(torch.bfloat16)
from transformers import Qwen2_5_VLForConditionalGeneration
self.text_encoder = (
Qwen2_5_VLForConditionalGeneration.from_pretrained(
model_path, subfolder="text_encoder"
)
.to(get_local_torch_device())
.to(torch.bfloat16)
)
self.tokenizer = tokenizer
self.processor = processor
self.transformer = transformer
self.scheduler = scheduler
self.vae_scale_factor = (
2 ** len(self.vae.temperal_downsample) if getattr(self, "vae", None) else 8
)
self.image_processor = VaeImageProcessor(
vae_scale_factor=self.vae_scale_factor * 2
)
self.vl_processor = processor
self.tokenizer_max_length = 1024
self.latent_channels = self.vae.z_dim if getattr(self, "vae", None) else 16
self.prompt_template_encode = "<|im_start|>system\nDescribe the image by detailing the color, shape, size, texture, quantity, text, spatial relationships of the objects and background:<|im_end|>\n<|im_start|>user\n{}<|im_end|>\n<|im_start|>assistant\n"
self.prompt_template_encode_start_idx = 34
self.image_caption_prompt_cn = """<|im_start|>system\nYou are a helpful assistant.<|im_end|>\n<|im_start|>user\n# 图像标注器\n你是一个专业的图像标注器。请基于输入图像,撰写图注:\n1.
使用自然、描述性的语言撰写图注,不要使用结构化形式或富文本形式。\n2. 通过加入以下内容,丰富图注细节:\n - 对象的属性:如数量、颜色、形状、大小、位置、材质、状态、动作等\n -
对象间的视觉关系:如空间关系、功能关系、动作关系、从属关系、比较关系、因果关系等\n - 环境细节:例如天气、光照、颜色、纹理、气氛等\n - 文字内容:识别图像中清晰可见的文字,不做翻译和解释,用引号在图注中强调\n3.
保持真实性与准确性:\n - 不要使用笼统的描述\n -
描述图像中所有可见的信息,但不要加入没有在图像中出现的内容\n<|vision_start|><|image_pad|><|vision_end|><|im_end|>\n<|im_start|>assistant\n"""
self.image_caption_prompt_en = """<|im_start|>system\nYou are a helpful assistant.<|im_end|>\n<|im_start|>user\n# Image Annotator\nYou are a professional
image annotator. Please write an image caption based on the input image:\n1. Write the caption using natural,
descriptive language without structured formats or rich text.\n2. Enrich caption details by including: \n - Object
attributes, such as quantity, color, shape, size, material, state, position, actions, and so on\n - Vision Relations
between objects, such as spatial relations, functional relations, possessive relations, attachment relations, action
relations, comparative relations, causal relations, and so on\n - Environmental details, such as weather, lighting,
colors, textures, atmosphere, and so on\n - Identify the text clearly visible in the image, without translation or
explanation, and highlight it in the caption with quotation marks\n3. Maintain authenticity and accuracy:\n - Avoid
generalizations\n - Describe all visible information in the image, while do not add information not explicitly shown in
the image\n<|vision_start|><|image_pad|><|vision_end|><|im_end|>\n<|im_start|>assistant\n"""
self.default_sample_size = 128
# Copied from diffusers.pipelines.qwenimage.pipeline_qwenimage.QwenImagePipeline._extract_masked_hidden
def _extract_masked_hidden(self, hidden_states: torch.Tensor, mask: torch.Tensor):
bool_mask = mask.bool()
valid_lengths = bool_mask.sum(dim=1)
selected = hidden_states[bool_mask]
split_result = torch.split(selected, valid_lengths.tolist(), dim=0)
return split_result
def get_image_caption(self, prompt_image, use_en_prompt=True, device=None):
if use_en_prompt:
prompt = self.image_caption_prompt_en
else:
prompt = self.image_caption_prompt_cn
model_inputs = self.vl_processor(
text=prompt,
images=prompt_image,
padding=True,
return_tensors="pt",
).to(device)
with set_forward_context(current_timestep=0, attn_metadata=None):
generated_ids = self.text_encoder.generate(
**model_inputs, max_new_tokens=512
)
generated_ids_trimmed = [
out_ids[len(in_ids) :]
for in_ids, out_ids in zip(model_inputs.input_ids, generated_ids)
]
output_text = self.vl_processor.batch_decode(
generated_ids_trimmed,
skip_special_tokens=True,
clean_up_tokenization_spaces=False,
)[0]
return output_text.strip()
def _get_qwen_prompt_embeds(
self,
prompt: Union[str, List[str]] = None,
device: Optional[torch.device] = None,
dtype: Optional[torch.dtype] = None,
):
dtype = dtype or self.text_encoder.dtype
prompt = [prompt] if isinstance(prompt, str) else prompt
template = self.prompt_template_encode
drop_idx = self.prompt_template_encode_start_idx
txt = [template.format(e) for e in prompt]
txt_tokens = self.tokenizer(
txt,
padding=True,
return_tensors="pt",
).to(device)
encoder_hidden_states = self.text_encoder(
input_ids=txt_tokens.input_ids,
attention_mask=txt_tokens.attention_mask,
output_hidden_states=True,
)
hidden_states = encoder_hidden_states.hidden_states[-1]
split_hidden_states = self._extract_masked_hidden(
hidden_states, txt_tokens.attention_mask
)
split_hidden_states = [e[drop_idx:] for e in split_hidden_states]
attn_mask_list = [
torch.ones(e.size(0), dtype=torch.long, device=e.device)
for e in split_hidden_states
]
max_seq_len = max([e.size(0) for e in split_hidden_states])
prompt_embeds = torch.stack(
[
torch.cat([u, u.new_zeros(max_seq_len - u.size(0), u.size(1))])
for u in split_hidden_states
]
)
encoder_attention_mask = torch.stack(
[
torch.cat([u, u.new_zeros(max_seq_len - u.size(0))])
for u in attn_mask_list
]
)
prompt_embeds = prompt_embeds.to(dtype=dtype, device=device)
return prompt_embeds, encoder_attention_mask
@staticmethod
def _pack_latents(latents, batch_size, num_channels_latents, height, width, layers):
latents = latents.view(
batch_size, layers, num_channels_latents, height // 2, 2, width // 2, 2
)
latents = latents.permute(0, 1, 3, 5, 2, 4, 6)
latents = latents.reshape(
batch_size, layers * (height // 2) * (width // 2), num_channels_latents * 4
)
return latents
# Copied from diffusers.pipelines.qwenimage.pipeline_qwenimage.QwenImagePipeline.encode_prompt
def encode_prompt(
self,
prompt: Union[str, List[str]],
device: Optional[torch.device] = None,
num_images_per_prompt: int = 1,
prompt_embeds: Optional[torch.Tensor] = None,
prompt_embeds_mask: Optional[torch.Tensor] = None,
max_sequence_length: int = 1024,
):
r"""
Args:
prompt (`str` or `List[str]`, *optional*):
prompt to be encoded
device: (`torch.device`):
torch device
num_images_per_prompt (`int`):
number of images that should be generated per prompt
prompt_embeds (`torch.Tensor`, *optional*):
Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not
provided, text embeddings will be generated from `prompt` input argument.
"""
prompt = [prompt] if isinstance(prompt, str) else prompt
if prompt_embeds is None:
prompt_embeds, prompt_embeds_mask = self._get_qwen_prompt_embeds(
prompt, device
)
prompt_embeds = prompt_embeds[:, :max_sequence_length]
prompt_embeds_mask = prompt_embeds_mask[:, :max_sequence_length]
_, seq_len, _ = prompt_embeds.shape
prompt_embeds = prompt_embeds.repeat(1, num_images_per_prompt, 1)
prompt_embeds = prompt_embeds.view(num_images_per_prompt, seq_len, -1)
prompt_embeds_mask = prompt_embeds_mask.repeat(1, num_images_per_prompt, 1)
prompt_embeds_mask = prompt_embeds_mask.view(num_images_per_prompt, seq_len)
return prompt_embeds, prompt_embeds_mask
# Copied from diffusers.pipelines.qwenimage.pipeline_qwenimage_edit.QwenImageEditPipeline._encode_vae_image
def _encode_vae_image(self, image: torch.Tensor, generator: torch.Generator):
self.vae = self.vae.to(get_local_torch_device())
if isinstance(generator, list):
image_latents = [
retrieve_latents(
self.vae.encode(image[i : i + 1]),
generator=generator[i],
sample_mode="argmax",
)
for i in range(image.shape[0])
]
image_latents = torch.cat(image_latents, dim=0)
else:
image_latents = retrieve_latents(
self.vae.encode(image), generator=generator, sample_mode="argmax"
)
latents_mean = (
torch.tensor(self.vae.config.latents_mean)
.view(1, self.latent_channels, 1, 1, 1)
.to(image_latents.device, image_latents.dtype)
)
latents_std = (
torch.tensor(self.vae.config.latents_std)
.view(1, self.latent_channels, 1, 1, 1)
.to(image_latents.device, image_latents.dtype)
)
image_latents = (image_latents - latents_mean) / latents_std
self.vae.to("cpu")
return image_latents
def prepare_latents(
self,
image,
batch_size,
num_channels_latents,
height,
width,
layers,
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,
layers + 1,
num_channels_latents,
height,
width,
) ### the generated first image is combined image
image_latents = None
if image is not None:
image = image.to(device=device, dtype=dtype)
if image.shape[1] != self.latent_channels:
image_latents = self._encode_vae_image(image=image, generator=generator)
else:
image_latents = image
if (
batch_size > image_latents.shape[0]
and batch_size % image_latents.shape[0] == 0
):
# expand init_latents for batch_size
additional_image_per_prompt = batch_size // image_latents.shape[0]
image_latents = torch.cat(
[image_latents] * additional_image_per_prompt, dim=0
)
elif (
batch_size > image_latents.shape[0]
and batch_size % image_latents.shape[0] != 0
):
raise ValueError(
f"Cannot duplicate `image` of batch size {image_latents.shape[0]} to {batch_size} text prompts."
)
else:
image_latents = torch.cat([image_latents], dim=0)
image_latent_height, image_latent_width = image_latents.shape[3:]
image_latents = image_latents.permute(
0, 2, 1, 3, 4
) # (b, c, f, h, w) -> (b, f, c, h, w)
image_latents = self._pack_latents(
image_latents,
batch_size,
num_channels_latents,
image_latent_height,
image_latent_width,
1,
)
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."
)
if latents is None:
latents = randn_tensor(
shape, generator=generator, device=device, dtype=dtype
)
latents = self._pack_latents(
latents, batch_size, num_channels_latents, height, width, layers + 1
)
else:
latents = latents.to(device=device, dtype=dtype)
return latents, image_latents
def forward(
self,
batch: Req,
server_args: ServerArgs,
) -> Req:
use_en_prompt = True
device = get_local_torch_device()
layers = batch.num_frames
num_inference_steps = batch.num_inference_steps
generator = batch.generator
assert batch.image_path is not None
image = load_image(batch.image_path[0])
image = image.convert("RGBA")
image_size = image.size
resolution = 640 # TODO: support user-specified resolution
calculated_width, calculated_height = calculate_dimensions(
resolution * resolution, image_size[0] / image_size[1]
)
height = calculated_height
width = calculated_width
multiple_of = self.vae_scale_factor * 2
width = width // multiple_of * multiple_of
height = height // multiple_of * multiple_of
# if image is not None and not (isinstance(image, torch.Tensor) and image.size(1) == self.latent_channels):
image = self.image_processor.resize(image, calculated_height, calculated_width)
prompt_image = image
image = self.image_processor.preprocess(
image, calculated_height, calculated_width
)
image = image.unsqueeze(2)
image = image.to(dtype=torch.bfloat16)
prompt = self.get_image_caption(
prompt_image, use_en_prompt=use_en_prompt, device=device
)
prompt_embeds, prompt_embeds_mask = self.encode_prompt(
prompt=prompt,
device=device,
)
negative_prompt_embeds, negative_prompt_embeds_mask = self.encode_prompt(
prompt=batch.negative_prompt,
device=device,
)
num_channels_latents = self.transformer.config.in_channels // 4
latents, image_latents = self.prepare_latents(
image,
1,
num_channels_latents,
height,
width,
layers,
prompt_embeds.dtype,
device,
generator,
)
img_shapes = [
[
*[
(
1,
height // self.vae_scale_factor // 2,
width // self.vae_scale_factor // 2,
)
for _ in range(layers + 1)
],
(
1,
calculated_height // self.vae_scale_factor // 2,
calculated_width // self.vae_scale_factor // 2,
),
]
]
# 5. Prepare timesteps
sigmas = np.linspace(1.0, 0, num_inference_steps + 1)[:-1]
image_seq_len = latents.shape[1]
base_seqlen = 256 * 256 / 16 / 16
mu = (image_latents.shape[1] / base_seqlen) ** 0.5
timesteps, num_inference_steps = retrieve_timesteps(
self.scheduler,
num_inference_steps,
device,
sigmas=sigmas,
mu=mu,
)
txt_seq_lens = (
prompt_embeds_mask.sum(dim=1).tolist()
if prompt_embeds_mask is not None
else None
)
negative_txt_seq_lens = (
negative_prompt_embeds_mask.sum(dim=1).tolist()
if negative_prompt_embeds_mask is not None
else None
)
is_rgb = torch.tensor([0]).to(device=device, dtype=torch.long)
batch.prompt_embeds = [prompt_embeds]
batch.prompt_embeds_mask = [prompt_embeds_mask]
batch.negative_prompt_embeds = [negative_prompt_embeds]
batch.negative_prompt_embeds_mask = [negative_prompt_embeds_mask]
batch.latents = latents
batch.image_latent = image_latents
batch.num_inference_steps = num_inference_steps
batch.sigmas = sigmas.tolist() # Convert numpy array to list for validation
batch.generator = torch.manual_seed(0)
batch.original_condition_image_size = image_size
batch.raw_latent_shape = latents.shape
batch.txt_seq_lens = txt_seq_lens
batch.img_shapes = img_shapes
return batch

View File

@@ -445,6 +445,7 @@ class QwenImageEncoder3d(nn.Module):
temperal_downsample=[True, True, False],
dropout=0.0,
non_linearity: str = "silu",
input_channels: int = 3,
):
super().__init__()
# dim = config.arch_config.dim
@@ -468,7 +469,7 @@ class QwenImageEncoder3d(nn.Module):
scale = 1.0
# init block
self.conv_in = QwenImageCausalConv3d(3, dims[0], 3, padding=1)
self.conv_in = QwenImageCausalConv3d(input_channels, dims[0], 3, padding=1)
# downsample blocks
self.down_blocks = nn.ModuleList([])
@@ -649,6 +650,7 @@ class QwenImageDecoder3d(nn.Module):
temperal_upsample=[False, True, True],
dropout=0.0,
non_linearity: str = "silu",
input_channels=3,
):
super().__init__()
self.dim = dim
@@ -701,7 +703,7 @@ class QwenImageDecoder3d(nn.Module):
# output blocks
self.norm_out = QwenImageRMS_norm(out_dim, images=False)
self.conv_out = QwenImageCausalConv3d(out_dim, 3, 3, padding=1)
self.conv_out = QwenImageCausalConv3d(out_dim, input_channels, 3, padding=1)
self.gradient_checkpointing = False
@@ -783,15 +785,19 @@ class AutoencoderKLQwenImage(nn.Module):
self.z_dim = z_dim
self.temperal_downsample = temperal_downsample
self.temperal_upsample = temperal_downsample[::-1]
self.input_channels = config.arch_config.input_channels
self.latents_mean = config.arch_config.latents_mean
self.config = config.arch_config
self.encoder = QwenImageEncoder3d(
base_dim, z_dim * 2, dim_mult, num_res_blocks, attn_scales, self.temperal_downsample, dropout
base_dim, z_dim * 2, dim_mult, num_res_blocks, attn_scales, self.temperal_downsample, dropout, input_channels=self.input_channels
)
self.quant_conv = QwenImageCausalConv3d(z_dim * 2, z_dim * 2, 1)
self.post_quant_conv = QwenImageCausalConv3d(z_dim, z_dim, 1)
self.decoder = QwenImageDecoder3d(
base_dim, z_dim, dim_mult, num_res_blocks, attn_scales, self.temperal_upsample, dropout
base_dim, z_dim, dim_mult, num_res_blocks, attn_scales, self.temperal_upsample, dropout, input_channels=self.input_channels
)
self.spatial_compression_ratio = 2 ** len(self.temperal_downsample)

View File

@@ -3,6 +3,9 @@
# SPDX-License-Identifier: Apache-2.0
from diffusers.image_processor import VaeImageProcessor
from sglang.multimodal_gen.runtime.models.model_stages.qwen_image_layered import (
QwenImageLayeredBeforeDenoisingStage,
)
from sglang.multimodal_gen.runtime.pipelines_core import LoRAPipeline
from sglang.multimodal_gen.runtime.pipelines_core.composed_pipeline_base import (
ComposedPipelineBase,
@@ -196,4 +199,62 @@ class QwenImageEditPlusPipeline(QwenImageEditPipeline):
pipeline_name = "QwenImageEditPlusPipeline"
EntryClass = [QwenImagePipeline, QwenImageEditPipeline, QwenImageEditPlusPipeline]
def prepare_mu_layered(batch: Req, server_args: ServerArgs):
base_seqlen = 256 * 256 / 16 / 16
mu = (batch.image_latent.shape[1] / base_seqlen) ** 0.5
return "mu", mu
class QwenImageLayeredPipeline(QwenImageEditPipeline):
pipeline_name = "QwenImageLayeredPipeline"
_required_config_modules = [
"vae",
"tokenizer",
"processor",
"transformer",
"scheduler",
]
def create_pipeline_stages(self, server_args: ServerArgs):
"""Set up pipeline stages with proper dependency injection."""
self.add_stage(
stage_name="QwenImageLayeredBeforeDenoisingStage",
stage=QwenImageLayeredBeforeDenoisingStage(
vae=self.get_module("vae"),
tokenizer=self.get_module("tokenizer"),
processor=self.get_module("processor"),
transformer=self.get_module("transformer"),
scheduler=self.get_module("scheduler"),
model_path=self.model_path,
),
)
self.add_stage(
stage_name="timestep_preparation_stage",
stage=TimestepPreparationStage(
scheduler=self.get_module("scheduler"),
prepare_extra_set_timesteps_kwargs=[prepare_mu_layered],
),
)
self.add_stage(
stage_name="denoising_stage",
stage=DenoisingStage(
transformer=self.get_module("transformer"),
scheduler=self.get_module("scheduler"),
),
)
self.add_stage(
stage_name="decoding_stage", stage=DecodingStage(vae=self.get_module("vae"))
)
EntryClass = [
QwenImagePipeline,
QwenImageEditPipeline,
QwenImageEditPlusPipeline,
QwenImageLayeredPipeline,
]