Patch Qwen transformer attention with matmul instead of SDPA

Monkey-patches dispatch_attention_fn to use simple Q@K^T@V matmul,
bypassing SDPA dispatch entirely. Enables editing at full 1024x1024
(was limited to 512x512 due to SDPA crashes) and is ~3x faster.
This commit is contained in:
Michele Rossi
2026-07-06 20:19:01 +02:00
parent c791b08689
commit 10d07d44b1
2 changed files with 33 additions and 23 deletions

View File

@@ -1,7 +1,7 @@
"""Custom attention processor that avoids SDPA on ROCm."""
"""Custom attention processors that avoid SDPA on ROCm."""
import torch
import torch.nn.functional as F
from diffusers.models.attention_processor import Attention
from diffusers.models.attention_processor import Attention, AttnProcessor
def simple_attention_forward(
@@ -15,37 +15,45 @@ def simple_attention_forward(
batch_size, seq_len, _ = hidden_states.shape
inner_dim = attn.inner_dim if hasattr(attn, 'inner_dim') else hidden_states.shape[-1]
# Project to Q, K, V
query = attn.to_q(hidden_states)
key = attn.to_k(hidden_states if encoder_hidden_states is None else encoder_hidden_states)
value = attn.to_v(hidden_states if encoder_hidden_states is None else encoder_hidden_states)
# Reshape to (batch, heads, seq, head_dim)
head_dim = inner_dim // attn.heads
query = query.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2)
key = key.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2)
value = value.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2)
# Attention: softmax(Q @ K^T / sqrt(d)) @ V
scale = head_dim ** -0.5
attn_weights = query @ key.transpose(-2, -1) * scale
attn_weights = F.softmax(attn_weights, dim=-1).to(query.dtype)
hidden_states = attn_weights @ value
# Reshape back
hidden_states = hidden_states.transpose(1, 2).reshape(batch_size, -1, attn.heads * head_dim)
# Output projection
hidden_states = attn.to_out[0](hidden_states)
if len(attn.to_out) > 1:
hidden_states = attn.to_out[1](hidden_states) # dropout
hidden_states = attn.to_out[1](hidden_states)
return hidden_states
def _matmul_attention(query, key, value, attn_mask, dropout_p, is_causal, backend, parallel_config):
"""Simple matmul attention replacing dispatch_attention_fn. Returns 4D tensor."""
batch_size, seq_len, num_heads, head_dim = query.shape
scale = head_dim ** -0.5
q = query.transpose(1, 2)
k = key.transpose(1, 2)
v = value.transpose(1, 2)
w = q @ k.transpose(-2, -1) * scale
if attn_mask is not None:
w = w + attn_mask
w = F.softmax(w, dim=-1).to(q.dtype)
out = w @ v
return out.transpose(1, 2)
def patch_unet_attention(unet):
"""Replace all attention processors with simple matmul-based attention."""
from diffusers.models.attention_processor import AttnProcessor
class SimpleAttnProcessor(AttnProcessor):
def __call__(
self,
@@ -57,5 +65,15 @@ def patch_unet_attention(unet):
) -> torch.Tensor:
return simple_attention_forward(attn, hidden_states, encoder_hidden_states, attention_mask, **kwargs)
processor = SimpleAttnProcessor()
unet.set_attn_processor(processor)
unet.set_attn_processor(SimpleAttnProcessor())
def patch_qwen_transformer(transformer):
"""Patch Qwen transformer to use matmul attention instead of SDPA."""
from diffusers.models.attention_dispatch import dispatch_attention_fn
# Monkey-patch the dispatch function
import diffusers.models.attention_dispatch as ad
ad.dispatch_attention_fn = _matmul_attention
# Also patch the module that imports it
import diffusers.models.transformers.transformer_qwenimage as tq
tq.dispatch_attention_fn = _matmul_attention

View File

@@ -14,6 +14,8 @@ from diffusers.models.autoencoders import AutoencoderKLQwenImage
from diffusers.models.transformers import QwenImageTransformer2DModel
from transformers import Qwen2_5_VLForConditionalGeneration, Qwen2Tokenizer, Qwen2VLProcessor
from .attention import patch_qwen_transformer
TEXT_ENCODER_ID = "Qwen/Qwen2.5-VL-7B-Instruct"
VAE_ID = "Qwen/Qwen-Image"
@@ -89,21 +91,11 @@ def edit(
transformer=transformer,
)
pipe.to(device)
patch_qwen_transformer(transformer)
t_load = time.perf_counter() - t0
# Resize large inputs to avoid GPU crashes. The Qwen transformer does
# O(N^2) attention on image patches. 512px is safe on this GPU.
input_image = Image.open(input_path).convert("RGB")
w, h = input_image.size
max_dim = 512
if w > max_dim or h > max_dim:
scale = max_dim / max(w, h)
new_w, new_h = int(w * scale), int(h * scale)
new_w = (new_w // 16) * 16
new_h = (new_h // 16) * 16
input_image = input_image.resize((new_w, new_h), Image.LANCZOS)
print(f"Resized input {w}x{h} -> {new_w}x{new_h}")
generator = torch.Generator(device=device).manual_seed(seed)
t1 = time.perf_counter()