Repository navigation
Open
Labels
bugSomething isn't workingSomething isn't working
Description
Describe the bug
QwenImage21Rope features a call to image_pad_mask.tolist(). When the transformer in the Qwen Image 2.1 pipeline is torch.compile-ed and then run, it causes the following error:
Unsupported: Tensor.tolist() with non-integer tensor
Explanation: Dynamo currently does not support tracing `tolist()` on non-integer tensors.
Hint: Ensure the input tensor to `tolist()` is an integer type (e.g., int8, int16, int32, int64).
Developer debug context: call_method TensorVariable() to_list
For more details about this graph break, please visit: https://meta-pytorch.github.io/compile-graph-break-site/gb/gb0109.html
from user code:
File "[redacted]\.venv\Lib\site-packages\diffusers\models\transformers\transformer_qwenimage21.py", line 925, in forward
rotary_emb = self.pos_embed(img_shapes[0], image_pad_mask, device=hidden_states.device)
File "[redacted]\.venv\Lib\site-packages\diffusers\models\transformers\transformer_qwenimage21.py", line 686, in forward
is_image_token = image_pad_mask.tolist()
Reproduction
import torch
from diffusers import QwenImage21Pipeline
pipe = QwenImage21Pipeline.from_pretrained(
"Qwen/Qwen-Image-2.1",
torch_dtype=torch.bfloat16,
).to("cuda")
# Some of this might be unrelated
torch._inductor.config.conv_1x1_as_mm = True
torch._inductor.config.coordinate_descent_tuning = True
torch._inductor.config.epilogue_fusion = False
torch._inductor.config.coordinate_descent_check_all_directions = True
torch._dynamo.config.capture_scalar_outputs = True
pipe.transformer.compile(
mode="max-autotune", fullgraph=True,
)
image = pipe(
prompt="The Mona Lisa or whatever.",
width=2048, height=2048,
num_inference_steps=40,
generator=torch.Generator("cuda").manual_seed(42),
).images[0]System Info
- 🤗 Diffusers version: 0.41.0.dev0
- Platform: Windows-11-10.0.26200-SP0
- Running on Google Colab?: No
- Python version: 3.14.2
- PyTorch version (GPU?): 2.14.0+cu130 (True)
- Huggingface_hub version: 1.32.0
- Transformers version: 5.17.0
- Accelerate version: 1.15.0
- PEFT version: not installed
- Safetensors version: 0.8.0
- xFormers version: not installed
- Accelerator: NVIDIA GeForce RTX 5090, 32607 MiB
- Using GPU in script?: yes
- Using distributed or parallel set-up in script?: no
Who can help?
@naykun @yiyixuxu , based on https://github.com/huggingface/diffusers/blame/main/src/diffusers/models/transformers/transformer_qwenimage21.py
Metadata
Metadata
Assignees
Labels
bugSomething isn't workingSomething isn't working
Activity
chinmayirm commentedon Sep 22, 2026
Hi @abel1502, thanks for the report and the repro.
I agree with the diagnosis.
pipe.transformer.compile(..., fullgraph=True)tracesQwenImage21Transformer2DModel.forward, which callsQwenImage21Rope.forward, and that doesis_image_token = image_pad_mask.tolist()(transformer_qwenimage21.pyaround line 686). The traceback matches the source onmain. The inductor flags in the repro are unrelated.Root cause:
image_pad_maskis bool. It istorch.repeat_interleave(img_mask[0], repeats), and the pipeline buildsimg_maskas a bool tensor. Dynamo only tracesTensor.tolist()forint8,int16,int32, andint64(gb0109), so a bool mask fails before the rest of the forward is traced.fullgraph=Trueturns that into a hard error. This isQwenImage21Rope, which runs for both attention processors.Casting the mask with
.to(torch.int32).tolist()only satisfies the dtype check. Integertolist()is lowered to one.item()per token, and the Pythonlist.index/rangewalk then branches on those values. That is still a host sync and still not one graph. This file already treatstolist()as a device sync in_qwenimage21_prefix_segments.The same forward has three more host-dependent spots, so fixing RoPE alone still leaves this repro unable to compile:
build_token_metadatausesnonzero, so the index vector length depends on mask values.use_kv_cachedefaults toTrue, so step 0 is"extract"and later steps are"cached".prefix_len = int((~target_token_mask).sum())syncs a scalar on every step and then slices with it.QwenImage21AttnProcessor,_qwenimage21_prefix_segmentscallstolist()on the whole prefix.compile_repeated_blocks()does not cover this. RoPE runs in the parentforward, and_repeated_blocksis onlyQwenImage21TransformerBlock.Proposed approach:
img_shapesalready gives each block's(frame, height, width)as Python ints. The mask only says where those blocks sit between text tokens. The current walk is:frame = height = width, advancing by 1max(height, width)Build that with static-shape tensor ops. Let
maskbe 0/1 andseq_len = mask.shape[0]:img_before = cumsum(mask) - masktext_before = cumsum(1 - mask) - (1 - mask)seenimage tokens have been consumed, addmax(height, width)whereverimg_before >= seenframe_index = text_before + that advanceChecked against the current Python walk for text/image/text, adjacent images, and a trailing text tail. For example, mask
F F T T T T Fwith one2x2block produces frame ids[0, 1, 2, 2, 2, 2, 4].The centered grid does not need the mask. Per block:
Repeat height across width and width down height, then concatenate in block order.
masked_scatterwrites that static vector onto theTruepositions.height_indexandwidth_indexstart as clones offrame_index, so text tokens keep the frame id on all three axes.torch._check(mask.sum() == image_token_count)keeps the scatter length static for Dynamo. Eager keeps today'sValueErrorwhenimg_shapesand the mask disagree.The other three spots, in the same change:
sum(math.prod(shape) for shape in img_shapes). Buildimage_idsandtarget_token_maskwithmasked_scatterof a static id vector, and use the sametorch._checkinstead ofnonzero.prefix_len = seq_len - math.prod(img_shapes[0][-1])matchesint((~target_token_mask).sum())with no.item().argmaxon the image cumsum. The SDPA loop stays a Python loop over that static count, with 0-dim tensor slice bounds. A zero-length text gap (two image blocks with no text between them, whichbuild_token_metadataalready calls out) is an empty slice, andtorch.catdrops it.QwenImage21Ropealso rebindsself.freqswith.to(device)on every forward. Those tables are fixed at init and are not buffers, so they stay unregistered.QwenEmbedRope._get_device_freqsalready caches this transfer behindlru_cache_unless_export. I would use that here so the first call moves them and later calls do not.Tests:
On the tiny config already in
tests/models/transformers/test_models_transformer_qwenimage21.py:torch.compile(model, fullgraph=True)matches eager, then a second call witherror_on_recompile=True.QwenImage21AttnProcessorandQwenImage21FlexAttnProcessor."extract"then"cached"underfullgraph=True.TorchCompileTesterMixinto this model. The Qwen-Image 1.x transformer tests already have it. This 2.1 file does not, which is consistent with compile being broken.I would be glad to open a PR for this once the approach looks right.