[Diffusion][Attention] Support per-role attention backend via CLI (#2681)
Signed-off-by: gcanlin <canlinguosdu@gmail.com> Signed-off-by: Hongsheng Liu <liuhongsheng4@huawei.com> Co-authored-by: Hongsheng Liu <liuhongsheng4@huawei.com>
This commit is contained in:
1 parent
26d481fc84
commit
11c4c7f0ff
29 files changed
+1262
-106
No files matched your search
+206
-14
@@ -1,7 +1,7 @@
|
||||
# adapted from sglang and fastvideo
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
import enum
|
||||
import copy
|
||||
import os
|
||||
import random
|
||||
from collections.abc import Callable, Mapping
|
||||
@@ -367,7 +367,7 @@ class OmniDiffusionConfig:
|
||||
tf_model_config: TransformerConfig = field(default_factory=TransformerConfig)
|
||||
|
||||
# Attention
|
||||
attention_backend: str | None = None
|
||||
attention_config: "AttentionConfig" = field(default_factory=lambda: AttentionConfig())
|
||||
|
||||
# Running mode
|
||||
# mode: ExecutionMode = ExecutionMode.INFERENCE
|
||||
@@ -653,6 +653,10 @@ class OmniDiffusionConfig:
|
||||
f"got {type(self.quantization_config)!r}"
|
||||
)
|
||||
|
||||
# Match vLLM's config flow: parse entrypoint shorthands before the
|
||||
# config object is built, and keep a single runtime truth source.
|
||||
self.attention_config = build_attention_config(self.attention_config)
|
||||
|
||||
if self.max_cpu_loras is None:
|
||||
self.max_cpu_loras = 1
|
||||
elif self.max_cpu_loras < 1:
|
||||
@@ -794,6 +798,24 @@ class OmniDiffusionConfig:
|
||||
else:
|
||||
kwargs.pop("quantization", None)
|
||||
|
||||
# Map "diffusion_attention_config" to "attention_config" so the
|
||||
# engine_args key (namespaced to avoid collision with vLLM's own
|
||||
# attention_config) is mapped to the dataclass field.
|
||||
if "diffusion_attention_config" in kwargs and "attention_config" not in kwargs:
|
||||
kwargs["attention_config"] = kwargs.pop("diffusion_attention_config")
|
||||
else:
|
||||
kwargs.pop("diffusion_attention_config", None)
|
||||
|
||||
# Handle "diffusion_attention_backend" shorthand: merge into
|
||||
# attention_config before field filtering.
|
||||
diffusion_attn_backend = kwargs.pop("diffusion_attention_backend", None)
|
||||
if diffusion_attn_backend is not None:
|
||||
existing = kwargs.get("attention_config")
|
||||
kwargs["attention_config"] = parse_attention_config(
|
||||
existing,
|
||||
attention_backend=diffusion_attn_backend,
|
||||
)
|
||||
|
||||
# Check environment variable as fallback for cache_backend
|
||||
# Support both old DIFFUSION_CACHE_ADAPTER and new DIFFUSION_CACHE_BACKEND for backwards compatibility
|
||||
if "cache_backend" not in kwargs:
|
||||
@@ -848,19 +870,189 @@ class DiffusionRequestAbortedError(RuntimeError):
|
||||
"""Raised when a diffusion request ends via user-visible abort."""
|
||||
|
||||
|
||||
class AttentionBackendEnum(enum.Enum):
|
||||
FA = enum.auto()
|
||||
SLIDING_TILE_ATTN = enum.auto()
|
||||
TORCH_SDPA = enum.auto()
|
||||
SAGE_ATTN = enum.auto()
|
||||
SAGE_ATTN_THREE = enum.auto()
|
||||
VIDEO_SPARSE_ATTN = enum.auto()
|
||||
VMOBA_ATTN = enum.auto()
|
||||
AITER = enum.auto()
|
||||
NO_ATTENTION = enum.auto()
|
||||
@dataclass
|
||||
class AttentionSpec:
|
||||
"""Specifies a backend and its backend-specific parameters for one attention role."""
|
||||
|
||||
def __str__(self):
|
||||
return self.name.lower()
|
||||
backend: str # registry name, e.g. "FLASH_ATTN"
|
||||
extra: dict[str, Any] = field(default_factory=dict) # backend-specific kwargs
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not isinstance(self.backend, str):
|
||||
raise TypeError(f"Expected str for AttentionSpec.backend, got {type(self.backend)!r}")
|
||||
|
||||
if self.extra is None:
|
||||
self.extra = {}
|
||||
elif isinstance(self.extra, Mapping):
|
||||
self.extra = dict(self.extra)
|
||||
else:
|
||||
raise TypeError(f"Expected dict for AttentionSpec.extra, got {type(self.extra)!r}")
|
||||
|
||||
|
||||
@dataclass
|
||||
class AttentionConfig:
|
||||
"""Per-role attention backend configuration.
|
||||
|
||||
Lookup precedence for a given (role, role_category):
|
||||
1. per_role[role] — exact match
|
||||
2. per_role[role_category] — category fallback (e.g. "ltx2.audio_to_video" → "cross")
|
||||
3. default — global default
|
||||
4. platform default — unchanged platform logic
|
||||
"""
|
||||
|
||||
default: AttentionSpec | None = None
|
||||
per_role: dict[str, AttentionSpec] = field(default_factory=dict)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.default is not None:
|
||||
self.default = self._coerce_spec_or_none(self.default, "default")
|
||||
|
||||
normalized_per_role: dict[str, AttentionSpec] = {}
|
||||
for role_key, spec_data in self._normalize_per_role_mapping(self.per_role).items():
|
||||
spec = self._coerce_spec_or_none(spec_data, f"per_role[{role_key!r}]")
|
||||
if spec is not None:
|
||||
normalized_per_role[role_key] = spec
|
||||
self.per_role = normalized_per_role
|
||||
|
||||
@staticmethod
|
||||
def _coerce_spec(spec_data: Any, field_name: str) -> AttentionSpec:
|
||||
if isinstance(spec_data, AttentionSpec):
|
||||
return spec_data
|
||||
if isinstance(spec_data, str):
|
||||
return AttentionSpec(backend=spec_data)
|
||||
if isinstance(spec_data, Mapping):
|
||||
return AttentionSpec(**dict(spec_data))
|
||||
raise TypeError(f"Expected str, dict, or AttentionSpec for {field_name}, got {type(spec_data)!r}")
|
||||
|
||||
@classmethod
|
||||
def _coerce_spec_or_none(cls, spec_data: Any, field_name: str) -> AttentionSpec | None:
|
||||
spec = cls._coerce_spec(spec_data, field_name)
|
||||
if spec.backend.lower() == "auto":
|
||||
return None
|
||||
return spec
|
||||
|
||||
@classmethod
|
||||
def _normalize_per_role_mapping(cls, raw_per_role: Any) -> dict[str, Any]:
|
||||
if raw_per_role is None:
|
||||
return {}
|
||||
if not isinstance(raw_per_role, Mapping):
|
||||
raise TypeError(f"Expected dict for AttentionConfig.per_role, got {type(raw_per_role)!r}")
|
||||
|
||||
normalized: dict[str, Any] = {}
|
||||
for role_key, spec_data in raw_per_role.items():
|
||||
cls._flatten_per_role_entry([role_key], spec_data, normalized)
|
||||
return normalized
|
||||
|
||||
@classmethod
|
||||
def _flatten_per_role_entry(
|
||||
cls,
|
||||
path: list[str],
|
||||
node: Any,
|
||||
normalized: dict[str, Any],
|
||||
) -> None:
|
||||
role = ".".join(path)
|
||||
if not isinstance(node, Mapping):
|
||||
normalized[role] = node
|
||||
return
|
||||
|
||||
spec_keys = {"backend", "extra"}
|
||||
node_dict = dict(node)
|
||||
node_keys = set(node_dict)
|
||||
if node_keys & spec_keys:
|
||||
if not node_keys <= spec_keys:
|
||||
raise ValueError(
|
||||
f"Invalid per_role entry for role {role!r}: cannot mix backend/extra with nested role keys."
|
||||
)
|
||||
normalized[role] = node_dict
|
||||
return
|
||||
|
||||
if not node_dict:
|
||||
raise ValueError(f"Empty per_role entry for role {role!r}")
|
||||
|
||||
for child_key, child_value in node_dict.items():
|
||||
cls._flatten_per_role_entry([*path, child_key], child_value, normalized)
|
||||
|
||||
def resolve_with_source(
|
||||
self,
|
||||
role: str = "self",
|
||||
role_category: str | None = None,
|
||||
) -> tuple[AttentionSpec | None, str | None]:
|
||||
"""Resolve the AttentionSpec and report which config entry matched."""
|
||||
spec = self.per_role.get(role)
|
||||
if spec is not None:
|
||||
return spec, f"attention_config.per_role[{role!r}]"
|
||||
if role_category is not None:
|
||||
spec = self.per_role.get(role_category)
|
||||
if spec is not None:
|
||||
return spec, f"attention_config.per_role[{role_category!r}] (role_category fallback)"
|
||||
if self.default is not None:
|
||||
return self.default, "attention_config.default"
|
||||
return None, None
|
||||
|
||||
|
||||
def parse_attention_config(
|
||||
attention_config: AttentionConfig | Mapping[str, Any] | None = None,
|
||||
*,
|
||||
attention_backend: str | None = None,
|
||||
) -> AttentionConfig:
|
||||
"""Pure type-conversion: coerce *attention_config* to an AttentionConfig.
|
||||
|
||||
Optionally merges an ``attention_backend`` shorthand into the config's
|
||||
``default`` field. This does **not** read environment variables —
|
||||
use :func:`build_attention_config` for the full normalisation that
|
||||
should happen exactly once in ``OmniDiffusionConfig.__post_init__``.
|
||||
"""
|
||||
if attention_config is None:
|
||||
normalized = AttentionConfig()
|
||||
elif isinstance(attention_config, AttentionConfig):
|
||||
normalized = copy.deepcopy(attention_config)
|
||||
elif isinstance(attention_config, Mapping):
|
||||
normalized = AttentionConfig(**dict(attention_config))
|
||||
else:
|
||||
raise TypeError(
|
||||
f"attention_config must be an AttentionConfig, mapping, or None; got {type(attention_config)!r}"
|
||||
)
|
||||
|
||||
if attention_backend is not None:
|
||||
if normalized.default is not None:
|
||||
raise ValueError(
|
||||
"--diffusion-attention-backend is mutually exclusive with --diffusion-attention-config.default.backend."
|
||||
)
|
||||
if attention_backend.lower() != "auto":
|
||||
normalized.default = AttentionSpec(backend=attention_backend)
|
||||
|
||||
return normalized
|
||||
|
||||
|
||||
def build_attention_config(
|
||||
attention_config: AttentionConfig | Mapping[str, Any] | None = None,
|
||||
) -> AttentionConfig:
|
||||
"""Normalize diffusion attention config — the single authoritative entry point.
|
||||
|
||||
Called exactly once in ``OmniDiffusionConfig.__post_init__``.
|
||||
Handles type-conversion **and** env-var fallback
|
||||
(``DIFFUSION_ATTENTION_BACKEND``).
|
||||
"""
|
||||
normalized = parse_attention_config(attention_config)
|
||||
|
||||
if normalized.default is not None:
|
||||
return normalized
|
||||
|
||||
env_attention_backend = os.environ.get("DIFFUSION_ATTENTION_BACKEND")
|
||||
if env_attention_backend is None:
|
||||
return normalized
|
||||
|
||||
if env_attention_backend.lower() == "auto":
|
||||
return normalized
|
||||
|
||||
normalized.default = AttentionSpec(backend=env_attention_backend)
|
||||
logger.info(
|
||||
"Parsed attention config from DIFFUSION_ATTENTION_BACKEND '%s': default=%s, per_role=%s",
|
||||
env_attention_backend,
|
||||
normalized.default,
|
||||
{k: v.backend for k, v in normalized.per_role.items()},
|
||||
)
|
||||
return normalized
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
Reference in new issue
Block a user