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:
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user