huggingface / huggingface/diffusers

`wan` model/pipeline review

Open
#13,578 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Python
Stars
34.5k
Forks
7.3k
Avg merge
3d 3h
Merged PRs (30d)
91

Description

# `wan` model/pipeline review

Commit tested: `0f1abc4ae8b0eb2a3b40e82a310507281144c423`

Review performed against the repository review rules.

Duplicate search status: searched existing `huggingface/diffusers` Issues and PRs for `Wan`, affected class names, and specific failure modes. I found related but non-duplicate Wan issues/PRs, including `#12348`, `#12574`, `#12496`, and `#11582`; no likely duplicates for the actionable findings below.

## Issue 1: `WanImageToVideoPipeline` does not expand image conditioning for `num_videos_per_prompt`

Affected code:
https://github.com/huggingface/diffusers/blob/0f1abc4ae8b0eb2a3b40e82a310507281144c423/src/diffusers/pipelines/wan/pipeline_wan_i2v.py#L691-L699
https://github.com/huggingface/diffusers/blob/0f1abc4ae8b0eb2a3b40e82a310507281144c423/src/diffusers/pipelines/wan/pipeline_wan_i2v.py#L770-L786

Problem:
`image_embeds` is repeated by `batch_size`, but not by `num_videos_per_prompt`. With `num_videos_per_prompt > 1`, latent inputs are expanded to the final effective batch while image embeddings remain smaller, causing a transformer concat failure.

Impact:
Users cannot generate multiple videos per prompt in image-to-video mode. This also leaves prompt/image batching semantics inconsistent with the rest of the pipeline.

Reproduction:
```python
import torch
from PIL import Image
from transformers import AutoConfig, AutoTokenizer, T5EncoderModel, CLIPImageProcessor, CLIPVisionConfig, CLIPVisionModelWithProjection
from diffusers import AutoencoderKLWan, FlowMatchEulerDiscreteScheduler, WanImageToVideoPipeline, WanTransformer3DModel

vae = AutoencoderKLWan(base_dim=3, z_dim=16, dim_mult=[1, 1, 1, 1], num_res_blocks=1, temperal_downsample=[False, True, True])
scheduler = FlowMatchEulerDiscreteScheduler(shift=7.0)
config = AutoConfig.from_pretrained("hf-internal-testing/tiny-random-t5")
text_encoder = T5EncoderModel(config)
tokenizer = AutoTokenizer.from_pretrained("hf-internal-testing/tiny-random-t5")
transformer = WanTransformer3DModel(
patch_size=(1, 2, 2), num_attention_heads=2, attention_head_dim=12,
in_channels=36, out_channels=16, text_dim=32, freq_dim=256,
ffn_dim=32, num_layers=1, cross_attn_norm=True,
qk_norm="rms_norm_across_heads", rope_max_seq_len=32, image_dim=4,
)
image_encoder_config = CLIPVisionConfig(
hidden_size=4, projection_dim=4, num_hidden_layers=1,
num_attention_heads=2, image_size=32, intermediate_size=16, patch_size=1,
)
pipe = WanImageToVideoPipeline(
tokenizer, text_encoder, vae, scheduler,
CLIPImageProcessor(crop_size=32, size=32),
CLIPVisionModelWithProjection(image_encoder_config),
transformer=transformer,
).to("cpu")
pipe.set_progress_bar_config(disable=True)

pipe(
image=Image.new("RGB", (16, 16)),
prompt="test prompt",
negative_prompt="negative",
height=16,
width=16,
num_frames=9,
num_inference_steps=1,
guidance_scale=1.0,
num_videos_per_prompt=2,
max_sequence_length=16,
output_type="latent",
generator=torch.Generator(device="cpu").manual_seed(0),
)
```

Relevant precedent:
Standard text/video batch expansion in Wan uses `batch_size * num_videos_per_prompt` before denoising.

Suggested fix:
```python
target_batch_size = batch_size * num_videos_per_prompt

if image_embeds.shape[0] == 1:
image_embeds = image_embeds.repeat_interleave(target_batch_size, dim=0)
elif image_embeds.shape[0] == batch_size:
image_embeds = image_embeds.repeat_interleave(num_videos_per_prompt, dim=0)
elif image_embeds.shape[0] != target_batch_size:
raise ValueError(
f"`image_embeds` batch size must be 1, {batch_size}, or {target_batch_size}, "
f"but got {image_embeds.shape[0]}."
)
```

## Issue 2: `WanImageToVideoPipeline` rejects documented list image inputs

Affected code:
https://github.com/huggingface/diffusers/blob/0f1abc4ae8b0eb2a3b40e82a310507281144c423/src/diffusers/pipelines/wan/pipeline_wan_i2v.py#L350-L355
https://github.com/huggingface/diffusers/blob/0f1abc4ae8b0eb2a3b40e82a310507281144c423/src/diffusers/pipelines/wan/pipeline_wan_i2v.py#L509-L527

