huggingface / huggingface/diffusers
`qwenimage` model/pipeline review
- Vorherrschende Sprache
- Python
- Sterne
- 34.5k
- Forks
- 7.3k
- Ø Merge
- 3 T. 3 Std.
- Gemergte PRs (30 T.)
- 91
Beschreibung
# `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.
Beitragsleitfaden
Rechercherichtung
Teile die Überprüfung in einzelne Korrekturen auf und beginne mit den betroffenen Dateien unter src/diffusers/pipelines/qwenimage, src/diffusers/models/transformers und src/diffusers/models/autoencoders. Führe die bereitgestellten Reproduktionen aus und untersuche anschließend die vorhandenen Tests in tests/pipelines/qwenimage sowie die referenzierten Modelltests. Als erledigt gilt die Aufgabe, wenn jeder gemeldete Fehler behoben und durch einen fokussierten Regressionstest abgedeckt ist, einschließlich der aufgeführten Export- und Abdeckungslücken.
Vom Indexierungsmodell aus dem Issue-Text verfasst.
Bewertung
- Tech-Stack
- python, pytorch
- Bereich
- machine-learning, testing-qa
- Issue-Typ
- Bug
- Schwierigkeit
- 4/5
- Geschätzter Aufwand
- 3-5 Tage
- Aktivitätsstatus
- Ruhig
- Klarheit
- Größtenteils klar
- Anfängerfreundlichkeit
- 42/100