Signed-off-by: zwhzzz0821 <zwhzzz0821@users.noreply.github.com> Co-authored-by: zwhzzz0821 <zwhzzz0821@users.noreply.github.com>
376 lines
16 KiB
Python
376 lines
16 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
# Copyright (c) Microsoft Corporation and Jiarui Fang
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
# DeepSpeed Team & Jiarui Fang
|
|
# Adapted from
|
|
# https://github.com/feifeibear/long-context-attention/blob/main/yunchang/attention/layer.py
|
|
|
|
|
|
from dataclasses import replace
|
|
|
|
import torch
|
|
import torch.nn as nn
|
|
from vllm.config import VllmConfig, set_current_vllm_config
|
|
from vllm.logger import init_logger
|
|
from vllm.model_executor.models.utils import extract_layer_index
|
|
from vllm.v1.kv_cache_interface import FullAttentionSpec, KVCacheSpec
|
|
|
|
from vllm_omni.diffusion.attention.backends.abstract import AttentionMetadata
|
|
from vllm_omni.diffusion.attention.backends.sdpa import SDPABackend
|
|
from vllm_omni.diffusion.attention.parallel import build_parallel_attention_strategy
|
|
from vllm_omni.diffusion.attention.parallel.base import NoParallelAttention
|
|
from vllm_omni.diffusion.attention.parallel.ring import RingParallelAttention
|
|
from vllm_omni.diffusion.attention.selector import get_attn_backend_for_role
|
|
from vllm_omni.diffusion.config import get_current_diffusion_config_or_none
|
|
from vllm_omni.diffusion.distributed.parallel_state import get_sp_group
|
|
from vllm_omni.diffusion.forward_context import get_forward_context, is_forward_context_available
|
|
from vllm_omni.platforms import current_omni_platform
|
|
|
|
logger = init_logger(__name__)
|
|
|
|
|
|
def _try_extract_layer_index(prefix: str) -> int | None:
|
|
if not prefix:
|
|
return None
|
|
try:
|
|
return extract_layer_index(prefix)
|
|
except (AssertionError, ValueError):
|
|
return None
|
|
|
|
|
|
class Attention(nn.Module):
|
|
def __init__(
|
|
self,
|
|
num_heads: int,
|
|
head_size: int,
|
|
causal: bool,
|
|
softmax_scale: float,
|
|
num_kv_heads: int | None = None,
|
|
prefix: str = "",
|
|
# Per-role backend selection (RFC: per-role attention backend)
|
|
role: str = "self",
|
|
role_category: str | None = None,
|
|
# Model-defined Q/K/V tensor layout hint for backend execution.
|
|
qkv_layout: str | None = None,
|
|
# ulysses attention
|
|
scatter_idx: int = 2,
|
|
gather_idx: int = 1,
|
|
use_sync: bool = False,
|
|
skip_sequence_parallel: bool = False,
|
|
# Opt-out for KV-cache quantization at this specific attention layer.
|
|
# Set by the model author when quant is known to degrade quality or
|
|
# perf for this layer (e.g. Wan2.2 cross-attn has short sequences and
|
|
# block-FP8 quant offers no win). Default False = follow global config.
|
|
disable_kv_quant: bool = False,
|
|
# Opt-in marker for Scheduler-managed paged KV. Unmarked diffusion
|
|
# attention remains dense and contributes no native KVCacheSpec.
|
|
paged_kv_cache_role: str | None = None,
|
|
paged_kv_cache_dtype: torch.dtype | None = None,
|
|
):
|
|
super().__init__()
|
|
|
|
self.role = role
|
|
self.role_category = role_category
|
|
self.qkv_layout = qkv_layout
|
|
if paged_kv_cache_role == "":
|
|
raise ValueError("paged_kv_cache_role must be non-empty when provided")
|
|
self.paged_kv_cache_role = paged_kv_cache_role
|
|
self.paged_kv_cache_dtype = paged_kv_cache_dtype
|
|
self.num_heads = num_heads
|
|
self.num_kv_heads = num_kv_heads if num_kv_heads is not None else num_heads
|
|
self.head_size = head_size
|
|
|
|
# Resolve backend via role-aware config.
|
|
# The global diffusion config is set during model init via
|
|
# set_current_diffusion_config(); no env-var re-parsing needed here.
|
|
backend_kwargs: dict | None = None
|
|
self.backend_pref = None
|
|
|
|
config = get_current_diffusion_config_or_none()
|
|
attention_config = config.diffusion_attention_config if config is not None else None
|
|
|
|
from vllm_omni.diffusion.model_metadata import get_diffusion_model_metadata
|
|
|
|
model_class_name = getattr(config, "model_class_name", None) if config is not None else None
|
|
allow_trtllm_default = get_diffusion_model_metadata(model_class_name).attention_mask_free
|
|
|
|
attn_backend_cls, spec = get_attn_backend_for_role(
|
|
role=role,
|
|
head_size=head_size,
|
|
attention_config=attention_config,
|
|
role_category=role_category,
|
|
allow_trtllm_default=allow_trtllm_default,
|
|
)
|
|
parallel_config = getattr(config, "parallel_config", None)
|
|
allgather_degree = getattr(parallel_config, "allgather_degree", 1)
|
|
# TODO: Move AllGather-KV compatibility into an AttentionBackend capability
|
|
# so validation does not depend on backend names.
|
|
if not skip_sequence_parallel and allgather_degree > 1 and attn_backend_cls.get_name() == "TRTLLM_ATTN":
|
|
raise ValueError(
|
|
"TRTLLM_ATTN does not support AllGather-KV sequence parallelism. "
|
|
"Set --allgather-degree 1 or select another diffusion attention backend."
|
|
)
|
|
if spec is not None:
|
|
backend_kwargs = spec.backend_kwargs()
|
|
self.backend_pref = spec.backend
|
|
logger.debug("Attention(role=%s) → backend=%s", role, spec.backend)
|
|
else:
|
|
logger.debug("Attention(role=%s) → platform default", role)
|
|
|
|
self.attn_backend = attn_backend_cls
|
|
self.attn_impl_cls = self.attn_backend.get_impl_cls()
|
|
self.attention = self.attn_impl_cls(
|
|
num_heads=num_heads,
|
|
head_size=head_size,
|
|
softmax_scale=softmax_scale,
|
|
causal=causal,
|
|
num_kv_heads=num_kv_heads,
|
|
qkv_layout=qkv_layout,
|
|
prefix=prefix,
|
|
backend_kwargs=backend_kwargs,
|
|
role=role,
|
|
)
|
|
# Instantiate fallback backend for float32 support
|
|
self.sdpa_fallback = SDPABackend.get_impl_cls()(
|
|
num_heads=num_heads,
|
|
head_size=head_size,
|
|
softmax_scale=softmax_scale,
|
|
causal=causal,
|
|
num_kv_heads=num_kv_heads,
|
|
qkv_layout=qkv_layout,
|
|
)
|
|
|
|
self.softmax_scale = softmax_scale
|
|
self.scatter_idx = scatter_idx
|
|
self.gather_idx = gather_idx
|
|
self.use_sync = use_sync
|
|
self.causal = causal
|
|
self.skip_sequence_parallel = skip_sequence_parallel
|
|
|
|
self.use_ring = False
|
|
self.ring_pg = None
|
|
self.ring_runner = None
|
|
|
|
if config is not None:
|
|
if config.parallel_config.ring_degree > 1:
|
|
self.use_ring = True
|
|
try:
|
|
sp_group = get_sp_group()
|
|
self.ring_pg = sp_group.ring_group
|
|
self.ring_runner = RingParallelAttention(
|
|
sp_group,
|
|
attn_backend_pref=self.backend_pref,
|
|
)
|
|
except Exception:
|
|
self.use_ring = False
|
|
self.ring_runner = None
|
|
|
|
self.parallel_strategy = build_parallel_attention_strategy(
|
|
scatter_idx=scatter_idx,
|
|
gather_idx=gather_idx,
|
|
use_sync=use_sync,
|
|
causal=causal,
|
|
)
|
|
# Fallback strategy when SP is not active (outside sharded regions)
|
|
self._no_parallel_strategy = NoParallelAttention()
|
|
|
|
self.layer_idx: int | None = _try_extract_layer_index(prefix)
|
|
|
|
self._kv_cache_dtype: str | None = None
|
|
self._kv_cache_skip_steps: set[int] | None = None
|
|
self._kv_cache_skip_layers: set[int] | None = None
|
|
# Per-layer opt-out from KV-cache quantization (set by model author).
|
|
self._disable_kv_quant: bool = disable_kv_quant
|
|
self._init_kv_cache_quantization(config)
|
|
|
|
def get_kv_cache_spec(self, vllm_config: VllmConfig) -> KVCacheSpec | None:
|
|
"""Return native rank-local geometry for an opted-in paged cache."""
|
|
|
|
if self.paged_kv_cache_role is None:
|
|
return None
|
|
dtype = self.paged_kv_cache_dtype or vllm_config.model_config.dtype
|
|
# Keep backend layout discovery under the same config context used by
|
|
# upstream vLLM's attention-spec collector.
|
|
with set_current_vllm_config(vllm_config):
|
|
indexes_kv_by_block_stride = self.attn_backend.indexes_kv_by_block_stride()
|
|
return FullAttentionSpec(
|
|
block_size=vllm_config.cache_config.block_size,
|
|
num_kv_heads=self.num_kv_heads,
|
|
head_size=self.head_size,
|
|
dtype=dtype,
|
|
indexes_kv_by_block_stride=indexes_kv_by_block_stride,
|
|
non_causal=not self.causal,
|
|
)
|
|
|
|
def _get_active_parallel_strategy(self):
|
|
"""Get the parallel strategy based on current SP active state.
|
|
|
|
Returns NoParallelAttention if we're outside an SP sharded region
|
|
(e.g., in noise_refiner/context_refiner before unified_prepare in Z-Image).
|
|
This avoids unnecessary SP communication for layers not covered by _sp_plan.
|
|
"""
|
|
if self.skip_sequence_parallel:
|
|
return self._no_parallel_strategy
|
|
if is_forward_context_available():
|
|
ctx = get_forward_context()
|
|
if not ctx.sp_active:
|
|
return self._no_parallel_strategy
|
|
return self.parallel_strategy
|
|
|
|
def _init_kv_cache_quantization(self, config) -> None:
|
|
if config is None:
|
|
return
|
|
dtype = getattr(config, "diffusion_kv_cache_dtype", None)
|
|
if dtype == "auto":
|
|
dtype = None
|
|
parallel_config = getattr(config, "parallel_config", None)
|
|
ring_degree = getattr(parallel_config, "ring_degree", 1)
|
|
if dtype:
|
|
if ring_degree > 1:
|
|
raise ValueError(
|
|
"KV quantization is not compatible with ring attention "
|
|
"(ring_degree > 1). Ring kernels do not propagate quantization descale "
|
|
"factors. Use Ulysses SP instead."
|
|
)
|
|
platform_key = current_omni_platform.device_name
|
|
if not self.attention.supports_kv_cache_dtype(dtype, platform_key):
|
|
logger.warning_once(
|
|
"Attention backend %s does not support kv_cache_dtype='%s' on %s. "
|
|
"KV quantization will be disabled.",
|
|
self.attn_backend.get_name(),
|
|
dtype,
|
|
platform_key,
|
|
)
|
|
dtype = None
|
|
self._kv_cache_dtype = dtype
|
|
self._kv_cache_skip_steps = getattr(config, "diffusion_kv_cache_skip_step_indices", None)
|
|
self._kv_cache_skip_layers = getattr(config, "diffusion_kv_cache_skip_layer_indices", None)
|
|
|
|
def _should_apply_kv_cache_quant(self) -> bool:
|
|
skip_steps = self._kv_cache_skip_steps
|
|
skip_layers = self._kv_cache_skip_layers
|
|
if skip_steps is not None:
|
|
step_idx = get_forward_context().denoise_step_idx if is_forward_context_available() else None
|
|
if step_idx is not None and step_idx in skip_steps:
|
|
return False
|
|
if skip_layers is not None:
|
|
if self.layer_idx is not None and self.layer_idx in skip_layers:
|
|
return False
|
|
return True
|
|
|
|
def _with_kv_cache_dtype(self, attn_metadata: AttentionMetadata | None) -> AttentionMetadata | None:
|
|
kv_cache_dtype = self._kv_cache_dtype
|
|
if kv_cache_dtype is None or self._disable_kv_quant or not self._should_apply_kv_cache_quant():
|
|
if attn_metadata is None or "kv_cache_dtype" not in attn_metadata.extra:
|
|
return attn_metadata
|
|
extra = dict(attn_metadata.extra)
|
|
extra.pop("kv_cache_dtype", None)
|
|
return replace(attn_metadata, extra=extra)
|
|
|
|
if attn_metadata is None:
|
|
return AttentionMetadata(extra={"kv_cache_dtype": kv_cache_dtype})
|
|
extra = dict(attn_metadata.extra)
|
|
extra["kv_cache_dtype"] = kv_cache_dtype
|
|
return replace(attn_metadata, extra=extra)
|
|
|
|
def forward(
|
|
self,
|
|
query: torch.Tensor,
|
|
key: torch.Tensor,
|
|
value: torch.Tensor,
|
|
attn_metadata: AttentionMetadata | None = None,
|
|
) -> torch.Tensor:
|
|
if torch.compiler.is_compiling() and is_forward_context_available():
|
|
od_config = get_forward_context().omni_diffusion_config
|
|
parallel_config = getattr(od_config, "parallel_config", None)
|
|
if getattr(parallel_config, "use_hsdp", False):
|
|
# Keep HSDP/FSDP2 parameter all-gather outside Inductor's
|
|
# attention graph; otherwise scheduler dependency analysis can
|
|
# fail on the fused attention region.
|
|
return self._forward_hsdp_compile_boundary(query, key, value, attn_metadata)
|
|
|
|
return self._forward_impl(query, key, value, attn_metadata)
|
|
|
|
@torch.compiler.disable
|
|
def _forward_hsdp_compile_boundary(
|
|
self,
|
|
query: torch.Tensor,
|
|
key: torch.Tensor,
|
|
value: torch.Tensor,
|
|
attn_metadata: AttentionMetadata | None = None,
|
|
) -> torch.Tensor:
|
|
return self._forward_impl(query, key, value, attn_metadata)
|
|
|
|
def _forward_impl(
|
|
self,
|
|
query: torch.Tensor,
|
|
key: torch.Tensor,
|
|
value: torch.Tensor,
|
|
attn_metadata: AttentionMetadata | None = None,
|
|
) -> torch.Tensor:
|
|
# Get the appropriate parallel strategy based on SP active state
|
|
strategy = self._get_active_parallel_strategy()
|
|
|
|
# 1. Prepare inputs (Communication / Resharding)
|
|
# For Ulysses: AllToAll Q/K/V; Slicing joint_q/k/v
|
|
# For Ring: Concat joint_q
|
|
query, key, value, attn_metadata, ctx = strategy.pre_attention(query, key, value, attn_metadata)
|
|
|
|
attn_metadata = self._with_kv_cache_dtype(attn_metadata)
|
|
|
|
# 2. Kernel Execution (Computation)
|
|
if self.use_ring and strategy is not self._no_parallel_strategy:
|
|
out = self._run_ring_attention(query, key, value, attn_metadata)
|
|
else:
|
|
out = self._run_local_attention(query, key, value, attn_metadata)
|
|
|
|
# 3. Post-processing (Reverse Communication)
|
|
# For Ulysses: AllToAll Output, and AllGather Joint Output
|
|
out = strategy.post_attention(out, ctx)
|
|
|
|
return out
|
|
|
|
def _run_local_attention(self, query, key, value, attn_metadata):
|
|
self._assert_piecewise_compatible(attn_metadata)
|
|
|
|
if query.dtype == torch.float32:
|
|
logger.warning_once(
|
|
f"Only SDPA supports float32. Overriding user config {type(self.attention)} "
|
|
f"attention_backend='{self.backend_pref}' to 'sdpa' for dtype={query.dtype}."
|
|
)
|
|
return self.sdpa_fallback.forward(query, key, value, attn_metadata)
|
|
|
|
# Fallback to standard attention
|
|
return self.attention.forward(query, key, value, attn_metadata)
|
|
|
|
def _assert_piecewise_compatible(self, attn_metadata: AttentionMetadata | None) -> None:
|
|
if attn_metadata is None or attn_metadata.full_attn_spans is None:
|
|
return
|
|
if attn_metadata.attn_mask is not None and attn_metadata.attn_mask.ndim == 4:
|
|
return
|
|
backend_name = self.attn_backend.get_name()
|
|
if not self.attn_backend.supports_piecewise_spans:
|
|
raise ValueError(
|
|
f"Attention backend '{backend_name}' does not support "
|
|
f"piecewise attention (full_attn_spans without a 4D attn_mask). "
|
|
f"Use a Flash backend (FLASH_ATTN / FLASH_ATTN_HUB / FLASH_ATTN_3_HUB), "
|
|
f"or provide a 4D attn_mask that encodes the mixed causal/full pattern."
|
|
)
|
|
|
|
def _run_ring_attention(self, query, key, value, attn_metadata):
|
|
skip = getattr(self.attention, "skip", None)
|
|
if skip is not None and getattr(skip, "configured", False):
|
|
raise NotImplementedError(
|
|
"Skip-Softmax (TRTLLM_ATTN) is not supported with ring sequence parallelism: "
|
|
"the ring path bypasses the backend, so the skip config would be silently ignored. "
|
|
"Use Ulysses SP instead, or remove the skip_softmax config."
|
|
)
|
|
# Delegate to RingParallelAttention strategy if available
|
|
if self.ring_runner is not None:
|
|
return self.ring_runner.run_attention(
|
|
query, key, value, attn_metadata, softmax_scale=self.softmax_scale, causal=self.causal
|
|
)
|
|
|
|
raise RuntimeError("Ring attention is enabled but strategy is not RingParallelAttention")
|