Problem:
The public call signature accepts `PipelineImageInput`, but `check_inputs` only accepts a single `PIL.Image.Image` or `torch.Tensor`. A list of PIL images is rejected before preprocessing.

Impact:
Prompt/image batching through list inputs does not work, despite being part of the public type contract.

Reproduction:
```python
from types import SimpleNamespace
from PIL import Image
from diffusers import WanImageToVideoPipeline

pipe_like = SimpleNamespace(
_callback_tensor_inputs=["latents", "prompt_embeds", "negative_prompt_embeds"],
config=SimpleNamespace(boundary_ratio=None),
)

WanImageToVideoPipeline.check_inputs(
pipe_like,
prompt="test",
negative_prompt=None,
image=[Image.new("RGB", (16, 16))],
height=16,
width=16,
prompt_embeds=None,
negative_prompt_embeds=None,
image_embeds=None,
callback_on_step_end_tensor_inputs=["latents"],
guidance_scale_2=None,
)
```

Relevant precedent:
Other image-conditioned pipelines validate `PipelineImageInput` through shared image/video processor semantics instead of rejecting lists at `check_inputs`.

Suggested fix:
Allow `list`/`tuple` image inputs in `check_inputs`, validate their members, and align the resulting image latents and image embeddings to `batch_size * num_videos_per_prompt`.

## Issue 3: `WanVACEPipeline` returns reference latents in `output_type="latent"`

Affected code:
https://github.com/huggingface/diffusers/blob/0f1abc4ae8b0eb2a3b40e82a310507281144c423/src/diffusers/pipelines/wan/pipeline_wan_vace.py#L930-L935
https://github.com/huggingface/diffusers/blob/0f1abc4ae8b0eb2a3b40e82a310507281144c423/src/diffusers/pipelines/wan/pipeline_wan_vace.py#L1020-L1036

Problem:
When `reference_images` are provided, VACE prepends reference latents. The decode path trims those latents, but the latent-output path returns them untrimmed.

Impact:
`output_type="latent"` returns a latent video with extra frames. Callers expecting returned latents to correspond to the requested video length get an off-by-reference count shape.

Reproduction:
```python
import torch
from PIL import Image
from transformers import AutoConfig, AutoTokenizer, T5EncoderModel
from diffusers import AutoencoderKLWan, FlowMatchEulerDiscreteScheduler, WanVACEPipeline, WanVACETransformer3DModel

vae = AutoencoderKLWan(base_dim=3, z_dim=16, dim_mult=[1, 1, 1, 1], num_res_blocks=1, temperal_downsample=[False, True, True])
scheduler = FlowMatchEulerDiscreteScheduler(shift=7.0)
config = AutoConfig.from_pretrained("hf-internal-testing/tiny-random-t5")
text_encoder = T5EncoderModel(config)
tokenizer = AutoTokenizer.from_pretrained("hf-internal-testing/tiny-random-t5")
transformer = WanVACETransformer3DModel(
patch_size=(1, 2, 2), num_attention_heads=2, attention_head_dim=12,
in_channels=16, out_channels=16, text_dim=32, freq_dim=256,
ffn_dim=32, num_layers=3, cross_attn_norm=True,
qk_norm="rms_norm_across_heads", rope_max_seq_len=32,
vace_layers=[0, 2], vace_in_channels=96,
)
pipe = WanVACEPipeline(tokenizer, text_encoder, vae, scheduler, transformer=transformer).to("cpu")
pipe.set_progress_bar_config(disable=True)

num_frames = 17
video = [Image.new("RGB", (16, 16))] * num_frames
mask = [Image.new("L", (16, 16), 0)] * num_frames

latents = pipe(
video=video,
mask=mask,
reference_images=Image.new("RGB", (16, 16)),
prompt="test prompt",
negative_prompt="negative",
num_inference_steps=1,
guidance_scale=1.0,
height=16,
width=16,
num_frames=num_frames,
max_sequence_length=16,
output_type="latent",
generator=torch.Generator(device="cpu").manual_seed(0),
).frames

print(tuple(latents.shape))
print("expected latent frames:", (num_frames - 1) // vae.config.scale_factor_temporal + 1)
```

Relevant precedent:
The same pipeline already trims reference latents before VAE decode.

Suggested fix:
```python
if num_reference_images:
latents = latents[:, :, num_reference_images:]

if output_type != "latent":
latents = latents.to(vae_dtype)
video = self.vae.decode(latents, return_dict=False)[0]
video = self.video_processor.postprocess_video(video, output_type=output_type)
else:
video = latents
```

