Files
vllm-omni/vllm_omni/diffusion/models/hunyuan_image3/pipeline_hunyuan_image3.py
T
2026-05-27 19:18:01 +08:00

1473 lines
65 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import logging
from collections.abc import Iterable
from typing import Any
import numpy as np
import torch
import torch.nn as nn
from diffusers.schedulers.scheduling_flow_match_euler_discrete import (
FlowMatchEulerDiscreteScheduler,
)
from PIL import Image as PILImage
from transformers.generation.configuration_utils import GenerationConfig
from transformers.generation.utils import ALL_CACHE_NAMES, GenerationMixin
from transformers.utils.generic import ModelOutput
from vllm.config.vllm import get_current_vllm_config
from vllm.model_executor.models.utils import AutoWeightsLoader, WeightsMapper
from vllm.transformers_utils.config import get_config
from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig
from vllm_omni.diffusion.distributed.utils import get_local_device
from vllm_omni.diffusion.model_loader.diffusers_loader import DiffusersPipelineLoader
from vllm_omni.diffusion.models.interface import SupportImageInput
from vllm_omni.diffusion.profiler.diffusion_pipeline_profiler import (
DiffusionPipelineProfilerMixin,
)
from vllm_omni.diffusion.request import OmniDiffusionRequest
from vllm_omni.inputs.data import OmniTextPrompt
from vllm_omni.model_executor.models.hunyuan_image3.siglip2 import Siglip2VisionTransformer
from .hunyuan_image3_tokenizer import TokenizerWrapper
from .hunyuan_image3_transformer import (
CausalMMOutputWithPast,
HunyuanImage3ImageProcessor,
HunyuanImage3Model,
HunyuanImage3PreTrainedModel,
HunyuanImage3Text2ImagePipeline,
ImageInfo,
JointImageInfo,
LightProjector,
TimestepEmbedder,
UNetDown,
UNetUp,
build_batch_2d_rope,
real_batched_index_select,
)
from .system_prompt import get_system_prompt
logger = logging.getLogger(__name__)
BatchRaggedImages = torch.Tensor | list[torch.Tensor | list[torch.Tensor]]
BatchRaggedTensor = torch.Tensor | list[torch.Tensor]
def default(val, d):
return val if val is not None else d
def to_device(data, device):
if device is None:
return data
if isinstance(data, torch.Tensor):
return data.to(device)
elif isinstance(data, list):
return [to_device(x, device) for x in data]
else:
return data
def _to_pil_image(image: Any) -> PILImage.Image:
if isinstance(image, PILImage.Image):
return image
if isinstance(image, str):
return PILImage.open(image)
if isinstance(image, np.ndarray):
array = image
if array.dtype != np.uint8:
if np.issubdtype(array.dtype, np.floating):
if float(np.min(array)) < 0.0:
array = (np.clip(array, -1.0, 1.0) + 1.0) / 2.0
if float(np.max(array)) <= 1.0:
array = array * 255.0
array = np.clip(array, 0, 255).astype(np.uint8)
if array.ndim == 3 and array.shape[0] in (1, 3, 4):
array = np.transpose(array, (1, 2, 0))
return PILImage.fromarray(array)
if isinstance(image, torch.Tensor):
tensor = image.detach().cpu()
if tensor.ndim == 4:
if tensor.shape[0] != 1:
raise ValueError(f"Only a single image tensor is supported, but got shape {tuple(tensor.shape)}.")
tensor = tensor.squeeze(0)
if tensor.ndim == 3 and tensor.shape[0] in (1, 3, 4):
tensor = tensor.permute(1, 2, 0)
if tensor.dtype.is_floating_point:
if float(tensor.min()) < 0.0:
tensor = (tensor.clamp(-1.0, 1.0) + 1.0) / 2.0
if float(tensor.max()) > 1.0:
tensor = tensor / 255.0
tensor = (tensor.clamp(0.0, 1.0) * 255.0).to(torch.uint8)
else:
tensor = tensor.to(torch.uint8)
return PILImage.fromarray(tensor.numpy())
raise TypeError(f"Unsupported image input type: {type(image)}")
def _resize_and_crop_center(image: PILImage.Image, target_width: int, target_height: int) -> PILImage.Image:
# Mirrors HunyuanImage3Processor._resize_and_crop in
# vllm_omni.model_executor.models.hunyuan_image3.hunyuan_image3 so the AR
# and DiT stages preprocess condition images identically.
tw, th = target_width, target_height
w, h = image.size
tr = th / tw
r = h / w
if r < tr:
resize_height = th
resize_width = int(round(th / h * w))
else:
resize_width = tw
resize_height = int(round(tw / w * h))
resized = image.resize((resize_width, resize_height), PILImage.Resampling.LANCZOS)
crop_top = int(round((resize_height - th) / 2.0))
crop_left = int(round((resize_width - tw) / 2.0))
return resized.crop((crop_left, crop_top, crop_left + tw, crop_top + th))
def _to_python_scalar(value: Any) -> Any:
if isinstance(value, np.generic):
return value.item()
return value
def _image_info_to_payload(image_info: ImageInfo) -> dict[str, Any]:
return {
"image_type": image_info.image_type,
"image_tensor": image_info.image_tensor,
"image_width": _to_python_scalar(image_info.image_width),
"image_height": _to_python_scalar(image_info.image_height),
"token_width": _to_python_scalar(image_info.token_width),
"token_height": _to_python_scalar(image_info.token_height),
"image_token_length": _to_python_scalar(image_info.image_token_length),
"base_size": _to_python_scalar(image_info.base_size),
"ratio_index": _to_python_scalar(image_info.ratio_index),
"add_timestep_token": image_info.add_timestep_token,
"add_guidance_token": image_info.add_guidance_token,
"use_front_boi_token": image_info.use_front_boi_token,
"add_image_shape_token": image_info.add_image_shape_token,
}
def _to_tensor_if_needed(value: Any) -> Any:
if isinstance(value, np.generic):
return value.item()
if isinstance(value, list):
return torch.tensor(value)
return value
def _image_info_from_payload(payload: dict[str, Any]) -> ImageInfo:
return ImageInfo(
image_type=payload.get("image_type"),
image_tensor=_to_tensor_if_needed(payload.get("image_tensor")),
image_width=payload.get("image_width"),
image_height=payload.get("image_height"),
token_width=payload.get("token_width"),
token_height=payload.get("token_height"),
image_token_length=payload.get("image_token_length"),
base_size=payload.get("base_size"),
ratio_index=payload.get("ratio_index"),
add_timestep_token=payload.get("add_timestep_token", True),
add_guidance_token=payload.get("add_guidance_token", False),
use_front_boi_token=payload.get("use_front_boi_token", True),
add_image_shape_token=payload.get("add_image_shape_token", True),
)
def _joint_image_info_to_payload(joint_image_info: JointImageInfo) -> dict[str, Any]:
return {
"type": "joint_image_info",
"vae_image_info": _image_info_to_payload(joint_image_info.vae_image_info),
"vision_image_info": _image_info_to_payload(joint_image_info.vision_image_info),
"vision_encoder_kwargs": joint_image_info.vision_encoder_kwargs,
}
def _joint_image_info_from_payload(payload: Any) -> JointImageInfo:
if isinstance(payload, JointImageInfo):
return payload
if not isinstance(payload, dict):
raise TypeError(f"Expected dict or JointImageInfo for conditional image payload, got {type(payload)}.")
vae_image_info = _image_info_from_payload(payload["vae_image_info"])
vision_image_info = _image_info_from_payload(payload["vision_image_info"])
vision_encoder_kwargs = payload.get("vision_encoder_kwargs") or {}
if isinstance(vision_encoder_kwargs, dict):
vision_encoder_kwargs = {key: _to_tensor_if_needed(value) for key, value in vision_encoder_kwargs.items()}
return JointImageInfo(
vae_image_info=vae_image_info,
vision_image_info=vision_image_info,
vision_encoder_kwargs=vision_encoder_kwargs,
)
def get_hunyuan_image_3_pre_process_func(od_config: OmniDiffusionConfig):
hf_config = get_config(od_config.model, trust_remote_code=True)
image_processor = HunyuanImage3ImageProcessor(hf_config)
vae_h_factor = hf_config.vae_downsample_factor[0] * hf_config.patch_size
vae_w_factor = hf_config.vae_downsample_factor[1] * hf_config.patch_size
vit_patch_size = getattr(image_processor.vision_encoder_processor, "patch_size", 1)
if isinstance(vit_patch_size, tuple | list):
vit_patch_size = int(vit_patch_size[0])
def _build_cond_joint_image(raw_image: Any) -> dict[str, Any]:
pil_image = _to_pil_image(raw_image).convert("RGB")
orig_width, orig_height = pil_image.size
target_width, target_height = image_processor.reso_group.get_target_size(orig_width, orig_height)
target_width = int(target_width)
target_height = int(target_height)
vae_input = _resize_and_crop_center(pil_image, target_width, target_height)
vae_tensor = image_processor.vae_processor(vae_input)
base_size, ratio_idx = image_processor.reso_group.get_base_size_and_ratio_index(orig_width, orig_height)
base_size = int(base_size)
ratio_idx = int(ratio_idx)
vae_info = ImageInfo(
image_type="vae",
image_tensor=vae_tensor,
image_width=target_width,
image_height=target_height,
token_width=target_width // vae_w_factor,
token_height=target_height // vae_h_factor,
base_size=base_size,
ratio_index=ratio_idx,
)
vit_inputs = image_processor.vision_encoder_processor(pil_image, return_tensors="pt")
vit_tensor = vit_inputs["pixel_values"]
spatial_shapes = vit_inputs["spatial_shapes"].squeeze(0)
pixel_attention_mask = vit_inputs["pixel_attention_mask"].squeeze(0)
vit_token_h = int(spatial_shapes[0].item())
vit_token_w = int(spatial_shapes[1].item())
vit_info = ImageInfo(
image_type="siglip2",
image_tensor=vit_tensor,
image_width=vit_token_w * vit_patch_size,
image_height=vit_token_h * vit_patch_size,
token_width=vit_token_w,
token_height=vit_token_h,
image_token_length=int(vit_tensor.shape[1]),
)
return _joint_image_info_to_payload(
JointImageInfo(
vae_image_info=vae_info,
vision_image_info=vit_info,
vision_encoder_kwargs={
"spatial_shapes": spatial_shapes,
"pixel_attention_mask": pixel_attention_mask,
},
)
)
def pre_process_func(request: OmniDiffusionRequest):
for i, prompt in enumerate(request.prompts):
if isinstance(prompt, str):
prompt = OmniTextPrompt(prompt=prompt)
if "additional_information" not in prompt:
prompt["additional_information"] = {}
multi_modal_data = prompt.get("multi_modal_data") or {}
raw_images = multi_modal_data.get("image")
if raw_images is None:
raw_images = prompt.get("pil_image")
has_images = raw_images is not None and (not isinstance(raw_images, list) or len(raw_images) > 0)
if has_images:
image_list = raw_images if isinstance(raw_images, list) else [raw_images]
cond_image_infos = [_build_cond_joint_image(image) for image in image_list]
prompt["additional_information"]["batch_cond_image_info"] = cond_image_infos
bridge_h = prompt.get("height") if isinstance(prompt, dict) else None
bridge_w = prompt.get("width") if isinstance(prompt, dict) else None
first_image_w, first_image_h = _to_pil_image(image_list[0]).size
if request.sampling_params.width is None:
request.sampling_params.width = int(bridge_w or first_image_w)
if request.sampling_params.height is None:
request.sampling_params.height = int(bridge_h or first_image_h)
request.prompts[i] = prompt
return request
return pre_process_func
class HunyuanImage3Pipeline(
HunyuanImage3PreTrainedModel,
GenerationMixin,
SupportImageInput,
DiffusionPipelineProfilerMixin,
):
support_image_input = True
hf_to_vllm_mapper = WeightsMapper(
orig_to_new_prefix={
"model.": "",
},
orig_to_new_substr={
"mlp.gate.wg.": "mlp.gate.",
"gate_and_up_proj.": "gate_up_proj.",
},
)
_PROFILER_TARGETS = [
"model.forward",
"model.layers[0].forward",
"model.layers[0].self_attn.forward",
"model.layers[0].mlp.forward",
"vae.encode",
"vae.decode",
"patch_embed.forward",
"final_layer.forward",
]
def __init__(self, od_config: OmniDiffusionConfig) -> None:
self.hf_config = get_config(od_config.model, trust_remote_code=True)
super().__init__(self.hf_config)
# update diffusion config
self.generation_config = GenerationConfig.from_pretrained(od_config.model)
self.od_config = od_config
self.weights_sources = [
DiffusersPipelineLoader.ComponentSource(
model_or_path=od_config.model,
subfolder=None,
revision=od_config.revision,
prefix="",
fall_back_to_pt=True,
)
]
quant_config = od_config.quantization_config
self.model = HunyuanImage3Model(self.hf_config, quant_config=quant_config)
self.transformer = self.model
# Lazy import to break circular dependency:
# autoencoder_kl_hunyuan -> hunyuan_image3/__init__ -> pipeline_hunyuan_image3 -> autoencoder_kl_hunyuan
from vllm_omni.diffusion.distributed.autoencoders.autoencoder_kl_hunyuan import ( # noqa: PLC0415
DistributedAutoencoderKLHunyuan,
)
self.vae = DistributedAutoencoderKLHunyuan.from_config(self.hf_config.vae)
self.vae.use_spatial_tiling = self.od_config.vae_use_tiling
self._pipeline = None
self._tkwrapper = TokenizerWrapper(od_config.model)
self.image_processor = HunyuanImage3ImageProcessor(self.hf_config)
self.vision_model = Siglip2VisionTransformer(self.hf_config.vit)
# self.vision_model = vision_model.vision_model
self.vision_aligner = LightProjector(self.hf_config.vit_aligner)
self.timestep_emb = TimestepEmbedder(hidden_size=self.hf_config.hidden_size)
if self.hf_config.img_proj_type != "unet":
raise ValueError(f"Unknown img_proj_type: {self.hf_config.img_proj_type}")
self.patch_embed = UNetDown(
patch_size=self.hf_config.patch_size,
emb_channels=self.hf_config.hidden_size,
in_channels=self.hf_config.vae["latent_channels"],
hidden_channels=self.hf_config.patch_embed_hidden_dim,
out_channels=self.hf_config.hidden_size,
)
self.time_embed = TimestepEmbedder(hidden_size=self.hf_config.hidden_size)
self.final_layer = UNetUp(
patch_size=self.hf_config.patch_size,
emb_channels=self.hf_config.hidden_size,
in_channels=self.hf_config.hidden_size,
hidden_channels=self.hf_config.patch_embed_hidden_dim,
out_channels=self.hf_config.vae["latent_channels"],
out_norm=True,
)
self.time_embed_2 = TimestepEmbedder(hidden_size=self.hf_config.hidden_size)
self.lm_head = nn.Linear(self.hf_config.hidden_size, self.hf_config.vocab_size, bias=False)
self.vllm_config = get_current_vllm_config()
self.post_init()
self.setup_diffusion_pipeline_profiler(
enable_diffusion_pipeline_profiler=self.od_config.enable_diffusion_pipeline_profiler,
)
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
skip_prefixes = ["lm_head."] if self.hf_config.tie_word_embeddings else []
# List of unexpected keywords in weight names
non_model_layer_prefixes = [
"vae",
"vision_model",
"vision_aligner",
"lm_head",
"patch_embed",
"timestep_emb",
"model.wte",
"model.ln_f",
"time_embed",
"time_embed_2",
"final_layer.model",
]
device = get_local_device()
named_modules = dict(self.named_modules())
for prefix in non_model_layer_prefixes:
mod = named_modules.get(prefix)
if mod:
mod.to(device)
unexpected_keywords = [
"guidance_emb",
"timestep_r_emb",
]
skip_prefixes.extend(unexpected_keywords)
loader = AutoWeightsLoader(
self,
skip_prefixes=skip_prefixes,
)
return loader.load_weights(weights)
def prepare_seed(self, seed=None, batch_size=1):
# random seed
if seed is not None:
return [seed + i for i in range(batch_size)]
else:
import random
return [random.randint(0, 2**32 - 1) for _ in range(batch_size)]
@property
def pipeline(self):
if self._pipeline is None:
# shift hard code
self.scheduler = FlowMatchEulerDiscreteScheduler(
num_train_timesteps=1000,
shift=self.generation_config.flow_shift,
use_dynamic_shifting=False,
base_shift=0.5,
max_shift=1.15,
time_shift_type="exponential",
stochastic_sampling=False,
)
self._pipeline = HunyuanImage3Text2ImagePipeline(model=self, scheduler=self.scheduler, vae=self.vae)
return self._pipeline
@staticmethod
def get_pos_emb(custom_pos_emb, position_ids):
cos, sin = custom_pos_emb
cos = real_batched_index_select(cos, dim=1, idx=position_ids)
sin = real_batched_index_select(sin, dim=1, idx=position_ids)
return cos, sin
def instantiate_vae_image_tokens(
self,
x: torch.Tensor,
images: BatchRaggedImages,
ts: BatchRaggedTensor,
image_mask: torch.Tensor,
):
r"""
Instantiate the VAE image embeddings into the input embedding sequence.
Args:
x (`torch.Tensor`):
Input sequence tensor with shape `(batch_size, seq_len, n_embd)`.
images (`BatchRaggedImages`):
Batch of images to embed. Can be:
- A 4-D tensor (batch, channels, height, width)
- A list of 4-D tensors (variable number of images per batch)
- A list of lists of 3-D tensors (ragged batch structure)
ts (`BatchRaggedTensor`, *optional*):
Timestep tensor(s) for conditioning. Can be:
- A 1-D tensor (single timestep per batch)
- A list of 1-D tensors (variable timesteps per batch)
image_mask (`torch.Tensor`, *optional*):
Boolean mask tensor with shape `(batch_size, seq_len)` indicating which positions
should be replaced with image embeddings.
"""
batch_size, seq_len, n_embd = x.shape
if isinstance(images, list):
index = torch.arange(seq_len, device=x.device).unsqueeze(0).repeat(batch_size, 1)
t_emb = []
for i, (image_i, t_i) in enumerate(zip(images, ts)):
if isinstance(image_i, torch.Tensor):
# time_embed needs a 1-D tensor as input
t_i_emb = self.time_embed(t_i)
# n_{i} x one_image_seq_len x n_embd
image_i_seq, _, _ = self.patch_embed(image_i, t_i_emb)
# 1 x (n_{i} * one_image_seq_len)
image_i_scatter_index = index[i : i + 1].masked_select(image_mask[i : i + 1].bool()).reshape(1, -1)
x[i : i + 1].scatter_(
dim=1,
index=image_i_scatter_index.unsqueeze(-1).repeat(1, 1, n_embd),
# 1 x (n_{i} * one_image_seq_len) x n_embd
src=image_i_seq.reshape(1, -1, n_embd), # 1 x (n_{i} * one_image_seq_len) x n_embd
)
t_emb.append(t_i_emb)
elif isinstance(image_i, list):
# time_embed needs a 1-D tensor as input
t_i_emb = self.time_embed(t_i) # n_{i} x d
image_i_seq_list = []
for j in range(len(image_i)):
image_ij = image_i[j]
if image_ij.dim() == 4:
assert image_i[j].shape[0] == 1, "image_i[j] should have a batch dimension of 1"
elif image_ij.dim() == 3:
image_ij = image_ij.unsqueeze(0)
else:
raise ValueError(f"image_i[j] should have 3 or 4 dimensions, got {image_ij.dim()}")
# 1 x one_image_seq_len_{j} x n_embd
image_i_seq_j, _, _ = self.patch_embed(image_ij, t_i_emb[j : j + 1])
image_i_seq_list.append(image_i_seq_j)
# 1 x sum_{j}(one_image_seq_len_{j}) x n_embd
image_i_seq = torch.cat(image_i_seq_list, dim=1)
# 1 x sum_{j}(one_image_seq_len_{j})
image_i_scatter_index = index[i : i + 1].masked_select(image_mask[i : i + 1].bool()).reshape(1, -1)
x[i : i + 1].scatter_(
dim=1,
index=image_i_scatter_index.unsqueeze(-1).repeat(1, 1, n_embd),
# 1 x sum_{j}(one_image_seq_len_{j}) x n_embd
src=image_i_seq.reshape(1, -1, n_embd), # 1 x sum_{j}(one_image_seq_len_{j}) x n_embd
)
t_emb.append(t_i_emb)
else:
raise TypeError(f"image_i should be a torch.Tensor or a list, got {type(image_i)}")
token_h, token_w = None, None
else:
# images is a 4-D tensor
batch_size, seq_len, n_embd = x.shape
index = torch.arange(seq_len, device=x.device).unsqueeze(0).repeat(batch_size, 1)
t_emb = self.time_embed(ts)
image_seq, token_h, token_w = self.patch_embed(images, t_emb)
image_scatter_index = index.masked_select(image_mask.bool()).reshape(batch_size, -1)
x.scatter_(
dim=1,
index=image_scatter_index.unsqueeze(-1).repeat(1, 1, n_embd),
src=image_seq,
)
return x, token_h, token_w
def instantiate_timestep_tokens(
self,
x: torch.Tensor,
t: BatchRaggedTensor,
timestep_scatter_index: BatchRaggedTensor,
):
batch_size, seq_len, n_embd = x.shape
# `_encode_cond_image` returns `t` as list[Tensor] for the
# multi-image branch (outer length = batch_size, currently fixed
# at 1 by the stage runtime `max_batch_size`); flatten to a Tensor
# before reshape.
if isinstance(t, list):
t = torch.cat([ti.reshape(-1) for ti in t], dim=0)
timestep_scatter_src = self.timestep_emb(t.reshape(-1)).reshape(batch_size, -1, n_embd)
x.scatter_(
dim=1,
index=timestep_scatter_index.unsqueeze(-1).repeat(1, 1, n_embd),
src=timestep_scatter_src,
)
return x
def instantiate_vit_image_tokens(
self,
x: torch.Tensor,
cond_vit_images: torch.Tensor | list[torch.Tensor],
cond_vit_image_mask: torch.Tensor,
vit_kwargs: dict[str, Any],
):
# 1. Forward the vit encoder and vit aligner to get the vit image embeddings and align them to the
# transformer hidden size
cond_vit_image_embeds = []
for batch_idx, image in enumerate(cond_vit_images):
cur_kwargs = {k: v[batch_idx] for k, v in vit_kwargs.items()}
image_embed = self.vision_model(image, **cur_kwargs).last_hidden_state
image_embed = self.vision_aligner(image_embed)
n, seq_len, dim = image_embed.shape
image_embed = image_embed.reshape(n * seq_len, dim)
cond_vit_image_embeds.append(image_embed)
# 2. Instantiate the vit image embeddings into the input sequence
batch_size, seq_len, n_embd = x.shape
index = torch.arange(seq_len, device=x.device).unsqueeze(0).repeat(batch_size, 1)
for i, (image_embed, mask) in enumerate(zip(cond_vit_image_embeds, cond_vit_image_mask)):
image_scatter_index = index[i : i + 1].masked_select(mask.bool()).reshape(1, -1)
x[i : i + 1].scatter_(
dim=1,
index=image_scatter_index.unsqueeze(-1).repeat(1, 1, n_embd),
src=image_embed.reshape(1, -1, n_embd),
)
return x
def ragged_final_layer(self, x, image_mask, timestep, token_h, token_w, first_step):
bsz, seq_len, n_embd = x.shape
if first_step:
image_output = x.masked_select(image_mask.unsqueeze(-1).bool()).reshape(bsz, -1, n_embd)
else:
image_output = x[:, 1:, :]
timestep_emb = self.time_embed_2(timestep)
pred = self.final_layer(image_output, timestep_emb, token_h, token_w)
return pred
@staticmethod
def build_batch_rope_image_info(output, sections):
rope_image_info = []
for image_slices, sections_i in zip(output.all_image_slices, sections):
image_shapes = []
for section in sections_i:
if "image" in section["type"]:
if isinstance(section["token_height"], list):
assert len(section["token_height"]) == len(section["token_width"]), (
f"token_height and token_width should have the same length, "
f"but got {len(section['token_height'])} and {len(section['token_width'])}"
)
image_shapes.extend(list(zip(section["token_height"], section["token_width"])))
else:
image_shapes.append((section["token_height"], section["token_width"]))
assert len(image_slices) == len(image_shapes), (
f"Size miss matching: Image slices({len(image_slices)}) != image shapes({len(image_shapes)})"
)
rope_image_info.append(list(zip(image_slices, image_shapes)))
return rope_image_info
def vae_encode(self, image, cfg_factor=1):
config = self.vae.config
if image.ndim == 3:
image = image.unsqueeze(0)
if image.ndim == 4:
image = image.unsqueeze(2)
if image.ndim != 5:
raise ValueError(f"Expected image tensor with 3/4/5 dims, got shape {tuple(image.shape)}.")
with torch.autocast(device_type=self.model.device.type, dtype=torch.float16, enabled=True):
vae_encode_result = self.vae.encode(image)
if isinstance(vae_encode_result, torch.Tensor):
latents = vae_encode_result
else:
# Match HunyuanImage-3's cond encode path: sample the
# posterior, but use a fixed generator so repeated online
# requests are deterministic.
_cond_vae_gen = torch.Generator(device=image.device).manual_seed(0)
latents = vae_encode_result.latent_dist.sample(_cond_vae_gen)
if hasattr(config, "shift_factor") and config.shift_factor:
latents.sub_(config.shift_factor)
if hasattr(config, "scaling_factor") and config.scaling_factor:
latents.mul_(config.scaling_factor)
if hasattr(self.vae, "ffactor_temporal"):
assert latents.shape[2] == 1, "latents should have shape [B, C, T, H, W] and T should be 1"
latents = latents.squeeze(2)
# Here we always use t=0 to declare it is a clean conditional image
t = torch.zeros((latents.shape[0],))
if cfg_factor > 1:
t = t.repeat(cfg_factor)
latents = latents.repeat(cfg_factor, 1, 1, 1)
return t, latents
def _encode_cond_image(
self,
batch_cond_image_info_list: list[list[JointImageInfo]],
cfg_factor: int = 1,
):
# VAE encode one by one, as we assume cond images have different sizes
batch_cond_vae_images, batch_cond_t, batch_cond_vit_images = [], [], []
for cond_image_info_list in batch_cond_image_info_list:
cond_vae_image_list, cond_t_list, cond_vit_image_list = [], [], []
for image_info in cond_image_info_list:
cond_t_, cond_vae_image_ = self.vae_encode(
image_info.vae_image_info.image_tensor.to(self.device),
)
cond_vit_image_list.append(image_info.vision_image_info.image_tensor)
cond_vae_image_list.append(cond_vae_image_.squeeze(0))
cond_t_list.append(cond_t_)
batch_cond_vae_images.append(cond_vae_image_list)
batch_cond_t.append(cond_t_list)
batch_cond_vit_images.append(torch.cat(cond_vit_image_list, dim=0))
# If only one cond image for each sample and all have the same size, we can batch them together
# In this case, cond_vae_images is a 4-D tensor.
if all([len(items) == 1 for items in batch_cond_vae_images]) and all(
items[0].shape == batch_cond_vae_images[0][0].shape for items in batch_cond_vae_images
):
cond_vae_images = torch.stack([items[0] for items in batch_cond_vae_images], dim=0)
cond_t = torch.cat([items[0] for items in batch_cond_t], dim=0)
if cfg_factor > 1:
cond_t = cond_t.repeat(cfg_factor)
cond_vae_images = cond_vae_images.repeat(cfg_factor, 1, 1, 1)
else:
# In this case, cond_vae_images is a list of 4-D tensors or a list of lists of 3-D tensors.
cond_t = [torch.cat(item, dim=0) for item in batch_cond_t]
cond_vae_images = []
for items in batch_cond_vae_images:
if all(items[0].shape == item.shape for item in items):
cond_vae_images.append(torch.stack(items, dim=0))
else:
cond_vae_images.append(items)
if cfg_factor > 1:
cond_t = cond_t * cfg_factor
cond_vae_images = cond_vae_images * cfg_factor
if cfg_factor > 1:
batch_cond_vit_images = batch_cond_vit_images * cfg_factor
return cond_vae_images, cond_t, batch_cond_vit_images
@staticmethod
def check_inputs(prompt=None, message_list=None):
if prompt is None and message_list is None:
raise ValueError("Either `prompt` or `message_list` should be provided.")
if prompt is not None and message_list is not None:
raise ValueError("Only one of `prompt` or `message_list` should be provided.")
if prompt is not None:
assert isinstance(prompt, str) or isinstance(prompt, list), (
f"`prompt` should be a string or a list of strings, but got {type(prompt)}."
)
if isinstance(prompt, list):
assert len(prompt) > 0 and all(isinstance(p, str) for p in prompt), (
"`prompt` should be a non-empty list of strings."
)
if message_list is not None:
if not isinstance(message_list, list):
raise ValueError(f"`message_list` should be a list of messages, but got {type(message_list)}.")
assert len(message_list) > 0, "`message_list` should be a non-empty list."
for message in message_list:
assert isinstance(message, list) or isinstance(message, dict), (
f"Each message should be a list of dicts or a dict, but got {type(message)}."
)
@staticmethod
def _normalize_cot_text(cot: str | None) -> str | None:
"""
Ensure cot_text starts with the appropriate opening tag.
AR-generated text may omit the leading <think> or <recaption> tag
(since it was used as a generation trigger). This normalizes the text
so downstream parsing in get_cot_sections works correctly.
"""
if not cot:
return cot
if "</think>" in cot and not cot.startswith("<think>"):
cot = "<think>" + cot
return cot
if "</recaption>" in cot and not cot.startswith("<recaption>"):
cot = "<recaption>" + cot
return cot
return cot
def prepare_model_inputs(
self,
prompt=None,
mode="gen_image",
system_prompt=None,
cot_text=None,
num_inference_steps=50,
guidance_scale=5.0,
image_size="auto",
message_list=None,
device=None,
max_new_tokens=None,
**kwargs,
):
# 1. Sanity check
self.check_inputs(prompt, message_list)
device = default(device, self.device)
# 2. Format inputs
batch_message_list = message_list
batch_prompt = prompt
batch_cot_text = cot_text
batch_system_prompt = system_prompt
batch_gen_image_info = None
batch_cond_image_info = kwargs.pop("batch_cond_image_info", None)
# -- 2.1 message_list
if batch_message_list is not None:
if isinstance(batch_message_list[0], dict):
batch_message_list = [batch_message_list]
batch_size = len(batch_message_list)
batch_gen_image_info = [
[message["content"] for message in message_list_ if message["type"] == "gen_image"]
for message_list_ in batch_message_list
]
# At most one gen_image is allowed for each message_list
batch_gen_image_info = [info[-1] if len(info) > 0 else None for info in batch_gen_image_info]
# Multiple cond images are allowed.
batch_cond_image_info = [
[message["content"] for message in message_list_ if message["type"] == "joint_image"]
for message_list_ in batch_message_list
]
# -- 2.2 Prompt, cot text, system prompt
else:
if isinstance(batch_prompt, str):
batch_prompt = [batch_prompt]
batch_size = len(batch_prompt)
if batch_cot_text is not None:
if isinstance(batch_cot_text, str):
batch_cot_text = [batch_cot_text]
else:
assert isinstance(batch_cot_text, list) and len(batch_cot_text) == batch_size, (
"`cot_text` should be a string or a list of strings with the same length as `prompt`."
)
if batch_system_prompt is not None:
if isinstance(batch_system_prompt, str):
batch_system_prompt = [batch_system_prompt]
else:
assert isinstance(batch_system_prompt, list) and len(batch_system_prompt) == batch_size, (
"`system_prompts` should be a string or a list of strings with the same length as `prompt`."
)
if mode == "gen_image":
batch_gen_image_info = [self.image_processor.build_image_info(image_size) for _ in range(batch_size)]
if batch_cond_image_info is not None:
assert isinstance(batch_cond_image_info, list) and len(batch_cond_image_info) == batch_size, (
"`batch_cond_image_info` should be a list with the same batch size as `prompt`."
)
batch_cond_image_info = [cond if isinstance(cond, list) else [cond] for cond in batch_cond_image_info]
# -- 2.3 seed
generator = kwargs.get("generator", None)
if generator is None:
seeds = self.prepare_seed(seed=kwargs.get("seed"), batch_size=batch_size)
generator = [torch.Generator(self.device).manual_seed(seed) for seed in seeds]
# 3. apply chat template
cfg_factor = {"gen_text": 1, "gen_image": 1 + int(guidance_scale > 1.0)}
bot_task = kwargs.pop("bot_task", "auto")
# If `drop_think` enabled, always drop <think> parts in the context.
drop_think = kwargs.get("drop_think", self.generation_config.drop_think)
# Pull sequence_template from the model's generation_config so the DiT
# text prefix matches how the model was trained (Instruct for the
# HunyuanImage-3.0-Instruct checkpoint). Falling back to "pretrain"
# only if the config does not specify it.
sequence_template = getattr(self.generation_config, "sequence_template", "pretrain")
# Apply batched prompt or batched message_list to build input sequence with associated info.
out = self._tkwrapper.apply_chat_template(
batch_prompt=batch_prompt,
batch_message_list=batch_message_list,
mode=mode,
batch_gen_image_info=batch_gen_image_info,
batch_cond_image_info=batch_cond_image_info,
batch_system_prompt=batch_system_prompt,
batch_cot_text=batch_cot_text,
max_length=kwargs.get("max_length"),
bot_task=bot_task,
image_base_size=self.config.image_base_size,
sequence_template=sequence_template,
cfg_factor=cfg_factor[mode],
drop_think=drop_think,
)
output, sections = out["output"], out["sections"]
# 4. Encode conditional images
if batch_cond_image_info is not None and len(batch_cond_image_info[0]) > 0:
cond_vae_images, cond_timestep, cond_vit_images = self._encode_cond_image(
batch_cond_image_info, cfg_factor[mode]
)
vit_kwargs = {"spatial_shapes": [], "attention_mask": []}
for cond_image_info in batch_cond_image_info:
vit_kwargs["spatial_shapes"].append(
torch.stack([item.vision_encoder_kwargs["spatial_shapes"] for item in cond_image_info])
)
vit_kwargs["attention_mask"].append(
torch.stack([item.vision_encoder_kwargs["pixel_attention_mask"] for item in cond_image_info])
)
if cfg_factor[mode] > 1:
vit_kwargs["spatial_shapes"] = vit_kwargs["spatial_shapes"] * cfg_factor[mode]
vit_kwargs["attention_mask"] = vit_kwargs["attention_mask"] * cfg_factor[mode]
else:
cond_vae_images, cond_timestep, cond_vit_images = None, None, None
vit_kwargs = None
# 5. Build position embeddings
rope_image_info = self.build_batch_rope_image_info(output, sections)
if mode == "gen_text":
seq_len = self.generation_config.max_length
else:
seq_len = output.tokens.shape[1]
cos, sin = build_batch_2d_rope(
image_infos=rope_image_info,
seq_len=seq_len,
n_elem=self.config.attention_head_dim,
device=device,
base=self.config.rope_theta,
)
# 6. Build kv cache
if bot_task == "img_ratio":
max_new_tokens = 1
# 7. Build position ids
batch_input_pos = torch.arange(0, output.tokens.shape[1], dtype=torch.long, device=device)[None].expand(
batch_size * cfg_factor[mode], -1
) # use expand to share indices to save memory
# 8. Build model input kwargs
tkw = self._tkwrapper
if image_size == "auto":
extra_auto_stops = [tkw.special_token_map[f"<img_ratio_{i}>"] for i in range(33)]
else:
extra_auto_stops = [tkw.boi_token_id]
stop_token_id = dict(
auto=[tkw.eos_token_id] + extra_auto_stops,
image=[tkw.eos_token_id],
recaption=[
tkw.end_recaption_token_id,
tkw.end_answer_token_id,
tkw.eos_token_id,
],
think=[
tkw.end_recaption_token_id,
tkw.end_answer_token_id,
tkw.eos_token_id,
],
img_ratio=extra_auto_stops,
)
model_input_kwargs = dict(
input_ids=output.tokens.to(device),
position_ids=batch_input_pos,
past_key_values=None,
custom_pos_emb=(cos, sin),
mode=mode,
num_inference_steps=num_inference_steps,
guidance_scale=guidance_scale,
image_mask=to_device(output.gen_image_mask, device),
gen_timestep_scatter_index=to_device(output.gen_timestep_scatter_index, device),
cond_vae_images=to_device(cond_vae_images, device),
cond_timestep=to_device(cond_timestep, device),
cond_vae_image_mask=to_device(output.cond_vae_image_mask, device),
cond_vit_images=to_device(cond_vit_images, device),
cond_vit_image_mask=to_device(output.cond_vit_image_mask, device),
vit_kwargs={k: to_device(v, self.device) for k, v in vit_kwargs.items()}
if vit_kwargs is not None
else None,
cond_timestep_scatter_index=to_device(output.cond_timestep_scatter_index, device),
# for inner usage
tokenizer_output=output,
batch_gen_image_info=batch_gen_image_info,
generator=generator,
# generation config
eos_token_id=stop_token_id[bot_task],
max_new_tokens=max_new_tokens,
)
return model_input_kwargs
def _prepare_attention_mask_for_generation(
self,
inputs_tensor: torch.Tensor,
generation_config: GenerationConfig,
model_kwargs: dict[str, Any],
) -> torch.Tensor:
# create `4d` bool attention mask (b, 1, seqlen, seqlen) using this implementation to bypass the 2d requirement
# in the `transformers.generation_utils.GenerationMixin.generate`.
# This implementation can handle sequences with text and image modalities, where text tokens use causal
# attention and image tokens use full attention.
bsz, seq_len = inputs_tensor.shape
tokenizer_output = model_kwargs["tokenizer_output"]
batch_image_slices = [
tokenizer_output.joint_image_slices[i] + tokenizer_output.gen_image_slices[i] for i in range(bsz)
]
attention_mask = torch.ones(seq_len, seq_len, dtype=torch.bool).tril(diagonal=0).repeat(bsz, 1, 1)
full_attn_spans: list[list[tuple[int, int]]] = [[] for _ in range(bsz)]
for i in range(bsz):
for j, image_slice in enumerate(batch_image_slices[i]):
attention_mask[i, image_slice, image_slice] = True
start = image_slice.start if image_slice.start is not None else 0
stop = image_slice.stop if image_slice.stop is not None else seq_len
assert start < stop, f"Invalid image slice: {image_slice}"
full_attn_spans[i].append((int(start), int(stop)))
if full_attn_spans[i]:
full_attn_spans[i].sort(key=lambda x: x[0])
attention_mask = attention_mask.unsqueeze(1)
model_kwargs["full_attn_spans"] = full_attn_spans
return attention_mask
def prepare_inputs_for_generation(
self,
input_ids,
past_key_values=None,
attention_mask=None,
inputs_embeds=None,
tokenizer_output=None,
batch_gen_image_info=None,
generator=None,
**kwargs,
):
# if `inputs_embeds` are passed, we only want to use them in the 1st generation step
if inputs_embeds is not None and past_key_values is None:
model_inputs = {"inputs_embeds": inputs_embeds}
else:
if input_ids is not None and input_ids.shape[1] != kwargs["position_ids"].shape[1]: # in decode steps
input_ids = torch.gather(input_ids, dim=1, index=kwargs["position_ids"])
model_inputs = {"input_ids": input_ids}
model_inputs.update(
{
"attention_mask": attention_mask,
"position_ids": kwargs["position_ids"],
"past_key_values": past_key_values,
"use_cache": kwargs.get("use_cache"),
"custom_pos_emb": kwargs["custom_pos_emb"],
"mode": kwargs["mode"],
"images": kwargs.get("images"),
"image_mask": kwargs.get("image_mask"),
"timestep": kwargs.get("timestep"),
"gen_timestep_scatter_index": kwargs.get("gen_timestep_scatter_index"),
"cond_vae_images": kwargs.get("cond_vae_images"),
"cond_timestep": kwargs.get("cond_timestep"),
"cond_vae_image_mask": kwargs.get("cond_vae_image_mask"),
"cond_vit_images": kwargs.get("cond_vit_images"),
"cond_vit_image_mask": kwargs.get("cond_vit_image_mask"),
"vit_kwargs": kwargs.get("vit_kwargs"),
"cond_timestep_scatter_index": kwargs.get("cond_timestep_scatter_index"),
"query_lens": kwargs.get("query_lens"),
"seq_lens": kwargs.get("seq_lens"),
"num_image_tokens": kwargs.get("num_image_tokens"),
"ar_kv_reuse_len": kwargs.get("ar_kv_reuse_len", 0),
"full_attn_spans": kwargs.get("full_attn_spans"),
}
)
return model_inputs
def _update_model_kwargs_for_generation(
self,
outputs: ModelOutput,
model_kwargs: dict[str, Any],
is_encoder_decoder: bool = False,
num_new_tokens: int = 1,
) -> dict[str, Any]:
mode = model_kwargs["mode"]
updated_model_kwargs = {
"mode": mode,
"custom_pos_emb": model_kwargs["custom_pos_emb"],
"num_image_tokens": model_kwargs["num_image_tokens"],
}
if "full_attn_spans" in model_kwargs:
updated_model_kwargs["full_attn_spans"] = model_kwargs["full_attn_spans"]
# update past_key_values keeping its naming used in model code
for possible_cache_name in ALL_CACHE_NAMES:
if possible_cache_name in outputs:
# TODO (joao): remove output/input mismatch when these old models (xlnet, reformer) are deprecated
if possible_cache_name in ("past_buckets_states", "mems"):
cache_name = "past_key_values"
else:
cache_name = possible_cache_name
updated_model_kwargs[cache_name] = getattr(outputs, possible_cache_name)
break
if "tokenizer_output" in model_kwargs:
if mode == "gen_text":
# When enable batching, we use right padding, which requires a real_pos to index the valid
# end position of the sequence. If tokenizer_output in model_kwargs, it means we are in the
# prefill step of generation.
real_pos = to_device(model_kwargs["tokenizer_output"].real_pos, self.device)
updated_model_kwargs["position_ids"] = real_pos
else:
# position ids
image_mask = model_kwargs["image_mask"]
bsz, seq_len = image_mask.shape
offset = model_kwargs.get("ar_kv_reuse_offset", 0) # should be an absolute position.
index = torch.arange(offset, offset + seq_len, device=image_mask.device).unsqueeze(0).repeat(bsz, 1)
position_ids = index.masked_select(image_mask.bool()).reshape(bsz, -1)
timestep_position_ids = index[
torch.arange(bsz), model_kwargs["gen_timestep_scatter_index"][:, -1]
].unsqueeze(-1)
updated_model_kwargs["position_ids"] = torch.cat([timestep_position_ids, position_ids], dim=1)
# attention mask
mask_list = []
current_starts = timestep_position_ids.reshape(-1)
max_current_start = int(current_starts.max().item())
for attention_mask_i, position_ids_i, current_start_i in zip(
model_kwargs["attention_mask"], updated_model_kwargs["position_ids"], current_starts
):
query_mask = torch.index_select(attention_mask_i, dim=1, index=position_ids_i.reshape(-1) - offset)
current_start = int(current_start_i.item())
prefix_mask = torch.index_select(
query_mask,
dim=2,
index=torch.arange(current_start, device=image_mask.device),
)
if current_start < max_current_start:
prefix_pad = query_mask.new_zeros(
query_mask.shape[0],
query_mask.shape[1],
max_current_start - current_start,
)
prefix_mask = torch.cat([prefix_mask, prefix_pad], dim=2)
current_mask = torch.index_select(query_mask, dim=2, index=position_ids_i.reshape(-1))
mask_list.append(torch.cat([prefix_mask, current_mask], dim=2))
attention_mask = torch.stack(mask_list, dim=0)
updated_model_kwargs["attention_mask"] = attention_mask
updated_model_kwargs["gen_timestep_scatter_index"] = model_kwargs["gen_timestep_scatter_index"]
else:
if mode == "gen_text":
# Now we are in the decode steps.
updated_model_kwargs["position_ids"] = model_kwargs["position_ids"] + 1
else:
updated_model_kwargs["position_ids"] = model_kwargs["position_ids"]
updated_model_kwargs["attention_mask"] = model_kwargs["attention_mask"]
updated_model_kwargs["gen_timestep_scatter_index"] = model_kwargs["gen_timestep_scatter_index"]
return updated_model_kwargs
def _generate(
self,
generator: list[torch.Generator] | None = None,
**kwargs,
):
mode = kwargs.get("mode", "gen_text")
# verbose > 1 not support
if mode == "gen_text":
raise NotImplementedError("Not support gen text for hunyuan image")
elif mode == "gen_image":
batch_gen_image_info: list[ImageInfo] = kwargs.get("batch_gen_image_info")
if batch_gen_image_info is None:
raise ValueError("`batch_gen_image_info` should be provided when `mode` is `gen_image`.")
image_info: ImageInfo = batch_gen_image_info[0]
num_image_tokens = (
image_info.image_token_length
+ (1 if image_info.add_timestep_token else 0)
+ (1 if image_info.add_guidance_token else 0)
)
kwargs["num_image_tokens"] = num_image_tokens
# 50 and 5.0 hard code
results = self.pipeline(
batch_size=len(batch_gen_image_info),
image_size=[
batch_gen_image_info[0].image_height,
batch_gen_image_info[0].image_width,
],
num_inference_steps=kwargs.get("num_inference_steps", 50),
guidance_scale=kwargs.get("guidance_scale", 5.0),
generator=generator,
model_kwargs=kwargs,
)
samples = results[0]
return samples
else:
raise ValueError(f"Unknown mode {mode}, only `gen_text` and `gen_image` are supported.")
@staticmethod
def _check_inputs(cond, target, check_list):
if cond:
for name, item in check_list:
assert item is not None, f"`{name}` should be provided when `{target}`."
def forward_call(
self,
input_ids: torch.LongTensor | None = None,
attention_mask: torch.Tensor | None = None,
position_ids: torch.LongTensor | None = None,
past_key_values: list[torch.FloatTensor] | None = None,
use_cache: bool | None = None,
output_attentions: bool | None = None,
output_hidden_states: bool | None = None,
return_dict: bool | None = None,
custom_pos_emb: tuple[torch.FloatTensor] | None = None,
mode: str = "gen_text",
first_step: bool | None = None,
# for gen image
images: BatchRaggedImages | None = None,
image_mask: torch.Tensor | None = None,
timestep: BatchRaggedTensor | None = None,
gen_timestep_scatter_index: torch.Tensor | None = None,
# for cond image
cond_vae_images: BatchRaggedImages | None = None,
cond_timestep: BatchRaggedTensor | None = None,
cond_vae_image_mask: torch.Tensor | None = None,
cond_vit_images: BatchRaggedImages | None = None,
cond_vit_image_mask: torch.Tensor | None = None,
vit_kwargs: dict[str, Any] | None = None,
cond_timestep_scatter_index: torch.Tensor | None = None,
query_lens: list[int] | None = None,
seq_lens: list[int] | None = None,
num_image_tokens: int | None = None,
uncond_cfg_prefill: bool = False,
ar_kv_reuse_len: int = 0,
full_attn_spans: list[list[tuple[int, int]]] | None = None,
) -> tuple | CausalMMOutputWithPast:
return_dict = return_dict if return_dict is not None else self.config.use_return_dict
# Sanity Check of Inputs
self._check_inputs(
mode == "gen_image" and not uncond_cfg_prefill,
"in `gen_image` mode",
[
("images", images),
("timestep", timestep),
("gen_timestep_scatter_index", gen_timestep_scatter_index),
],
)
self._check_inputs(
mode == "gen_image" and first_step and not uncond_cfg_prefill,
"in `gen_image` mode at the first step",
[
("image_mask", image_mask),
],
)
self._check_inputs(
cond_vae_images is not None,
"`cond_vae_images` is provided",
[
("cond_timestep", cond_timestep),
("cond_vae_image_mask", cond_vae_image_mask),
("cond_timestep_scatter_index", cond_timestep_scatter_index),
],
)
self._check_inputs(
cond_vit_images is not None,
"`cond_vit_images` is provided",
[
("cond_vit_image_mask", cond_vit_image_mask),
("vit_kwargs", vit_kwargs),
],
)
custom_pos_emb = self.get_pos_emb(custom_pos_emb, position_ids)
if input_ids is not None:
inputs_embeds = self.model.embed_tokens(input_ids)
bsz, seq_len, n_embd = inputs_embeds.shape
else:
inputs_embeds = None
bsz = images.shape[0] if isinstance(images, torch.Tensor) else len(images)
seq_len = 0
n_embd = self.config.hidden_size
# Instantiate placeholder tokens: <timestep>, <img> for the gen image
if mode == "gen_text":
# For gen_text, make sure gen_timestep_scatter_index is None
gen_timestep_scatter_index = None
token_h, token_w = None, None
elif uncond_cfg_prefill:
token_h, token_w = None, None
else:
if first_step:
assert inputs_embeds is not None
inputs_embeds, token_h, token_w = self.instantiate_vae_image_tokens(
inputs_embeds, images, timestep, image_mask
)
inputs_embeds = self.instantiate_timestep_tokens(inputs_embeds, timestep, gen_timestep_scatter_index)
else:
t_emb = self.time_embed(timestep)
image_emb, token_h, token_w = self.patch_embed(images, t_emb)
timestep_emb = self.timestep_emb(timestep).reshape(bsz, -1, n_embd)
inputs_embeds = torch.cat([timestep_emb, image_emb], dim=1)
# Instantiate placeholder tokens: <timestep>, <img> for cond images
# Should only run once with kv-cache enabled.
if cond_vae_images is not None:
inputs_embeds, _, _ = self.instantiate_vae_image_tokens(
inputs_embeds, cond_vae_images, cond_timestep, cond_vae_image_mask
)
inputs_embeds = self.instantiate_timestep_tokens(inputs_embeds, cond_timestep, cond_timestep_scatter_index)
if cond_vit_images is not None:
inputs_embeds = self.instantiate_vit_image_tokens(
inputs_embeds, cond_vit_images, cond_vit_image_mask, vit_kwargs
)
assert inputs_embeds is not None
bsz, seq_len, n_embd = inputs_embeds.shape
# decoder outputs consists of (dec_features, layer_state, dec_hidden, dec_attn)
from vllm.forward_context import set_forward_context
with set_forward_context(None, self.vllm_config):
outputs = self.model(
input_ids=input_ids,
attention_mask=attention_mask,
position_ids=position_ids,
past_key_values=past_key_values,
inputs_embeds=inputs_embeds,
use_cache=use_cache,
output_attentions=output_attentions,
output_hidden_states=output_hidden_states,
return_dict=return_dict,
custom_pos_emb=custom_pos_emb,
mode=mode,
first_step=first_step,
query_lens=query_lens,
seq_lens=seq_lens,
num_image_tokens=num_image_tokens,
gen_timestep_scatter_index=gen_timestep_scatter_index,
uncond_cfg_prefill=uncond_cfg_prefill,
ar_kv_reuse_len=ar_kv_reuse_len,
full_attn_spans=full_attn_spans,
)
hidden_states = outputs[0]
if mode == "gen_text":
hidden_states = self.model.ln_f(hidden_states)
logits = self.lm_head(hidden_states)
logits = logits.float()
diffusion_prediction = None
elif uncond_cfg_prefill:
logits = None
diffusion_prediction = None
else:
logits = None
hidden_states = hidden_states.to(inputs_embeds.device)
assert hidden_states.numel() == bsz * seq_len * n_embd, (
f"Shape mismatch: {hidden_states.shape} cannot reshape to ({bsz}, {seq_len}, {n_embd})"
)
hidden_states = hidden_states.reshape(bsz, seq_len, n_embd)
diffusion_prediction = self.ragged_final_layer(
hidden_states, image_mask, timestep, token_h, token_w, first_step
)
if not return_dict:
output = (logits,) + outputs[1:] + (diffusion_prediction,)
return output
output = CausalMMOutputWithPast(
logits=logits,
past_key_values=outputs.past_key_values,
hidden_states=outputs.hidden_states,
attentions=outputs.attentions,
diffusion_prediction=diffusion_prediction,
)
return output
def inject_ar_kv_into_layers(
self,
ar_kv_data: dict[int, dict[str, torch.Tensor]],
positive_reuse_len: int,
) -> None:
"""Inject AR-stage KV cache into each layer's ImageKVCacheManager.
Truncates to positive_reuse_len and sets image_kv_cache_map directly.
"""
for layer in self.model.layers:
layer_idx = layer.layer_idx
if layer_idx not in ar_kv_data:
continue
kv = ar_kv_data[layer_idx]
cache_mgr = layer.self_attn.image_attn
k, v = kv["key"], kv["value"]
cache_mgr._injected_ar_kv = [(k[:positive_reuse_len], v[:positive_reuse_len])]
def _extract_ar_kv_from_request(self, req) -> dict[str, Any]:
kv = getattr(getattr(req, "sampling_params", None), "past_key_values", None)
if kv is None:
return {}
key_cache = getattr(kv, "key_cache", None)
value_cache = getattr(kv, "value_cache", None)
if not key_cache or not value_cache:
return {}
ar_kv_data = {
i: {"key": k, "value": v}
for i, (k, v) in enumerate(zip(key_cache, value_cache))
if k is not None and v is not None
}
if not ar_kv_data:
return {}
logger.info(
f"[AR KV Reuse] Extracted {len(ar_kv_data)} layers of AR KV, "
f"each with length: {next(iter(ar_kv_data.values()))['key'].shape}"
)
return {"ar_kv_data": ar_kv_data}
def forward(
self,
req: OmniDiffusionRequest,
prompt: str | list[str] = "",
image_size="auto",
height: int = 1024,
width: int = 1024,
num_inference_steps: int = 50,
guidance_scale: float = 5.0,
generator: torch.Generator | list[torch.Generator] | None = None,
**kwargs,
) -> DiffusionOutput:
extra_args = getattr(getattr(req, "sampling_params", None), "extra_args", {}) or {}
use_system_prompt = extra_args.get("use_system_prompt")
system_prompt = extra_args.get("system_prompt")
# Fall back to per-prompt use_system_prompt forwarded by ar2diffusion
if req.prompts:
first_prompt = req.prompts[0]
if isinstance(first_prompt, dict):
if use_system_prompt is None:
use_system_prompt = first_prompt.get("use_system_prompt")
if system_prompt is None:
system_prompt = first_prompt.get("system_prompt")
if use_system_prompt is not None:
system_prompt = get_system_prompt(use_system_prompt, "image", system_prompt)
system_prompt = system_prompt.strip() if system_prompt is not None else ""
prompt = [p if isinstance(p, str) else (p.get("prompt") or "") for p in req.prompts] or prompt
cot_text_list = [
(p.get("extra", {}).get("ar_generated_text") if isinstance(p, dict) else None) or None for p in req.prompts
]
cot_text = (
[self._normalize_cot_text(t) for t in cot_text_list] if any(t is not None for t in cot_text_list) else None
)
batch_cond_image_info: list[list[JointImageInfo]] | None = None
if any(not isinstance(p, str) for p in req.prompts):
batch_cond_image_info = []
for prompt_item in req.prompts:
if isinstance(prompt_item, str):
batch_cond_image_info.append([])
continue
prompt_additional_information = prompt_item.get("additional_information") or {}
prompt_cond_infos = prompt_additional_information.get("batch_cond_image_info", [])
if isinstance(prompt_cond_infos, JointImageInfo | dict):
prompt_cond_infos = [prompt_cond_infos]
if prompt_cond_infos is None:
prompt_cond_infos = []
batch_cond_image_info.append([_joint_image_info_from_payload(item) for item in prompt_cond_infos])
has_cond_image = [len(cond_infos) > 0 for cond_infos in batch_cond_image_info]
if any(has_cond_image) and not all(has_cond_image):
raise ValueError(
"When batching Hunyuan image editing requests, every prompt must include input image(s)."
)
if not any(has_cond_image):
batch_cond_image_info = None
generator = req.sampling_params.generator or generator
height = req.sampling_params.height or height
width = req.sampling_params.width or width
num_inference_steps = req.sampling_params.num_inference_steps or num_inference_steps
if req.sampling_params.guidance_scale_provided:
guidance_scale = req.sampling_params.guidance_scale
if guidance_scale <= 1.0:
logger.info("HunyuanImage3.0 runs without classifier-free guidance when guidance_scale <= 1.0.")
image_size = (height, width)
# ---- AR KV Reuse: extract injected KV from request ----
ar_kv_kwargs = self._extract_ar_kv_from_request(req)
model_inputs = self.prepare_model_inputs(
prompt=prompt,
cot_text=cot_text,
system_prompt=system_prompt,
mode="gen_image",
generator=generator,
image_size=image_size,
num_inference_steps=num_inference_steps,
guidance_scale=guidance_scale,
batch_cond_image_info=batch_cond_image_info,
)
model_inputs.update(ar_kv_kwargs)
outputs = self._generate(**model_inputs, **kwargs)
custom_output = {}
if any(t is not None for t in cot_text_list):
custom_output["ar_generated_text"] = cot_text_list[0] if len(cot_text_list) == 1 else cot_text_list
return DiffusionOutput(
output=outputs[0],
custom_output=custom_output,
stage_durations=self.stage_durations if hasattr(self, "stage_durations") else None,
)