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
|
||||||
import torch.nn.functional as F
|
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(
|
def simple_attention_forward(
|
||||||
@@ -15,37 +15,45 @@ def simple_attention_forward(
|
|||||||
batch_size, seq_len, _ = hidden_states.shape
|
batch_size, seq_len, _ = hidden_states.shape
|
||||||
inner_dim = attn.inner_dim if hasattr(attn, 'inner_dim') else hidden_states.shape[-1]
|
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)
|
query = attn.to_q(hidden_states)
|
||||||
key = attn.to_k(hidden_states if encoder_hidden_states is None else encoder_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)
|
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
|
head_dim = inner_dim // attn.heads
|
||||||
query = query.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2)
|
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)
|
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)
|
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
|
scale = head_dim ** -0.5
|
||||||
attn_weights = query @ key.transpose(-2, -1) * scale
|
attn_weights = query @ key.transpose(-2, -1) * scale
|
||||||
attn_weights = F.softmax(attn_weights, dim=-1).to(query.dtype)
|
attn_weights = F.softmax(attn_weights, dim=-1).to(query.dtype)
|
||||||
hidden_states = attn_weights @ value
|
hidden_states = attn_weights @ value
|
||||||
|
|
||||||
# Reshape back
|
|
||||||
hidden_states = hidden_states.transpose(1, 2).reshape(batch_size, -1, attn.heads * head_dim)
|
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)
|
hidden_states = attn.to_out[0](hidden_states)
|
||||||
if len(attn.to_out) > 1:
|
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
|
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):
|
def patch_unet_attention(unet):
|
||||||
"""Replace all attention processors with simple matmul-based attention."""
|
"""Replace all attention processors with simple matmul-based attention."""
|
||||||
from diffusers.models.attention_processor import AttnProcessor
|
|
||||||
|
|
||||||
class SimpleAttnProcessor(AttnProcessor):
|
class SimpleAttnProcessor(AttnProcessor):
|
||||||
def __call__(
|
def __call__(
|
||||||
self,
|
self,
|
||||||
@@ -57,5 +65,15 @@ def patch_unet_attention(unet):
|
|||||||
) -> torch.Tensor:
|
) -> torch.Tensor:
|
||||||
return simple_attention_forward(attn, hidden_states, encoder_hidden_states, attention_mask, **kwargs)
|
return simple_attention_forward(attn, hidden_states, encoder_hidden_states, attention_mask, **kwargs)
|
||||||
|
|
||||||
processor = SimpleAttnProcessor()
|
unet.set_attn_processor(SimpleAttnProcessor())
|
||||||
unet.set_attn_processor(processor)
|
|
||||||
|
|
||||||
|
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 diffusers.models.transformers import QwenImageTransformer2DModel
|
||||||
from transformers import Qwen2_5_VLForConditionalGeneration, Qwen2Tokenizer, Qwen2VLProcessor
|
from transformers import Qwen2_5_VLForConditionalGeneration, Qwen2Tokenizer, Qwen2VLProcessor
|
||||||
|
|
||||||
|
from .attention import patch_qwen_transformer
|
||||||
|
|
||||||
|
|
||||||
TEXT_ENCODER_ID = "Qwen/Qwen2.5-VL-7B-Instruct"
|
TEXT_ENCODER_ID = "Qwen/Qwen2.5-VL-7B-Instruct"
|
||||||
VAE_ID = "Qwen/Qwen-Image"
|
VAE_ID = "Qwen/Qwen-Image"
|
||||||
@@ -89,21 +91,11 @@ def edit(
|
|||||||
transformer=transformer,
|
transformer=transformer,
|
||||||
)
|
)
|
||||||
pipe.to(device)
|
pipe.to(device)
|
||||||
|
patch_qwen_transformer(transformer)
|
||||||
|
|
||||||
t_load = time.perf_counter() - t0
|
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")
|
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)
|
generator = torch.Generator(device=device).manual_seed(seed)
|
||||||
|
|
||||||
t1 = time.perf_counter()
|
t1 = time.perf_counter()
|
||||||
|
|||||||
Reference in New Issue
Block a user