## Issue 4: VACE, video-to-video, and modular Wan ignore VAE scale-factor config

Affected code:
https://github.com/huggingface/diffusers/blob/0f1abc4ae8b0eb2a3b40e82a310507281144c423/src/diffusers/pipelines/wan/pipeline_wan_vace.py#L199-L200
https://github.com/huggingface/diffusers/blob/0f1abc4ae8b0eb2a3b40e82a310507281144c423/src/diffusers/pipelines/wan/pipeline_wan_video2video.py#L217-L218
https://github.com/huggingface/diffusers/blob/0f1abc4ae8b0eb2a3b40e82a310507281144c423/src/diffusers/modular_pipelines/wan/modular_pipeline.py#L69-L80

Problem:
These paths derive scale factors from `len(vae.temperal_downsample)` and hard-code spatial scale as `8`, instead of using `vae.config.scale_factor_temporal` and `vae.config.scale_factor_spatial`.

Impact:
Custom or future Wan VAE configs serialize valid scale factors but the affected pipelines preprocess sizes and frame counts incorrectly.

Reproduction:
```python
from diffusers import AutoencoderKLWan, WanVACEPipeline, WanVideoToVideoPipeline

vae = AutoencoderKLWan(
base_dim=3,
z_dim=16,
dim_mult=[1, 1, 1, 1],
num_res_blocks=1,
temperal_downsample=[False, True, True],
scale_factor_spatial=4,
)

v2v = WanVideoToVideoPipeline(tokenizer=None, text_encoder=None, transformer=None, vae=vae, scheduler=None)
vace = WanVACEPipeline(tokenizer=None, text_encoder=None, transformer=None, transformer_2=None, vae=vae, scheduler=None)

print("vae config:", vae.config.scale_factor_spatial)
print("video2video:", v2v.vae_scale_factor_spatial)
print("vace:", vace.vae_scale_factor_spatial)
```

Relevant precedent:
`WanPipeline`, `WanImageToVideoPipeline`, and `WanAnimatePipeline` use the VAE config scale factors.

Suggested fix:
```python
self.vae_scale_factor_temporal = self.vae.config.scale_factor_temporal if getattr(self, "vae", None) else 4
self.vae_scale_factor_spatial = self.vae.config.scale_factor_spatial if getattr(self, "vae", None) else 8
```

Apply the same config-based source in the modular Wan pipeline state initialization.

## Issue 5: `WanAnimateImageProcessor` drops constructor config values

Affected code:
https://github.com/huggingface/diffusers/blob/0f1abc4ae8b0eb2a3b40e82a310507281144c423/src/diffusers/pipelines/wan/image_processor.py#L57-L71
https://github.com/huggingface/diffusers/blob/0f1abc4ae8b0eb2a3b40e82a310507281144c423/src/diffusers/pipelines/wan/pipeline_wan_animate.py#L227-L233

Problem:
`WanAnimateImageProcessor.__init__` accepts `vae_scale_factor`, `vae_latent_channels`, and `resample`, but then calls `super().__init__()` without forwarding them. The base class re-registers default values and overwrites the subclass config.

Impact:
The animate pipeline constructs its processor with Wan-specific values, but the stored config reports base defaults. This can affect serialization, reload behavior, and downstream code that reads processor config.

Reproduction:
```python
from diffusers.pipelines.wan.image_processor import WanAnimateImageProcessor

processor = WanAnimateImageProcessor(
vae_scale_factor=16,
vae_latent_channels=16,
spatial_patch_size=(3, 5),
resample="bilinear",
fill_color=123,
)

print(processor.config.vae_scale_factor)
print(processor.config.vae_latent_channels)
print(processor.config.resample)
print(processor.config.spatial_patch_size)
```

Relevant precedent:
`VaeImageProcessor.__init__` accepts these fields directly and should be initialized with the subclass values.

Suggested fix:
```python
super().__init__(
do_resize=do_resize,
vae_scale_factor=vae_scale_factor,
vae_latent_channels=vae_latent_channels,
resample=resample,
reducing_gap=reducing_gap,
do_normalize=do_normalize,
do_binarize=do_binarize,
do_convert_rgb=do_convert_rgb,
do_convert_grayscale=do_convert_grayscale,
)
self.register_to_config(spatial_patch_size=spatial_patch_size, fill_color=fill_color)
```

## Issue 6: Modular Wan casts scheduler timesteps to model dtype

Affected code:
https://github.com/huggingface/diffusers/blob/0f1abc4ae8b0eb2a3b40e82a310507281144c423/src/diffusers/modular_pipelines/wan/before_denoise.py#L252-L265
https://github.com/huggingface/diffusers/blob/0f1abc4ae8b0eb2a3b40e82a310507281144c423/src/diffusers/modular_pipelines/wan/denoise.py#L205-L211
https://github.com/huggingface/diffusers/blob/0f1abc4ae8b0eb2a3b40e82a310507281144c423/src/diffusers/modular_pipelines/wan/denoise.py#L340-L346

