z-image / src /utils /attention.py
royhsieh80's picture
Upload folder using huggingface_hub
df3bd3c verified
Raw
History Blame Contribute Delete
17.4 kB
"""Attention backend utilities for Z-Image."""
# Modified from https://github.com/huggingface/diffusers/blob/main/src/diffusers/models/attention_dispatch.py
from enum import Enum
import functools
import inspect
from typing import Callable, Dict, List, Optional, Union
import torch
import torch.nn.functional as F
from .import_utils import is_flash_attn_3_available, is_flash_attn_available, is_torch_version
_CAN_USE_FLASH_ATTN_2 = is_flash_attn_available()
_CAN_USE_FLASH_ATTN_3 = is_flash_attn_3_available()
# MPS Flash Attention (Apple Silicon)
try:
import mps_flash_attn
_CAN_USE_MPS_FLASH = mps_flash_attn.is_available()
except ImportError:
_CAN_USE_MPS_FLASH = False
mps_flash_attn = None
_TORCH_VERSION_CHECK = is_torch_version(">=", "2.5.0") # have enable_gqa func call in SPDA
if not _TORCH_VERSION_CHECK:
raise RuntimeError("PyTorch version must be >= 2.5.0 to use this backend.")
else:
print("PyTorch version is >= 2.5.0, check pass.")
if _CAN_USE_FLASH_ATTN_2:
from flash_attn import flash_attn_func, flash_attn_varlen_func
else:
flash_attn_func = None
flash_attn_varlen_func = None
if _CAN_USE_FLASH_ATTN_3:
from flash_attn_interface import (
flash_attn_func as flash_attn_3_func,
flash_attn_varlen_func as flash_attn_3_varlen_func,
)
_flash_attn_3_sig = inspect.signature(flash_attn_3_func)
_FLASH_ATTN_3_SUPPORTS_RETURN_PROBS = "return_attn_probs" in _flash_attn_3_sig.parameters
else:
flash_attn_3_func = None
flash_attn_3_varlen_func = None
_FLASH_ATTN_3_SUPPORTS_RETURN_PROBS = False
class AttentionBackend(str, Enum):
"""Supported attention backends."""
# Flash Attention
FLASH = "flash"
FLASH_VARLEN = "flash_varlen"
FLASH_3 = "_flash_3"
FLASH_VARLEN_3 = "_flash_varlen_3"
# MPS Flash Attention (Apple Silicon)
MPS_FLASH = "mps_flash"
# PyTorch Native Backends
NATIVE = "native"
NATIVE_FLASH = "_native_flash"
NATIVE_MATH = "_native_math"
@classmethod
def print_available_backends(cls):
available_backends = [backend.value for backend in cls.__members__.values()]
print(f"Available attention backends list: {available_backends}")
# Registry for attention implementations
_ATTENTION_BACKENDS: Dict[str, Callable] = {}
_ATTENTION_CONSTRAINTS: Dict[str, List[Callable]] = {}
def register_backend(name: str, constraints: Optional[List[Callable]] = None):
def decorator(func):
_ATTENTION_BACKENDS[name] = func
_ATTENTION_CONSTRAINTS[name] = constraints or []
return func
return decorator
# --- Checks ---
def _check_device_cuda(query: torch.Tensor, **kwargs) -> None:
if query.device.type != "cuda":
raise ValueError("Query must be on a CUDA device.")
def _check_qkv_dtype_bf16_or_fp16(query: torch.Tensor, **kwargs) -> None:
if query.dtype not in (torch.bfloat16, torch.float16):
raise ValueError("Query must be either bfloat16 or float16.")
def _check_device_mps(query: torch.Tensor, **kwargs) -> None:
if query.device.type != "mps":
raise ValueError("Query must be on MPS device.")
def _process_mask(attn_mask: Optional[torch.Tensor], dtype: torch.dtype):
if attn_mask is None:
return None
if attn_mask.ndim == 2:
attn_mask = attn_mask[:, None, None, :]
# Convert bool mask to float additive mask
if attn_mask.dtype == torch.bool:
# NOTE: We skip checking for all-True mask (torch.all) to avoid graph breaks in torch.compile
new_mask = torch.zeros_like(attn_mask, dtype=dtype)
new_mask.masked_fill_(~attn_mask, float("-inf"))
return new_mask
return attn_mask
def _normalize_attn_mask(attn_mask: torch.Tensor, batch_size: int, seq_len_k: int) -> torch.Tensor:
"""Normalize an attention mask to shape [batch_size, seq_len_k] (bool)."""
if attn_mask.dtype != torch.bool:
# Try to convert float mask back to bool if possible, or assume it's float mask
# For varlen flash attn, we strictly need bool mask indicating valid tokens
if torch.is_floating_point(attn_mask):
return attn_mask > -1 # Assuming -inf is masked
# raise ValueError(f"Attention mask must be of type bool, got {attn_mask.dtype}.")
if attn_mask.ndim == 1:
attn_mask = attn_mask.unsqueeze(0).expand(batch_size, seq_len_k)
elif attn_mask.ndim == 2:
if attn_mask.size(0) not in [1, batch_size]:
attn_mask = attn_mask.expand(batch_size, seq_len_k)
elif attn_mask.ndim == 3:
attn_mask = attn_mask.any(dim=1)
attn_mask = attn_mask.expand(batch_size, seq_len_k)
elif attn_mask.ndim == 4:
attn_mask = attn_mask.expand(batch_size, -1, -1, seq_len_k)
attn_mask = attn_mask.any(dim=(1, 2))
if attn_mask.shape != (batch_size, seq_len_k):
# Fallback reshape
return attn_mask.view(batch_size, seq_len_k)
return attn_mask
@functools.lru_cache(maxsize=128)
def _prepare_for_flash_attn_varlen_without_mask(
batch_size: int,
seq_len_q: int,
seq_len_kv: int,
device: Optional[torch.device] = None,
):
# Optimized to avoid Inductor "pointless_cumsum_replacement" crash and remove graph breaks
seqlens_q = torch.full((batch_size,), seq_len_q, dtype=torch.int32, device=device)
seqlens_k = torch.full((batch_size,), seq_len_kv, dtype=torch.int32, device=device)
cu_seqlens_q = torch.arange(batch_size + 1, dtype=torch.int32, device=device) * seq_len_q
cu_seqlens_k = torch.arange(batch_size + 1, dtype=torch.int32, device=device) * seq_len_kv
return (seqlens_q, seqlens_k), (cu_seqlens_q, cu_seqlens_k), (seq_len_q, seq_len_kv)
def _prepare_for_flash_attn_varlen_with_mask(
batch_size: int,
seq_len_q: int,
attn_mask: torch.Tensor,
device: Optional[torch.device] = None,
):
seqlens_q = torch.full((batch_size,), seq_len_q, dtype=torch.int32, device=device)
seqlens_k = attn_mask.sum(dim=1, dtype=torch.int32)
# Use arange for Q to avoid Inductor crash
cu_seqlens_q = torch.arange(batch_size + 1, dtype=torch.int32, device=device) * seq_len_q
cu_seqlens_k = torch.zeros(batch_size + 1, dtype=torch.int32, device=device)
cu_seqlens_k[1:] = torch.cumsum(seqlens_k, dim=0)
max_seqlen_q = seq_len_q
max_seqlen_k = attn_mask.shape[1] # not max().item(), static shape to avoid graph break
return (seqlens_q, seqlens_k), (cu_seqlens_q, cu_seqlens_k), (max_seqlen_q, max_seqlen_k)
def _prepare_for_flash_attn_varlen(
batch_size: int,
seq_len_q: int,
seq_len_kv: int,
attn_mask: Optional[torch.Tensor] = None,
device: Optional[torch.device] = None,
) -> None:
if attn_mask is None:
return _prepare_for_flash_attn_varlen_without_mask(batch_size, seq_len_q, seq_len_kv, device)
return _prepare_for_flash_attn_varlen_with_mask(batch_size, seq_len_q, attn_mask, device)
@register_backend(AttentionBackend.FLASH, constraints=[_check_device_cuda, _check_qkv_dtype_bf16_or_fp16])
def _flash_attention(
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attn_mask: Optional[torch.Tensor] = None,
dropout_p: float = 0.0,
is_causal: bool = False,
scale: Optional[float] = None,
) -> torch.Tensor:
if not _CAN_USE_FLASH_ATTN_2:
raise RuntimeError(
f"Flash Attention backend '{AttentionBackend.FLASH}' is not usable because of missing package."
)
out = flash_attn_func(
q=query,
k=key,
v=value,
dropout_p=dropout_p,
softmax_scale=scale,
causal=is_causal,
)
return out
@register_backend(AttentionBackend.FLASH_VARLEN, constraints=[_check_device_cuda, _check_qkv_dtype_bf16_or_fp16])
def _flash_varlen_attention(
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attn_mask: Optional[torch.Tensor] = None,
dropout_p: float = 0.0,
is_causal: bool = False,
scale: Optional[float] = None,
) -> torch.Tensor:
if not _CAN_USE_FLASH_ATTN_2:
raise RuntimeError(f"Backend '{AttentionBackend.FLASH_VARLEN}' requires flash-attn.")
batch_size, seq_len_q, _, _ = query.shape
_, seq_len_kv, _, _ = key.shape
if attn_mask is not None:
attn_mask = _normalize_attn_mask(attn_mask, batch_size, seq_len_kv)
(_, seqlens_k), (cu_seqlens_q, cu_seqlens_k), (max_seqlen_q, max_seqlen_k) = _prepare_for_flash_attn_varlen(
batch_size, seq_len_q, seq_len_kv, attn_mask=attn_mask, device=query.device
)
query_packed = query.flatten(0, 1)
if attn_mask is not None:
key_valid = []
value_valid = []
for b in range(batch_size):
valid_len = seqlens_k[b]
key_valid.append(key[b, :valid_len])
value_valid.append(value[b, :valid_len])
key_packed = torch.cat(key_valid, dim=0)
value_packed = torch.cat(value_valid, dim=0)
else:
key_packed = key.flatten(0, 1)
value_packed = value.flatten(0, 1)
out = flash_attn_varlen_func(
q=query_packed,
k=key_packed,
v=value_packed,
cu_seqlens_q=cu_seqlens_q,
cu_seqlens_k=cu_seqlens_k,
max_seqlen_q=max_seqlen_q,
max_seqlen_k=max_seqlen_k,
dropout_p=dropout_p,
softmax_scale=scale,
causal=is_causal,
)
out = out.unflatten(0, (batch_size, -1))
return out
@register_backend(AttentionBackend.FLASH_3, constraints=[_check_device_cuda, _check_qkv_dtype_bf16_or_fp16])
def _flash_attention_3(
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attn_mask: Optional[torch.Tensor] = None, # Unused in simple FA3 func
dropout_p: float = 0.0,
is_causal: bool = False,
scale: Optional[float] = None,
) -> torch.Tensor:
if not _CAN_USE_FLASH_ATTN_3:
raise RuntimeError(f"Backend '{AttentionBackend.FLASH_3}' requires Flash Attention 3 beta.")
kwargs = {
"q": query,
"k": key,
"v": value,
"softmax_scale": scale,
"causal": is_causal,
}
if _FLASH_ATTN_3_SUPPORTS_RETURN_PROBS:
kwargs["return_attn_probs"] = False
out = flash_attn_3_func(**kwargs)
if isinstance(out, tuple):
out = out[0]
return out
@register_backend(AttentionBackend.FLASH_VARLEN_3, constraints=[_check_device_cuda, _check_qkv_dtype_bf16_or_fp16])
def _flash_varlen_attention_3(
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attn_mask: Optional[torch.Tensor] = None,
dropout_p: float = 0.0,
is_causal: bool = False,
scale: Optional[float] = None,
) -> torch.Tensor:
if not _CAN_USE_FLASH_ATTN_3:
raise RuntimeError(f"Backend '{AttentionBackend.FLASH_VARLEN_3}' requires Flash Attention 3 beta.")
batch_size, seq_len_q, _, _ = query.shape
_, seq_len_kv, _, _ = key.shape
if attn_mask is not None:
attn_mask = _normalize_attn_mask(attn_mask, batch_size, seq_len_kv)
(_, seqlens_k), (cu_seqlens_q, cu_seqlens_k), (max_seqlen_q, max_seqlen_k) = _prepare_for_flash_attn_varlen(
batch_size, seq_len_q, seq_len_kv, attn_mask=attn_mask, device=query.device
)
query_packed = query.flatten(0, 1)
if attn_mask is not None:
key_valid = []
value_valid = []
for b in range(batch_size):
valid_len = seqlens_k[b]
key_valid.append(key[b, :valid_len])
value_valid.append(value[b, :valid_len])
key_packed = torch.cat(key_valid, dim=0)
value_packed = torch.cat(value_valid, dim=0)
else:
key_packed = key.flatten(0, 1)
value_packed = value.flatten(0, 1)
kwargs = {
"q": query_packed,
"k": key_packed,
"v": value_packed,
"cu_seqlens_q": cu_seqlens_q,
"cu_seqlens_k": cu_seqlens_k,
"max_seqlen_q": max_seqlen_q,
"max_seqlen_k": max_seqlen_k,
"softmax_scale": scale,
"causal": is_causal,
}
supports_return_probs = "return_attn_probs" in inspect.signature(flash_attn_3_varlen_func).parameters
if supports_return_probs:
kwargs["return_attn_probs"] = False
out = flash_attn_3_varlen_func(**kwargs)
if isinstance(out, tuple):
out = out[0]
out = out.unflatten(0, (batch_size, -1))
return out
@register_backend(AttentionBackend.MPS_FLASH, constraints=[_check_device_mps, _check_qkv_dtype_bf16_or_fp16])
def _mps_flash_attention(
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attn_mask: Optional[torch.Tensor] = None,
dropout_p: float = 0.0,
is_causal: bool = False,
scale: Optional[float] = None,
) -> torch.Tensor:
"""MPS Flash Attention for Apple Silicon (M1/M2/M3/M4)."""
if not _CAN_USE_MPS_FLASH:
raise RuntimeError(
f"MPS Flash Attention backend '{AttentionBackend.MPS_FLASH}' requires mps-flash-attn package. "
"Install with: pip install mps-flash-attn"
)
# Convert from (B, S, H, D) to (B, H, S, D) for mps-flash-attn
query = query.transpose(1, 2)
key = key.transpose(1, 2)
value = value.transpose(1, 2)
# Convert mask to MFA format (bool, True = masked)
mfa_mask = None
if attn_mask is not None:
mfa_mask = mps_flash_attn.convert_mask(_process_mask(attn_mask, query.dtype))
out = mps_flash_attn.flash_attention(
query, key, value,
is_causal=is_causal,
scale=scale,
attn_mask=mfa_mask,
)
# Convert back to (B, S, H, D)
return out.transpose(1, 2).contiguous()
def _native_attention_wrapper(
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attn_mask: Optional[torch.Tensor] = None,
dropout_p: float = 0.0,
is_causal: bool = False,
scale: Optional[float] = None,
backend_kernel=None,
) -> torch.Tensor:
query = query.transpose(1, 2)
key = key.transpose(1, 2)
value = value.transpose(1, 2)
attn_mask = _process_mask(attn_mask, query.dtype)
if backend_kernel is not None:
with torch.nn.attention.sdpa_kernel(backend_kernel):
out = F.scaled_dot_product_attention(
query, key, value, attn_mask=attn_mask, dropout_p=dropout_p, is_causal=is_causal, scale=scale
)
else:
out = F.scaled_dot_product_attention(
query, key, value, attn_mask=attn_mask, dropout_p=dropout_p, is_causal=is_causal, scale=scale
)
return out.transpose(1, 2).contiguous()
@register_backend(AttentionBackend.NATIVE_FLASH)
def _native_flash_attention(
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attn_mask: Optional[torch.Tensor] = None,
dropout_p: float = 0.0,
is_causal: bool = False,
scale: Optional[float] = None,
) -> torch.Tensor:
return _native_attention_wrapper(
query,
key,
value,
attn_mask=None,
dropout_p=dropout_p,
is_causal=is_causal,
scale=scale,
backend_kernel=torch.nn.attention.SDPBackend.FLASH_ATTENTION,
)
@register_backend(AttentionBackend.NATIVE_MATH)
def _math_attention(*args, **kwargs):
return _native_attention_wrapper(*args, **kwargs, backend_kernel=torch.nn.attention.SDPBackend.MATH)
@register_backend(AttentionBackend.NATIVE)
def _native_attention(*args, **kwargs):
return _native_attention_wrapper(*args, **kwargs, backend_kernel=None)
def dispatch_attention(
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attn_mask: Optional[torch.Tensor] = None,
dropout_p: float = 0.0,
is_causal: bool = False,
scale: Optional[float] = None,
backend: Union[str, AttentionBackend, None] = None,
) -> torch.Tensor:
if isinstance(backend, AttentionBackend):
backend = backend.value
elif backend is None:
backend = AttentionBackend.NATIVE
else:
backend = str(backend)
# Explicit dispatch to avoid dynamo guard issues on global dict
if backend == AttentionBackend.FLASH:
return _flash_attention(query, key, value, attn_mask, dropout_p, is_causal, scale)
elif backend == AttentionBackend.FLASH_VARLEN:
return _flash_varlen_attention(query, key, value, attn_mask, dropout_p, is_causal, scale)
elif backend == AttentionBackend.FLASH_3:
return _flash_attention_3(query, key, value, attn_mask, dropout_p, is_causal, scale)
elif backend == AttentionBackend.FLASH_VARLEN_3:
return _flash_varlen_attention_3(query, key, value, attn_mask, dropout_p, is_causal, scale)
elif backend == AttentionBackend.MPS_FLASH:
return _mps_flash_attention(query, key, value, attn_mask, dropout_p, is_causal, scale)
elif backend == AttentionBackend.NATIVE_FLASH:
return _native_flash_attention(query, key, value, attn_mask, dropout_p, is_causal, scale)
elif backend == AttentionBackend.NATIVE_MATH:
return _math_attention(query, key, value, attn_mask, dropout_p, is_causal, scale)
else:
return _native_attention(query, key, value, attn_mask, dropout_p, is_causal, scale)
def set_attention_backend(backend: Union[str, AttentionBackend, None]):
try:
from zimage.transformer import ZImageAttention
if backend is not None:
backend = str(backend)
ZImageAttention._attention_backend = backend
except ImportError:
pass