Conversation
Ascend NPU (like AWS Neuron) cannot execute torch's complex64 ops, so Qwen-Image, Qwen-Image 2.1 and Z-Image crash on it (huggingface#12668). - Qwen-Image: register "npu" in the existing ROPE_PER_DEVICE dispatch next to Neuron (huggingface#13718); factor the backend list into COMPLEX_LESS_ROPE_BACKENDS. - Qwen-Image 2.1: add apply_rotary_emb_qwen_real, picked by carrier dtype; QwenImage21Rope keeps its complex64 table on CPU, runs the per-request selection there, and returns view_as_real pairs whose operands are bit-exact to the complex path's. - Z-Image: hoist the processor RoPE helper to module level with the same dtype-driven dual path; RopeEmbedder uses the same CPU-selection scheme. controlnet_z_image re-imports the shared pieces (fix-copies). The complex path is untouched and stays the default everywhere. New tests pin the real fallback against it on CPU: frequency selection is bit-exact, the rotation matches within float32 rounding.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Title: Extend the real-valued RoPE fallback to complex-less backends (Qwen-Image 2.1, Z-Image; register NPU in Qwen-Image)
What does this PR do?
The Qwen-Image-lineage RoPE (
torch.polarfrequency table → complex64 index/mulin
apply_rotary_emb_qwen(..., use_real=False)) only works on backends withcomplex64 kernels. On backends without them it is a hard failure, not a slowdown:
index/mul;QwenImage21Rope.forwardfails at runtime withaclnnIndex EZ1001the momentthe complex frequency table is indexed on device. Qwen-Image-2.1 currently
cannot run on Ascend NPUs without an out-of-tree monkey-patch.
torch.polarhas no MIL equivalent(RoPE implemetation, Torch.polar op not available in MIL requesting support apple/coremltools#2258).
is unaffected, which is why this has stayed low-pressure upstream.
#13718 already solved exactly this for AWS Neuron: a numerically equivalent
real-valued path (
apply_rotary_emb_qwen_neuron), a per-device dispatch(
ROPE_PER_DEVICE), and angle extraction at the device boundary(
_get_device_freqs,torch.anglewhile the table is still on CPU). However,the two pipelines added since never received the dispatch:
transformer_qwenimage21.pystill hardcodestorch.polar+ on-device complexindexing +
use_real=False,transformer_z_image.pylikewise (torch.polar,view_as_complex).This PR extends the established Neuron pattern instead of introducing a new one:
transformer_qwenimage21.py): portROPE_PER_DEVICEand reuse the real-valued apply. The per-request frequency selection in
QwenImage21Rope.forwardkeeps the complex table on CPU, indexes there, andcrosses the device boundary as reals — see the parity section below.
transformer_z_image.py): same port — the attentionprocessor's RoPE helper moves to module level as
apply_rotary_emb_zimage(dtype-driven dual path), and
RopeEmbedderkeeps its tables on CPU,indexes there, and returns
view_as_realpairs. The# Copied fromblocksin
controlnet_z_image.pypick this up viamake fix-copies, importing theshared helper/constant from
transformer_z_image(same pattern ascontrolnet_qwenimage.py).transformer_qwenimage.py): register"npu"in the existingROPE_PER_DEVICE, reusing the Neuron real path unchanged.Scope note, deliberately: this does not flip the CUDA default to the
real-valued path, so #12668's torch.compile aspect on CUDA is mitigated for
complex-less backends but not closed. Making the real path the universal default
(numerically equivalent, inductor-friendly) is a reasonable follow-up; kept out
here to keep the diff minimal and behavior-neutral on the reference path.
Related: #12668 (does not fully fix — see above), apple/coremltools#2258.
Numerical parity (measured, CPU, torch 2.14.0)
Two real-valued formulations are available at the device boundary:
torch.angle(table)on CPU →cos/sinon device.Measured to add up to 1.19e-07 error from the
atan2round-trip.complex table on CPU, then
torch.view_as_real. Sincetorch.polar(1, θ)stores exactly
(cos θ, sin θ), the extracted operands are bit-identicalto the complex path's — the selected frequency rows compare at 0 ulp.
The rotation arithmetic itself expands identically in both paths:
(xr + i·xi)(cos + i·sin) = (xr·cos − xi·sin) + i(xr·sin + xi·cos)but torch's complex64 multiply kernel contracts FMAs differently from explicit
real multiply-add, so outputs differ by ≤ 0.82 ulp of the fp32 compute domain
(both paths upcast with
x.float()). Measured: max 4.77e-07 acrossfp64/fp32/bf16 inputs at shapes up to [2, 4099, 8, 128]. For reference,
upstream's own
use_real=Truepath shows the same ≤ 0.82 ulp relationship tothe complex path — the candidate is exactly as exact as the real-valued RoPE
diffusers already ships for flux et al.
Happy to switch to
torch.anglefor consistency with #13718 if reviewers preferone formulation; the parity test covers either choice.
Hardware validation
patch has been run against Qwen-Image-2.1 BF16 — RoPE outputs match the
complex reference to ≤ 1 ulp, and the pipeline produces expected outputs at
1024×1024 / 40 steps.
upstream module as reference — no re-implementation): covers the apply math
(fp64/fp32/bf16 × 3 shapes), the full
QwenImage21Rope.forwardselection(t2i / two-image edit / trailing text), and the
torch.angleround-tripcomparison. All rows PASS at ≤ 1 ulp; the forward selection is bit-exact.
Before submitting
self-reviewskill on the diff?Self-review notes (final round)
controlnet_z_image.pycopy-syncsZSingleStreamAttnProcessor/RopeEmbedder; propagated withpython utils/check_copies.py --fix_and_overwrite(no hand-edited copiedblocks) and
check_copiespasses.device on every call; the fallback keeps that shape (target = CPU for
complex-less backends), so a table cached for a complex-capable device is
moved back if a later call targets a complex-less one (offload transitions).
ruff check,ruff format --check,check_copies, the three newparity tests, and the full transformer test files for the three models —
pass/fail sets identical to the base commit in this environment (the 32
pre-existing failures are group-offloading/disk tests, unrelated to RoPE).
note);
apply_rotary_emb_qwen_neuronis reused rather than renamed to ageneric
*_real— a rename touches [core] Support tensor parallelism for model inference (CUDA, Neuron) #13718's public-ish surface for cosmeticgain, left to reviewer preference.
"mps"dispatch entry. PyTorch MPS also lacks fullcomplex64, so it is a natural one-line addition once someone with the
hardware confirms it.
QwenImage21Rope.forward's CPU-side indexing mirrors thecurrent upstream logic 1:1 (including the Python-loop position bookkeeping);
the diff only changes where it runs and the dtype crossing the boundary.
_qwenimage21_prepare_qkv, which all KV-cache modes also route through.Who can review?
@sayakpaul (engaged on #12668), @dg845 (pipelines/models; prior Qwen-Image RoPE
work), @JingyaHuang (author of #13718 — this PR extends your Neuron pattern).
Appendix: parity harness and log
upstream_pr_rope_parity.py(pure CPU; imports the pinned upstream module as reference)Full log (all PASS)