huggingface / huggingface/diffusers
`qwenimage` model/pipeline review
- Lingua principale
- Python
- Stelle
- 34.5k
- Fork
- 7.3k
- Merge medio
- 3g 3h
- PR unite (30g)
- 91
Descrizione
# `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.
Guida per i contributori
Apri la guida per i contributori
Direzione di ricerca
Suddividi la revisione in correzioni individuali e inizia dai file interessati in src/diffusers/pipelines/qwenimage, src/diffusers/models/transformers e src/diffusers/models/autoencoders. Esegui le riproduzioni fornite, quindi esamina i test esistenti in tests/pipelines/qwenimage e i test dei modelli referenziati. Il lavoro è completato quando ogni errore segnalato è corretto e coperto da un test di regressione mirato, incluse le lacune di esportazione e copertura elencate.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Valutazione
- Stack tecnologico
- python, pytorch
- Ambito
- machine-learning, testing-qa
- Tipo di issue
- Bug
- Difficoltà
- 4/5
- Tempo stimato
- 3-5 giorni
- Stato di attività
- Tranquilla
- Chiarezza
- Abbastanza chiara
- Idoneità per principianti
- 42/100