# SPDX-License-Identifier: AGPL-3.0-only # Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 """Select the diffusion transformer's attention backend. diffusers exposes a unified ``transformer.set_attention_backend(name)`` dispatcher that swaps the scaled-dot-product-attention kernel, validating hardware/package requirements at set time and otherwise leaving the default (``native`` = ``F.scaled_dot_product_attention``). Attention is memory-bandwidth bound, so a better kernel is a real end-to-end win that is orthogonal to the linear-weight quantisation (it speeds the QK/PV matmuls torchao never touches) and composes with torch.compile. auto - the best *exact* (non-quantized) backend for the device. On NVIDIA CUDA that is cuDNN's fused attention (``_native_cudnn``), measured ~1.18x end-to-end on a B200 with LPIPS ~0.004 vs the default (below the compile/quant noise floor). On AMD/Intel/Apple/CPU it stays ``native`` (the dispatcher already routes those). ``auto`` only upgrades when a speed profile is active, so ``speed_mode=off`` stays bit-identical. native - force the default SDPA (bit-identical reference). cudnn - cuDNN fused attention (exact; NVIDIA). flash / flash3 / flash4 - FlashAttention 2 / 3 (Hopper) / 4 (SM100); exact, kernel-gated. sage - SageAttention (INT8 QK); quantized, a small quality cost, consumer-friendly. xformers / aiter - memory-efficient (NVIDIA) / AITER (AMD ROCm). Best-effort: an unavailable backend (missing kernel / wrong arch) is caught and the load falls back to the diffusers default rather than failing. torch/diffusers imported lazily. """ from __future__ import annotations from typing import Any, Optional ATTN_AUTO = "auto" ATTN_NATIVE = "native" # User-facing alias -> the diffusers dispatcher backend name. _ALIASES: dict[str, str] = { "native": "native", "sdpa": "native", "cudnn": "_native_cudnn", "flash": "flash", "flash2": "flash", "flash3": "_flash_3_hub", "flash4": "flash_4_hub", "sage": "sage", "xformers": "xformers", "aiter": "aiter", } ATTN_ALIASES = (ATTN_AUTO,) + tuple(dict.fromkeys(_ALIASES)) def normalize_attention_backend(value: Optional[str]) -> Optional[str]: """Lower/strip a requested attention backend; None / "" / "auto" -> "auto". Raises ValueError for an unsupported alias so a bad request is rejected cheaply.""" if value is None: return ATTN_AUTO normalized = str(value).strip().lower() if not normalized: return ATTN_AUTO if normalized not in ATTN_ALIASES: raise ValueError( f"Unsupported attention_backend '{value}'. Use one of: {', '.join(ATTN_ALIASES)}." ) return normalized # Backends diffusers validates only by *package* at set time (``_check_attention_backend_ # requirements`` checks the ``kernels`` install, not the GPU), but whose kernels need a # specific CUDA arch at run time -- so an explicit request on the wrong card loads/sets fine # and then crashes mid-generation. Gate them up front by a (min, max-exclusive) compute # capability range. FlashAttention 3 is a Hopper-SM90 rewrite with no Blackwell kernel, so it # needs an upper bound: an explicit flash3 on a B200 (SM100) must drop to native instead of # setting fine then crashing at generation. FlashAttention 4 is Blackwell+ (no upper bound). _ARCH_CAPABILITY: dict[str, tuple[tuple[int, int], Optional[tuple[int, int]]]] = { "_flash_3_hub": ((9, 0), (10, 0)), # FlashAttention 3 -> Hopper (SM90) only "flash_4_hub": ((10, 0), None), # FlashAttention 4 -> Blackwell (SM100)+ } def _cuda_capability() -> Optional[tuple[int, int]]: """(major, minor) compute capability of the active CUDA device, or None if unknown.""" try: import torch if not torch.cuda.is_available(): return None return tuple(torch.cuda.get_device_capability()) # type: ignore[return-value] except Exception: # noqa: BLE001 return None def _backend_arch_supported(backend: str) -> bool: """False only when ``backend`` needs a CUDA arch outside this device's supported range. Unknown capability (no CUDA / detection failure) returns True so we never block on a guess -- diffusers' own set-time check still guards the package, and a genuine run-time failure falls back to native.""" bounds = _ARCH_CAPABILITY.get(backend) if bounds is None: return True have = _cuda_capability() if have is None: return True low, high = bounds return have >= low and (high is None or have < high) def _is_cuda_nvidia(target: Any) -> bool: """CUDA device on an NVIDIA (non-ROCm) build -- where cuDNN attention applies.""" if getattr(target, "device", None) != "cuda": return False try: import torch return getattr(torch.version, "hip", None) is None except Exception: # noqa: BLE001 return False def select_attention_backend( target: Any, requested: Optional[str], *, speed_active: bool ) -> Optional[str]: """The dispatcher backend name to apply, or None to leave the diffusers default. An explicit alias is honored verbatim (apply falls back if its kernel is unavailable). ``auto`` upgrades to cuDNN on NVIDIA CUDA only when a speed profile is active (so ``off`` stays bit-identical); everywhere else it returns None (native default).""" alias = normalize_attention_backend(requested) if alias != ATTN_AUTO: backend = _ALIASES[alias] if backend == "native": return None # Every explicit kernel here (cuDNN / flash* / sage) is CUDA+NVIDIA-only; on # ROCm / MPS / CPU diffusers accepts the name at set time and the first # generation crashes, so drop to the native default up front. if not _is_cuda_nvidia(target): return None # An arch-gated kernel (flash3/flash4) on a card that can't run it would set fine # then crash mid-generation, so drop it to the native default up front. if not _backend_arch_supported(backend): return None # cuDNN fused SDPA needs Ampere+ (SM80); diffusers accepts it on pre-SM80 cards # (T4/V100) then fails at the first generation, so apply the same gate to an # explicit cuDNN request as the auto path already does. if backend == "_native_cudnn" and not _cudnn_attention_supported(): return None return backend # auto if speed_active and _is_cuda_nvidia(target) and _cudnn_attention_supported(): return "_native_cudnn" return None def _cudnn_attention_supported() -> bool: """cuDNN fused SDPA needs Ampere+ (SM80). On pre-SM80 NVIDIA cards (T4 SM75 / V100 SM70) diffusers accepts ``_native_cudnn`` at set time but the kernel fails at the first generation, so gate the auto-cuDNN upgrade on capability. Unknown capability allows it (diffusers' set-time check + the run-time fallback still guard).""" have = _cuda_capability() return have is None or have >= (8, 0) def apply_attention_backend( pipe: Any, backend: Optional[str], *, logger: Any = None, ) -> Optional[str]: """Set ``backend`` on ``pipe.transformer`` via the diffusers dispatcher. Returns the backend actually engaged, or None when left at the native default (either because ``backend`` was None or because the requested kernel was unavailable -> graceful fallback, never a load failure). diffusers keeps a *process-wide* active attention backend that ``set_attention_backend`` also updates, and a fresh transformer's processors follow it (their ``_attention_backend`` defaults to None). So a load that wants native must restore it explicitly: otherwise it silently inherits a backend an earlier load pinned (e.g. cuDNN under a speed profile), breaking the bit-identical/``off`` guarantee. Best-effort throughout.""" transformer = getattr(pipe, "transformer", None) fn = getattr(transformer, "set_attention_backend", None) if not callable(fn): return None if backend is not None: try: fn(backend) # set_attention_backend also pins the backend in diffusers' process-wide # registry. This transformer's own processors keep it locally (their # _attention_backend is now explicit), so reset the global default back to # native -- otherwise a later component whose processors are unconfigured # (backend None) silently inherits this kernel. _reset_global_backend_to_native(logger) if logger is not None: logger.info("diffusion.attention: backend=%s", backend) return backend except Exception as exc: # noqa: BLE001 — unavailable kernel -> restore native below _warn(logger, backend, exc) # No backend requested, or the requested one failed: pin the native default so a stale # process-wide backend from a previous load can't leak into this one. _restore_native_backend(fn, logger) return None def _active_attention_backend() -> Optional[str]: """The diffusers process-wide active attention backend name, or None if undeterminable.""" try: from diffusers.models.attention_dispatch import _AttentionBackendRegistry # get_active_backend() returns a (AttentionBackendName, fn) tuple (or None), so # take element 0 and read its .value (e.g. "native"); reading .value off the # tuple itself would yield a junk string that never compares equal to a name. active = _AttentionBackendRegistry.get_active_backend() if active is None: return None name = active[0] if isinstance(active, tuple) else active return getattr(name, "value", str(name)) except Exception: # noqa: BLE001 return None def _reset_global_backend_to_native(logger: Any) -> None: """Reset diffusers' process-wide active attention backend to native after a successful per-transformer set, so a later component whose processors are unconfigured (backend None) does not inherit this transformer's kernel. The transformer's own processors keep the backend just set. Best-effort and silent: if the diffusers internals move, the prior (leaking) behavior is unchanged.""" if _active_attention_backend() == ATTN_NATIVE: return try: from diffusers.models.attention_dispatch import ( AttentionBackendName, _AttentionBackendRegistry, ) _AttentionBackendRegistry.set_active_backend(AttentionBackendName.NATIVE) except Exception: # noqa: BLE001 — best-effort; leave the global as-is on any change pass def _restore_native_backend(set_backend_fn: Any, logger: Any) -> None: """Force the native default when the global active backend isn't already native.""" if _active_attention_backend() == ATTN_NATIVE: return # already native -> avoid redundant work and an extra dispatcher warning try: set_backend_fn(ATTN_NATIVE) except Exception as exc: # noqa: BLE001 — best-effort restore _warn(logger, ATTN_NATIVE, exc) def _warn(logger: Any, what: str, exc: Exception) -> None: if logger is not None: logger.warning("diffusion.attention: %s unavailable (%s); using default", what, exc)