hunyuan_image model/pipeline review
Commit tested: 0f1abc4ae8b0eb2a3b40e82a310507281144c423
Review performed against the repository review rules.
Duplicate-search status: searched hunyuan_image, HunyuanImagePipeline, AutoencoderKLHunyuanImage, AutoencoderKLHunyuanImageRefiner, plus targeted tiling/shortcut/shape terms. Broad matches were the original integration PR #12333 and HunyuanImage 3.0 request #12412; I did not find duplicates for the issues below. A second gh search batch hit GitHub API rate limits, so I completed targeted duplicate checks with web search.
Issue 1: Base VAE default scaling factor breaks pipeline decode
Affected code:
| @register_to_config |
| def__init__( |
| self, |
| in_channels: int, |
| out_channels: int, |
| latent_channels: int, |
| block_out_channels: tuple[int, ...], |
| layers_per_block: int, |
| spatial_compression_ratio: int, |
| sample_size: int, |
| scaling_factor: float=None, |
| downsample_match_channel: bool=True, |
| upsample_match_channel: bool=True, |
| ) ->None: |
| # fmt: on |
| super().__init__() |
| |
| self.encoder=HunyuanImageEncoder2D( |
| in_channels=in_channels, |
| z_channels=latent_channels, |
| block_out_channels=block_out_channels, |
| num_res_blocks=layers_per_block, |
| spatial_compression_ratio=spatial_compression_ratio, |
| downsample_match_channel=downsample_match_channel, |
| ) |
| |
| self.decoder=HunyuanImageDecoder2D( |
| z_channels=latent_channels, |
| out_channels=out_channels, |
| block_out_channels=list(reversed(block_out_channels)), |
| num_res_blocks=layers_per_block, |
| spatial_compression_ratio=spatial_compression_ratio, |
| upsample_match_channel=upsample_match_channel, |
| ) |
| |
| # Tiling and slicing configuration |
| self.use_slicing=False |
| self.use_tiling=False |
| |
| # Tiling parameters |
| self.tile_sample_min_size=sample_size |
| self.tile_latent_min_size=sample_size//spatial_compression_ratio |
| self.tile_overlap_factor=0.25 |
| ifoutput_type=="latent": |
| image=latents |
| else: |
| latents=latents.to(self.vae.dtype) /self.vae.config.scaling_factor |
| image=self.vae.decode(latents, return_dict=False)[0] |
| image=self.image_processor.postprocess(image, output_type=output_type) |
Problem:
AutoencoderKLHunyuanImage defaults scaling_factor to None, but HunyuanImagePipeline always divides latents by self.vae.config.scaling_factor before decoding.
Impact:
A pipeline assembled from default tiny components can denoise successfully and then fail at decode with TypeError. The refiner VAE already has a numeric default.
Reproduction:
importtorchfromdiffusersimportAutoencoderKLHunyuanImage, FlowMatchEulerDiscreteScheduler, HunyuanImagePipeline, HunyuanImageTransformer2DModeltransformer=HunyuanImageTransformer2DModel(
in_channels=4, out_channels=4, num_attention_heads=1, attention_head_dim=4,
num_layers=0, num_single_layers=0, num_refiner_layers=0,
patch_size=(1, 1), text_embed_dim=4, text_embed_2_dim=4, rope_axes_dim=(2, 2),
)
vae=AutoencoderKLHunyuanImage(
in_channels=3, out_channels=3, latent_channels=4, block_out_channels=(32,),
layers_per_block=1, spatial_compression_ratio=8, sample_size=32,
)
pipe=HunyuanImagePipeline(
scheduler=FlowMatchEulerDiscreteScheduler(), vae=vae, text_encoder=None, tokenizer=None,
text_encoder_2=None, tokenizer_2=None, transformer=transformer, guider=None, ocr_guider=None,
)
pipe(
prompt=None,
prompt_embeds=torch.randn(1, 1, 4),
prompt_embeds_mask=torch.ones(1, 1, dtype=torch.long),
prompt_embeds_2=torch.zeros(1, 1, 4),
prompt_embeds_mask_2=torch.zeros(1, 1, dtype=torch.long),
height=16, width=16, num_inference_steps=1, output_type="pt",
)
Relevant precedent:
| @register_to_config |
| def__init__( |
| self, |
| in_channels: int=3, |
| out_channels: int=3, |
| latent_channels: int=32, |
| block_out_channels: tuple[int, ...] = (128, 256, 512, 1024, 1024), |
| layers_per_block: int=2, |
| spatial_compression_ratio: int=16, |
| temporal_compression_ratio: int=4, |
| downsample_match_channel: bool=True, |
| upsample_match_channel: bool=True, |
| scaling_factor: float=1.03682, |
| ) ->None: |
Suggested fix:
scaling_factor: float=0.476986
Issue 2: HunyuanImagePipelineOutput is not exported from the pipeline package
Affected code:
| _import_structure["pipeline_hunyuanimage"] = ["HunyuanImagePipeline"] |
| _import_structure["pipeline_hunyuanimage_refiner"] = ["HunyuanImageRefinerPipeline"] |
| |
| ifTYPE_CHECKINGorDIFFUSERS_SLOW_IMPORT: |
| try: |
| ifnot (is_transformers_available() andis_torch_available()): |
| raiseOptionalDependencyNotAvailable() |
| |
| exceptOptionalDependencyNotAvailable: |
| from ...utils.dummy_torch_and_transformers_objectsimport* |
| else: |
| from .pipeline_hunyuanimageimportHunyuanImagePipeline |
| from .pipeline_hunyuanimage_refinerimportHunyuanImageRefinerPipeline |
| classHunyuanImagePipelineOutput(BaseOutput): |
| """ |
| Output class for HunyuanImage pipelines. |
| |
| Args: |
| images (`list[PIL.Image.Image]` or `np.ndarray`) |
| List of denoised PIL images of length `batch_size` or numpy array of shape `(batch_size, height, width, |
| num_channels)`. PIL images or numpy array present the denoised images of the diffusion pipeline. |
| """ |
| |
| images: list[PIL.Image.Image, np.ndarray] |
Problem:
The output class exists and is documented, but diffusers.pipelines.hunyuan_image only lazy-exports the two pipelines.
Impact:
Users cannot import the output type from the package namespace, unlike similar pipeline families.
Reproduction:
fromdiffusers.pipelines.hunyuan_imageimportHunyuanImagePipelineOutput
Relevant precedent:
| _import_structure= {"pipeline_output": ["QwenImagePipelineOutput", "QwenImagePriorReduxPipelineOutput"]} |
| |
| try: |
| ifnot (is_transformers_available() andis_torch_available()): |
| raiseOptionalDependencyNotAvailable() |
| exceptOptionalDependencyNotAvailable: |
| from ...utilsimportdummy_torch_and_transformers_objects# noqa F403 |
| |
| _dummy_objects.update(get_objects_from_module(dummy_torch_and_transformers_objects)) |
| else: |
| _import_structure["modeling_qwenimage"] = ["ReduxImageEncoder"] |
| _import_structure["pipeline_qwenimage"] = ["QwenImagePipeline"] |
| _import_structure["pipeline_qwenimage_controlnet"] = ["QwenImageControlNetPipeline"] |
| _import_structure["pipeline_qwenimage_controlnet_inpaint"] = ["QwenImageControlNetInpaintPipeline"] |
| _import_structure["pipeline_qwenimage_edit"] = ["QwenImageEditPipeline"] |
| _import_structure["pipeline_qwenimage_edit_inpaint"] = ["QwenImageEditInpaintPipeline"] |
| _import_structure["pipeline_qwenimage_edit_plus"] = ["QwenImageEditPlusPipeline"] |
| _import_structure["pipeline_qwenimage_img2img"] = ["QwenImageImg2ImgPipeline"] |
| _import_structure["pipeline_qwenimage_inpaint"] = ["QwenImageInpaintPipeline"] |
| _import_structure["pipeline_qwenimage_layered"] = ["QwenImageLayeredPipeline"] |
| |
| ifTYPE_CHECKINGorDIFFUSERS_SLOW_IMPORT: |
| try: |
| ifnot (is_transformers_available() andis_torch_available()): |
| raiseOptionalDependencyNotAvailable() |
| exceptOptionalDependencyNotAvailable: |
| from ...utils.dummy_torch_and_transformers_objectsimport*# noqa F403 |
| else: |
| from .pipeline_qwenimageimportQwenImagePipeline |
Suggested fix:
_import_structure["pipeline_output"] = ["HunyuanImagePipelineOutput"]
...
from .pipeline_outputimportHunyuanImagePipelineOutput
Issue 3: Base VAE tiled encode crashes on 4D image tensors
Affected code:
| deftiled_encode(self, x: torch.Tensor) ->torch.Tensor: |
| """ |
| Encode input using spatial tiling strategy. |
| |
| Args: |
| x (`torch.Tensor`): Input tensor of shape (B, C, T, H, W). |
| |
| Returns: |
| `torch.Tensor`: |
| The latent representation of the encoded images. |
| """ |
| _, _, _, height, width=x.shape |
| overlap_size=int(self.tile_sample_min_size* (1-self.tile_overlap_factor)) |
| blend_extent=int(self.tile_latent_min_size*self.tile_overlap_factor) |
| row_limit=self.tile_latent_min_size-blend_extent |
| |
| rows= [] |
| foriinrange(0, height, overlap_size): |
| row= [] |
| forjinrange(0, width, overlap_size): |
| tile=x[:, :, :, i : i+self.tile_sample_min_size, j : j+self.tile_sample_min_size] |
| tile=self.encoder(tile) |
| row.append(tile) |
| rows.append(row) |
| |
| result_rows= [] |
| fori, rowinenumerate(rows): |
| result_row= [] |
| forj, tileinenumerate(row): |
| ifi>0: |
| tile=self.blend_v(rows[i-1][j], tile, blend_extent) |
| ifj>0: |
| tile=self.blend_h(row[j-1], tile, blend_extent) |
| result_row.append(tile[:, :, :, :row_limit, :row_limit]) |
| result_rows.append(torch.cat(result_row, dim=-1)) |
| |
| moments=torch.cat(result_rows, dim=-2) |
| |
| returnmoments |
Problem:
AutoencoderKLHunyuanImage is a 2D image VAE, but tiled_encode() unpacks x.shape as if it were 5D and slices tiles with an extra temporal dimension.
Impact:
vae.enable_tiling(); vae.encode(image) fails for image tensors large enough to trigger tiling. Existing tests cover tiled decode through the pipeline, but not tiled encode.
Reproduction:
importtorchfromdiffusersimportAutoencoderKLHunyuanImagevae=AutoencoderKLHunyuanImage(
in_channels=3, out_channels=3, latent_channels=4, block_out_channels=(32,),
layers_per_block=1, spatial_compression_ratio=1, sample_size=8,
)
vae.enable_tiling(tile_sample_min_size=4)
vae.encode(torch.randn(1, 3, 8, 8))
Relevant precedent:
The same file's tiled_decode() correctly treats base VAE latents as 4D:
| deftiled_decode(self, z: torch.Tensor, return_dict: bool=True) ->DecoderOutput|torch.Tensor: |
| """ |
| Decode latent using spatial tiling strategy. |
| |
| Args: |
| z (`torch.Tensor`): Latent tensor of shape (B, C, H, W). |
| return_dict (`bool`, *optional*, defaults to `True`): |
| Whether or not to return a [`~models.vae.DecoderOutput`] instead of a plain tuple. |
| |
| Returns: |
| [`~models.vae.DecoderOutput`] or `tuple`: |
| If return_dict is True, a [`~models.vae.DecoderOutput`] is returned, otherwise a plain `tuple` is |
| returned. |
| """ |
| _, _, height, width=z.shape |
| overlap_size=int(self.tile_latent_min_size* (1-self.tile_overlap_factor)) |
| blend_extent=int(self.tile_sample_min_size*self.tile_overlap_factor) |
| row_limit=self.tile_sample_min_size-blend_extent |
| |
| rows= [] |
| foriinrange(0, height, overlap_size): |
| row= [] |
| forjinrange(0, width, overlap_size): |
| tile=z[:, :, i : i+self.tile_latent_min_size, j : j+self.tile_latent_min_size] |
| decoded=self.decoder(tile) |
| row.append(decoded) |
| rows.append(row) |
| |
| result_rows= [] |
| fori, rowinenumerate(rows): |
| result_row= [] |
| forj, tileinenumerate(row): |
| ifi>0: |
| tile=self.blend_v(rows[i-1][j], tile, blend_extent) |
| ifj>0: |
| tile=self.blend_h(row[j-1], tile, blend_extent) |
| result_row.append(tile[:, :, :row_limit, :row_limit]) |
| result_rows.append(torch.cat(result_row, dim=-1)) |
| |
| dec=torch.cat(result_rows, dim=-2) |
| ifnotreturn_dict: |
| return (dec,) |
| returnDecoderOutput(sample=dec) |
Suggested fix:
_, _, height, width=x.shape
...
tile=x[:, :, i : i+self.tile_sample_min_size, j : j+self.tile_sample_min_size]
...
result_row.append(tile[:, :, :row_limit, :row_limit])
Issue 4: Refiner VAE tiling mixes sample and latent units
Affected code:
| deftiled_encode(self, x: torch.Tensor) ->torch.Tensor: |
| r"""Encode a batch of images using a tiled encoder. |
| |
| Args: |
| x (`torch.Tensor`): Input batch of videos. |
| |
| Returns: |
| `torch.Tensor`: |
| The latent representation of the encoded videos. |
| """ |
| _, _, _, height, width=x.shape |
| |
| tile_latent_min_height=self.tile_sample_min_height//self.spatial_compression_ratio |
| tile_latent_min_width=self.tile_sample_min_width//self.spatial_compression_ratio |
| overlap_height=int(tile_latent_min_height* (1-self.tile_overlap_factor)) # 256 * (1 - 0.25) = 192 |
| overlap_width=int(tile_latent_min_width* (1-self.tile_overlap_factor)) # 256 * (1 - 0.25) = 192 |
| blend_height=int(tile_latent_min_height*self.tile_overlap_factor) # 8 * 0.25 = 2 |
| blend_width=int(tile_latent_min_width*self.tile_overlap_factor) # 8 * 0.25 = 2 |
| row_limit_height=tile_latent_min_height-blend_height# 8 - 2 = 6 |
| row_limit_width=tile_latent_min_width-blend_width# 8 - 2 = 6 |
| |
| rows= [] |
| foriinrange(0, height, overlap_height): |
| row= [] |
| forjinrange(0, width, overlap_width): |
| tile=x[ |
| :, |
| :, |
| :, |
| i : i+self.tile_sample_min_height, |
| j : j+self.tile_sample_min_width, |
| ] |
| tile=self.encoder(tile) |
| row.append(tile) |
| rows.append(row) |
| |
| result_rows= [] |
| fori, rowinenumerate(rows): |
| result_row= [] |
| forj, tileinenumerate(row): |
| ifi>0: |
| tile=self.blend_v(rows[i-1][j], tile, blend_height) |
| ifj>0: |
| tile=self.blend_h(row[j-1], tile, blend_width) |
| result_row.append(tile[:, :, :, :row_limit_height, :row_limit_width]) |
| result_rows.append(torch.cat(result_row, dim=-1)) |
| moments=torch.cat(result_rows, dim=-2) |
| |
| returnmoments |
| deftiled_decode(self, z: torch.Tensor) ->torch.Tensor: |
| r""" |
| Decode a batch of images using a tiled decoder. |
| |
| Args: |
| z (`torch.Tensor`): Input batch of latent vectors. |
| return_dict (`bool`, *optional*, defaults to `True`): |
| Whether or not to return a [`~models.vae.DecoderOutput`] instead of a plain tuple. |
| |
| Returns: |
| [`~models.vae.DecoderOutput`] or `tuple`: |
| If return_dict is True, a [`~models.vae.DecoderOutput`] is returned, otherwise a plain `tuple` is |
| returned. |
| """ |
| |
| _, _, _, height, width=z.shape |
| tile_latent_min_height=self.tile_sample_min_height//self.spatial_compression_ratio |
| tile_latent_min_width=self.tile_sample_min_width//self.spatial_compression_ratio |
| overlap_height=int(tile_latent_min_height* (1-self.tile_overlap_factor)) # 8 * (1 - 0.25) = 6 |
| overlap_width=int(tile_latent_min_width* (1-self.tile_overlap_factor)) # 8 * (1 - 0.25) = 6 |
| blend_height=int(tile_latent_min_height*self.tile_overlap_factor) # 256 * 0.25 = 64 |
| blend_width=int(tile_latent_min_width*self.tile_overlap_factor) # 256 * 0.25 = 64 |
| row_limit_height=tile_latent_min_height-blend_height# 256 - 64 = 192 |
| row_limit_width=tile_latent_min_width-blend_width# 256 - 64 = 192 |
| |
| rows= [] |
| foriinrange(0, height, overlap_height): |
| row= [] |
| forjinrange(0, width, overlap_width): |
| tile=z[ |
| :, |
| :, |
| :, |
| i : i+tile_latent_min_height, |
| j : j+tile_latent_min_width, |
| ] |
| decoded=self.decoder(tile) |
| row.append(decoded) |
| rows.append(row) |
| |
| result_rows= [] |
| fori, rowinenumerate(rows): |
| result_row= [] |
| forj, tileinenumerate(row): |
| ifi>0: |
| tile=self.blend_v(rows[i-1][j], tile, blend_height) |
| ifj>0: |
| tile=self.blend_h(row[j-1], tile, blend_width) |
| result_row.append(tile[:, :, :, :row_limit_height, :row_limit_width]) |
| result_rows.append(torch.cat(result_row, dim=-1)) |
| dec=torch.cat(result_rows, dim=-2) |
| |
| returndec |
Problem:
tiled_encode() iterates over sample-space height/width using latent-space overlap values. tiled_decode() crops decoded sample tiles using latent-space row limits.
Impact:
Tiled encode can generate invalid edge tiles, and tiled decode returns the wrong spatial size.
Reproduction:
importtorchfromdiffusersimportAutoencoderKLHunyuanImageRefinervae=AutoencoderKLHunyuanImageRefiner(
in_channels=3, out_channels=3, latent_channels=4, block_out_channels=(8, 8),
layers_per_block=0, spatial_compression_ratio=2, temporal_compression_ratio=1,
)
z=torch.randn(1, 4, 1, 8, 8)
plain=vae.decode(z).sample.shapevae.enable_tiling(tile_sample_min_height=8, tile_sample_min_width=8, tile_overlap_factor=0.25)
tiled=vae.decode(z).sample.shapeprint(plain, tiled) # plain is [1, 3, 1, 16, 16], tiled is [1, 3, 1, 9, 9]
Relevant precedent:
| deftiled_encode(self, x: torch.Tensor) ->torch.Tensor: |
| r"""Encode a batch of images using a tiled encoder. |
| |
| Args: |
| x (`torch.Tensor`): Input batch of videos. |
| |
| Returns: |
| `torch.Tensor`: |
| The latent representation of the encoded videos. |
| """ |
| _, _, _, height, width=x.shape |
| |
| overlap_height=int(self.tile_sample_min_height* (1-self.tile_overlap_factor)) # 256 * (1 - 0.25) = 192 |
| overlap_width=int(self.tile_sample_min_width* (1-self.tile_overlap_factor)) # 256 * (1 - 0.25) = 192 |
| blend_height=int(self.tile_latent_min_height*self.tile_overlap_factor) # 8 * 0.25 = 2 |
| blend_width=int(self.tile_latent_min_width*self.tile_overlap_factor) # 8 * 0.25 = 2 |
| row_limit_height=self.tile_latent_min_height-blend_height# 8 - 2 = 6 |
| row_limit_width=self.tile_latent_min_width-blend_width# 8 - 2 = 6 |
| |
| rows= [] |
| foriinrange(0, height, overlap_height): |
| row= [] |
| forjinrange(0, width, overlap_width): |
| tile=x[ |
| :, |
| :, |
| :, |
| i : i+self.tile_sample_min_height, |
| j : j+self.tile_sample_min_width, |
| ] |
| tile=self.encoder(tile) |
| row.append(tile) |
| rows.append(row) |
| |
| result_rows= [] |
| fori, rowinenumerate(rows): |
| result_row= [] |
| forj, tileinenumerate(row): |
| ifi>0: |
| tile=self.blend_v(rows[i-1][j], tile, blend_height) |
| ifj>0: |
| tile=self.blend_h(row[j-1], tile, blend_width) |
| result_row.append(tile[:, :, :, :row_limit_height, :row_limit_width]) |
| result_rows.append(torch.cat(result_row, dim=-1)) |
| moments=torch.cat(result_rows, dim=-2) |
| |
| returnmoments |
| |
| deftiled_decode(self, z: torch.Tensor) ->torch.Tensor: |
| r""" |
| Decode a batch of images using a tiled decoder. |
| |
| Args: |
| z (`torch.Tensor`): Input batch of latent vectors. |
| return_dict (`bool`, *optional*, defaults to `True`): |
| Whether or not to return a [`~models.vae.DecoderOutput`] instead of a plain tuple. |
| |
| Returns: |
| [`~models.vae.DecoderOutput`] or `tuple`: |
| If return_dict is True, a [`~models.vae.DecoderOutput`] is returned, otherwise a plain `tuple` is |
| returned. |
| """ |
| |
| _, _, _, height, width=z.shape |
| |
| overlap_height=int(self.tile_latent_min_height* (1-self.tile_overlap_factor)) # 8 * (1 - 0.25) = 6 |
| overlap_width=int(self.tile_latent_min_width* (1-self.tile_overlap_factor)) # 8 * (1 - 0.25) = 6 |
| blend_height=int(self.tile_sample_min_height*self.tile_overlap_factor) # 256 * 0.25 = 64 |
| blend_width=int(self.tile_sample_min_width*self.tile_overlap_factor) # 256 * 0.25 = 64 |
| row_limit_height=self.tile_sample_min_height-blend_height# 256 - 64 = 192 |
| row_limit_width=self.tile_sample_min_width-blend_width# 256 - 64 = 192 |
Suggested fix:
Use sample-space stride/overlap for encode iteration, latent-space crop sizes for encoded tiles, latent-space stride for decode iteration, and sample-space crop sizes for decoded tiles. The existing tile_sample_stride_height / tile_sample_stride_width fields should either be used or removed.
Issue 5: Base VAE shortcut projection is applied to the wrong tensor
Affected code:
| defforward(self, x): |
| # Apply shortcut connection |
| residual=x |
| |
| # First normalization and activation |
| x=self.norm1(x) |
| x=self.nonlinearity(x) |
| |
| x=self.conv1(x) |
| x=self.norm2(x) |
| x=self.nonlinearity(x) |
| x=self.conv2(x) |
| |
| ifself.conv_shortcutisnotNone: |
| x=self.conv_shortcut(x) |
| # Add residual connection |
| returnx+residual |
Problem:
When in_channels != out_channels, HunyuanImageResnetBlock.forward() applies conv_shortcut to x after conv2, not to the saved residual.
Impact:
Non-default serialized configs with downsample_match_channel=False or upsample_match_channel=False crash when a resnet block changes channels.
Reproduction:
importtorchfromdiffusersimportAutoencoderKLHunyuanImagevae=AutoencoderKLHunyuanImage(
in_channels=3, out_channels=3, latent_channels=4, block_out_channels=(32, 64),
layers_per_block=1, spatial_compression_ratio=2, sample_size=16,
downsample_match_channel=False,
)
vae.encode(torch.randn(1, 3, 16, 16))
Relevant precedent:
| defforward(self, hidden_states: torch.Tensor) ->torch.Tensor: |
| residual=hidden_states |
| |
| hidden_states=self.norm1(hidden_states) |
| hidden_states=self.nonlinearity(hidden_states) |
| hidden_states=self.conv1(hidden_states) |
| |
| hidden_states=self.norm2(hidden_states) |
| hidden_states=self.nonlinearity(hidden_states) |
| hidden_states=self.conv2(hidden_states) |
| |
| ifself.conv_shortcutisnotNone: |
| residual=self.conv_shortcut(residual) |
| |
| returnhidden_states+residual |
Suggested fix:
ifself.conv_shortcutisnotNone:
residual=self.conv_shortcut(residual)
returnx+residual
Issue 6: Transformer silently truncates latent sizes not divisible by patch_size
Affected code:
| post_patch_sizes=tuple(d//pford, pinzip(sizes, self.config.patch_size)) |
| |
| # 1. RoPE |
| image_rotary_emb=self.rope(hidden_states) |
| |
| # 2. Conditional embeddings |
| encoder_attention_mask=encoder_attention_mask.bool() |
| temb=self.time_guidance_embed(timestep, guidance=guidance, timestep_r=timestep_r) |
| hidden_states=self.x_embedder(hidden_states) |
| encoder_hidden_states=self.context_embedder(encoder_hidden_states, timestep, encoder_attention_mask) |
| |
| ifself.context_embedder_2isnotNoneandencoder_hidden_states_2isnotNone: |
| encoder_hidden_states_2=self.context_embedder_2(encoder_hidden_states_2) |
| |
| encoder_attention_mask_2=encoder_attention_mask_2.bool() |
| |
| # reorder and combine text tokens: combine valid tokens first, then padding |
| new_encoder_hidden_states= [] |
| new_encoder_attention_mask= [] |
| |
| fortext, text_mask, text_2, text_mask_2inzip( |
| encoder_hidden_states, encoder_attention_mask, encoder_hidden_states_2, encoder_attention_mask_2 |
| ): |
| # Concatenate: [valid_mllm, valid_byt5, invalid_mllm, invalid_byt5] |
| new_encoder_hidden_states.append( |
| torch.cat( |
| [ |
| text_2[text_mask_2], # valid byt5 |
| text[text_mask], # valid mllm |
| text_2[~text_mask_2], # invalid byt5 |
| text[~text_mask], # invalid mllm |
| ], |
| dim=0, |
| ) |
| ) |
| |
| # Apply same reordering to attention masks |
| new_encoder_attention_mask.append( |
| torch.cat( |
| [ |
| text_mask_2[text_mask_2], |
| text_mask[text_mask], |
| text_mask_2[~text_mask_2], |
| text_mask[~text_mask], |
| ], |
| dim=0, |
| ) |
| ) |
| |
| encoder_hidden_states=torch.stack(new_encoder_hidden_states) |
| encoder_attention_mask=torch.stack(new_encoder_attention_mask) |
| |
| attention_mask=torch.nn.functional.pad(encoder_attention_mask, (hidden_states.shape[1], 0), value=True) |
| attention_mask=attention_mask.unsqueeze(1).unsqueeze(2) |
| # 3. Transformer blocks |
| iftorch.is_grad_enabled() andself.gradient_checkpointing: |
| forblockinself.transformer_blocks: |
| hidden_states, encoder_hidden_states=self._gradient_checkpointing_func( |
| block, |
| hidden_states, |
| encoder_hidden_states, |
| temb, |
| attention_mask=attention_mask, |
| image_rotary_emb=image_rotary_emb, |
| ) |
| |
| forblockinself.single_transformer_blocks: |
| hidden_states, encoder_hidden_states=self._gradient_checkpointing_func( |
| block, |
| hidden_states, |
| encoder_hidden_states, |
| temb, |
| attention_mask=attention_mask, |
| image_rotary_emb=image_rotary_emb, |
| ) |
| |
| else: |
| forblockinself.transformer_blocks: |
| hidden_states, encoder_hidden_states=block( |
| hidden_states, |
| encoder_hidden_states, |
| temb, |
| attention_mask=attention_mask, |
| image_rotary_emb=image_rotary_emb, |
| ) |
| |
| forblockinself.single_transformer_blocks: |
| hidden_states, encoder_hidden_states=block( |
| hidden_states, |
| encoder_hidden_states, |
| temb, |
| attention_mask=attention_mask, |
| image_rotary_emb=image_rotary_emb, |
| ) |
| |
| # 4. Output projection |
| hidden_states=self.norm_out(hidden_states, temb) |
| hidden_states=self.proj_out(hidden_states) |
| |
| # 5. unpatchify |
| # reshape: [batch_size, *post_patch_dims, channels, *patch_size] |
| out_channels=self.config.out_channels |
| reshape_dims= [batch_size] +list(post_patch_sizes) + [out_channels] +list(self.config.patch_size) |
| hidden_states=hidden_states.reshape(*reshape_dims) |
| |
| # create permutation pattern: batch, channels, then interleave post_patch and patch dims |
| # For 4D: [0, 3, 1, 4, 2, 5] -> batch, channels, post_patch_height, patch_size_height, post_patch_width, patch_size_width |
| # For 5D: [0, 4, 1, 5, 2, 6, 3, 7] -> batch, channels, post_patch_frame, patch_size_frame, post_patch_height, patch_size_height, post_patch_width, patch_size_width |
| ndim=len(post_patch_sizes) |
| permute_pattern= [0, ndim+1] # batch, channels |
| foriinrange(ndim): |
| permute_pattern.extend([i+1, ndim+2+i]) # post_patch_sizes[i], patch_sizes[i] |
| hidden_states=hidden_states.permute(*permute_pattern) |
| |
| # flatten patch dimensions: flatten each (post_patch_size, patch_size) pair |
| # batch_size, channels, post_patch_sizes[0] * patch_sizes[0], post_patch_sizes[1] * patch_sizes[1], ... |
| final_dims= [batch_size, out_channels] + [ |
| post_patch*patchforpost_patch, patchinzip(post_patch_sizes, self.config.patch_size) |
| ] |
| hidden_states=hidden_states.reshape(*final_dims) |
| ifheight% (self.vae_scale_factor*2) !=0orwidth% (self.vae_scale_factor*2) !=0: |
| logger.warning( |
| f"`height` and `width` have to be divisible by {self.vae_scale_factor*2} but are {height} and {width}. Dimensions will be resized accordingly" |
| ) |
| |
| ifcallback_on_step_end_tensor_inputsisnotNoneandnotall( |
| kinself._callback_tensor_inputsforkincallback_on_step_end_tensor_inputs |
| ): |
| raiseValueError( |
| f"`callback_on_step_end_tensor_inputs` has to be in {self._callback_tensor_inputs}, but found {[kforkincallback_on_step_end_tensor_inputsifknotinself._callback_tensor_inputs]}" |
| ) |
| |
| ifpromptisnotNoneandprompt_embedsisnotNone: |
| raiseValueError( |
| f"Cannot forward both `prompt`: {prompt} and `prompt_embeds`: {prompt_embeds}. Please make sure to" |
| " only forward one of the two." |
| ) |
| elifpromptisNoneandprompt_embedsisNone: |
| raiseValueError( |
| "Provide either `prompt` or `prompt_embeds`. Cannot leave both `prompt` and `prompt_embeds` undefined." |
| ) |
| elifpromptisnotNoneand (notisinstance(prompt, str) andnotisinstance(prompt, list)): |
| raiseValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}") |
| |
| ifnegative_promptisnotNoneandnegative_prompt_embedsisnotNone: |
| raiseValueError( |
| f"Cannot forward both `negative_prompt`: {negative_prompt} and `negative_prompt_embeds`:" |
| f" {negative_prompt_embeds}. Please make sure to only forward one of the two." |
| ) |
| |
| ifprompt_embedsisnotNoneandprompt_embeds_maskisNone: |
| raiseValueError( |
| "If `prompt_embeds` are provided, `prompt_embeds_mask` also have to be passed. Make sure to generate `prompt_embeds_mask` from the same text encoder that was used to generate `prompt_embeds`." |
| ) |
| ifnegative_prompt_embedsisnotNoneandnegative_prompt_embeds_maskisNone: |
| raiseValueError( |
| "If `negative_prompt_embeds` are provided, `negative_prompt_embeds_mask` also have to be passed. Make sure to generate `negative_prompt_embeds_mask` from the same text encoder that was used to generate `negative_prompt_embeds`." |
| ) |
| |
| ifpromptisNoneandprompt_embeds_2isNone: |
| raiseValueError( |
| "Provide either `prompt` or `prompt_embeds_2`. Cannot leave both `prompt` and `prompt_embeds_2` undefined." |
| ) |
| |
| ifprompt_embeds_2isnotNoneandprompt_embeds_mask_2isNone: |
| raiseValueError( |
| "If `prompt_embeds_2` are provided, `prompt_embeds_mask_2` also have to be passed. Make sure to generate `prompt_embeds_mask_2` from the same text encoder that was used to generate `prompt_embeds_2`." |
| ) |
| ifnegative_prompt_embeds_2isnotNoneandnegative_prompt_embeds_mask_2isNone: |
| raiseValueError( |
| "If `negative_prompt_embeds_2` are provided, `negative_prompt_embeds_mask_2` also have to be passed. Make sure to generate `negative_prompt_embeds_mask_2` from the same text encoder that was used to generate `negative_prompt_embeds_2`." |
| ) |
| |
| defprepare_latents( |
| self, |
| batch_size, |
| num_channels_latents, |
| height, |
| width, |
| dtype, |
| device, |
| generator, |
| latents=None, |
| ): |
| height=int(height) //self.vae_scale_factor |
| width=int(width) //self.vae_scale_factor |
| |
| shape= (batch_size, num_channels_latents, height, width) |
| |
| iflatentsisnotNone: |
| returnlatents.to(device=device, dtype=dtype) |
| |
| ifisinstance(generator, list) andlen(generator) !=batch_size: |
| raiseValueError( |
| f"You have passed a list of generators of length {len(generator)}, but requested an effective batch" |
| f" size of {batch_size}. Make sure the batch size matches the length of the generators." |
| ) |
| |
| latents=randn_tensor(shape, generator=generator, device=device, dtype=dtype) |
| |
| returnlatents |
| ifheight% (self.vae_scale_factor*2) !=0orwidth% (self.vae_scale_factor*2) !=0: |
| logger.warning( |
| f"`height` and `width` have to be divisible by {self.vae_scale_factor*2} but are {height} and {width}. Dimensions will be resized accordingly" |
| ) |
| |
| ifcallback_on_step_end_tensor_inputsisnotNoneandnotall( |
| kinself._callback_tensor_inputsforkincallback_on_step_end_tensor_inputs |
| ): |
| raiseValueError( |
| f"`callback_on_step_end_tensor_inputs` has to be in {self._callback_tensor_inputs}, but found {[kforkincallback_on_step_end_tensor_inputsifknotinself._callback_tensor_inputs]}" |
| ) |
| |
| ifpromptisnotNoneandprompt_embedsisnotNone: |
| raiseValueError( |
| f"Cannot forward both `prompt`: {prompt} and `prompt_embeds`: {prompt_embeds}. Please make sure to" |
| " only forward one of the two." |
| ) |
| elifpromptisNoneandprompt_embedsisNone: |
| raiseValueError( |
| "Provide either `prompt` or `prompt_embeds`. Cannot leave both `prompt` and `prompt_embeds` undefined." |
| ) |
| elifpromptisnotNoneand (notisinstance(prompt, str) andnotisinstance(prompt, list)): |
| raiseValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}") |
| |
| ifnegative_promptisnotNoneandnegative_prompt_embedsisnotNone: |
| raiseValueError( |
| f"Cannot forward both `negative_prompt`: {negative_prompt} and `negative_prompt_embeds`:" |
| f" {negative_prompt_embeds}. Please make sure to only forward one of the two." |
| ) |
| |
| ifprompt_embedsisnotNoneandprompt_embeds_maskisNone: |
| raiseValueError( |
| "If `prompt_embeds` are provided, `prompt_embeds_mask` also have to be passed. Make sure to generate `prompt_embeds_mask` from the same text encoder that was used to generate `prompt_embeds`." |
| ) |
| ifnegative_prompt_embedsisnotNoneandnegative_prompt_embeds_maskisNone: |
| raiseValueError( |
| "If `negative_prompt_embeds` are provided, `negative_prompt_embeds_mask` also have to be passed. Make sure to generate `negative_prompt_embeds_mask` from the same text encoder that was used to generate `negative_prompt_embeds`." |
| ) |
| |
| defprepare_latents( |
| self, |
| image_latents, |
| batch_size, |
| num_channels_latents, |
| height, |
| width, |
| dtype, |
| device, |
| generator, |
| latents=None, |
| strength=0.25, |
| ): |
| height=int(height) //self.vae_scale_factor |
| width=int(width) //self.vae_scale_factor |
| |
| shape= (batch_size, num_channels_latents, 1, height, width) |
| |
| iflatentsisNone: |
| latents=randn_tensor(shape, generator=generator, device=device, dtype=dtype) |
| else: |
| latents=latents.to(device=device, dtype=dtype) |
| |
| ifbatch_size>image_latents.shape[0] andbatch_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) |
| elifbatch_size>image_latents.shape[0] andbatch_size%image_latents.shape[0] !=0: |
| raiseValueError( |
| f"Cannot duplicate `image` of batch size {image_latents.shape[0]} to {batch_size} text prompts." |
| ) |
| |
| ifisinstance(generator, list) andlen(generator) !=batch_size: |
| raiseValueError( |
| 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." |
| ) |
| |
| noise=randn_tensor(shape, generator=generator, device=device, dtype=dtype) |
| cond_latents=strength*noise+ (1-strength) *image_latents |
| |
| returnlatents, cond_latents |
Problem:
The transformer computes post_patch_sizes = d // p and unpatchifies to post_patch * patch, silently dropping remainder pixels. The pipelines warn about divisibility but do not round latent sizes to the transformer patch grid.
Impact:
Custom configs with patch_size > 1 can produce a model output smaller than the input latents, leading to scheduler shape errors or silent shape loss when the model is used directly.
Reproduction:
importtorchfromdiffusersimportHunyuanImageTransformer2DModelmodel=HunyuanImageTransformer2DModel(
in_channels=4, out_channels=4, num_attention_heads=1, attention_head_dim=4,
num_layers=0, num_single_layers=0, num_refiner_layers=0,
patch_size=(2, 2), text_embed_dim=4, rope_axes_dim=(2, 2),
)
out=model(
hidden_states=torch.randn(1, 4, 3, 3),
timestep=torch.ones(1),
encoder_hidden_states=torch.randn(1, 1, 4),
encoder_attention_mask=torch.ones(1, 1, dtype=torch.long),
).sampleprint(out.shape) # torch.Size([1, 4, 2, 2])
Relevant precedent:
| # latent height and width to be divisible by 2. |
| height=2* (int(height) // (vae_scale_factor*2)) |
| width=2* (int(width) // (vae_scale_factor*2)) |
Suggested fix:
ifany(size%patch!=0forsize, patchinzip(sizes, self.config.patch_size)):
raiseValueError(f"`hidden_states` spatial/temporal sizes {sizes} must be divisible by patch_size {self.config.patch_size}.")Issue 7: Base VAE has checkpointing code but disables the public capability flag
Affected code:
| defforward(self, x: torch.Tensor) ->torch.Tensor: |
| x=self.conv_in(x) |
| |
| ## downsamples |
| fordown_blockinself.down_blocks: |
| iftorch.is_grad_enabled() andself.gradient_checkpointing: |
| x=self._gradient_checkpointing_func(down_block, x) |
| else: |
| x=down_block(x) |
| |
| ## middle |
| iftorch.is_grad_enabled() andself.gradient_checkpointing: |
| x=self._gradient_checkpointing_func(self.mid_block, x) |
| defforward(self, x: torch.Tensor) ->torch.Tensor: |
| h=self.conv_in(x) +x.repeat_interleave(repeats=self.repeat, dim=1) |
| |
| iftorch.is_grad_enabled() andself.gradient_checkpointing: |
| h=self._gradient_checkpointing_func(self.mid_block, h) |
| else: |
| h=self.mid_block(h) |
| |
| forup_blockinself.up_blocks: |
| iftorch.is_grad_enabled() andself.gradient_checkpointing: |
| h=self._gradient_checkpointing_func(up_block, h) |
| _supports_gradient_checkpointing=False |
Problem:
The encoder and decoder contain self._gradient_checkpointing_func(...) branches, but AutoencoderKLHunyuanImage._supports_gradient_checkpointing is False.
Impact:
Training or fine-tuning code cannot enable gradient checkpointing on the base VAE even though the implementation paths exist.
Reproduction:
fromdiffusersimportAutoencoderKLHunyuanImagevae=AutoencoderKLHunyuanImage(
in_channels=3, out_channels=3, latent_channels=4, block_out_channels=(32,),
layers_per_block=1, spatial_compression_ratio=1, sample_size=8,
)
vae.enable_gradient_checkpointing()
Relevant precedent:
| _supports_gradient_checkpointing=True |
Suggested fix:
_supports_gradient_checkpointing=True
Issue 8: Test coverage is incomplete, including missing slow tests
Affected code:
| classHunyuanImagePipelineFastTests( |
| PipelineTesterMixin, |
| FirstBlockCacheTesterMixin, |
| unittest.TestCase, |
| ): |
| pipeline_class=HunyuanImagePipeline |
Problem:
There is one fast pipeline test class for HunyuanImagePipeline. I found no model fast tests for HunyuanImageTransformer2DModel, AutoencoderKLHunyuanImage, or AutoencoderKLHunyuanImageRefiner, no fast tests for HunyuanImageRefinerPipeline, and no @slow tests for this family.
Impact:
The broken VAE encode tiling, refiner tiling, refiner pipeline paths, model patch-size behavior, and default scaling-factor failure are not covered.
Reproduction:
frompathlibimportPathfiles=list(Path("tests").rglob("*.py"))
hunyuan_files= [pforpinfilesif"hunyuan_image"instr(p).lower() or"hunyuanimage"inp.read_text(encoding="utf-8", errors="ignore")]
print([str(p) forpinhunyuan_files])
print("has_refiner_pipeline_test", any("HunyuanImageRefinerPipeline"inp.read_text(encoding="utf-8", errors="ignore") forpinhunyuan_files))
print("has_slow_test", any("@slow"inp.read_text(encoding="utf-8", errors="ignore") forpinhunyuan_files))Relevant precedent:
Other families keep slow pipeline tests alongside fast tests, for example:
Suggested fix:
Add focused model tests under tests/models/ for the transformer and both VAEs, add fast refiner pipeline tests with tiny components, and add at least one slow test for the base and refiner pretrained checkpoints. I attempted the existing fast Hunyuan test file locally with .venv, but collection failed before running tests because this environment's PyTorch build lacks torch._C._distributed_c10d.
hunyuan_imagemodel/pipeline reviewCommit tested:
0f1abc4ae8b0eb2a3b40e82a310507281144c423Review performed against the repository review rules.
Duplicate-search status: searched
hunyuan_image,HunyuanImagePipeline,AutoencoderKLHunyuanImage,AutoencoderKLHunyuanImageRefiner, plus targeted tiling/shortcut/shape terms. Broad matches were the original integration PR #12333 and HunyuanImage 3.0 request #12412; I did not find duplicates for the issues below. A secondgh searchbatch hit GitHub API rate limits, so I completed targeted duplicate checks with web search.Issue 1: Base VAE default scaling factor breaks pipeline decode
Affected code:
diffusers/src/diffusers/models/autoencoders/autoencoder_kl_hunyuanimage.py
Lines 423 to 465 in 0f1abc4
diffusers/src/diffusers/pipelines/hunyuan_image/pipeline_hunyuanimage.py
Lines 855 to 860 in 0f1abc4
Problem:
AutoencoderKLHunyuanImagedefaultsscaling_factortoNone, butHunyuanImagePipelinealways divides latents byself.vae.config.scaling_factorbefore decoding.Impact:
A pipeline assembled from default tiny components can denoise successfully and then fail at decode with
TypeError. The refiner VAE already has a numeric default.Reproduction:
Relevant precedent:
diffusers/src/diffusers/models/autoencoders/autoencoder_kl_hunyuanimage_refiner.py
Lines 604 to 617 in 0f1abc4
Suggested fix:
Issue 2:
HunyuanImagePipelineOutputis not exported from the pipeline packageAffected code:
diffusers/src/diffusers/pipelines/hunyuan_image/__init__.py
Lines 25 to 37 in 0f1abc4
diffusers/src/diffusers/pipelines/hunyuan_image/pipeline_output.py
Lines 10 to 20 in 0f1abc4
Problem:
The output class exists and is documented, but
diffusers.pipelines.hunyuan_imageonly lazy-exports the two pipelines.Impact:
Users cannot import the output type from the package namespace, unlike similar pipeline families.
Reproduction:
Relevant precedent:
diffusers/src/diffusers/pipelines/qwenimage/__init__.py
Lines 15 to 43 in 0f1abc4
Suggested fix:
Issue 3: Base VAE tiled encode crashes on 4D image tensors
Affected code:
diffusers/src/diffusers/models/autoencoders/autoencoder_kl_hunyuanimage.py
Lines 582 to 620 in 0f1abc4
Problem:
AutoencoderKLHunyuanImageis a 2D image VAE, buttiled_encode()unpacksx.shapeas if it were 5D and slices tiles with an extra temporal dimension.Impact:
vae.enable_tiling(); vae.encode(image)fails for image tensors large enough to trigger tiling. Existing tests cover tiled decode through the pipeline, but not tiled encode.Reproduction:
Relevant precedent:
The same file's
tiled_decode()correctly treats base VAE latents as 4D:diffusers/src/diffusers/models/autoencoders/autoencoder_kl_hunyuanimage.py
Lines 622 to 664 in 0f1abc4
Suggested fix:
Issue 4: Refiner VAE tiling mixes sample and latent units
Affected code:
diffusers/src/diffusers/models/autoencoders/autoencoder_kl_hunyuanimage_refiner.py
Lines 793 to 841 in 0f1abc4
diffusers/src/diffusers/models/autoencoders/autoencoder_kl_hunyuanimage_refiner.py
Lines 843 to 895 in 0f1abc4
Problem:
tiled_encode()iterates over sample-space height/width using latent-space overlap values.tiled_decode()crops decoded sample tiles using latent-space row limits.Impact:
Tiled encode can generate invalid edge tiles, and tiled decode returns the wrong spatial size.
Reproduction:
Relevant precedent:
diffusers/src/diffusers/models/autoencoders/autoencoder_kl_hunyuanvideo15.py
Lines 829 to 899 in 0f1abc4
Suggested fix:
Use sample-space stride/overlap for encode iteration, latent-space crop sizes for encoded tiles, latent-space stride for decode iteration, and sample-space crop sizes for decoded tiles. The existing
tile_sample_stride_height/tile_sample_stride_widthfields should either be used or removed.Issue 5: Base VAE shortcut projection is applied to the wrong tensor
Affected code:
diffusers/src/diffusers/models/autoencoders/autoencoder_kl_hunyuanimage.py
Lines 61 to 77 in 0f1abc4
Problem:
When
in_channels != out_channels,HunyuanImageResnetBlock.forward()appliesconv_shortcuttoxafterconv2, not to the saved residual.Impact:
Non-default serialized configs with
downsample_match_channel=Falseorupsample_match_channel=Falsecrash when a resnet block changes channels.Reproduction:
Relevant precedent:
diffusers/src/diffusers/models/autoencoders/autoencoder_kl_hunyuanimage_refiner.py
Lines 253 to 267 in 0f1abc4
Suggested fix:
Issue 6: Transformer silently truncates latent sizes not divisible by
patch_sizeAffected code:
diffusers/src/diffusers/models/transformers/transformer_hunyuanimage.py
Lines 766 to 885 in 0f1abc4
diffusers/src/diffusers/pipelines/hunyuan_image/pipeline_hunyuanimage.py
Lines 406 to 486 in 0f1abc4
diffusers/src/diffusers/pipelines/hunyuan_image/pipeline_hunyuanimage_refiner.py
Lines 295 to 375 in 0f1abc4
Problem:
The transformer computes
post_patch_sizes = d // pand unpatchifies topost_patch * patch, silently dropping remainder pixels. The pipelines warn about divisibility but do not round latent sizes to the transformer patch grid.Impact:
Custom configs with
patch_size > 1can produce a model output smaller than the input latents, leading to scheduler shape errors or silent shape loss when the model is used directly.Reproduction:
Relevant precedent:
diffusers/src/diffusers/pipelines/qwenimage/pipeline_qwenimage.py
Lines 346 to 348 in 0f1abc4
Suggested fix:
Issue 7: Base VAE has checkpointing code but disables the public capability flag
Affected code:
diffusers/src/diffusers/models/autoencoders/autoencoder_kl_hunyuanimage.py
Lines 296 to 308 in 0f1abc4
diffusers/src/diffusers/models/autoencoders/autoencoder_kl_hunyuanimage.py
Lines 393 to 403 in 0f1abc4
diffusers/src/diffusers/models/autoencoders/autoencoder_kl_hunyuanimage.py
Line 420 in 0f1abc4
Problem:
The encoder and decoder contain
self._gradient_checkpointing_func(...)branches, butAutoencoderKLHunyuanImage._supports_gradient_checkpointingisFalse.Impact:
Training or fine-tuning code cannot enable gradient checkpointing on the base VAE even though the implementation paths exist.
Reproduction:
Relevant precedent:
diffusers/src/diffusers/models/autoencoders/autoencoder_kl_hunyuanimage_refiner.py
Line 602 in 0f1abc4
Suggested fix:
Issue 8: Test coverage is incomplete, including missing slow tests
Affected code:
diffusers/tests/pipelines/hunyuan_image_21/test_hunyuanimage.py
Lines 43 to 48 in 0f1abc4
Problem:
There is one fast pipeline test class for
HunyuanImagePipeline. I found no model fast tests forHunyuanImageTransformer2DModel,AutoencoderKLHunyuanImage, orAutoencoderKLHunyuanImageRefiner, no fast tests forHunyuanImageRefinerPipeline, and no@slowtests for this family.Impact:
The broken VAE encode tiling, refiner tiling, refiner pipeline paths, model patch-size behavior, and default scaling-factor failure are not covered.
Reproduction:
Relevant precedent:
Other families keep slow pipeline tests alongside fast tests, for example:
diffusers/tests/pipelines/wan/test_wan.py
Line 185 in 0f1abc4
Suggested fix:
Add focused model tests under
tests/models/for the transformer and both VAEs, add fast refiner pipeline tests with tiny components, and add at least one slow test for the base and refiner pretrained checkpoints. I attempted the existing fast Hunyuan test file locally with.venv, but collection failed before running tests because this environment's PyTorch build lackstorch._C._distributed_c10d.