diffusion: support Qwen-Image-Layered (#15817)
Co-authored-by: yhyang201 <yhyang201@gmail.com>
This commit is contained in:
@@ -20,7 +20,7 @@ class QwenImageVAEArchConfig(VAEArchConfig):
|
|||||||
dropout: float = 0.0
|
dropout: float = 0.0
|
||||||
|
|
||||||
is_residual: bool = False
|
is_residual: bool = False
|
||||||
in_channels: int = 3
|
input_channels: int = 3
|
||||||
out_channels: int = 3
|
out_channels: int = 3
|
||||||
patch_size: int | None = None
|
patch_size: int | None = None
|
||||||
scale_factor_temporal: int = 4
|
scale_factor_temporal: int = 4
|
||||||
|
|||||||
@@ -299,6 +299,9 @@ class PipelineConfig:
|
|||||||
|
|
||||||
return shape
|
return shape
|
||||||
|
|
||||||
|
def allow_set_num_frames(self):
|
||||||
|
return False
|
||||||
|
|
||||||
def get_decode_scale_and_shift(self, device, dtype, vae):
|
def get_decode_scale_and_shift(self, device, dtype, vae):
|
||||||
vae_arch_config = self.vae_config.arch_config
|
vae_arch_config = self.vae_config.arch_config
|
||||||
scaling_factor = getattr(vae_arch_config, "scaling_factor", None)
|
scaling_factor = getattr(vae_arch_config, "scaling_factor", None)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from sglang.multimodal_gen.configs.models.vaes.qwenimage import QwenImageVAEConf
|
|||||||
from sglang.multimodal_gen.configs.pipeline_configs.base import (
|
from sglang.multimodal_gen.configs.pipeline_configs.base import (
|
||||||
ImagePipelineConfig,
|
ImagePipelineConfig,
|
||||||
ModelTaskType,
|
ModelTaskType,
|
||||||
|
maybe_unpad_latents,
|
||||||
shard_rotary_emb_for_sp,
|
shard_rotary_emb_for_sp,
|
||||||
)
|
)
|
||||||
from sglang.multimodal_gen.runtime.models.vision_utils import resize
|
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)),
|
"freqs_cis": ((img_cos, img_sin), (txt_cos, txt_sin)),
|
||||||
"img_shapes": img_shapes,
|
"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
|
||||||
|
|||||||
@@ -21,3 +21,17 @@ class QwenImageEditPlusSamplingParams(QwenImageSamplingParams):
|
|||||||
guidance_scale: float = 4.0
|
guidance_scale: float = 4.0
|
||||||
# true_cfg_scale: float = 4.0
|
# true_cfg_scale: float = 4.0
|
||||||
num_inference_steps: int = 40
|
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
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ class DataType(Enum):
|
|||||||
|
|
||||||
def get_default_extension(self) -> str:
|
def get_default_extension(self) -> str:
|
||||||
if self == DataType.IMAGE:
|
if self == DataType.IMAGE:
|
||||||
return "jpg"
|
return "png"
|
||||||
else:
|
else:
|
||||||
return "mp4"
|
return "mp4"
|
||||||
|
|
||||||
@@ -279,8 +279,10 @@ class SamplingParams:
|
|||||||
|
|
||||||
if pipeline_config.task_type.is_image_gen():
|
if pipeline_config.task_type.is_image_gen():
|
||||||
# settle num_frames
|
# settle num_frames
|
||||||
|
if not server_args.pipeline_config.allow_set_num_frames():
|
||||||
logger.debug(f"num_frames set to 1 for image generation model")
|
logger.debug(f"num_frames set to 1 for image generation model")
|
||||||
self.num_frames = 1
|
self.num_frames = 1
|
||||||
|
|
||||||
elif self.adjust_frames:
|
elif self.adjust_frames:
|
||||||
# NOTE: We must apply adjust_num_frames BEFORE the SP alignment logic below.
|
# 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
|
# If we apply it after, adjust_num_frames might modify the frame count
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ from sglang.multimodal_gen.configs.pipeline_configs.flux import Flux2PipelineCon
|
|||||||
from sglang.multimodal_gen.configs.pipeline_configs.qwen_image import (
|
from sglang.multimodal_gen.configs.pipeline_configs.qwen_image import (
|
||||||
QwenImageEditPipelineConfig,
|
QwenImageEditPipelineConfig,
|
||||||
QwenImageEditPlusPipelineConfig,
|
QwenImageEditPlusPipelineConfig,
|
||||||
|
QwenImageLayeredPipelineConfig,
|
||||||
QwenImagePipelineConfig,
|
QwenImagePipelineConfig,
|
||||||
)
|
)
|
||||||
from sglang.multimodal_gen.configs.pipeline_configs.wan import (
|
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 (
|
from sglang.multimodal_gen.configs.sample.qwenimage import (
|
||||||
QwenImageEditPlusSamplingParams,
|
QwenImageEditPlusSamplingParams,
|
||||||
|
QwenImageLayeredSamplingParams,
|
||||||
QwenImageSamplingParams,
|
QwenImageSamplingParams,
|
||||||
)
|
)
|
||||||
from sglang.multimodal_gen.configs.sample.stepvideo import StepVideoT2VSamplingParams
|
from sglang.multimodal_gen.configs.sample.stepvideo import StepVideoT2VSamplingParams
|
||||||
@@ -443,5 +445,11 @@ def _register_configs():
|
|||||||
hf_model_paths=["Qwen/Qwen-Image-Edit-2511"],
|
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()
|
_register_configs()
|
||||||
|
|||||||
@@ -183,14 +183,13 @@ class DiffGenerator:
|
|||||||
raise ValueError(f"No prompts found in file: {prompt_txt_path}")
|
raise ValueError(f"No prompts found in file: {prompt_txt_path}")
|
||||||
|
|
||||||
logger.info("Found %d prompts in %s", len(prompts), 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):
|
if isinstance(prompt, str):
|
||||||
prompts.append(prompt)
|
prompts.append(prompt)
|
||||||
elif isinstance(prompt, list):
|
elif isinstance(prompt, list):
|
||||||
prompts.extend(prompt)
|
prompts.extend(prompt)
|
||||||
else:
|
|
||||||
raise ValueError("Either prompt or prompt_txt must be provided")
|
|
||||||
|
|
||||||
sampling_params = SamplingParams.from_user_sampling_params_args(
|
sampling_params = SamplingParams.from_user_sampling_params_args(
|
||||||
self.server_args.model_path,
|
self.server_args.model_path,
|
||||||
server_args=self.server_args,
|
server_args=self.server_args,
|
||||||
|
|||||||
@@ -93,6 +93,15 @@ def post_process_sample(
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
quality = 75
|
quality = 75
|
||||||
|
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)
|
imageio.imwrite(save_file_path, frames[0], quality=quality)
|
||||||
logger.info(f"Output saved to {CYAN}{save_file_path}{RESET}")
|
logger.info(f"Output saved to {CYAN}{save_file_path}{RESET}")
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ def _get_qkv_projections(
|
|||||||
|
|
||||||
|
|
||||||
class QwenTimestepProjEmbeddings(nn.Module):
|
class QwenTimestepProjEmbeddings(nn.Module):
|
||||||
def __init__(self, embedding_dim):
|
def __init__(self, embedding_dim, use_additional_t_cond=False):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
self.time_proj = Timesteps(
|
self.time_proj = Timesteps(
|
||||||
@@ -53,14 +53,25 @@ class QwenTimestepProjEmbeddings(nn.Module):
|
|||||||
self.timestep_embedder = TimestepEmbedding(
|
self.timestep_embedder = TimestepEmbedding(
|
||||||
in_channels=256, time_embed_dim=embedding_dim
|
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_proj = self.time_proj(timestep)
|
||||||
timesteps_emb = self.timestep_embedder(
|
timesteps_emb = self.timestep_embedder(
|
||||||
timesteps_proj.to(dtype=hidden_states.dtype)
|
timesteps_proj.to(dtype=hidden_states.dtype)
|
||||||
) # (N, D)
|
) # (N, D)
|
||||||
|
|
||||||
conditioning = timesteps_emb
|
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
|
return conditioning
|
||||||
|
|
||||||
@@ -232,6 +243,200 @@ class QwenEmbedRope(nn.Module):
|
|||||||
return freqs.clone().contiguous()
|
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):
|
class QwenImageCrossAttention(nn.Module):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -553,16 +758,30 @@ class QwenImageTransformer2DModel(CachableDiT):
|
|||||||
num_attention_heads = config.arch_config.num_attention_heads
|
num_attention_heads = config.arch_config.num_attention_heads
|
||||||
joint_attention_dim = config.arch_config.joint_attention_dim
|
joint_attention_dim = config.arch_config.joint_attention_dim
|
||||||
axes_dims_rope = config.arch_config.axes_dims_rope
|
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.out_channels = out_channels or in_channels
|
||||||
self.inner_dim = num_attention_heads * attention_head_dim
|
self.inner_dim = num_attention_heads * attention_head_dim
|
||||||
self.zero_cond_t = zero_cond_t
|
|
||||||
|
|
||||||
|
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(
|
self.rotary_emb = QwenEmbedRope(
|
||||||
theta=10000, axes_dim=list(axes_dims_rope), scale_rope=True
|
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)
|
self.time_text_embed = QwenTimestepProjEmbeddings(
|
||||||
|
embedding_dim=self.inner_dim,
|
||||||
|
use_additional_t_cond=self.use_additional_t_cond,
|
||||||
|
)
|
||||||
|
|
||||||
self.txt_norm = RMSNorm(joint_attention_dim, eps=1e-6)
|
self.txt_norm = RMSNorm(joint_attention_dim, eps=1e-6)
|
||||||
|
|
||||||
@@ -575,7 +794,7 @@ class QwenImageTransformer2DModel(CachableDiT):
|
|||||||
dim=self.inner_dim,
|
dim=self.inner_dim,
|
||||||
num_attention_heads=num_attention_heads,
|
num_attention_heads=num_attention_heads,
|
||||||
attention_head_dim=attention_head_dim,
|
attention_head_dim=attention_head_dim,
|
||||||
zero_cond_t=zero_cond_t,
|
zero_cond_t=self.zero_cond_t,
|
||||||
)
|
)
|
||||||
for _ in range(num_layers)
|
for _ in range(num_layers)
|
||||||
]
|
]
|
||||||
@@ -597,6 +816,7 @@ class QwenImageTransformer2DModel(CachableDiT):
|
|||||||
img_shapes: Optional[List[Tuple[int, int, int]]] = None,
|
img_shapes: Optional[List[Tuple[int, int, int]]] = None,
|
||||||
txt_seq_lens: Optional[List[int]] = None,
|
txt_seq_lens: Optional[List[int]] = None,
|
||||||
freqs_cis: tuple[torch.Tensor, torch.Tensor] = 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
|
guidance: torch.Tensor = None, # TODO: this should probably be removed
|
||||||
attention_kwargs: Optional[Dict[str, Any]] = None,
|
attention_kwargs: Optional[Dict[str, Any]] = None,
|
||||||
controlnet_block_samples=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_norm(encoder_hidden_states)
|
||||||
encoder_hidden_states = self.txt_in(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
|
image_rotary_emb = freqs_cis
|
||||||
for index_block, block in enumerate(self.transformer_blocks):
|
for index_block, block in enumerate(self.transformer_blocks):
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -445,6 +445,7 @@ class QwenImageEncoder3d(nn.Module):
|
|||||||
temperal_downsample=[True, True, False],
|
temperal_downsample=[True, True, False],
|
||||||
dropout=0.0,
|
dropout=0.0,
|
||||||
non_linearity: str = "silu",
|
non_linearity: str = "silu",
|
||||||
|
input_channels: int = 3,
|
||||||
):
|
):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
# dim = config.arch_config.dim
|
# dim = config.arch_config.dim
|
||||||
@@ -468,7 +469,7 @@ class QwenImageEncoder3d(nn.Module):
|
|||||||
scale = 1.0
|
scale = 1.0
|
||||||
|
|
||||||
# init block
|
# 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
|
# downsample blocks
|
||||||
self.down_blocks = nn.ModuleList([])
|
self.down_blocks = nn.ModuleList([])
|
||||||
@@ -649,6 +650,7 @@ class QwenImageDecoder3d(nn.Module):
|
|||||||
temperal_upsample=[False, True, True],
|
temperal_upsample=[False, True, True],
|
||||||
dropout=0.0,
|
dropout=0.0,
|
||||||
non_linearity: str = "silu",
|
non_linearity: str = "silu",
|
||||||
|
input_channels=3,
|
||||||
):
|
):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.dim = dim
|
self.dim = dim
|
||||||
@@ -701,7 +703,7 @@ class QwenImageDecoder3d(nn.Module):
|
|||||||
|
|
||||||
# output blocks
|
# output blocks
|
||||||
self.norm_out = QwenImageRMS_norm(out_dim, images=False)
|
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
|
self.gradient_checkpointing = False
|
||||||
|
|
||||||
@@ -783,15 +785,19 @@ class AutoencoderKLQwenImage(nn.Module):
|
|||||||
self.z_dim = z_dim
|
self.z_dim = z_dim
|
||||||
self.temperal_downsample = temperal_downsample
|
self.temperal_downsample = temperal_downsample
|
||||||
self.temperal_upsample = temperal_downsample[::-1]
|
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(
|
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.quant_conv = QwenImageCausalConv3d(z_dim * 2, z_dim * 2, 1)
|
||||||
self.post_quant_conv = QwenImageCausalConv3d(z_dim, z_dim, 1)
|
self.post_quant_conv = QwenImageCausalConv3d(z_dim, z_dim, 1)
|
||||||
|
|
||||||
self.decoder = QwenImageDecoder3d(
|
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)
|
self.spatial_compression_ratio = 2 ** len(self.temperal_downsample)
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
from diffusers.image_processor import VaeImageProcessor
|
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 import LoRAPipeline
|
||||||
from sglang.multimodal_gen.runtime.pipelines_core.composed_pipeline_base import (
|
from sglang.multimodal_gen.runtime.pipelines_core.composed_pipeline_base import (
|
||||||
ComposedPipelineBase,
|
ComposedPipelineBase,
|
||||||
@@ -196,4 +199,62 @@ class QwenImageEditPlusPipeline(QwenImageEditPipeline):
|
|||||||
pipeline_name = "QwenImageEditPlusPipeline"
|
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,
|
||||||
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user