Problem:
`WanTextInputStep` derives `block_state.dtype` from `prompt_embeds.dtype`, despite the comment saying transformer dtype should be used. The denoise blocks then cast scheduler timesteps to that dtype. With bf16/fp16 model paths, timestep values can be rounded before reaching the model.

Impact:
The modular pipeline can diverge from standard Wan pipeline behavior and lose scheduler precision.

Reproduction:
```python
import torch

for value in [999.0, 998.0, 995.0, 750.3]:
timestep = torch.tensor([value], dtype=torch.float32)
print(value, "->", timestep.to(torch.bfloat16).to(torch.float32).item())
```

Relevant precedent:
The standard Wan pipelines cast prompt embeddings and latents to transformer dtype, but pass the scheduler timestep tensor without pre-casting it to bf16/fp16.

Suggested fix:
```python
# before denoise
block_state.dtype = components.transformer.dtype
block_state.prompt_embeds = block_state.prompt_embeds.to(block_state.dtype)
if block_state.negative_prompt_embeds is not None:
block_state.negative_prompt_embeds = block_state.negative_prompt_embeds.to(block_state.dtype)

# denoise
timestep = t.expand(block_state.latent_model_input.shape[0])
```

For Wan 2.2 modular denoising, choose the latent/model dtype after selecting the current transformer, but keep scheduler timestep precision intact.

## Issue 7: Slow test coverage is missing for several public Wan workflows

Affected code:
https://github.com/huggingface/diffusers/blob/0f1abc4ae8b0eb2a3b40e82a310507281144c423/tests/pipelines/wan/test_wan.py#L185-L201
https://github.com/huggingface/diffusers/blob/0f1abc4ae8b0eb2a3b40e82a310507281144c423/tests/pipelines/wan/test_wan_animate.py#L224-L240
https://github.com/huggingface/diffusers/blob/0f1abc4ae8b0eb2a3b40e82a310507281144c423/tests/pipelines/wan/test_wan_image_to_video.py#L40-L173
https://github.com/huggingface/diffusers/blob/0f1abc4ae8b0eb2a3b40e82a310507281144c423/tests/pipelines/wan/test_wan_vace.py#L39-L285
https://github.com/huggingface/diffusers/blob/0f1abc4ae8b0eb2a3b40e82a310507281144c423/tests/pipelines/wan/test_wan_video_to_video.py#L35-L149
https://github.com/huggingface/diffusers/blob/0f1abc4ae8b0eb2a3b40e82a310507281144c423/tests/modular_pipelines/wan/test_modular_pipeline_wan.py#L23-L48

Problem:
`WanPipeline` and `WanAnimatePipeline` have slow integration coverage. I did not find slow tests for Wan image-to-video, Wan 2.2 text-to-video, Wan 2.2 image-to-video, VACE, video-to-video, or modular Wan parity.

Impact:
Large public workflows can regress without any real-checkpoint coverage. The issues above are examples that fast synthetic tests did not catch.

Reproduction:
```python
from pathlib import Path

files = [
"tests/pipelines/wan/test_wan.py",
"tests/pipelines/wan/test_wan_22.py",
"tests/pipelines/wan/test_wan_image_to_video.py",
"tests/pipelines/wan/test_wan_22_image_to_video.py",
"tests/pipelines/wan/test_wan_vace.py",
"tests/pipelines/wan/test_wan_video_to_video.py",
"tests/pipelines/wan/test_wan_animate.py",
"tests/modular_pipelines/wan/test_modular_pipeline_wan.py",
]

for file in files:
text = Path(file).read_text(encoding="utf-8")
print(file, "@slow" in text)
```

Relevant precedent:
The existing slow tests for `WanPipeline` and `WanAnimatePipeline` provide the pattern.

Suggested fix:
Add at least one `@slow` integration test for each missing public workflow, using fixed seeds and slice assertions. Include VACE coverage with `reference_images` and `output_type="latent"`, and modular-vs-standard parity coverage for a representative Wan pipeline.

Contributor guide

Open the contributing guide

Research direction

Start with the referenced Wan pipeline and image-processor files, especially pipeline_wan_i2v.py, pipeline_wan_vace.py, pipeline_wan_video2video.py, modular_pipeline.py, and image_processor.py. Run the inline reproductions against commit 0f1abc4ae8b0eb2a3b40e82a310507281144c423, then verify each reported batching, latent-shape, scale-factor, and configuration behavior is corrected without regressing existing pipeline behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
python, pytorch
Domain
machine-learning
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
38/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.