huggingface / huggingface/diffusers
`qwenimage` model/pipeline review
- Dominant language
- Python
- Stars
- 34.5k
- Forks
- 7.3k
- Avg merge
- 3d 3h
- Merged PRs (30d)
- 91
Description
# `qwenimage` model/pipeline review
Commit tested: `0f1abc4ae8b0eb2a3b40e82a310507281144c423`
Review performed against the repository review rules. Duplicate searches were run against `huggingface/diffusers` issues and PRs for `qwenimage`, affected classes/files, and the failure modes below. No exact duplicates were found; related but non-identical issues include `#12075`, `#12294`, `#12458`, `#12698`, and broad issue `#12295`.
## Issue 1: Broken qwenimage lazy exports
Affected code:
https://github.com/huggingface/diffusers/blob/0f1abc4ae8b0eb2a3b40e82a310507281144c423/src/diffusers/pipelines/qwenimage/__init__.py#L15-L25
Problem:
`diffusers.pipelines.qwenimage.__init__` exports `QwenImagePriorReduxPipelineOutput` from `pipeline_output.py`, but that class does not exist. It also lazily exports `ReduxImageEncoder` from `modeling_qwenimage`, but there is no `src/diffusers/pipelines/qwenimage/modeling_qwenimage.py`.
Impact:
Subpackage imports fail at runtime and lazy-loading advertises unavailable objects.
Reproduction:
```python
for name in ["QwenImagePriorReduxPipelineOutput", "ReduxImageEncoder"]:
try:
ns = {}
exec(f"from diffusers.pipelines.qwenimage import {name}", ns)
print(name, "ok")
except Exception as e:
print(name, type(e).__name__, e)
```
Relevant precedent:
`ReduxImageEncoder` exists under Flux, not QwenImage.
Suggested fix:
```python
_import_structure = {"pipeline_output": ["QwenImagePipelineOutput"]}
# Remove:
# _import_structure["modeling_qwenimage"] = ["ReduxImageEncoder"]
```
## Issue 2: `guidance_embeds=True` transformer path always raises
Affected code:
https://github.com/huggingface/diffusers/blob/0f1abc4ae8b0eb2a3b40e82a310507281144c423/src/diffusers/models/transformers/transformer_qwenimage.py#L932-L938
Problem:
When `guidance` is passed, `QwenImageTransformer2DModel.forward` calls `self.time_text_embed(timestep, guidance, hidden_states, additional_t_cond)`, but `QwenTimestepProjEmbeddings.forward` accepts only `(timestep, hidden_states, addition_t_cond=None)`.
Impact:
Any guidance-distilled QwenImage transformer configuration crashes before denoising.
Reproduction:
```python
import torch
from diffusers import QwenImageTransformer2DModel
model = QwenImageTransformer2DModel(
patch_size=1, in_channels=4, out_channels=4, num_layers=1,
attention_head_dim=4, num_attention_heads=1, joint_attention_dim=8,
axes_dims_rope=(2, 2, 4),
)
model(
hidden_states=torch.randn(1, 4, 4),
encoder_hidden_states=torch.randn(1, 3, 8),
encoder_hidden_states_mask=torch.ones(1, 3, dtype=torch.bool),
timestep=torch.tensor([1]),
img_shapes=[(1, 2, 2)],
guidance=torch.tensor([1.0]),
)
```
Relevant precedent:
Flux uses a guidance-aware embedding module:
https://github.com/huggingface/diffusers/blob/0f1abc4ae8b0eb2a3b40e82a310507281144c423/src/diffusers/models/transformers/transformer_flux.py#L600-L604
Suggested fix:
Add a guidance embedder or remove the unsupported config path. A small local fix would make the embedding signature explicit:
```python
def forward(self, timestep, hidden_states, addition_t_cond=None, guidance=None):
timesteps_emb = self.timestep_embedder(self.time_proj(timestep).to(dtype=hidden_states.dtype))
conditioning = timesteps_emb
if guidance is not None:
guidance_emb = self.guidance_embedder(self.time_proj(guidance).to(dtype=hidden_states.dtype))
conditioning = conditioning + guidance_emb
if self.addition_time_embedder is not None:
conditioning = conditioning + self.addition_time_embedder(addition_t_cond)
return conditioning
```
## Issue 3: Prompt masks are duplicated in the wrong order
Affected code:
https://github.com/huggingface/diffusers/blob/0f1abc4ae8b0eb2a3b40e82a310507281144c423/src/diffusers/pipelines/qwenimage/pipeline_qwenimage.py#L256-L264
Problem:
`prompt_embeds` are duplicated as `[p0, p0, p1, p1]`, but 2D `prompt_embeds_mask.repeat(1, num_images_per_prompt, 1).view(...)` produces `[p0, p1, p0, p1]`. The same pattern appears across QwenImage standard pipelines and modular inputs.
Impact:
For batched prompts with `num_images_per_prompt > 1`, text attention masks can be paired with the wrong prompt embeddings, causing incorrect conditioning.
Reproduction:
```python
import torch
from diffusers import QwenImagePipeline
pipe = object.__new__(QwenImagePipeline)
embeds = torch.arange(2 * 4, dtype=torch.float32).view(2, 4, 1)
mask = torch.tensor([[1, 1, 0, 0], [1, 0, 1, 0]], dtype=torch.bool)
expanded_embeds, expanded_mask = QwenImagePipeline.encode_prompt(
pipe,
prompt=["a", "b"],
device=torch.device("cpu"),
num_images_per_prompt=2,
prompt_embeds=embeds,
prompt_embeds_mask=mask,
max_sequence_length=4,
)
print(expanded_embeds[:, :, 0])
print(expanded_mask)
print(mask.repeat_interleave(2, dim=0))
```
Relevant precedent:
Related batching/mask reports exist in `#12075` and `#12458`, but neither is this exact mask-order bug.
Suggested fix:
```python
prompt_embeds = prompt_embeds.repeat_interleave(num_images_per_prompt, dim=0)
prompt_embeds_mask = prompt_embeds_mask.repeat_interleave(num_images_per_prompt, dim=0)
negative_prompt_embeds_mask = negative_prompt_embeds_mask.repeat_interleave(num_images_per_prompt, dim=0)
```
## Issue 4: Layered zero-conditioned transformer fails for batch size greater than one
Affected code:
https://github.com/huggingface/diffusers/blob/0f1abc4ae8b0eb2a3b40e82a310507281144c423/src/diffusers/models/transformers/transformer_qwenimage.py#L912-L938
Problem:
When `zero_cond_t=True`, `timestep` is doubled, but `additional_t_cond` is not. With `use_additional_t_cond=True`, the timestep embedding has batch `2B` while the additional condition embedding still has batch `B`.
Impact:
Layered QwenImage transformer variants fail for batched inputs.
Reproduction:
```python
import torch
from diffusers import QwenImageTransformer2DModel
model = QwenImageTransformer2DModel(
patch_size=1, in_channels=4, out_channels=4, num_layers=1,
attention_head_dim=4, num_attention_heads=1, joint_attention_dim=8,
axes_dims_rope=(2, 2, 4),
zero_cond_t=True,
use_additional_t_cond=True,
use_layer3d_rope=True,
)
model(
hidden_states=torch.randn(2, 8, 4),
encoder_hidden_states=torch.randn(2, 3, 8),
encoder_hidden_states_mask=torch.ones(2, 3, dtype=torch.bool),
timestep=torch.tensor([1.0, 1.0]),
img_shapes=[[(1, 2, 2), (1, 2, 2)], [(1, 2, 2), (1, 2, 2)]],
additional_t_cond=torch.tensor([0, 1], dtype=torch.long),
)
```
Relevant precedent:
No exact duplicate found.
Suggested fix:
```python
if self.zero_cond_t:
timestep = torch.cat([timestep, timestep * 0], dim=0)
if additional_t_cond is not None:
additional_t_cond = torch.cat([additional_t_cond, additional_t_cond], dim=0)
```
## Issue 5: Tiled QwenImage VAE decode skips output clamping
Affected code:
https://github.com/huggingface/diffusers/blob/0f1abc4ae8b0eb2a3b40e82a310507281144c423/src/diffusers/models/autoencoders/autoencoder_kl_qwenimage.py#L1030-L1033
Problem:
Regular `_decode` clamps decoded samples to `[-1, 1]`, but `tiled_decode` returns the blended tensor without clamping.
Impact:
The same latent can produce different value ranges depending on whether VAE tiling is enabled.
Reproduction:
```python
import torch
from diffusers import AutoencoderKLQwenImage
vae = AutoencoderKLQwenImage(
base_dim=4, z_dim=1, dim_mult=[1], num_res_blocks=1,
temperal_downsample=[], latents_mean=[0.0], latents_std=[1.0],
)
with torch.no_grad():
vae.decoder.conv_out.weight.zero_()
vae.decoder.conv_out.bias.fill_(2.0)
z = torch.zeros(1, 1, 1, 8, 8)
plain = vae.decode(z).sample
vae.enable_tiling(
tile_sample_min_height=4,
tile_sample_min_width=4,
tile_sample_stride_height=4,
tile_sample_stride_width=4,
)
tiled = vae.decode(z).sample
print(plain.max().item(), tiled.max().item())
```
Relevant precedent:
Wan’s tiled VAE decode clamps after tiling:
https://github.com/huggingface/diffusers/blob/0f1abc4ae8b0eb2a3b40e82a310507281144c423/src/diffusers/models/autoencoders/autoencoder_kl_wan.py#L1398-L1403
Suggested fix:
```python
dec = self.blend_v(a, b, blend_extent)
dec = torch.clamp(dec, min=-1.0, max=1.0)
return DecoderOutput(sample=dec)
```
## Issue 6: Tensor image inputs crash before preprocessing in edit-family pipelines
Affected code:
https://github.com/huggingface/diffusers/blob/0f1abc4ae8b0eb2a3b40e82a310507281144c423/src/diffusers/pipelines/qwenimage/pipeline_qwenimage_edit.py#L673-L675
Problem:
Several QwenImage image-conditioned pipelines read `image.size` as if it were a PIL tuple before preprocessing. For `torch.Tensor`, `image.size` is a method, so indexing it crashes.
Impact:
Documented tensor image inputs are rejected before the pipeline image processor can normalize them. The same pattern appears in edit-inpaint, edit-plus, layered, and modular encoders.
Reproduction:
```python
import torch
from diffusers import QwenImageEditPipeline
pipe = object.__new__(QwenImageEditPipeline)
QwenImageEditPipeline.__call__(
pipe,
image=torch.zeros(1, 3, 32, 32),
prompt_embeds=torch.zeros(1, 4, 8),
prompt_embeds_mask=torch.ones(1, 4, dtype=torch.bool),
true_cfg_scale=1.0,
num_inference_steps=1,
output_type="latent",
)
```
Relevant precedent:
Related batch/image handling work exists in `#12458` and `#12698`, but this tensor `.size` crash is broader.
Suggested fix:
```python
def _get_image_size(image):
if isinstance(image, torch.Tensor):
return int(image.shape[-1]), int(image.shape[-2])
return image.size
```
Use this helper before resizing logic and apply it consistently for list/tuple image inputs.
## Issue 7: `QwenImageLayeredPipeline(output_type="latent")` returns an undefined variable
Affected code:
https://github.com/huggingface/diffusers/blob/0f1abc4ae8b0eb2a3b40e82a310507281144c423/src/diffusers/pipelines/qwenimage/pipeline_qwenimage_layered.py#L872-L908
Problem:
The latent branch assigns `image = latents`, but the return path always returns `images`. `images` is only assigned in the decode branch.
Impact:
`output_type="latent"` raises `UnboundLocalError` instead of returning latents.
Reproduction:
```python
def same_tail(output_type, latents):
if output_type == "latent":
image = latents
else:
images = []
return images
same_tail("latent", object())
```
Relevant precedent:
Other QwenImage pipelines assign and return the same variable in the latent branch.
Suggested fix:
```python
if output_type == "latent":
images = latents
else:
latents = latents.to(self.vae.dtype)
latents_mean = ...
images = self.vae.decode(latents, return_dict=False)[0]
```
## Issue 8: Test coverage gaps for exported QwenImage variants
Affected code:
https://github.com/huggingface/diffusers/blob/0f1abc4ae8b0eb2a3b40e82a310507281144c423/src/diffusers/pipelines/qwenimage/__init__.py#L26-L34
https://github.com/huggingface/diffusers/blob/0f1abc4ae8b0eb2a3b40e82a310507281144c423/src/diffusers/modular_pipelines/qwenimage/__init__.py#L24-L32
Problem:
Fast tests exist for base, img2img, inpaint, edit, edit-plus, controlnet, transformer, LoRA, and several modular workflows. Missing coverage includes slow tests for the QwenImage family, fast standard tests for `QwenImageControlNetInpaintPipeline`, `QwenImageEditInpaintPipeline`, and `QwenImageLayeredPipeline`, direct model tests for `AutoencoderKLQwenImage` and `QwenImageControlNetModel`, and modular layered tests.
Impact:
Several exported public classes can regress without CI coverage. The missing layered tests would have caught the latent-return and batch additional-condition bugs above.
Reproduction:
```python
from pathlib import Path
pipeline_tests = {p.name for p in Path("tests/pipelines/qwenimage").glob("test_*.py")}
print(sorted(pipeline_tests))
for expected in [
"test_qwenimage_controlnet_inpaint.py",
"test_qwenimage_edit_inpaint.py",
"test_qwenimage_layered.py",
]:
assert expected in pipeline_tests, expected
slow_qwen_tests = [
str(p)
for p in Path("tests").rglob("*.py")
if "qwen" in str(p).lower()
and ("@slow" in p.read_text(errors="ignore") or "slow(" in p.read_text(errors="ignore"))
]
assert slow_qwen_tests, "no qwenimage slow tests found"
```
Relevant precedent:
Other pipeline families generally carry both fast dummy tests and at least one slow smoke test for public pipelines.
Suggested fix:
Add fast dummy tests for every exported standard and modular QwenImage pipeline variant, direct model tests for the VAE and ControlNet, and at least one `@slow` smoke test per public workflow class or shared slow test coverage that instantiates each exported variant.
Contributor guide
Research direction
Split the review into individual fixes and begin with the affected files under src/diffusers/pipelines/qwenimage, src/diffusers/models/transformers, and src/diffusers/models/autoencoders. Run the provided reproductions, then inspect the existing tests in tests/pipelines/qwenimage and the referenced model tests. Done means each reported failure is fixed and covered by a focused regression test, including the listed export and coverage gaps.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python, pytorch
- Domain
- machine-learning, testing-qa
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 42/100