chore: import upstream snapshot with attribution
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:38:16 +08:00
commit 94057c3d3e
7152 changed files with 2120455 additions and 0 deletions
@@ -0,0 +1 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
@@ -0,0 +1,207 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
# SPDX-License-Identifier: Apache-2.0
import logging
import os
import aiter
import torch
from sglang.multimodal_gen.runtime.layers.attention.backends.attention_backend import (
AttentionBackend,
AttentionImpl,
AttentionMetadata,
AttentionMetadataBuilder,
)
from sglang.multimodal_gen.runtime.platforms import AttentionBackendEnum
from sglang.multimodal_gen.runtime.platforms.aiter import USE_AITER_GFX95
logger = logging.getLogger(__name__)
_use_fp8_attn = os.environ.get("SGLANG_DIFFUSION_AITER_FP8_ATTN", "0") == "1"
_fp8_dtype = torch.float8_e4m3fn
# fmha_fwd_hd128_fp8_gfx950 ASM kernel. Support full MHA with q/k/v head_dim == 128 -- e.g., Wan 2.2 self- and cross-attention.
_FMHA_FP8_HEAD_DIM = 128
if _use_fp8_attn:
logger.info("DiT FP8 attention enabled via SGLANG_DIFFUSION_AITER_FP8_ATTN=1")
def _can_use_fmha_fp8_prefill(
q_head_dim: int,
k_head_dim: int,
v_head_dim: int,
num_heads: int,
num_kv_heads: int,
) -> bool:
"""True if MHA q/k/v head_dim==128 on a gfx950-class arch."""
if not USE_AITER_GFX95:
return False
if num_kv_heads != num_heads:
return False
return q_head_dim == k_head_dim == v_head_dim == _FMHA_FP8_HEAD_DIM
def _fmha_fp8_prefill_attention(
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor,
softmax_scale: float,
is_causal: bool,
q_scale: torch.Tensor,
k_scale: torch.Tensor,
v_scale: torch.Tensor,
) -> torch.Tensor:
"""
FP8 FMHA prefill via aiter.flash_attn_fp8_pertensor_func.
Expects q, k, v as (batch, seqlen, nheads, 128) FP8, contiguous.
"""
def _ensure_fp8_descale(scale: torch.Tensor) -> torch.Tensor:
"""Per-tensor descale as shape (1,) float32 for flash_attn_fp8_pertensor_func."""
return scale.to(dtype=torch.float32).reshape(1).contiguous()
q = q.contiguous()
k = k.contiguous()
v = v.contiguous()
q_descale = _ensure_fp8_descale(q_scale)
k_descale = _ensure_fp8_descale(k_scale)
v_descale = _ensure_fp8_descale(v_scale)
return aiter.flash_attn_fp8_pertensor_func(
q,
k,
v,
q_descale,
k_descale,
v_descale,
causal=is_causal,
softmax_scale=softmax_scale,
window_size=(-1, -1, 0),
)
class AITerBackend(AttentionBackend):
"""
Backend for AITemplate attention implementation.
"""
@staticmethod
def get_enum() -> AttentionBackendEnum:
return AttentionBackendEnum.AITER
@staticmethod
def get_impl_cls() -> type["AITerImpl"]:
return AITerImpl
@staticmethod
def get_metadata_cls() -> type["AttentionMetadata"]:
# AITer backend does not require special metadata.
return AttentionMetadata
@staticmethod
def get_builder_cls() -> type["AttentionMetadataBuilder"]:
raise NotImplementedError("AITer backend does not have a metadata builder.")
class AITerImpl(AttentionImpl):
"""
Implementation of attention using AITemplate.
"""
def __init__(
self,
num_heads: int,
head_size: int,
softmax_scale: float,
causal: bool = False,
num_kv_heads: int | None = None,
prefix: str = "",
dropout_p: float = 0.0,
**extra_impl_args,
) -> None:
if num_kv_heads is not None and num_kv_heads != num_heads:
raise NotImplementedError(
"AITer backend does not support Grouped Query Attention yet."
)
self.causal = causal
self.dropout_p = dropout_p
self.softmax_scale = softmax_scale
@torch.compiler.disable
def forward(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attn_metadata: AttentionMetadata | None = None,
) -> torch.Tensor:
"""
Performs attention using one of:
- _fmha_fp8_prefill_attention (FP8, SGLANG_DIFFUSION_AITER_FP8_ATTN=1 when eligible)
- flash_attn_func (BF16, default or FP8 fallback for unsupported shapes)
Args:
query: Query tensor of shape [batch_size, seq_len, num_heads, head_dim]
key: Key tensor of shape [batch_size, seq_len, num_heads, head_dim]
value: Value tensor of shape [batch_size, seq_len, num_heads, head_dim]
attn_metadata: Metadata for the attention operation (unused).
Returns:
Output tensor of shape [batch_size, seq_len, num_heads, head_dim]
"""
if _use_fp8_attn:
if query.dtype != _fp8_dtype:
q_fp8, q_scale = aiter.per_tensor_quant(query, quant_dtype=_fp8_dtype)
k_fp8, k_scale = aiter.per_tensor_quant(key, quant_dtype=_fp8_dtype)
v_fp8, v_scale = aiter.per_tensor_quant(value, quant_dtype=_fp8_dtype)
else:
q_fp8, k_fp8, v_fp8 = query, key, value
one = torch.tensor(1.0, dtype=torch.float32, device=query.device)
q_scale = k_scale = v_scale = one
d_q = q_fp8.shape[-1]
d_k = k_fp8.shape[-1]
d_v = v_fp8.shape[-1]
h_q = q_fp8.shape[2]
h_kv = k_fp8.shape[2]
if _can_use_fmha_fp8_prefill(d_q, d_k, d_v, h_q, h_kv):
return _fmha_fp8_prefill_attention(
q_fp8,
k_fp8,
v_fp8,
softmax_scale=self.softmax_scale,
is_causal=self.causal,
q_scale=q_scale,
k_scale=k_scale,
v_scale=v_scale,
)
logger.warning_once(
"FP8 FMHA prefill unsupported for this shape (need gfx950-class AITER, "
"full MHA, q/k/v head_dim=%d; got q=%d, k=%d, v=%d, num_heads=%d, "
"num_kv_heads=%d). Falling back to BF16.",
_FMHA_FP8_HEAD_DIM,
d_q,
d_k,
d_v,
h_q,
h_kv,
)
# BF16 path
output, _ = aiter.flash_attn_func(
query,
key,
value,
dropout_p=self.dropout_p,
causal=self.causal,
return_attn_probs=False,
return_lse=True,
)
return output
@@ -0,0 +1,81 @@
# SPDX-License-Identifier: Apache-2.0
import torch
from sglang.multimodal_gen.runtime.layers.attention.backends.attention_backend import (
AttentionBackend,
AttentionImpl,
AttentionMetadata,
AttentionMetadataBuilder,
)
from sglang.multimodal_gen.runtime.platforms import AttentionBackendEnum
class AITERSageBackend(AttentionBackend):
@staticmethod
def get_enum() -> AttentionBackendEnum:
return AttentionBackendEnum.AITER_SAGE
@staticmethod
def get_impl_cls() -> type["AITERSageImpl"]:
return AITERSageImpl
@staticmethod
def get_metadata_cls() -> type["AttentionMetadata"]:
# AITER Sage backend does not require special metadata.
return AttentionMetadata
@staticmethod
def get_builder_cls() -> type["AttentionMetadataBuilder"]:
raise NotImplementedError(
"AITER Sage backend does not have a metadata builder."
)
class AITERSageImpl(AttentionImpl):
def __init__(
self,
num_heads: int,
head_size: int,
softmax_scale: float,
causal: bool = False,
num_kv_heads: int | None = None,
prefix: str = "",
dropout_p: float = 0.0,
**extra_impl_args,
) -> None:
try:
from aiter.ops.triton.attention.fav3_sage import fav3_sage_wrapper_func
self.aiter_sage_attn_fn = fav3_sage_wrapper_func
except ImportError:
raise ImportError(
"AITER Sage attention is not available, please update AITER version."
)
def forward(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attn_metadata: AttentionMetadata | None = None,
) -> torch.Tensor:
"""
Performs attention using aiter sage backend.
Args:
query: Query tensor of shape [batch_size, seq_len, head_num, head_dim]
key: Key tensor of shape [batch_size, seq_len, head_num, head_dim]
value: Value tensor of shape [batch_size, seq_len, head_num, head_dim]
attn_metadata: Metadata for the attention operation (unused).
Returns:
Output tensor of shape [batch_size, seq_len, head_num, head_dim]
"""
output = self.aiter_sage_attn_fn(query, key, value)
return output
@@ -0,0 +1,104 @@
from dataclasses import dataclass
from typing import Any
import torch
from sglang.multimodal_gen.runtime.layers.attention.backends.attention_backend import (
AttentionBackend,
AttentionImpl,
AttentionMetadata,
AttentionMetadataBuilder,
)
from sglang.multimodal_gen.runtime.platforms import AttentionBackendEnum
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__)
@dataclass
class AscendFAMetadata:
pass
class AscendFAMetadataBuilder(AttentionMetadataBuilder):
def __init__(self) -> None:
pass
def prepare(self) -> None:
pass
def build(
self,
**kwargs: dict[str, Any],
) -> AttentionMetadata:
return AscendFAMetadata()
class AscendFABackend(AttentionBackend):
@staticmethod
def get_enum() -> AttentionBackendEnum:
return AttentionBackendEnum.FA
@staticmethod
def get_impl_cls() -> type["AscendFAImpl"]:
return AscendFAImpl
@staticmethod
def get_metadata_cls() -> type["AttentionMetadata"]:
raise NotImplementedError
@staticmethod
def get_builder_cls() -> type["AttentionMetadataBuilder"]:
return AscendFAMetadataBuilder
class AscendFAImpl(AttentionImpl):
def __init__(
self,
num_heads: int,
head_size: int,
causal: bool,
softmax_scale: float,
num_kv_heads: int | None = None,
prefix: str = "",
**extra_impl_args,
) -> None:
self.causal = causal
self.softmax_scale = softmax_scale
def forward(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attn_metadata: AttentionMetadata,
return_softmax_lse: bool = False,
) -> torch.Tensor:
mask = None
num_heads, num_key_value_heads = query.shape[2], key.shape[2]
if self.causal:
seq_len = query.shape[1]
mask = torch.triu(
torch.ones(seq_len, seq_len, device=query.device), diagonal=1
).bool()
# transpose to bs, heads, seq_len, head_dim
query = query.transpose(1, 2)
key = key.transpose(1, 2)
value = value.transpose(1, 2)
output, lse = torch.ops.npu.npu_fused_infer_attention_score(
query,
key,
value,
num_heads=num_heads,
num_key_value_heads=num_key_value_heads,
scale=self.softmax_scale,
input_layout="BNSD",
softmax_lse_flag=return_softmax_lse,
atten_mask=mask,
)
output = output.transpose(1, 2)
if return_softmax_lse:
return output, lse
return output
@@ -0,0 +1,179 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
# SPDX-License-Identifier: Apache-2.0
# Adapted from vllm: https://github.com/vllm-project/vllm/blob/v0.7.3/vllm/attention/backends/abstract.py
from abc import ABC, abstractmethod
from dataclasses import dataclass, fields
from typing import TYPE_CHECKING, Any, Generic, Protocol, TypeVar
if TYPE_CHECKING:
pass
import torch
from sglang.kernel_api_logging import wrap_method_with_debug_kernel_once
from sglang.multimodal_gen.runtime.platforms import AttentionBackendEnum
class AttentionBackend(ABC):
"""Abstract class for attention backends."""
# For some attention backends, we allocate an output tensor before
# calling the custom op. When piecewise cudagraph is enabled, this
# makes sure the output tensor is allocated inside the cudagraph.
accept_output_buffer: bool = False
@staticmethod
@abstractmethod
def get_enum() -> AttentionBackendEnum:
raise NotImplementedError
@staticmethod
@abstractmethod
def get_impl_cls() -> type["AttentionImpl"]:
raise NotImplementedError
@staticmethod
@abstractmethod
def get_metadata_cls() -> type["AttentionMetadata"]:
raise NotImplementedError
# @staticmethod
# @abstractmethod
# def get_state_cls() -> Type["AttentionState"]:
# raise NotImplementedError
# @classmethod
# def make_metadata(cls, *args, **kwargs) -> "AttentionMetadata":
# return cls.get_metadata_cls()(*args, **kwargs)
@staticmethod
@abstractmethod
def get_builder_cls() -> type["AttentionMetadataBuilder"]:
return None
@dataclass
class AttentionMetadata:
"""Attention metadata for prefill and decode batched together."""
# Current step of diffusion process
current_timestep: int
def asdict_zerocopy(self, skip_fields: set[str] | None = None) -> dict[str, Any]:
"""Similar to dataclasses.asdict, but avoids deepcopying."""
if skip_fields is None:
skip_fields = set()
# Note that if we add dataclasses as fields, they will need
# similar handling.
return {
field.name: getattr(self, field.name)
for field in fields(self)
if field.name not in skip_fields
}
T = TypeVar("T", bound=AttentionMetadata)
class AttentionMetadataBuilder(ABC, Generic[T]):
"""Abstract class for attention metadata builders."""
@abstractmethod
def __init__(self) -> None:
"""Create the builder, remember some configuration and parameters."""
raise NotImplementedError
@abstractmethod
def prepare(self) -> None:
"""Prepare for one batch."""
raise NotImplementedError
@abstractmethod
def build(
self,
**kwargs: dict[str, Any],
) -> AttentionMetadata:
"""Build attention metadata with on-device tensors."""
raise NotImplementedError
class AttentionLayer(Protocol):
_k_scale: torch.Tensor
_v_scale: torch.Tensor
_k_scale_float: float
_v_scale_float: float
def forward(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
kv_cache: torch.Tensor,
attn_metadata: AttentionMetadata,
) -> torch.Tensor: ...
class AttentionImpl(ABC, Generic[T]):
@abstractmethod
def __init__(
self,
num_heads: int,
head_size: int,
softmax_scale: float,
causal: bool = False,
num_kv_heads: int | None = None,
prefix: str = "",
**extra_impl_args,
) -> None:
raise NotImplementedError
def preprocess_qkv(self, qkv: torch.Tensor, attn_metadata: T) -> torch.Tensor:
"""Preprocess QKV tensor before performing attention operation.
Default implementation returns the tensor unchanged.
Subclasses can override this to implement custom preprocessing
like reshaping, tiling, scaling, or other transformations.
Called AFTER all_to_all for distributed attention
"""
return qkv
def postprocess_output(
self,
output: torch.Tensor,
attn_metadata: T,
) -> torch.Tensor:
"""Postprocess the output tensor after the attention operation.
Default implementation returns the tensor unchanged.
Subclasses can override this to implement custom postprocessing
like untiling, scaling, or other transformations.
Called BEFORE all_to_all for distributed attention
"""
return output
@abstractmethod
def forward(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attn_metadata: T,
) -> torch.Tensor:
raise NotImplementedError
def wrap_attention_impl_forward(attn_impl: AttentionImpl) -> AttentionImpl:
return wrap_method_with_debug_kernel_once(
attn_impl,
"forward",
op_name=f"diffusion.attn_impl.{attn_impl.__class__.__name__}.forward",
)
@@ -0,0 +1,279 @@
from dataclasses import dataclass
from typing import Any
import attentions # noqa: F401
import torch
from sglang.multimodal_gen.runtime.layers.attention.backends.attention_backend import (
AttentionBackend,
AttentionImpl,
AttentionMetadata,
AttentionMetadataBuilder,
)
from sglang.multimodal_gen.runtime.layers.attention.backends.laser_attn import (
LaserAttentionBackend,
)
from sglang.multimodal_gen.runtime.platforms import AttentionBackendEnum
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__)
BSA_BLOCK_SIZE = 128
class BlockSparseAttentionBackend(AttentionBackend):
accept_output_buffer: bool = True
@staticmethod
def get_supported_head_sizes() -> list[int]:
return [32, 64, 96, 128]
@staticmethod
def get_enum() -> AttentionBackendEnum:
return AttentionBackendEnum.BLOCK_SPARSE_ATTN
@staticmethod
def get_impl_cls() -> type["BlockSparseAttentionImpl"]:
return BlockSparseAttentionImpl
@staticmethod
def get_metadata_cls() -> type["BlockSparseAttentionMetadata"]:
return BlockSparseAttentionMetadata
@staticmethod
def get_builder_cls() -> type["BlockSparseAttentionMetadataBuilder"]:
return BlockSparseAttentionMetadataBuilder
@dataclass
class BlockSparseAttentionMetadata(AttentionMetadata):
current_timestep: int
skip_first_steps: int
sparsity: float
block_frame_stride: int
class BlockSparseAttentionMetadataBuilder(AttentionMetadataBuilder):
def __init__(self) -> None:
pass
def prepare(self) -> None:
pass
def build(
self,
current_timestep: int,
skip_first_steps: int,
sparsity: float,
raw_latent_shape: list[int],
patch_size: tuple[int, int, int],
**kwargs: dict[str, Any],
) -> BlockSparseAttentionMetadata:
"""
Builds BlockSparseAttention metadata.
Args:
current_timestep: The current diffusion timestep.
skip_first_steps: Number of initial timesteps to skip before applying
sparsity. Must be nonnegative.
sparsity: Fraction of tokens to drop (blockwise) in the block sparse
attention mechanism. Must be in the range [0.0, 1.0).
raw_latent_shape: Shape of the latent tensor before patching.
patch_size: Patch size as (T, height, width). Only the height
and width components are used to divide the latent dimensions.
**kwargs: Additional keyword arguments (ignored, but accepted for
compatibility with base class or calling conventions).
Returns:
BlockSparseAttentionMetadata
Note:
The `block_frame_stride` is needed to set the first blocks to be nonsparse.
"""
if not (skip_first_steps >= 0 and 0.0 <= sparsity < 1.0):
raise ValueError(
(
"Invalid attention metadata values."
f"Sparsity should be in [0, 1), skip_first_steps should be non-negative."
f"Got sparsity={sparsity}, skip_first_steps={skip_first_steps}"
)
)
if sparsity == 0.0:
logger.warning(
(
"Sparsity is set to 0.0, which means no tokens will be dropped."
"For better performance use Laser Attention or increase sparsity."
)
)
if len(raw_latent_shape) >= 5:
latent_height, latent_width = raw_latent_shape[3:5]
else:
latent_height, latent_width = raw_latent_shape[-2:]
latent_height //= patch_size[1]
latent_width //= patch_size[2]
frame_stride = latent_height * latent_width
block_frame_stride = (frame_stride + BSA_BLOCK_SIZE - 1) // BSA_BLOCK_SIZE
return BlockSparseAttentionMetadata(
current_timestep=current_timestep,
skip_first_steps=skip_first_steps,
sparsity=sparsity,
block_frame_stride=block_frame_stride,
)
class BlockSparseAttentionImpl(AttentionImpl):
def __init__(
self,
num_heads: int,
head_size: int,
causal: bool,
softmax_scale: float,
num_kv_heads: int | None = None,
prefix: str = "",
**extra_impl_args,
) -> None:
self.causal = causal
self.softmax_scale = softmax_scale
self.num_heads = num_heads
self.num_kv_heads = num_kv_heads or num_heads
self.block_size = BSA_BLOCK_SIZE
self.stride = 8
self.default_tokens = 214748647
self.laser_attn_impl = LaserAttentionBackend.get_impl_cls()(
num_heads,
head_size,
causal,
softmax_scale,
num_kv_heads,
prefix,
**extra_impl_args,
)
def _get_estimate_mask(
self,
query: torch.Tensor,
key: torch.Tensor,
sparsity: float,
) -> tuple[torch.Tensor, torch.Tensor]:
return torch.ops.attentions.sparse_block_estimate(
query=query,
key=key,
actual_seq_lengths=None,
actual_seq_lengths_kv=None,
input_layout="BNSD",
stride=self.stride,
sparse_size=self.block_size,
num_heads=query.shape[1],
num_key_value_heads=key.shape[1],
scale_value=self.softmax_scale / self.stride,
threshold=1.0,
causal=self.causal,
keep_sink=True,
keep_recent=True,
row_sparse=1.0 - sparsity,
)
def _block_sparse_attention(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
smask: torch.Tensor,
sct: torch.Tensor,
) -> torch.Tensor:
return torch.ops.attentions.block_sparse_attention(
query=query,
key=key,
value=value,
sparse_mask=smask,
sparse_count_table=sct,
input_layout="BNSD",
sparse_size=self.block_size,
num_heads=query.shape[1],
num_key_value_heads=key.shape[1],
scale_value=self.softmax_scale,
causal=self.causal,
inner_precise=1,
pre_tokens=self.default_tokens,
next_tokens=self.default_tokens,
actual_seq_lengths=None,
actual_seq_lengths_kv=None,
)
def _get_smask(
self,
query: torch.Tensor,
key: torch.Tensor,
block_frame_stride: int,
sparsity: float,
) -> tuple[torch.Tensor, torch.Tensor]:
smask, sct = self._get_estimate_mask(
query,
key,
sparsity,
)
seq_len = smask.shape[2]
# Set the first blocks to be non-sparse to ensure the quality of the first few steps
smask[:, :, :block_frame_stride, :seq_len] = 1
smask[:, :, :seq_len, :block_frame_stride] = 1
smask = smask.to(torch.int8)
sct = smask.sum(dim=-1, dtype=torch.int32)
return smask, sct
def _adaptive_block_sparse_attention(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
block_frame_stride: int,
sparsity: float,
) -> torch.Tensor:
# TODO Currently implementation for BSND input layout has quality issues
# When the implementation is improved, transposes can be removed
q = query.permute(0, 2, 1, 3).contiguous()
k = key.permute(0, 2, 1, 3).contiguous()
v = value.permute(0, 2, 1, 3).contiguous()
smask, sct = self._get_smask(
q,
k,
block_frame_stride,
sparsity,
)
output = self._block_sparse_attention(q, k, v, smask, sct)
output = output.permute(0, 2, 1, 3).contiguous()
return output
def forward(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attn_metadata: AttentionMetadata,
) -> torch.Tensor:
if attn_metadata.current_timestep < attn_metadata.skip_first_steps:
output = self.laser_attn_impl.forward(
query,
key,
value,
attn_metadata,
)
else:
output = self._adaptive_block_sparse_attention(
query,
key,
value,
attn_metadata.block_frame_stride,
attn_metadata.sparsity,
)
return output
@@ -0,0 +1,445 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
# SPDX-License-Identifier: Apache-2.0
from dataclasses import dataclass
from typing import Any, List, Optional, Tuple
import torch
from sglang.jit_kernel.flash_attention import flash_attn_varlen_func
from sglang.multimodal_gen.runtime.layers.utils import register_custom_op
from sglang.multimodal_gen.runtime.platforms import (
AttentionBackendEnum,
)
def maybe_contiguous(x: Optional[torch.Tensor]) -> Optional[torch.Tensor]:
return x.contiguous() if x is not None and x.stride(-1) != 1 else x
# -----------------------------
# Fake implementations for schema / tracing
# custom op schema requires FIXED return structure.
# We provide TWO ops:
# 1) out-only op: always returns Tensor
# 2) out+lse op: always returns Tuple[Tensor, Tensor]
# -----------------------------
def flash_attn_varlen_func_fake_out(
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor,
cu_seqlens_q: Optional[torch.Tensor] = None,
cu_seqlens_k: Optional[torch.Tensor] = None,
max_seqlen_q: Optional[int] = None,
max_seqlen_k: Optional[int] = None,
seqused_q: Optional[torch.Tensor] = None,
seqused_k: Optional[torch.Tensor] = None,
page_table: Optional[torch.Tensor] = None,
softmax_scale: Optional[float] = None,
causal: bool = False,
qv: Optional[torch.Tensor] = None,
q_descale: Optional[torch.Tensor] = None,
k_descale: Optional[torch.Tensor] = None,
v_descale: Optional[torch.Tensor] = None,
window_size: Optional[List[int]] = None,
attention_chunk: int = 0,
softcap: float = 0.0,
num_splits: int = 1,
pack_gqa: Optional[bool] = None,
sm_margin: int = 0,
return_softmax_lse: bool = False,
sinks: Optional[torch.Tensor] = None,
ver: int = 4,
) -> torch.Tensor:
assert ver == 4, "only support flash attention v4"
q, k, v = [maybe_contiguous(t) for t in (q, k, v)]
num_head, head_dim = q.shape[-2:]
if cu_seqlens_q is None:
batch_size, seqlen_q = q.shape[:2]
else:
batch_size = cu_seqlens_q.shape[0] - 1
seqlen_q = None
head_dim_v = v.shape[-1]
if cu_seqlens_q is not None:
assert cu_seqlens_q.shape == (
batch_size + 1,
), "cu_seqlens_q must have shape (batch_size + 1,)"
assert cu_seqlens_q.dtype == torch.int32, "cu_seqlens_q must be int32"
assert cu_seqlens_q.stride(0) == 1, "cu_seqlens_q must be contiguous"
assert q.dtype in [
torch.float16,
torch.bfloat16,
], "inputs must be float16 or bfloat16"
assert q.dtype == k.dtype == v.dtype, "inputs must have the same dtype"
assert head_dim <= 256, "head_dim must be less than or equal to 256"
alignment = 16 // q.element_size()
assert head_dim_v % alignment == 0, f"head_dim_v must be divisible by {alignment}"
q_batch_seqlen_shape = (
(batch_size, seqlen_q) if cu_seqlens_q is None else (q.shape[0],)
)
out = q.new_empty(*q_batch_seqlen_shape, num_head, head_dim_v)
return out
def flash_attn_varlen_func_fake_out_lse(
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor,
cu_seqlens_q: Optional[torch.Tensor] = None,
cu_seqlens_k: Optional[torch.Tensor] = None,
max_seqlen_q: Optional[int] = None,
max_seqlen_k: Optional[int] = None,
seqused_q: Optional[torch.Tensor] = None,
seqused_k: Optional[torch.Tensor] = None,
page_table: Optional[torch.Tensor] = None,
softmax_scale: Optional[float] = None,
causal: bool = False,
qv: Optional[torch.Tensor] = None,
q_descale: Optional[torch.Tensor] = None,
k_descale: Optional[torch.Tensor] = None,
v_descale: Optional[torch.Tensor] = None,
window_size: Optional[List[int]] = None,
attention_chunk: int = 0,
softcap: float = 0.0,
num_splits: int = 1,
pack_gqa: Optional[bool] = None,
sm_margin: int = 0,
return_softmax_lse: bool = True,
sinks: Optional[torch.Tensor] = None,
ver: int = 4,
) -> Tuple[torch.Tensor, torch.Tensor]:
assert ver == 4, "only support flash attention v4"
q, k, v = [maybe_contiguous(t) for t in (q, k, v)]
num_head, head_dim = q.shape[-2:]
if cu_seqlens_q is None:
batch_size, seqlen_q = q.shape[:2]
total_q = batch_size * seqlen_q
else:
batch_size = cu_seqlens_q.shape[0] - 1
seqlen_q = None
total_q = q.shape[0]
head_dim_v = v.shape[-1]
if cu_seqlens_q is not None:
assert cu_seqlens_q.shape == (
batch_size + 1,
), "cu_seqlens_q must have shape (batch_size + 1,)"
assert cu_seqlens_q.dtype == torch.int32, "cu_seqlens_q must be int32"
assert cu_seqlens_q.stride(0) == 1, "cu_seqlens_q must be contiguous"
assert q.dtype in [
torch.float16,
torch.bfloat16,
], "inputs must be float16 or bfloat16"
assert q.dtype == k.dtype == v.dtype, "inputs must have the same dtype"
assert head_dim <= 256, "head_dim must be less than or equal to 256"
alignment = 16 // q.element_size()
assert head_dim_v % alignment == 0, f"head_dim_v must be divisible by {alignment}"
q_batch_seqlen_shape = (
(batch_size, seqlen_q) if cu_seqlens_q is None else (total_q,)
)
lse_shape = (
(batch_size, num_head, seqlen_q)
if cu_seqlens_q is None
else (num_head, total_q)
)
out = q.new_empty(*q_batch_seqlen_shape, num_head, head_dim_v)
lse = q.new_empty(lse_shape, dtype=torch.float32)
return out, lse
# -----------------------------
# Registered custom ops
# NOTE: fixed return schemas to avoid:
# "Object of type 'Tensor' is not an instance of 'sequence'"
# -----------------------------
@register_custom_op(fake_impl=flash_attn_varlen_func_fake_out)
def flash_attn_varlen_func_op(
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor,
cu_seqlens_q: Optional[torch.Tensor] = None,
cu_seqlens_k: Optional[torch.Tensor] = None,
max_seqlen_q: Optional[int] = None,
max_seqlen_k: Optional[int] = None,
seqused_q: Optional[torch.Tensor] = None,
seqused_k: Optional[torch.Tensor] = None,
page_table: Optional[torch.Tensor] = None,
softmax_scale: Optional[float] = None,
causal: bool = False,
qv: Optional[torch.Tensor] = None,
q_descale: Optional[torch.Tensor] = None,
k_descale: Optional[torch.Tensor] = None,
v_descale: Optional[torch.Tensor] = None,
window_size: Optional[List[int]] = None,
attention_chunk: int = 0,
softcap: float = 0.0,
num_splits: int = 1,
pack_gqa: Optional[bool] = None,
sm_margin: int = 0,
return_softmax_lse: bool = False,
sinks: Optional[torch.Tensor] = None,
ver: int = 4,
) -> torch.Tensor:
if window_size is None:
window_size = [-1, -1]
if return_softmax_lse:
raise ValueError(
"flash_attn_varlen_func_op is out-only op; return_softmax_lse must be False. "
"Use flash_attn_varlen_func_op_lse for (out, lse)."
)
return flash_attn_varlen_func(
q,
k,
v,
cu_seqlens_q=cu_seqlens_q,
cu_seqlens_k=cu_seqlens_k,
max_seqlen_q=max_seqlen_q,
max_seqlen_k=max_seqlen_k,
seqused_q=seqused_q,
seqused_k=seqused_k,
page_table=page_table,
softmax_scale=softmax_scale,
causal=causal,
qv=qv,
q_descale=q_descale,
k_descale=k_descale,
v_descale=v_descale,
window_size=tuple(window_size),
attention_chunk=attention_chunk,
softcap=softcap,
num_splits=num_splits,
pack_gqa=pack_gqa,
sm_margin=sm_margin,
return_softmax_lse=False,
sinks=sinks,
ver=ver,
)
@register_custom_op(fake_impl=flash_attn_varlen_func_fake_out_lse)
def flash_attn_varlen_func_op_lse(
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor,
cu_seqlens_q: Optional[torch.Tensor] = None,
cu_seqlens_k: Optional[torch.Tensor] = None,
max_seqlen_q: Optional[int] = None,
max_seqlen_k: Optional[int] = None,
seqused_q: Optional[torch.Tensor] = None,
seqused_k: Optional[torch.Tensor] = None,
page_table: Optional[torch.Tensor] = None,
softmax_scale: Optional[float] = None,
causal: bool = False,
qv: Optional[torch.Tensor] = None,
q_descale: Optional[torch.Tensor] = None,
k_descale: Optional[torch.Tensor] = None,
v_descale: Optional[torch.Tensor] = None,
window_size: Optional[List[int]] = None,
attention_chunk: int = 0,
softcap: float = 0.0,
num_splits: int = 1,
pack_gqa: Optional[bool] = None,
sm_margin: int = 0,
return_softmax_lse: bool = True,
sinks: Optional[torch.Tensor] = None,
ver: int = 4,
) -> Tuple[torch.Tensor, torch.Tensor]:
if window_size is None:
window_size = [-1, -1]
if not return_softmax_lse:
raise ValueError(
"flash_attn_varlen_func_op_lse is out+lse op; return_softmax_lse must be True. "
"Use flash_attn_varlen_func_op for out-only."
)
return flash_attn_varlen_func(
q,
k,
v,
cu_seqlens_q=cu_seqlens_q,
cu_seqlens_k=cu_seqlens_k,
max_seqlen_q=max_seqlen_q,
max_seqlen_k=max_seqlen_k,
seqused_q=seqused_q,
seqused_k=seqused_k,
page_table=page_table,
softmax_scale=softmax_scale,
causal=causal,
qv=qv,
q_descale=q_descale,
k_descale=k_descale,
v_descale=v_descale,
window_size=tuple(window_size),
attention_chunk=attention_chunk,
softcap=softcap,
num_splits=num_splits,
pack_gqa=pack_gqa,
sm_margin=sm_margin,
return_softmax_lse=True,
sinks=sinks,
ver=ver,
)
from sglang.multimodal_gen.runtime.layers.attention.backends.attention_backend import (
AttentionBackend,
AttentionImpl,
AttentionMetadata,
AttentionMetadataBuilder,
)
fa_ver = 3
def set_fa_ver(ver: int) -> None:
global fa_ver
fa_ver = ver
@dataclass
class FlashAttentionMetadata:
# Sequence lengths for the forward batch
# Maximum sequence length for query
max_seqlen_q: int = 1
# Maximum sequence length for key
max_seqlen_k: int = 0
# Cumulative sequence lengths for query
cu_seqlens_q: torch.Tensor = None
# Cumulative sequence lengths for key
cu_seqlens_k: torch.Tensor = None
class FlashAttentionMetadataBuilder(AttentionMetadataBuilder):
def __init__(self) -> None:
pass
def prepare(self) -> None:
pass
def build( # type: ignore
self,
raw_latent_shape=list,
**kwargs: dict[str, Any],
) -> FlashAttentionMetadata:
# TODO: put empty values here to be set at first-run, since the q_len calculation can be complicated
return FlashAttentionMetadata(max_seqlen_q=None, max_seqlen_k=None)
class FlashAttentionBackend(AttentionBackend):
accept_output_buffer: bool = True
@staticmethod
def get_supported_head_sizes() -> list[int]:
return [32, 64, 96, 128, 160, 192, 224, 256]
@staticmethod
def get_enum() -> AttentionBackendEnum:
return AttentionBackendEnum.FA
@staticmethod
def get_impl_cls() -> type["FlashAttentionImpl"]:
return FlashAttentionImpl
@staticmethod
def get_metadata_cls() -> type["AttentionMetadata"]:
raise NotImplementedError
@staticmethod
def get_builder_cls() -> type["AttentionMetadataBuilder"]:
return FlashAttentionMetadataBuilder
class FlashAttentionImpl(AttentionImpl):
def __init__(
self,
num_heads: int,
head_size: int,
causal: bool,
softmax_scale: float,
num_kv_heads: int | None = None,
prefix: str = "",
**extra_impl_args,
) -> None:
self.num_heads = num_heads
self.num_kv_heads = num_kv_heads
self.head_size = head_size
self.causal = causal
self.softmax_scale = softmax_scale
self.attention_metadata = FlashAttentionMetadata()
def forward(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attn_metadata: AttentionMetadata = None,
*,
return_softmax_lse: bool = False,
):
if attn_metadata is not None:
if attn_metadata.max_seqlen_q is None:
attn_metadata.max_seqlen_q = query.shape[1]
if attn_metadata.max_seqlen_k is None:
attn_metadata.max_seqlen_k = key.shape[1]
max_seqlen_q = attn_metadata.max_seqlen_q
max_seqlen_k = attn_metadata.max_seqlen_k
else:
max_seqlen_q = query.shape[1]
max_seqlen_k = key.shape[1]
# FA version selection:
# - fa_ver == 3: call python function (can return Tensor or (Tensor, Tensor) depending on flag)
# - fa_ver == 4: call custom ops with FIXED return schema
if fa_ver == 3:
flash_attn_op = flash_attn_varlen_func
output = flash_attn_op(
q=query,
k=key,
v=value,
cu_seqlens_q=None,
cu_seqlens_k=None,
max_seqlen_q=max_seqlen_q,
max_seqlen_k=max_seqlen_k,
softmax_scale=self.softmax_scale,
causal=self.causal,
return_softmax_lse=return_softmax_lse,
ver=fa_ver,
)
return output
if fa_ver == 4:
if return_softmax_lse:
out_tensor, softmax_lse = flash_attn_varlen_func_op_lse(
q=query,
k=key,
v=value,
cu_seqlens_q=None,
cu_seqlens_k=None,
max_seqlen_q=max_seqlen_q,
max_seqlen_k=max_seqlen_k,
softmax_scale=self.softmax_scale,
causal=self.causal,
return_softmax_lse=True,
ver=fa_ver,
)
return out_tensor, softmax_lse
out_tensor = flash_attn_varlen_func_op(
q=query,
k=key,
v=value,
cu_seqlens_q=None,
cu_seqlens_k=None,
max_seqlen_q=max_seqlen_q,
max_seqlen_k=max_seqlen_k,
softmax_scale=self.softmax_scale,
causal=self.causal,
return_softmax_lse=False,
ver=fa_ver,
)
return out_tensor
raise ValueError(f"flash attention version {fa_ver} is not supported.")
@@ -0,0 +1,79 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
# SPDX-License-Identifier: Apache-2.0
import torch
from sglang.multimodal_gen.runtime.layers.attention.backends.attention_backend import (
AttentionBackend,
AttentionImpl,
AttentionMetadata,
AttentionMetadataBuilder,
)
from sglang.multimodal_gen.runtime.layers.attention.backends.flash_attn import (
flash_attn_func,
)
from sglang.multimodal_gen.runtime.platforms import AttentionBackendEnum
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__)
class FlashAttention2Backend(AttentionBackend):
accept_output_buffer: bool = True
@staticmethod
def get_supported_head_sizes() -> list[int]:
return [32, 64, 96, 128, 160, 192, 224, 256]
@staticmethod
def get_enum() -> AttentionBackendEnum:
return AttentionBackendEnum.FA2
@staticmethod
def get_impl_cls() -> type["FlashAttention2Impl"]:
return FlashAttention2Impl
@staticmethod
def get_metadata_cls() -> type["AttentionMetadata"]:
raise NotImplementedError
@staticmethod
def get_builder_cls() -> type["AttentionMetadataBuilder"]:
raise NotImplementedError
class FlashAttention2Impl(AttentionImpl):
def __init__(
self,
num_heads: int,
head_size: int,
causal: bool,
softmax_scale: float,
num_kv_heads: int | None = None,
prefix: str = "",
**extra_impl_args,
) -> None:
self.causal = causal
self.softmax_scale = softmax_scale
def forward(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attn_metadata: AttentionMetadata,
):
output = flash_attn_func(
q=query, # type: ignore[no-untyped-call]
k=key,
v=value,
cu_seqlens_q=None,
cu_seqlens_k=None,
max_seqlen_q=None,
max_seqlen_k=None,
softmax_scale=self.softmax_scale,
causal=self.causal,
)
return output
@@ -0,0 +1,191 @@
import torch
from sglang.multimodal_gen.runtime.layers.attention.backends.attention_backend import (
AttentionBackend,
AttentionImpl,
AttentionMetadata,
)
from sglang.multimodal_gen.runtime.layers.attention.backends.sdpa import SDPABackend
from sglang.multimodal_gen.runtime.platforms import AttentionBackendEnum
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
# Import to use torch.ops.attentions, install package with sgl_kernel_npu
try:
import attentions # noqa: F401
except ImportError as e:
raise ImportError(
(
"The required 'attentions' package is not installed."
"The package can be installed with sgl_kernel_npu"
)
) from e
logger = init_logger(__name__)
class LaserAttentionBackend(AttentionBackend):
accept_output_buffer: bool = True
@staticmethod
def get_supported_head_sizes() -> list[int]:
return [32, 64, 96, 128]
@staticmethod
def get_enum() -> AttentionBackendEnum:
return AttentionBackendEnum.LASER_ATTN
@staticmethod
def get_impl_cls() -> type["LaserAttentionImpl"]:
return LaserAttentionImpl
class LaserAttentionImpl(AttentionImpl):
def __init__(
self,
num_heads: int,
head_size: int,
causal: bool,
softmax_scale: float,
num_kv_heads: int | None = None,
prefix: str = "",
**extra_impl_args,
) -> None:
self.softmax_scale = softmax_scale
# After preprocess input layout should be BNSD.
self.seqlen_base = 256
self.seqlen_index = 2
self.dim_index = 3
self.dim_base = 128
self.max_token = 2**31 - 1
self.seq_len_pad_base = 256
# the laser attention operator has issues with small seq_len
self.min_seqlen = 2048
self.sdpa_impl = SDPABackend.get_impl_cls()(
num_heads,
head_size,
causal,
softmax_scale,
num_kv_heads,
prefix,
**extra_impl_args,
)
def _pad(self, input_tensor: torch.Tensor) -> torch.Tensor:
"""
Pad the input tensor along the sequence length and head dimension.
to multiples of base values. self.seqlen_index and self.dim_index should be positive integers.
"""
seq_len = input_tensor.size(self.seqlen_index)
head_dim = input_tensor.size(self.dim_index)
pad_seq = 0
if seq_len % self.seqlen_base != 0:
pad_seq = ((seq_len // self.seqlen_base) + 1) * self.seqlen_base - seq_len
pad_dim = 0
if head_dim % self.dim_base != 0:
pad_dim = ((head_dim // self.dim_base) + 1) * self.dim_base - head_dim
if pad_seq == 0 and pad_dim == 0:
return input_tensor
pad_list = [0] * (2 * input_tensor.ndim)
pad_list[len(pad_list) - 2 * self.seqlen_index - 1] = pad_seq
pad_list[len(pad_list) - 2 * self.dim_index - 1] = pad_dim
return torch.nn.functional.pad(input_tensor, pad_list)
def _la_preprocess_input(
self, query: torch.Tensor, key: torch.Tensor, value: torch.Tensor
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
# Currently BSND input layout is not supported
q = query.transpose(1, 2)
k = key.transpose(1, 2)
v = value.transpose(1, 2)
if q.dtype != torch.float16:
q = q.to(torch.float16)
k = k.to(torch.float16)
v = v.to(torch.float16)
q = self._pad(q)
k = self._pad(k)
v = self._pad(v)
return q, k, v
def _la_postprocess_output(
self,
attention_out: torch.Tensor,
dtype: torch.dtype,
qseqlen: int,
head_dim: int,
) -> torch.Tensor:
if dtype != attention_out.dtype:
attention_out = attention_out.to(dtype)
attention_out = attention_out[:, :, :qseqlen, :head_dim]
attention_out = attention_out.transpose(1, 2).contiguous()
return attention_out
def _laser_attention(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
head_num: int,
pre_tokens: int,
) -> tuple[torch.Tensor, torch.Tensor]:
return torch.ops.attentions.la(
query=query,
key=key,
value=value,
atten_mask=None,
alibi_mask=None,
drop_mask=None,
scale_value=self.softmax_scale,
head_num=head_num,
input_layout="BNSD",
keep_prob=1.0,
pre_tokens=pre_tokens,
next_tokens=1,
is_highPrecision=True,
)
def forward(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attn_metadata: AttentionMetadata,
) -> torch.Tensor:
q_seqlen, head_dim = query.shape[1], query.shape[3]
kv_seqlen = key.shape[1]
if q_seqlen < self.min_seqlen or kv_seqlen != q_seqlen:
output = self.sdpa_impl.forward(
query,
key,
value,
attn_metadata,
)
else:
pre_tokens = self.max_token
if kv_seqlen % self.seq_len_pad_base != 0:
pre_tokens = (
kv_seqlen // self.seq_len_pad_base + 1
) * self.seq_len_pad_base - kv_seqlen
q, k, v = self._la_preprocess_input(query, key, value)
_, la_output = self._laser_attention(q, k, v, q.shape[1], pre_tokens)
output = self._la_postprocess_output(
la_output, query.dtype, q_seqlen, head_dim
)
return output
@@ -0,0 +1,414 @@
import math
from dataclasses import dataclass
from typing import Any, List, Optional
import attentions # noqa: F401
import torch
from einops import rearrange
from sglang.multimodal_gen.runtime.layers.attention.backends.attention_backend import (
AttentionBackend,
AttentionImpl,
AttentionMetadata,
AttentionMetadataBuilder,
)
from sglang.multimodal_gen.runtime.layers.attention.backends.laser_attn import (
LaserAttentionBackend,
)
from sglang.multimodal_gen.runtime.platforms import AttentionBackendEnum
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__)
class RainFusionAttentionBackend(AttentionBackend):
accept_output_buffer: bool = True
@staticmethod
def get_supported_head_sizes() -> list[int]:
return [32, 64, 96, 128]
@staticmethod
def get_enum() -> AttentionBackendEnum:
return AttentionBackendEnum.RAIN_FUSION_ATTN
@staticmethod
def get_impl_cls() -> type["RainFusionAttentionImpl"]:
return RainFusionAttentionImpl
@staticmethod
def get_metadata_cls() -> type["RainFusionAttentionMetadata"]:
return RainFusionAttentionMetadata
@staticmethod
def get_builder_cls() -> type["RainFusionAttentionMetadataBuilder"]:
return RainFusionAttentionMetadataBuilder
@dataclass
class RainFusionAttentionMetadata(AttentionMetadata):
current_timestep: int
skip_first_steps: int
sparsity: float
latent_shape: list[int]
class RainFusionAttentionMetadataBuilder(AttentionMetadataBuilder):
def __init__(self) -> None:
pass
def prepare(self) -> None:
pass
def build(
self,
current_timestep: int,
skip_first_steps: int,
sparsity: float,
raw_latent_shape: list[int],
patch_size: tuple[int, int, int],
**kwargs: dict[str, Any],
) -> RainFusionAttentionMetadata:
if not (skip_first_steps >= 0 and 0.0 <= sparsity < 1.0):
raise ValueError(
(
"Invalid attention metadata values."
f"Sparsity should be in [0, 1), skip_first_steps should be non-negative."
f"Got sparsity={sparsity}, skip_first_steps={skip_first_steps}"
)
)
if sparsity == 0.0:
logger.warning(
(
"Sparsity is set to 0.0, which means no tokens will be dropped."
"For better performance use Laser Attention or increase sparsity."
)
)
latent_shape = raw_latent_shape[-3:]
latent_shape = [latent_shape[i] // patch_size[i] for i in range(3)]
return RainFusionAttentionMetadata(
current_timestep=current_timestep,
skip_first_steps=skip_first_steps,
sparsity=sparsity,
latent_shape=latent_shape,
)
class RainFusionAttentionImpl(AttentionImpl):
def __init__(
self,
num_heads: int,
head_size: int,
causal: bool,
softmax_scale: float,
num_kv_heads: int | None = None,
prefix: str = "",
**extra_impl_args,
) -> None:
self.causal = causal
self.softmax_scale = softmax_scale
self.block_size = 128
self.inner_precise = 0
self.laser_attn_impl = LaserAttentionBackend.get_impl_cls()(
num_heads,
head_size,
causal,
softmax_scale,
num_kv_heads,
prefix,
**extra_impl_args,
)
def _avgpool(
self, input_tensor: torch.Tensor, pool_size: int = 128
) -> torch.Tensor:
batch, seqlen, heads, dim = input_tensor.shape
x = input_tensor.permute(0, 2, 3, 1).reshape(batch * heads, dim, seqlen)
pooled = torch.nn.functional.avg_pool1d(
x, kernel_size=pool_size, stride=pool_size, ceil_mode=True
)
out = pooled.reshape(batch, heads, dim, -1).permute(0, 3, 1, 2).contiguous()
return out
def _get_mask_index(self, mask: torch.Tensor) -> torch.Tensor:
batch_size, num_heads, seq_len, _ = mask.shape
mask_reshaped = mask.reshape(-1, seq_len)
row_indices = torch.arange(
seq_len, device=mask.device, dtype=torch.float32
).unsqueeze(0)
sorted_vals = torch.where(mask_reshaped, row_indices, seq_len)
sorted_vals, _ = torch.sort(sorted_vals, dim=-1)
valid_count = mask_reshaped.sum(dim=-1, keepdim=True)
keep_mask = row_indices < valid_count
result = torch.where(keep_mask, sorted_vals, -1)
pos_matrix = result.reshape(batch_size, num_heads, seq_len, seq_len).to(
torch.int64
)
return pos_matrix
def _get_blockwise_mask(
self,
qkv_pool: torch.Tensor,
sparsity: float,
scale: float,
pool_size: int,
latent_shape: tuple,
) -> tuple[torch.Tensor, torch.Tensor]:
first_frame_len = latent_shape[1] * latent_shape[2]
query_pool, key_pool, value_pool = torch.chunk(qkv_pool, 3, dim=0)
attn_scores = (
query_pool.permute(0, 2, 1, 3) @ key_pool.permute(0, 2, 3, 1) * scale
)
keep_len = math.ceil(attn_scores.shape[-1] * (1 - sparsity))
topk_values, _ = torch.topk(attn_scores, k=keep_len, dim=-1)
mask = attn_scores >= topk_values[..., -1:]
firstframe_block_num = (first_frame_len + pool_size - 1) // pool_size
if firstframe_block_num > 0:
mask[:, :, :firstframe_block_num, :] = True
mask[:, :, :, :firstframe_block_num] = True
select_idx = self._get_mask_index(mask)
select_idx = select_idx[0].transpose(0, 1)
select_num_idx = mask[0].transpose(0, 1).sum(dim=-1)
return select_idx, select_num_idx
def _rearrange_with_remaining(
self, tensor: torch.Tensor, latent_shape: tuple[int, int, int]
) -> torch.Tensor:
"""
b (f hn hb wn wb) n d -> b (f hn wn hb wb) n d
or
b n (f hn hb wn wb) d -> b n (f hn wn hb wb) d
"""
tq, hq, wq = latent_shape
first_frame_len, frame_num = hq * wq, tq
b, s, n, d = tensor.shape
if (hq % 8 != 0) or (wq % 8 != 0):
tensor_first = tensor[:, :first_frame_len, :, :]
tensor = tensor[:, first_frame_len:, :, :]
tensor_hwt = rearrange(
tensor, "b (f h w) n d -> b f h w n d", f=frame_num - 1, h=hq, w=wq
)
if hq % 8 != 0:
tensor_hwt, tensor_h_r = torch.split(tensor_hwt, hq - (hq % 8), dim=2)
tensor_h_r = tensor_h_r.reshape(b, frame_num - 1, -1, n, d)
if wq % 8 != 0:
tensor_hwt, tensor_w_r = torch.split(tensor_hwt, wq - (wq % 8), dim=3)
tensor_w_r = tensor_w_r.reshape(b, frame_num - 1, -1, n, d)
tensor_hwt = rearrange(
tensor_hwt,
"b f (hn hb) (wn wb) n d -> b f (hn wn hb wb) n d",
f=frame_num - 1,
hb=8,
wb=8,
hn=hq // 8,
wn=wq // 8,
)
if hq % 8 != 0:
tensor_hwt = torch.cat((tensor_hwt, tensor_h_r), dim=2)
if wq % 8 != 0:
tensor_hwt = torch.cat((tensor_hwt, tensor_w_r), dim=2)
tensor_hwt = tensor_hwt.reshape(b, -1, n, d)
tensor_hwt = torch.cat([tensor_first, tensor_hwt], dim=1)
else:
tensor_hwt = rearrange(
tensor,
"b (f hn hb wn wb) n d -> b (f hn wn hb wb) n d",
f=frame_num,
hb=8,
wb=8,
hn=hq // 8,
wn=wq // 8,
)
return tensor_hwt
def _inv_rearrange_with_remaining(
self, tensor: torch.Tensor, latent_shape: tuple[int, int, int]
) -> torch.Tensor:
tq, hq, wq = latent_shape
first_frame_len, frame_num = hq * wq, tq
b, s, n, d = tensor.shape
if (hq % 8 != 0) or (wq % 8 != 0):
tensor_first = tensor[:, :first_frame_len, :, :]
tensor = tensor[:, first_frame_len:, :, :]
tensor_hwt = rearrange(
tensor, "b (f h w) n d -> b f h w n d", f=frame_num - 1, h=hq, w=wq
)
if hq % 8 != 0:
tensor_hwt, tensor_h_r = torch.split(tensor_hwt, hq - (hq % 8), dim=2)
if wq % 8 != 0:
tensor_hwt, tensor_w_r = torch.split(tensor_hwt, wq - (wq % 8), dim=3)
tensor_hwt = tensor_hwt.reshape(b, frame_num - 1, -1, n, d)
tensor_hwt = rearrange(
tensor_hwt,
"b f (hn wn hb wb) n d -> b f (hn hb) (wn wb) n d",
f=frame_num - 1,
hb=8,
wb=8,
hn=hq // 8,
wn=wq // 8,
)
if wq % 8 != 0:
tensor_hwt = torch.cat((tensor_hwt, tensor_w_r), dim=3)
if hq % 8 != 0:
tensor_hwt = torch.cat((tensor_hwt, tensor_h_r), dim=2)
tensor_hwt = tensor_hwt.reshape(b, -1, n, d)
tensor_hwt = torch.cat([tensor_first, tensor_hwt], dim=1)
else:
tensor_hwt = rearrange(
tensor,
"b (f hn wn hb wb) n h -> b (f hn hb wn wb) n h",
f=frame_num,
hb=8,
wb=8,
hn=hq // 8,
wn=wq // 8,
)
return tensor_hwt
def _do_tensor_rearrange_pooling(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
pool_size: int,
latent_shape: tuple[int, int, int],
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
"""
Tensor block rearrangement + pooling operation
"""
tensor = torch.cat((query, key, value), dim=0)
tensor = self._rearrange_with_remaining(tensor, latent_shape)
tensor_pool = self._avgpool(tensor, pool_size)
query_, key_, value_ = torch.chunk(tensor, 3, dim=0)
return query_, key_, value_, tensor_pool
def _rain_fusion_attention(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
select_idx: torch.Tensor,
select_num_idx: torch.Tensor,
blockshape: List[int],
scale: float = 1.0,
head_num: int = 1,
input_layout: str = "TND",
actual_seq_lengths=Optional[torch.Tensor],
actual_seq_lengths_kv=Optional[torch.Tensor],
) -> tuple[torch.Tensor, torch.Tensor]:
return torch.ops.attentions.rainfusionattention(
query=query,
key=key,
value=value,
select_idx=select_idx,
select_num_idx=select_num_idx,
blockshape=blockshape,
attn_mask=None,
actual_seq_qlen=actual_seq_lengths,
actual_seq_kvlen=actual_seq_lengths_kv,
block_table=None,
q_input_layout=input_layout,
kv_input_layout=input_layout,
head_num=head_num,
mask_type=0,
scale=scale,
inner_precise=self.inner_precise,
block_size=0,
)
def _rain_fusion_sparse_attention(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
latent_shape: tuple[int, int, int],
sparsity: float,
):
q, k, v, qkv_pool = self._do_tensor_rearrange_pooling(
query, key, value, self.block_size, latent_shape
)
select_idx, select_num_idx = self._get_blockwise_mask(
qkv_pool,
sparsity,
self.softmax_scale,
self.block_size,
latent_shape,
)
batch_size, seqlen_q, head_num, head_dim = q.shape
seqlen_kv = k.shape[1]
layout = "TND"
q = q.reshape(-1, head_num, head_dim)
k = k.reshape(-1, head_num, head_dim)
v = v.reshape(-1, head_num, head_dim)
actual_seq_lengths = [seqlen_q] * batch_size
actual_seq_lengths_kv = [seqlen_kv] * batch_size
out, _ = self._rain_fusion_attention(
q,
k,
v,
scale=self.softmax_scale,
head_num=head_num,
input_layout=layout,
select_idx=select_idx,
select_num_idx=select_num_idx,
blockshape=[self.block_size, self.block_size],
actual_seq_lengths=actual_seq_lengths,
actual_seq_lengths_kv=actual_seq_lengths_kv,
)
out = out.reshape(batch_size, seqlen_q, head_num, head_dim)
out = self._inv_rearrange_with_remaining(out, latent_shape)
return out
def forward(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attn_metadata: AttentionMetadata,
) -> torch.Tensor:
if attn_metadata.current_timestep < attn_metadata.skip_first_steps:
output = self.laser_attn_impl.forward(
query,
key,
value,
attn_metadata,
)
else:
output = self._rain_fusion_sparse_attention(
query,
key,
value,
attn_metadata.latent_shape,
attn_metadata.sparsity,
)
return output
@@ -0,0 +1,74 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
# SPDX-License-Identifier: Apache-2.0
import torch
from sageattention import sageattn
from sglang.multimodal_gen.runtime.layers.attention.backends.attention_backend import ( # FlashAttentionMetadata,
AttentionBackend,
AttentionImpl,
AttentionMetadata,
)
from sglang.multimodal_gen.runtime.platforms import AttentionBackendEnum
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__)
class SageAttentionBackend(AttentionBackend):
accept_output_buffer: bool = True
@staticmethod
def get_supported_head_sizes() -> list[int]:
return [32, 64, 96, 128, 160, 192, 224, 256]
@staticmethod
def get_enum() -> AttentionBackendEnum:
return AttentionBackendEnum.SAGE_ATTN
@staticmethod
def get_impl_cls() -> type["SageAttentionImpl"]:
return SageAttentionImpl
class SageAttentionImpl(AttentionImpl):
def __init__(
self,
num_heads: int,
head_size: int,
causal: bool,
softmax_scale: float,
num_kv_heads: int | None = None,
prefix: str = "",
**extra_impl_args,
) -> None:
self.causal = causal
self.softmax_scale = softmax_scale
self.dropout = extra_impl_args.get("dropout_p", 0.0)
def forward(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attn_metadata: AttentionMetadata,
*,
return_softmax_lse: bool = False,
) -> torch.Tensor:
output = sageattn(
query,
key,
value,
# since input is (batch_size, seq_len, head_num, head_dim)
tensor_layout="NHD",
is_causal=self.causal,
sm_scale=self.softmax_scale,
return_lse=return_softmax_lse,
)
if return_softmax_lse:
output, softmax_lse = output
return output, softmax_lse
return output
@@ -0,0 +1,92 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
# SPDX-License-Identifier: Apache-2.0
import torch
import torch.nn.functional as F
from sageattn3 import sageattn3_blackwell
from sglang.multimodal_gen.runtime.layers.attention.backends.attention_backend import (
AttentionBackend,
AttentionImpl,
AttentionMetadata,
)
from sglang.multimodal_gen.runtime.platforms import AttentionBackendEnum
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__)
class SageAttention3Backend(AttentionBackend):
accept_output_buffer: bool = True
@staticmethod
def get_supported_head_sizes() -> list[int]:
return [64, 128, 256]
@staticmethod
def get_enum() -> AttentionBackendEnum:
return AttentionBackendEnum.SAGE_ATTN_3
@staticmethod
def get_impl_cls() -> type["SageAttention3Impl"]:
return SageAttention3Impl
@staticmethod
def get_metadata_cls() -> type["AttentionMetadata"]:
raise NotImplementedError
class SageAttention3Impl(AttentionImpl):
_warned_gqa_fallback_global: bool = False
def __init__(
self,
num_heads: int,
head_size: int,
causal: bool,
softmax_scale: float,
num_kv_heads: int | None = None,
prefix: str = "",
**extra_impl_args,
) -> None:
self.causal = causal
self.softmax_scale = softmax_scale
self.dropout = extra_impl_args.get("dropout_p", 0.0)
def forward(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attn_metadata: AttentionMetadata,
) -> torch.Tensor:
query = query.transpose(1, 2)
key = key.transpose(1, 2)
value = value.transpose(1, 2)
# SageAttention3's Blackwell kernel assumes MHA (Hq == Hkv). For GQA/MQA
# (Hq != Hkv), fall back to torch SDPA which supports GQA.
if key.shape[1] != query.shape[1]:
if query.shape[1] % key.shape[1] != 0:
raise ValueError(
"GQA/MQA requires query heads to be a multiple of KV heads, "
f"got q_heads={query.shape[1]} and kv_heads={key.shape[1]}"
)
if not type(self)._warned_gqa_fallback_global:
logger.warning(
"SageAttention3 does not support GQA/MQA (Hq != Hkv); falling back to torch SDPA."
)
type(self)._warned_gqa_fallback_global = True
output = F.scaled_dot_product_attention(
query,
key,
value,
is_causal=self.causal,
dropout_p=self.dropout,
scale=self.softmax_scale,
enable_gqa=True,
)
else:
output = sageattn3_blackwell(query, key, value, is_causal=self.causal)
output = output.transpose(1, 2)
return output
@@ -0,0 +1,95 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
# SPDX-License-Identifier: Apache-2.0
from contextlib import nullcontext
import torch
from torch.nn.attention import SDPBackend, sdpa_kernel
from sglang.multimodal_gen.runtime.layers.attention.backends.attention_backend import ( # FlashAttentionMetadata,
AttentionBackend,
AttentionImpl,
AttentionMetadata,
)
from sglang.multimodal_gen.runtime.platforms import AttentionBackendEnum
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__)
_PYTORCH_DEFAULT_CUDA_SDP_BACKENDS = [
SDPBackend.CUDNN_ATTENTION,
SDPBackend.FLASH_ATTENTION,
SDPBackend.EFFICIENT_ATTENTION,
SDPBackend.MATH,
]
class SDPABackend(AttentionBackend):
accept_output_buffer: bool = True
@staticmethod
def get_supported_head_sizes() -> list[int]:
return [32, 64, 96, 128, 160, 192, 224, 256]
@staticmethod
def get_enum() -> AttentionBackendEnum:
return AttentionBackendEnum.TORCH_SDPA
@staticmethod
def get_impl_cls() -> type["SDPAImpl"]:
return SDPAImpl
# @staticmethod
# def get_metadata_cls() -> Type["AttentionMetadata"]:
# return FlashAttentionMetadata
class SDPAImpl(AttentionImpl):
def __init__(
self,
num_heads: int,
head_size: int,
causal: bool,
softmax_scale: float,
num_kv_heads: int | None = None,
prefix: str = "",
**extra_impl_args,
) -> None:
self.causal = causal
self.softmax_scale = softmax_scale
self.dropout = extra_impl_args.get("dropout_p", 0.0)
self.allow_cudnn_sdp = bool(extra_impl_args.get("allow_cudnn_sdp", False))
def forward(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attn_metadata: AttentionMetadata,
) -> torch.Tensor:
# transpose to bs, heads, seq_len, head_dim
query = query.transpose(1, 2)
key = key.transpose(1, 2)
value = value.transpose(1, 2)
attn_kwargs = {
"attn_mask": None,
"dropout_p": self.dropout,
"is_causal": self.causal,
"scale": self.softmax_scale,
}
if query.shape[1] != key.shape[1]:
attn_kwargs["enable_gqa"] = True
sdpa_context = (
sdpa_kernel(_PYTORCH_DEFAULT_CUDA_SDP_BACKENDS)
if self.allow_cudnn_sdp and query.device.type == "cuda"
else nullcontext()
)
with sdpa_context:
output = torch.nn.functional.scaled_dot_product_attention(
query, key, value, **attn_kwargs
)
output = output.transpose(1, 2)
return output
@@ -0,0 +1,316 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
# SPDX-License-Identifier: Apache-2.0
import json
from dataclasses import dataclass
from typing import Any
import torch
from einops import rearrange
from sglang.multimodal_gen.runtime.distributed import get_sp_group
from sglang.multimodal_gen.runtime.layers.attention.backends.attention_backend import (
AttentionBackend,
AttentionImpl,
AttentionMetadata,
AttentionMetadataBuilder,
)
from sglang.multimodal_gen.runtime.managers.forward_context import (
ForwardContext,
get_forward_context,
)
from sglang.multimodal_gen.runtime.platforms import AttentionBackendEnum
from sglang.multimodal_gen.runtime.server_args import get_global_server_args
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
from sglang.multimodal_gen.utils import dict_to_3d_list
try:
from st_attn import sliding_tile_attention
st_attn_backend_available = True
except Exception:
st_attn_backend_available = False
logger = init_logger(__name__)
class RangeDict(dict):
def __getitem__(self, item: int) -> str:
for key in self.keys():
if isinstance(key, tuple):
low, high = key
if low <= item <= high:
return str(super().__getitem__(key))
elif key == item:
return str(super().__getitem__(key))
raise KeyError(f"seq_len {item} not supported for STA")
class SlidingTileAttentionBackend(AttentionBackend):
accept_output_buffer: bool = True
@staticmethod
def get_supported_head_sizes() -> list[int]:
# TODO(will-refactor): check this
return [32, 64, 96, 128, 160, 192, 224, 256]
@staticmethod
def get_enum() -> AttentionBackendEnum:
return AttentionBackendEnum.SLIDING_TILE_ATTN
@staticmethod
def get_impl_cls() -> type["SlidingTileAttentionImpl"]:
return SlidingTileAttentionImpl
@staticmethod
def get_metadata_cls() -> type["SlidingTileAttentionMetadata"]:
return SlidingTileAttentionMetadata
@staticmethod
def get_builder_cls() -> type["SlidingTileAttentionMetadataBuilder"]:
return SlidingTileAttentionMetadataBuilder
@dataclass
class SlidingTileAttentionMetadata(AttentionMetadata):
current_timestep: int
STA_param: list[
list[Any]
] # each timestep with one metadata, shape [num_layers, num_heads]
class SlidingTileAttentionMetadataBuilder(AttentionMetadataBuilder):
def __init__(self):
pass
def prepare(self):
pass
def build( # type: ignore
self,
STA_param: list[list[Any]],
current_timestep: int,
**kwargs: dict[str, Any],
) -> SlidingTileAttentionMetadata:
param = STA_param
if param is None:
return SlidingTileAttentionMetadata(
current_timestep=current_timestep, STA_param=[]
)
return SlidingTileAttentionMetadata(
current_timestep=current_timestep, STA_param=param[current_timestep]
)
class SlidingTileAttentionImpl(AttentionImpl):
def __init__(
self,
num_heads: int,
head_size: int,
causal: bool,
softmax_scale: float,
num_kv_heads: int | None = None,
prefix: str = "",
**extra_impl_args,
) -> None:
if not st_attn_backend_available:
raise ValueError("st attn not supported")
# TODO(will-refactor): for now this is the mask strategy, but maybe we should
# have a more general config for STA?
mask_strategy_file_path = (
get_global_server_args().attention_backend_config.mask_strategy_file_path
)
if mask_strategy_file_path is None:
raise ValueError("SGLANG_DIFFUSION_ATTENTION_CONFIG is not set")
# TODO(kevin): get mask strategy for different STA modes
with open(mask_strategy_file_path) as f:
mask_strategy = json.load(f)
self.mask_strategy = dict_to_3d_list(mask_strategy)
self.prefix = prefix
sp_group = get_sp_group()
self.sp_size = sp_group.world_size
# STA config
self.STA_base_tile_size = [6, 8, 8]
self.dit_seq_shape_mapping = RangeDict(
{
(115200, 115456): "30x48x80",
82944: "36x48x48",
69120: "18x48x80",
}
)
self.full_window_mapping = {
"30x48x80": [5, 6, 10],
"36x48x48": [6, 6, 6],
"18x48x80": [3, 6, 10],
}
def tile(self, x: torch.Tensor) -> torch.Tensor:
return rearrange(
x,
"b (n_t ts_t n_h ts_h n_w ts_w) h d -> b (n_t n_h n_w ts_t ts_h ts_w) h d",
n_t=self.full_window_size[0],
n_h=self.full_window_size[1],
n_w=self.full_window_size[2],
ts_t=self.STA_base_tile_size[0],
ts_h=self.STA_base_tile_size[1],
ts_w=self.STA_base_tile_size[2],
)
def untile(self, x: torch.Tensor) -> torch.Tensor:
x = rearrange(
x,
"b (n_t n_h n_w ts_t ts_h ts_w) h d -> b (n_t ts_t n_h ts_h n_w ts_w) h d",
n_t=self.full_window_size[0],
n_h=self.full_window_size[1],
n_w=self.full_window_size[2],
ts_t=self.STA_base_tile_size[0],
ts_h=self.STA_base_tile_size[1],
ts_w=self.STA_base_tile_size[2],
)
return x
def preprocess_qkv(
self,
qkv: torch.Tensor,
attn_metadata: AttentionMetadata,
) -> torch.Tensor:
img_sequence_length = qkv.shape[1]
self.dit_seq_shape_str = self.dit_seq_shape_mapping[img_sequence_length]
self.full_window_size = self.full_window_mapping[self.dit_seq_shape_str]
self.dit_seq_shape_int = list(map(int, self.dit_seq_shape_str.split("x")))
self.img_seq_length = (
self.dit_seq_shape_int[0]
* self.dit_seq_shape_int[1]
* self.dit_seq_shape_int[2]
)
return self.tile(qkv)
def postprocess_output(
self,
output: torch.Tensor,
attn_metadata: SlidingTileAttentionMetadata,
) -> torch.Tensor:
return self.untile(output)
def forward(
self,
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor,
attn_metadata: SlidingTileAttentionMetadata,
) -> torch.Tensor:
if self.mask_strategy is None:
raise ValueError("mask_strategy cannot be None for SlidingTileAttention")
if self.mask_strategy[0] is None:
raise ValueError("mask_strategy[0] cannot be None for SlidingTileAttention")
timestep = attn_metadata.current_timestep
forward_context: ForwardContext = get_forward_context()
forward_batch = forward_context.forward_batch
if forward_batch is None:
raise ValueError("forward_batch cannot be None")
# pattern:'.double_blocks.0.attn.impl' or '.single_blocks.0.attn.impl'
layer_idx = int(self.prefix.split(".")[-3])
if attn_metadata.STA_param is None or len(attn_metadata.STA_param) <= layer_idx:
raise ValueError("Invalid STA_param")
STA_param = attn_metadata.STA_param[layer_idx]
text_length = q.shape[1] - self.img_seq_length
has_text = text_length > 0
query = q.transpose(1, 2).contiguous()
key = k.transpose(1, 2).contiguous()
value = v.transpose(1, 2).contiguous()
head_num = query.size(1)
sp_group = get_sp_group()
current_rank = sp_group.rank_in_group
start_head = current_rank * head_num
# searching or tuning mode
if len(STA_param) < head_num * sp_group.world_size:
sparse_attn_hidden_states_all = []
full_mask_window = STA_param[-1]
for window_size in STA_param[:-1]:
sparse_hidden_states = sliding_tile_attention(
query,
key,
value,
[window_size] * head_num,
text_length,
has_text,
self.dit_seq_shape_str,
).transpose(1, 2)
sparse_attn_hidden_states_all.append(sparse_hidden_states)
hidden_states = sliding_tile_attention(
query,
key,
value,
[full_mask_window] * head_num,
text_length,
has_text,
self.dit_seq_shape_str,
).transpose(1, 2)
attn_L2_loss = []
attn_L1_loss = []
# average loss across all heads
for sparse_attn_hidden_states in sparse_attn_hidden_states_all:
# L2 loss
attn_L2_loss_ = (
torch.mean(
(sparse_attn_hidden_states.float() - hidden_states.float())
** 2,
dim=[0, 1, 3],
)
.cpu()
.numpy()
)
attn_L2_loss_ = [round(float(x), 6) for x in attn_L2_loss_]
attn_L2_loss.append(attn_L2_loss_)
# L1 loss
attn_L1_loss_ = (
torch.mean(
torch.abs(
sparse_attn_hidden_states.float() - hidden_states.float()
),
dim=[0, 1, 3],
)
.cpu()
.numpy()
)
attn_L1_loss_ = [round(float(x), 6) for x in attn_L1_loss_]
attn_L1_loss.append(attn_L1_loss_)
layer_loss_save = {"L2_loss": attn_L2_loss, "L1_loss": attn_L1_loss}
if forward_batch.is_cfg_negative:
if forward_batch.mask_search_final_result_neg is not None:
forward_batch.mask_search_final_result_neg[timestep].append(
layer_loss_save
)
else:
if forward_batch.mask_search_final_result_pos is not None:
forward_batch.mask_search_final_result_pos[timestep].append(
layer_loss_save
)
else:
windows = [STA_param[head_idx + start_head] for head_idx in range(head_num)]
hidden_states = sliding_tile_attention(
query,
key,
value,
windows,
text_length,
has_text,
self.dit_seq_shape_str,
).transpose(1, 2)
return hidden_states
@@ -0,0 +1,695 @@
"""
Copyright (c) 2025 by SLA team.
Licensed under the Apache License, Version 2.0 (the "License");
This implementation is adapted from: from https://github.com/thu-ml/TurboDiffusion/blob/main/turbodiffusion/SLA/core.py and https://github.com/thu-ml/SLA/blob/main/SageSLA/core.py
Citation (please cite if you use this code):
@article{zhang2025sla,
title={SLA: Beyond Sparsity in Diffusion Transformers via Fine-Tunable Sparse-Linear Attention},
author={Jintao Zhang and Haoxu Wang and Kai Jiang and Shuo Yang and Kaiwen Zheng and Haocheng Xi and Ziteng Wang and Hongzhou Zhu and Min Zhao and Ion Stoica and Joseph E. Gonzalez and Jun Zhu and Jianfei Chen},
journal={arXiv preprint arXiv:2509.24006},
year={2025}
}
"""
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any
import torch
import torch.nn as nn
import torch.nn.functional as F
import triton
import triton.language as tl
from sglang.multimodal_gen.runtime.layers.attention.backends.attention_backend import (
AttentionBackend,
AttentionImpl,
AttentionMetadata,
AttentionMetadataBuilder,
)
from sglang.multimodal_gen.runtime.platforms import AttentionBackendEnum
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__)
# ==================================SLA Functions===================================
def get_block_map(q, k, topk_ratio, BLKQ=64, BLKK=64):
arg_k = k - torch.mean(
k, dim=-2, keepdim=True
) # smooth-k technique in SageAttention
pooled_qblocks = mean_pool(q, BLKQ)
pooled_kblocks = mean_pool(arg_k, BLKK)
pooled_score = pooled_qblocks @ pooled_kblocks.transpose(-1, -2)
K = pooled_score.shape[-1]
topk = min(K, int(topk_ratio * K))
lut = torch.topk(pooled_score, topk, dim=-1, sorted=False).indices
sparse_map = torch.zeros_like(pooled_score, dtype=torch.int8)
sparse_map.scatter_(-1, lut, 1)
return sparse_map, lut, topk
def mean_pool(x, BLK):
assert x.is_contiguous()
B, H, L, D = x.shape
L_BLOCKS = (L + BLK - 1) // BLK
x_mean = torch.empty((B, H, L_BLOCKS, D), device=x.device, dtype=x.dtype)
grid = (L_BLOCKS, B * H)
compress_kernel[grid](x, x_mean, L, D, BLK)
return x_mean
@triton.jit
def compress_kernel(
X,
XM,
L: tl.constexpr,
D: tl.constexpr,
BLOCK_L: tl.constexpr,
):
idx_l = tl.program_id(0)
idx_bh = tl.program_id(1)
offs_l = idx_l * BLOCK_L + tl.arange(0, BLOCK_L)
offs_d = tl.arange(0, D)
x_offset = idx_bh * L * D
xm_offset = idx_bh * ((L + BLOCK_L - 1) // BLOCK_L) * D
x = tl.load(
X + x_offset + offs_l[:, None] * D + offs_d[None, :], mask=offs_l[:, None] < L
)
nx = min(BLOCK_L, L - idx_l * BLOCK_L)
x_mean = tl.sum(x, axis=0, dtype=tl.float32) / nx
tl.store(XM + xm_offset + idx_l * D + offs_d, x_mean.to(XM.dtype.element_ty))
@triton.jit
def _attn_fwd(
Q,
K,
V,
qk_scale: tl.constexpr,
topk: tl.constexpr,
LUT,
LSE,
OS,
L: tl.constexpr,
M_BLOCKS: tl.constexpr,
D: tl.constexpr,
BLOCK_M: tl.constexpr,
BLOCK_N: tl.constexpr,
):
idx_m = tl.program_id(0).to(tl.int64)
idx_bh = tl.program_id(1).to(tl.int64)
qkv_offset = idx_bh * L * D
lut_offset = (idx_bh * M_BLOCKS + idx_m) * topk
lse_offset = idx_bh * L
offs_m = idx_m * BLOCK_M + tl.arange(0, BLOCK_M)
offs_n = tl.arange(0, BLOCK_N)
offs_d = tl.arange(0, D)
Q_ptrs = Q + qkv_offset + offs_m[:, None] * D + offs_d[None, :]
K_ptrs = K + qkv_offset + offs_n[None, :] * D + offs_d[:, None]
V_ptrs = V + qkv_offset + offs_n[:, None] * D + offs_d[None, :]
OS_ptrs = OS + qkv_offset + offs_m[:, None] * D + offs_d[None, :]
LUT_ptr = LUT + lut_offset
LSE_ptrs = LSE + lse_offset + offs_m
m_i = tl.full([BLOCK_M], -float("inf"), dtype=tl.float32)
l_i = tl.zeros([BLOCK_M], dtype=tl.float32)
o_s = tl.zeros([BLOCK_M, D], dtype=tl.float32)
q = tl.load(Q_ptrs, mask=offs_m[:, None] < L)
for block_idx in tl.range(topk):
idx_n = tl.load(LUT_ptr + block_idx)
n_mask = offs_n < L - idx_n * BLOCK_N
k = tl.load(K_ptrs + idx_n * BLOCK_N * D, mask=n_mask[None, :])
qk = tl.dot(q, k) * (qk_scale * 1.4426950408889634) # = 1 / ln(2)
if L - idx_n * BLOCK_N < BLOCK_N:
qk = tl.where(n_mask[None, :], qk, float("-inf"))
v = tl.load(V_ptrs + idx_n * BLOCK_N * D, mask=n_mask[:, None])
local_m = tl.max(qk, 1)
new_m = tl.maximum(m_i, local_m)
qk = qk - new_m[:, None]
p = tl.math.exp2(qk)
l_ij = tl.sum(p, 1)
alpha = tl.math.exp2(m_i - new_m)
o_s = o_s * alpha[:, None]
o_s += tl.dot(p.to(v.dtype), v)
l_i = l_i * alpha + l_ij
m_i = new_m
o_s = o_s / l_i[:, None]
tl.store(OS_ptrs, o_s.to(OS.type.element_ty), mask=offs_m[:, None] < L)
m_i += tl.math.log2(l_i)
tl.store(LSE_ptrs, m_i, mask=offs_m < L)
def _get_cuda_arch(device_index: int) -> str:
"""Get CUDA architecture string for the given device."""
major, minor = torch.cuda.get_device_capability(device_index)
return f"sm{major}{minor}"
# ==================================SLA Class===================================
class SparseLinearAttentionBackend(AttentionBackend):
"""Sparse Linear Attention Backend for efficient attention computation."""
accept_output_buffer: bool = True
@staticmethod
def get_supported_head_sizes() -> list[int]:
return [64, 128]
@staticmethod
def get_enum() -> AttentionBackendEnum:
return AttentionBackendEnum.SLA_ATTN
@staticmethod
def get_impl_cls() -> type["SparseLinearAttentionImpl"]:
return SparseLinearAttentionImpl
@staticmethod
def get_metadata_cls() -> type["SparseLinearAttentionMetadata"]:
return SparseLinearAttentionMetadata
@staticmethod
def get_builder_cls() -> type["SparseLinearAttentionMetadataBuilder"]:
return SparseLinearAttentionMetadataBuilder
@dataclass
class SparseLinearAttentionMetadata(AttentionMetadata):
"""Metadata for Sparse Linear Attention computation."""
# Basic attention parameters
current_timestep: int
# Sparse attention configuration
topk_ratio: float = 0.1
class SparseLinearAttentionMetadataBuilder(AttentionMetadataBuilder):
"""Builder for SparseLinearAttentionMetadata."""
def __init__(self) -> None:
pass
def prepare(self) -> None:
pass
def build(
self,
current_timestep: int,
topk_ratio: float = 0.1,
**kwargs: dict[str, Any],
) -> SparseLinearAttentionMetadata:
return SparseLinearAttentionMetadata(
current_timestep=current_timestep,
topk_ratio=topk_ratio,
)
class SparseLinearAttentionImpl(AttentionImpl, nn.Module):
"""Implementation of sparse linear attention for the backend."""
def __init__(
self,
num_heads: int,
head_size: int,
causal: bool = False,
softmax_scale: float | None = None,
num_kv_heads: int | None = None,
prefix: str = "",
# SLA-specific parameters - matched to TurboDiffusion defaults
topk_ratio: float = 0.1, # TurboDiffusion uses topk=0.1
feature_map: str = "softmax",
BLKQ: int = 128, # TurboDiffusion uses BLKQ=128
BLKK: int = 64, # TurboDiffusion uses BLKK=64
use_bf16: bool = True,
**extra_impl_args,
) -> None:
nn.Module.__init__(self)
# SLA-specific config
self.topk_ratio = topk_ratio
self.BLKQ = BLKQ
self.BLKK = BLKK
self.dtype = torch.bfloat16 if use_bf16 else torch.float16
# Learnable linear projection for combining sparse + linear attention
self.proj_l = nn.Linear(head_size, head_size, dtype=torch.float32)
# Feature map for linear attention
# Type annotation for callables
self.feature_map_q: Callable[[torch.Tensor], torch.Tensor]
self.feature_map_k: Callable[[torch.Tensor], torch.Tensor]
if feature_map == "elu":
self.feature_map_q = lambda x: F.elu(x) + 1
self.feature_map_k = lambda x: F.elu(x) + 1
elif feature_map == "relu":
self.feature_map_q = F.relu
self.feature_map_k = F.relu
elif feature_map == "softmax":
self.feature_map_q = lambda x: F.softmax(x, dim=-1)
self.feature_map_k = lambda x: F.softmax(x, dim=-1)
else:
raise ValueError(f"Unknown feature map: {feature_map}")
self._init_weights()
def _init_weights(self) -> None:
"""Initialize projection weights to zero for residual-like behavior."""
with torch.no_grad():
nn.init.zeros_(self.proj_l.weight)
nn.init.zeros_(self.proj_l.bias) # type: ignore[arg-type]
def _calc_linear_attention_with_torch(self, q, k, v):
kv = torch.matmul(k.transpose(-1, -2), v)
k_sum = torch.sum(k, dim=-2, keepdim=True)
return torch.matmul(q, kv) / (1e-5 + torch.matmul(q, k_sum.transpose(-1, -2)))
def forward(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attn_metadata: SparseLinearAttentionMetadata = None,
) -> torch.Tensor:
"""Forward pass for sparse linear attention.
Args:
query: query tensor of shape (B, H, L, D)
key: key tensor of shape (B, H, L, D)
value: value tensor of shape (B, H, L, D)
attn_metadata: attention metadata containing configuration
Returns:
output tensor of shape (B, H, L, D)
"""
dtype = query.dtype
# Transpose for computation
query = query.transpose(1, 2).contiguous()
key = key.transpose(1, 2).contiguous()
value = value.transpose(1, 2).contiguous()
# Get sparse attention map
sparse_map, lut, real_topk = get_block_map(
query, key, topk_ratio=self.topk_ratio, BLKQ=self.BLKQ, BLKK=self.BLKK
)
# Convert to computation dtype
query = query.to(self.dtype)
key = key.to(self.dtype)
value = value.to(self.dtype)
# Sparse attention computation
o_s = _attention.apply(
query, key, value, sparse_map, lut, real_topk, self.BLKQ, self.BLKK
)
# Apply feature maps
query = self.feature_map_q(query).to(self.dtype) # c_q
key = self.feature_map_k(key).to(self.dtype) # c_k
# Linear attention computation
o_l = self._calc_linear_attention_with_torch(query, key, value)
# Apply projection and combine results
with torch.amp.autocast("cuda", dtype=self.dtype):
o_l = self.proj_l(o_l)
# Combine sparse and linear attention
output = (o_s + o_l).to(dtype).transpose(1, 2)
return output
class _attention(torch.autograd.Function):
@staticmethod
def forward(ctx, q, k, v, k_block_id, lut, topk, BLOCK_M, BLOCK_N, qk_scale=None):
assert q.is_contiguous() and k.is_contiguous() and v.is_contiguous()
assert k_block_id.is_contiguous() and lut.is_contiguous()
# We recommend the following two settings
assert BLOCK_M == 64 or BLOCK_M == 128
assert BLOCK_N == 64
B, H, L, D = q.shape
if qk_scale is None:
qk_scale = D**-0.5
M_BLOCKS = triton.cdiv(L, BLOCK_M)
o_s = torch.empty_like(v)
lse = torch.empty(q.shape[:-1], device=q.device, dtype=torch.float32)
grid = (M_BLOCKS, B * H)
_attn_fwd[grid](
q,
k,
v,
qk_scale,
topk,
lut,
lse,
o_s,
L,
M_BLOCKS,
D,
BLOCK_M,
BLOCK_N,
num_warps=4 if q.shape[-1] == 64 else 8,
num_stages=3,
)
ctx.save_for_backward(q, k, v, k_block_id, lut, lse, o_s)
ctx.qk_scale = qk_scale
ctx.topk = topk
ctx.BLOCK_M = BLOCK_M
ctx.BLOCK_N = BLOCK_N
return o_s
# ==================================SageSLA Class===================================
SAGESLA_ENABLED = True
try:
import spas_sage_attn._fused as fused
import spas_sage_attn._qattn as qattn
from spas_sage_attn.utils import block_map_lut_triton, get_vanilla_qk_quant
except ImportError:
SAGESLA_ENABLED = False
SAGE2PP_ENABLED = True
try:
from spas_sage_attn._qattn import (
qk_int8_sv_f8_accum_f16_block_sparse_attn_inst_buf_fuse_v_scale_with_pv_threshold,
)
except ImportError:
SAGE2PP_ENABLED = False
class SageSparseLinearAttentionBackend(AttentionBackend):
"""Quantized Sparse-Linear Attention backend using SageAttention kernels."""
accept_output_buffer: bool = True
@staticmethod
def get_supported_head_sizes() -> list[int]:
return [64, 128]
@staticmethod
def get_enum() -> AttentionBackendEnum:
return AttentionBackendEnum.SAGE_SLA_ATTN
@staticmethod
def get_impl_cls() -> type["SageSparseLinearAttentionImpl"]:
return SageSparseLinearAttentionImpl
@staticmethod
def get_metadata_cls() -> type["SageSparseLinearAttentionMetadata"]:
return SageSparseLinearAttentionMetadata
@staticmethod
def get_builder_cls() -> type["SageSparseLinearAttentionMetadataBuilder"]:
return SageSparseLinearAttentionMetadataBuilder
@dataclass
class SageSparseLinearAttentionMetadata(AttentionMetadata):
"""Metadata for Sage Sparse Linear Attention computation."""
# Basic attention parameters
current_timestep: int
# Sparse attention configuration
topk_ratio: float = 0.1
class SageSparseLinearAttentionMetadataBuilder(AttentionMetadataBuilder):
"""Builder for SageSparseLinearAttentionMetadata."""
def __init__(self) -> None:
pass
def prepare(self) -> None:
pass
def build(
self,
current_timestep: int,
topk_ratio: float = 0.1,
**kwargs: dict[str, Any],
) -> SageSparseLinearAttentionMetadata:
return SageSparseLinearAttentionMetadata(
current_timestep=current_timestep,
topk_ratio=topk_ratio,
)
class SageSparseLinearAttentionImpl(AttentionImpl, nn.Module):
def __init__(
self,
num_heads: int,
head_size: int,
causal: bool = False,
softmax_scale: float | None = None,
num_kv_heads: int | None = None,
prefix: str = "",
topk_ratio: float = 0.5,
feature_map: str = "softmax",
use_bf16: bool = True,
**extra_impl_args,
) -> None:
nn.Module.__init__(self)
assert (
SAGESLA_ENABLED
), "Install spas_sage_attn(pip install git+https://github.com/thu-ml/SpargeAttn.git --no-build-isolation) first to enable SageSLA."
self.num_heads = num_heads
self.head_size = head_size
self.softmax_scale = softmax_scale if softmax_scale else head_size**-0.5
self.causal = causal
self.prefix = prefix
self.topk_ratio = topk_ratio
self.dtype = torch.bfloat16 if use_bf16 else torch.float16
# Learnable linear projection for combining sparse + linear attention
self.proj_l = nn.Linear(head_size, head_size, dtype=torch.float32)
# Feature map for linear attention
# Type annotation for callables
self.feature_map_q: Callable[[torch.Tensor], torch.Tensor]
self.feature_map_k: Callable[[torch.Tensor], torch.Tensor]
if feature_map == "elu":
self.feature_map_q = lambda x: F.elu(x) + 1
self.feature_map_k = lambda x: F.elu(x) + 1
elif feature_map == "relu":
self.feature_map_q = F.relu
self.feature_map_k = F.relu
elif feature_map == "softmax":
self.feature_map_q = lambda x: F.softmax(x, dim=-1)
self.feature_map_k = lambda x: F.softmax(x, dim=-1)
else:
raise ValueError(f"Unknown feature map: {feature_map}")
self._init_weights()
def _init_weights(self) -> None:
"""Initialize projection weights to zero for residual-like behavior."""
with torch.no_grad():
nn.init.zeros_(self.proj_l.weight)
nn.init.zeros_(self.proj_l.bias) # type: ignore[arg-type]
def _calc_linear_attention_with_torch(
self,
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor,
):
kv = torch.matmul(k.transpose(-1, -2), v)
k_sum = torch.sum(k, dim=-2, keepdim=True)
return torch.matmul(q, kv) / (1e-5 + torch.matmul(q, k_sum.transpose(-1, -2)))
def forward(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attn_metadata: AttentionMetadata,
) -> torch.Tensor:
"""Forward pass for Sage Sparse Linear attention with quantized kernels.
Args:
query: query tensor of shape (B, L, H, D)
key: key tensor of shape (B, L, H, D)
value: value tensor of shape (B, L, H, D)
attn_metadata: attention metadata containing configuration
Returns:
output tensor of shape (B, L, H, D)
"""
dtype = query.dtype
# Transpose from (B, L, H, D) to SLA format (B, H, L, D)
q = query.transpose(1, 2).contiguous()
k = key.transpose(1, 2).contiguous()
v = value.transpose(1, 2).contiguous()
# Determine block sizes based on GPU architecture
arch = _get_cuda_arch(q.device.index)
if arch == "sm90":
BLKQ = 64
BLKK = 128
else:
BLKQ = 128
BLKK = 64
# Compute block-sparse attention pattern
sparse_map, lut, real_topk = get_block_map(
q, k, topk_ratio=self.topk_ratio, BLKQ=BLKQ, BLKK=BLKK
)
# Convert to compute dtype
q = q.to(self.dtype)
k = k.to(self.dtype)
v = v.to(self.dtype)
########## SPARGE BEGIN ##########
km = k.mean(dim=-2, keepdim=True)
headdim = q.size(-1)
assert headdim in [
64,
128,
], "headdim should be in [64, 128]. For other headdim, you can use padding and specify the softmax scale."
# Quantize Q, K to INT8
q_int8, q_scale, k_int8, k_scale = get_vanilla_qk_quant(q, k, km, BLKQ, BLKK)
lut, valid_block_num = block_map_lut_triton(sparse_map)
scale = 1.0 / (headdim**0.5)
o_s = torch.empty_like(q)
if arch in ("sm80", "sm86", "sm87"):
pvthreshold = torch.full(
(q.shape[-3],), 1e6, dtype=torch.float32, device=q.device
)
v_fp16 = v.to(torch.float16)
qattn.qk_int8_sv_f16_accum_f16_block_sparse_attn_inst_buf_with_pv_threshold(
q_int8,
k_int8,
v_fp16,
o_s,
lut,
valid_block_num,
pvthreshold,
q_scale,
k_scale,
1,
False,
1,
scale,
0,
)
else:
b, h_kv, kv_len, head_dim = v.shape
padded_len = (kv_len + 127) // 128 * 128
v_transposed_permutted = torch.empty(
(b, h_kv, head_dim, padded_len), dtype=v.dtype, device=v.device
)
fused.transpose_pad_permute_cuda(v, v_transposed_permutted, 1)
v_fp8 = torch.empty(
v_transposed_permutted.shape, dtype=torch.float8_e4m3fn, device=v.device
)
v_scale = torch.empty(
(b, h_kv, head_dim), dtype=torch.float32, device=v.device
)
fused.scale_fuse_quant_cuda(
v_transposed_permutted, v_fp8, v_scale, kv_len, 2.25, 1
)
if arch == "sm90":
qattn.qk_int8_sv_f8_accum_f32_block_sparse_attn_inst_buf_fuse_v_scale_sm90(
q_int8,
k_int8,
v_fp8,
o_s,
lut,
valid_block_num,
q_scale,
k_scale,
v_scale,
1,
False,
1,
scale,
)
else:
pvthreshold = torch.full(
(q.shape[-3],), 1e6, dtype=torch.float32, device=q.device
)
if SAGE2PP_ENABLED:
qk_int8_sv_f8_accum_f16_block_sparse_attn_inst_buf_fuse_v_scale_with_pv_threshold(
q_int8,
k_int8,
v_fp8,
o_s,
lut,
valid_block_num,
pvthreshold,
q_scale,
k_scale,
v_scale,
1,
False,
1,
scale,
0,
)
else:
qattn.qk_int8_sv_f8_accum_f32_block_sparse_attn_inst_buf_fuse_v_scale_with_pv_threshold(
q_int8,
k_int8,
v_fp8,
o_s,
lut,
valid_block_num,
pvthreshold,
q_scale,
k_scale,
v_scale,
1,
False,
1,
scale,
0,
)
########## SPARGE END ##########
# Linear attention with feature maps
q_linear = self.feature_map_q(q).to(self.dtype)
k_linear = self.feature_map_k(k).to(self.dtype)
o_l = self._calc_linear_attention_with_torch(q_linear, k_linear, v)
# Project linear attention output and combine
with torch.amp.autocast("cuda", dtype=self.dtype):
o_l = self.proj_l(o_l)
# Combine sparse and linear outputs
output = (o_s + o_l).to(dtype).transpose(1, 2)
return output
@@ -0,0 +1,562 @@
"""
Sparse Video Gen 2 (SAP) attention backend.
This is a baseline integration that wires the backend into the
attention framework.
Adapted from https://github.com/svg-project/Sparse-VideoGen/blob/main/svg/models/wan/attention.py
"""
from dataclasses import dataclass, field
from typing import Any
import torch
import torch.nn.functional as F
from torch.nn.attention import SDPBackend, sdpa_kernel
try:
from svg.kernels.triton.permute import (
apply_inverse_permutation_triton,
permute_tensor_by_labels_triton,
)
from svg.kmeans_utils import (
batch_kmeans_Euclid,
dynamic_block_sparse_fwd_flashinfer,
identify_dynamic_map,
)
svg2_available = True
except ImportError:
svg2_available = False
from sglang.multimodal_gen.runtime.layers.attention.backends.attention_backend import (
AttentionBackend,
AttentionImpl,
AttentionMetadata,
AttentionMetadataBuilder,
)
from sglang.multimodal_gen.runtime.platforms import AttentionBackendEnum
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__)
class SparseVideoGen2AttentionBackend(AttentionBackend):
accept_output_buffer: bool = True
@staticmethod
def get_supported_head_sizes() -> list[int]:
return [64, 128, 256]
@staticmethod
def get_enum() -> AttentionBackendEnum:
return AttentionBackendEnum.SPARSE_VIDEO_GEN_2_ATTN
@staticmethod
def get_impl_cls() -> type["SparseVideoGen2AttentionImpl"]:
return SparseVideoGen2AttentionImpl
@staticmethod
def get_metadata_cls() -> type["SparseVideoGen2AttentionMetadata"]:
return SparseVideoGen2AttentionMetadata
@staticmethod
def get_builder_cls() -> type["SparseVideoGen2AttentionMetadataBuilder"]:
return SparseVideoGen2AttentionMetadataBuilder
@dataclass
class Svg2LayerCache:
# centroids for kmeans clustering
q_centroids: torch.Tensor | None = None
k_centroids: torch.Tensor | None = None
centroids_initialized: bool = False
@dataclass
class Svg2Cache:
layers: dict[int, Svg2LayerCache] = field(default_factory=dict)
def get_layer(self, layer_idx: int) -> Svg2LayerCache:
layer_cache = self.layers.get(layer_idx)
if layer_cache is None:
layer_cache = Svg2LayerCache()
self.layers[layer_idx] = layer_cache
return layer_cache
@dataclass
class SparseVideoGen2AttentionMetadata(AttentionMetadata):
current_timestep: int
num_q_centroids: int
num_k_centroids: int
top_p_kmeans: float
min_kc_ratio: float
kmeans_iter_init: int
kmeans_iter_step: int
zero_step_kmeans_init: bool
first_layers_fp: float
first_times_fp: float
context_length: int
num_frame: int
frame_size: int
cache: Svg2Cache
prompt_length: int | None = None
max_seqlen_q: int | None = None
max_seqlen_k: int | None = None
def _require_kwarg(kwargs: dict[str, Any], name: str) -> Any:
if name not in kwargs:
raise ValueError(
f"Missing required argument for SparseVideoGen2Attention: {name}"
)
return kwargs[name]
class SparseVideoGen2AttentionMetadataBuilder(AttentionMetadataBuilder):
def __init__(self) -> None:
pass
def prepare(self) -> None:
pass
def build( # type: ignore[override]
self,
current_timestep: int,
raw_latent_shape: tuple[int, ...],
patch_size: tuple[int, int, int],
cache: Svg2Cache,
num_q_centroids: int,
num_k_centroids: int,
top_p_kmeans: float,
min_kc_ratio: float,
kmeans_iter_init: int,
kmeans_iter_step: int,
zero_step_kmeans_init: bool,
first_layers_fp: float,
first_times_fp: float,
context_length: int = 0,
prompt_length: int | None = None,
**kwargs: dict[str, Any],
) -> SparseVideoGen2AttentionMetadata:
raw_shape = tuple(raw_latent_shape)
if len(raw_shape) == 5:
t, h, w = raw_shape[2:5]
elif len(raw_shape) == 3:
t, h, w = raw_shape
else:
raise ValueError(
"raw_latent_shape must be (T, H, W) or (B, C, T, H, W) for SAP attention"
)
pt, ph, pw = patch_size
if t % pt != 0 or h % ph != 0 or w % pw != 0:
raise ValueError(
"raw_latent_shape must be divisible by patch_size for SAP attention"
)
num_frame = t // pt
frame_size = (h // ph) * (w // pw)
return SparseVideoGen2AttentionMetadata(
current_timestep=current_timestep,
num_q_centroids=num_q_centroids,
num_k_centroids=num_k_centroids,
top_p_kmeans=top_p_kmeans,
min_kc_ratio=min_kc_ratio,
kmeans_iter_init=kmeans_iter_init,
kmeans_iter_step=kmeans_iter_step,
zero_step_kmeans_init=zero_step_kmeans_init,
first_layers_fp=first_layers_fp,
first_times_fp=first_times_fp,
context_length=context_length,
prompt_length=prompt_length,
num_frame=num_frame,
frame_size=frame_size,
cache=cache,
)
class SparseVideoGen2AttentionImpl(AttentionImpl):
def __init__(
self,
num_heads: int,
head_size: int,
causal: bool,
softmax_scale: float,
num_kv_heads: int | None = None,
prefix: str = "",
**extra_impl_args,
) -> None:
if causal:
raise ValueError(
"Sparse Video Gen 2 attention does not support causal attention"
)
if not svg2_available:
raise ImportError(
"Sparse Video Gen 2 attention backend requires svg package to be installed"
"Please install it by following the instructions at "
"https://github.com/svg-project/Sparse-VideoGen"
)
self.prefix = prefix
self.layer_idx = self._get_layer_idx(prefix)
def _get_layer_idx(self, prefix: str) -> int:
parts = prefix.split(".")
if len(parts) < 3:
raise ValueError(
f"Invalid prefix for SparseVideoGen2AttentionImpl: {prefix}"
)
return int(parts[-3])
def kmeans_init(
self,
query: torch.Tensor,
key: torch.Tensor,
attn_metadata: SparseVideoGen2AttentionMetadata,
):
cfg, num_heads, seq_len, dim = query.size()
qlabels, qcentroids, qcluster_sizes, qiter = batch_kmeans_Euclid(
query.reshape(cfg * num_heads, seq_len, dim),
n_clusters=attn_metadata.num_q_centroids,
max_iters=attn_metadata.kmeans_iter_init,
)
klabels, kcentroids, kcluster_sizes, kiter = batch_kmeans_Euclid(
key.reshape(cfg * num_heads, seq_len, dim),
n_clusters=attn_metadata.num_k_centroids,
max_iters=attn_metadata.kmeans_iter_init,
)
layer_cache = attn_metadata.cache.get_layer(self.layer_idx)
layer_cache.q_centroids = qcentroids
layer_cache.k_centroids = kcentroids
return (
qlabels,
qcentroids,
qcluster_sizes,
qiter,
klabels,
kcentroids,
kcluster_sizes,
kiter,
)
def kmeans_step(
self,
query: torch.Tensor,
key: torch.Tensor,
attn_metadata: SparseVideoGen2AttentionMetadata,
):
cfg, num_heads, seq_len, dim = query.size()
layer_cache = attn_metadata.cache.get_layer(self.layer_idx)
qlabels, qcentroids, qcluster_sizes, qiter = batch_kmeans_Euclid(
query.reshape(cfg * num_heads, seq_len, dim),
n_clusters=attn_metadata.num_q_centroids,
max_iters=attn_metadata.kmeans_iter_step,
init_centroids=layer_cache.q_centroids,
)
klabels, kcentroids, kcluster_sizes, kiter = batch_kmeans_Euclid(
key.reshape(cfg * num_heads, seq_len, dim),
n_clusters=attn_metadata.num_k_centroids,
max_iters=attn_metadata.kmeans_iter_step,
init_centroids=layer_cache.k_centroids,
)
layer_cache.q_centroids = qcentroids
layer_cache.k_centroids = kcentroids
return (
qlabels,
qcentroids,
qcluster_sizes,
qiter,
klabels,
kcentroids,
kcluster_sizes,
kiter,
)
def kmeans_clustering(
self,
query: torch.Tensor,
key: torch.Tensor,
attn_metadata: SparseVideoGen2AttentionMetadata,
):
layer_cache = attn_metadata.cache.get_layer(self.layer_idx)
if not layer_cache.centroids_initialized:
(
qlabels,
qcentroids,
qcluster_sizes,
qiter,
klabels,
kcentroids,
kcluster_sizes,
kiter,
) = self.kmeans_init(query, key, attn_metadata)
layer_cache.centroids_initialized = True
logger.debug(
"Centroids initialized at layer %s (init iters: %s).",
self.layer_idx,
attn_metadata.kmeans_iter_init,
)
else:
(
qlabels,
qcentroids,
qcluster_sizes,
qiter,
klabels,
kcentroids,
kcluster_sizes,
kiter,
) = self.kmeans_step(query, key, attn_metadata)
return (
qlabels,
qcentroids,
qcluster_sizes,
qiter,
klabels,
kcentroids,
kcluster_sizes,
kiter,
)
def semantic_aware_permutation(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attn_metadata: SparseVideoGen2AttentionMetadata,
):
cfg, num_heads, seq_len, dim = query.size()
# 1. Kmeans clustering
(
qlabels,
qcentroids,
qcluster_sizes,
qiter,
klabels,
kcentroids,
kcluster_sizes,
kiter,
) = self.kmeans_clustering(query, key, attn_metadata)
# 2. Identify dynamic map
q_cluster_sizes = qcluster_sizes.view(
cfg, num_heads, attn_metadata.num_q_centroids
)
k_cluster_sizes = kcluster_sizes.view(
cfg, num_heads, attn_metadata.num_k_centroids
)
dynamic_map = identify_dynamic_map(
qcentroids.view(cfg, num_heads, attn_metadata.num_q_centroids, dim),
kcentroids.view(cfg, num_heads, attn_metadata.num_k_centroids, dim),
q_cluster_sizes,
k_cluster_sizes,
attn_metadata.top_p_kmeans,
attn_metadata.min_kc_ratio,
)
# 3. Permute the query, key, value
q_permuted, q_sorted_indices = permute_tensor_by_labels_triton(
query, qlabels, dim=2
)
k_permuted, k_sorted_indices = permute_tensor_by_labels_triton(
key, klabels, dim=2
)
v_permuted, v_sorted_indices = permute_tensor_by_labels_triton(
value, klabels, dim=2, sorted_indices=k_sorted_indices
)
return (
q_permuted,
k_permuted,
v_permuted,
dynamic_map,
q_cluster_sizes,
k_cluster_sizes,
q_sorted_indices,
)
def _hunyuan_dynamic_map_post_processing(
self,
q_perm: torch.Tensor,
k_perm: torch.Tensor,
v_perm: torch.Tensor,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
dyn_map: torch.Tensor,
qc_sz_s: torch.Tensor,
kc_sz_s: torch.Tensor,
q_sorted_indices: torch.Tensor,
video_length: int,
context_length: int,
prompt_length: int,
unprompt_length: int,
) -> tuple[
torch.Tensor,
torch.Tensor,
torch.Tensor,
torch.Tensor,
torch.Tensor,
torch.Tensor,
torch.Tensor,
]:
# Place the permuted video tokens back and keep text tokens at the tail.
query[:, :, :-context_length, :] = q_perm
key[:, :, :-context_length, :] = k_perm
value[:, :, :-context_length, :] = v_perm
# Add prompt/unprompt clusters to the dynamic map.
dyn_map = F.pad(dyn_map, (0, 2, 0, 2), value=0)
dyn_map[:, :, -2, :-1] = True
dyn_map[:, :, :-1, -2] = True
dyn_map[:, :, -1, -1] = True
qc_sz_s = F.pad(qc_sz_s, (0, 2), value=0)
qc_sz_s[:, :, -2] = prompt_length
qc_sz_s[:, :, -1] = unprompt_length
kc_sz_s = F.pad(kc_sz_s, (0, 2), value=0)
kc_sz_s[:, :, -2] = prompt_length
kc_sz_s[:, :, -1] = unprompt_length
q_sorted_indices = F.pad(q_sorted_indices, (0, context_length), value=0)
q_sorted_indices[:, video_length:] = torch.arange(
video_length,
video_length + context_length,
device=q_sorted_indices.device,
)
return query, key, value, dyn_map, qc_sz_s, kc_sz_s, q_sorted_indices
def forward(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attn_metadata: SparseVideoGen2AttentionMetadata,
) -> torch.Tensor:
torch.backends.cuda.preferred_linalg_library(backend="magma")
res = None
# bshd -> bhsd
query = query.transpose(1, 2).contiguous()
key = key.transpose(1, 2).contiguous()
value = value.transpose(1, 2).contiguous()
batch_size, num_heads, seq_len, dim = query.size()
context_length, num_frame, frame_size = (
attn_metadata.context_length,
attn_metadata.num_frame,
attn_metadata.frame_size,
)
prompt_length = attn_metadata.prompt_length
if prompt_length is None:
prompt_length = context_length
assert (
seq_len == context_length + num_frame * frame_size
), f"Query Shape: {seq_len} is not equivalent to {context_length} + {num_frame} * {frame_size}"
# Determine if we use Full Attention to calculate
full_attention_flag = False
if self.layer_idx < attn_metadata.first_layers_fp:
full_attention_flag = True
if attn_metadata.current_timestep > attn_metadata.first_times_fp:
full_attention_flag = True
if full_attention_flag:
if attn_metadata.zero_step_kmeans_init:
video_length = attn_metadata.num_frame * attn_metadata.frame_size
query_video = query[:, :, :video_length, :].contiguous()
key_video = key[:, :, :video_length, :].contiguous()
self.kmeans_clustering(query_video, key_video, attn_metadata)
with sdpa_kernel(
SDPBackend.CUDNN_ATTENTION
): # not sure why we need to force cudnn here, but it's faster than flash attention
output_hidden_states = torch.nn.functional.scaled_dot_product_attention(
query, key, value, dropout_p=0.0, is_causal=False
)
res = output_hidden_states.reshape(
batch_size, num_heads, seq_len, dim
).transpose(1, 2)
else:
if context_length > 0:
video_length = num_frame * frame_size
unprompt_length = max(context_length - prompt_length, 0)
query_video = query[:, :, :video_length, :].contiguous()
key_video = key[:, :, :video_length, :].contiguous()
value_video = value[:, :, :video_length, :].contiguous()
(
q_perm,
k_perm,
v_perm,
dyn_map,
qc_sz_s,
kc_sz_s,
q_sorted_indices,
) = self.semantic_aware_permutation(
query_video, key_video, value_video, attn_metadata
)
(
q_perm,
k_perm,
v_perm,
dyn_map,
qc_sz_s,
kc_sz_s,
q_sorted_indices,
) = self._hunyuan_dynamic_map_post_processing(
q_perm,
k_perm,
v_perm,
query,
key,
value,
dyn_map,
qc_sz_s,
kc_sz_s,
q_sorted_indices,
video_length,
context_length,
prompt_length,
unprompt_length,
)
else:
(
q_perm,
k_perm,
v_perm,
dyn_map,
qc_sz_s,
kc_sz_s,
q_sorted_indices,
) = self.semantic_aware_permutation(query, key, value, attn_metadata)
output_permuted = dynamic_block_sparse_fwd_flashinfer(
q_perm, k_perm, v_perm, dyn_map, qc_sz_s, kc_sz_s, is_cpu=False
)
attn_output = apply_inverse_permutation_triton(
output_permuted, q_sorted_indices, dim=2
)
res = attn_output.reshape(batch_size, num_heads, seq_len, dim).transpose(
1, 2
)
torch.backends.cuda.preferred_linalg_library(
backend="default"
) # reset to default
return res.contiguous()
@@ -0,0 +1,330 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
# SPDX-License-Identifier: Apache-2.0
import functools
import math
from dataclasses import dataclass
import torch
try:
from vsa import video_sparse_attn
except ImportError:
video_sparse_attn = None
from typing import Any
from sglang.multimodal_gen.runtime.distributed import get_sp_group
from sglang.multimodal_gen.runtime.layers.attention.backends.attention_backend import (
AttentionBackend,
AttentionImpl,
AttentionMetadata,
AttentionMetadataBuilder,
)
from sglang.multimodal_gen.runtime.platforms import AttentionBackendEnum
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__)
VSA_TILE_SIZE = (4, 4, 4)
@functools.lru_cache(maxsize=10)
def get_tile_partition_indices(
dit_seq_shape: tuple[int, int, int],
tile_size: tuple[int, int, int],
device: torch.device,
) -> torch.LongTensor:
T, H, W = dit_seq_shape
ts, hs, ws = tile_size
indices = torch.arange(T * H * W, device=device, dtype=torch.long).reshape(T, H, W)
ls = []
for t in range(math.ceil(T / ts)):
for h in range(math.ceil(H / hs)):
for w in range(math.ceil(W / ws)):
ls.append(
indices[
t * ts : min(t * ts + ts, T),
h * hs : min(h * hs + hs, H),
w * ws : min(w * ws + ws, W),
].flatten()
)
index = torch.cat(ls, dim=0)
return index
@functools.lru_cache(maxsize=10)
def get_reverse_tile_partition_indices(
dit_seq_shape: tuple[int, int, int],
tile_size: tuple[int, int, int],
device: torch.device,
) -> torch.LongTensor:
return torch.argsort(get_tile_partition_indices(dit_seq_shape, tile_size, device))
@functools.lru_cache(maxsize=10)
def construct_variable_block_sizes(
dit_seq_shape: tuple[int, int, int],
num_tiles: tuple[int, int, int],
device: torch.device,
) -> torch.LongTensor:
"""
Compute the number of valid (nonpadded) tokens inside every
(ts_t × ts_h × ts_w) tile after padding ‑‑ flattened in the order
(ttile, htile, wtile) that `rearrange` uses.
Returns
-------
torch.LongTensor # shape: [∏ full_window_size]
"""
# unpack
t, h, w = dit_seq_shape
ts_t, ts_h, ts_w = VSA_TILE_SIZE
n_t, n_h, n_w = num_tiles
def _sizes(dim_len: int, tile: int, n_tiles: int) -> torch.LongTensor:
"""Vector with the size of each tile along one dimension."""
sizes = torch.full((n_tiles,), tile, dtype=torch.int, device=device)
# size of last (possibly partial) tile
remainder = dim_len - (n_tiles - 1) * tile
sizes[-1] = remainder if remainder > 0 else tile
return sizes
t_sizes = _sizes(t, ts_t, n_t) # [n_t]
h_sizes = _sizes(h, ts_h, n_h) # [n_h]
w_sizes = _sizes(w, ts_w, n_w) # [n_w]
# broadcastmultiply to get voxels per tile, then flatten
block_sizes = (
t_sizes[:, None, None] # [n_t, 1, 1]
* h_sizes[None, :, None] # [1, n_h, 1]
* w_sizes[None, None, :] # [1, 1, n_w]
).reshape(
-1
) # [n_t * n_h * n_w]
return block_sizes
@functools.lru_cache(maxsize=10)
def get_non_pad_index(
variable_block_sizes: torch.LongTensor,
max_block_size: int,
):
n_win = variable_block_sizes.shape[0]
device = variable_block_sizes.device
starts_pad = torch.arange(n_win, device=device) * max_block_size
index_pad = (
starts_pad[:, None] + torch.arange(max_block_size, device=device)[None, :]
)
index_mask = (
torch.arange(max_block_size, device=device)[None, :]
< variable_block_sizes[:, None]
)
return index_pad[index_mask]
class VideoSparseAttentionBackend(AttentionBackend):
accept_output_buffer: bool = True
@staticmethod
def get_supported_head_sizes() -> list[int]:
return [64, 128]
@staticmethod
def get_enum() -> AttentionBackendEnum:
return AttentionBackendEnum.VIDEO_SPARSE_ATTN
@staticmethod
def get_impl_cls() -> type["VideoSparseAttentionImpl"]:
return VideoSparseAttentionImpl
@staticmethod
def get_metadata_cls() -> type["VideoSparseAttentionMetadata"]:
return VideoSparseAttentionMetadata
@staticmethod
def get_builder_cls() -> type["VideoSparseAttentionMetadataBuilder"]:
return VideoSparseAttentionMetadataBuilder
@dataclass
class VideoSparseAttentionMetadata(AttentionMetadata):
current_timestep: int
dit_seq_shape: list[int]
VSA_sparsity: float
num_tiles: list[int]
total_seq_length: int
tile_partition_indices: torch.LongTensor
reverse_tile_partition_indices: torch.LongTensor
variable_block_sizes: torch.LongTensor
non_pad_index: torch.LongTensor
untile_combined_index: torch.LongTensor
tile_buf: torch.Tensor | None = None
# adaption for FastWan2.1-T2V-1.3B-Diffusers
# Sequence lengths for the forward batch
# Maximum sequence length for query
max_seqlen_q: int = 1
# Maximum sequence length for key
max_seqlen_k: int = 0
class VideoSparseAttentionMetadataBuilder(AttentionMetadataBuilder):
def __init__(self):
pass
def prepare(self):
pass
def build( # type: ignore
self,
current_timestep: int,
raw_latent_shape: tuple[int, int, int],
patch_size: tuple[int, int, int],
VSA_sparsity: float,
device: torch.device,
**kwargs: dict[str, Any],
) -> VideoSparseAttentionMetadata:
patch_size = patch_size
dit_seq_shape = (
raw_latent_shape[0] // patch_size[0],
raw_latent_shape[1] // patch_size[1],
raw_latent_shape[2] // patch_size[2],
)
num_tiles = (
math.ceil(dit_seq_shape[0] / VSA_TILE_SIZE[0]),
math.ceil(dit_seq_shape[1] / VSA_TILE_SIZE[1]),
math.ceil(dit_seq_shape[2] / VSA_TILE_SIZE[2]),
)
total_seq_length = math.prod(dit_seq_shape)
tile_partition_indices = get_tile_partition_indices(
dit_seq_shape, VSA_TILE_SIZE, device
)
reverse_tile_partition_indices = get_reverse_tile_partition_indices(
dit_seq_shape, VSA_TILE_SIZE, device
)
variable_block_sizes = construct_variable_block_sizes(
dit_seq_shape, num_tiles, device
)
non_pad_index = get_non_pad_index(
variable_block_sizes, math.prod(VSA_TILE_SIZE)
)
untile_combined_index = non_pad_index[reverse_tile_partition_indices]
return VideoSparseAttentionMetadata(
current_timestep=current_timestep,
dit_seq_shape=dit_seq_shape, # type: ignore
VSA_sparsity=VSA_sparsity, # type: ignore
num_tiles=num_tiles, # type: ignore
total_seq_length=total_seq_length, # type: ignore
tile_partition_indices=tile_partition_indices, # type: ignore
reverse_tile_partition_indices=reverse_tile_partition_indices,
variable_block_sizes=variable_block_sizes,
non_pad_index=non_pad_index,
untile_combined_index=untile_combined_index,
)
class VideoSparseAttentionImpl(AttentionImpl):
def __init__(
self,
num_heads: int,
head_size: int,
causal: bool,
softmax_scale: float,
num_kv_heads: int | None = None,
prefix: str = "",
**extra_impl_args,
) -> None:
self.prefix = prefix
sp_group = get_sp_group()
self.sp_size = sp_group.world_size
def tile(
self,
x: torch.Tensor,
attn_metadata: VideoSparseAttentionMetadata,
) -> torch.Tensor:
num_tiles = attn_metadata.num_tiles
t_padded_size = num_tiles[0] * VSA_TILE_SIZE[0]
h_padded_size = num_tiles[1] * VSA_TILE_SIZE[1]
w_padded_size = num_tiles[2] * VSA_TILE_SIZE[2]
target_shape = (
x.shape[0],
t_padded_size * h_padded_size * w_padded_size,
x.shape[-2],
x.shape[-1],
)
buf = attn_metadata.tile_buf
if (
buf is None
or buf.shape != target_shape
or buf.dtype != x.dtype
or buf.device != x.device
):
buf = torch.zeros(target_shape, device=x.device, dtype=x.dtype)
attn_metadata.tile_buf = buf
buf[:, attn_metadata.non_pad_index] = x[:, attn_metadata.tile_partition_indices]
return buf
def untile(
self,
x: torch.Tensor,
untile_combined_index: torch.LongTensor,
) -> torch.Tensor:
return x[:, untile_combined_index]
def preprocess_qkv(
self,
qkv: torch.Tensor,
attn_metadata: VideoSparseAttentionMetadata,
) -> torch.Tensor:
return self.tile(qkv, attn_metadata)
def postprocess_output(
self,
output: torch.Tensor,
attn_metadata: VideoSparseAttentionMetadata,
) -> torch.Tensor:
return self.untile(output, attn_metadata.untile_combined_index)
def forward( # type: ignore[override]
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
gate_compress: torch.Tensor,
attn_metadata: VideoSparseAttentionMetadata,
) -> torch.Tensor:
query = query.transpose(1, 2).contiguous()
key = key.transpose(1, 2).contiguous()
value = value.transpose(1, 2).contiguous()
gate_compress = gate_compress.transpose(1, 2).contiguous()
VSA_sparsity = attn_metadata.VSA_sparsity
cur_topk = math.ceil(
(1 - VSA_sparsity)
* (attn_metadata.total_seq_length / math.prod(VSA_TILE_SIZE))
)
if video_sparse_attn is None:
raise NotImplementedError("video_sparse_attn is not installed")
hidden_states = video_sparse_attn(
query,
key,
value,
variable_block_sizes=attn_metadata.variable_block_sizes,
topk=cur_topk,
block_size=VSA_TILE_SIZE,
compress_attn_weight=gate_compress,
).transpose(1, 2)
return hidden_states
@@ -0,0 +1,259 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
# SPDX-License-Identifier: Apache-2.0
import re
from dataclasses import dataclass
import torch
from einops import rearrange
from kernel.attn.vmoba_attn.vmoba import (
moba_attn_varlen,
process_moba_input,
process_moba_output,
)
from sglang.multimodal_gen.runtime.layers.attention.backends.attention_backend import (
AttentionBackend,
AttentionImpl,
AttentionMetadata,
AttentionMetadataBuilder,
)
from sglang.multimodal_gen.runtime.platforms import AttentionBackendEnum
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__)
class VMOBAAttentionBackend(AttentionBackend):
accept_output_buffer: bool = True
@staticmethod
def get_enum() -> AttentionBackendEnum:
return AttentionBackendEnum.VMOBA_ATTN
@staticmethod
def get_impl_cls() -> type["VMOBAAttentionImpl"]:
return VMOBAAttentionImpl
@staticmethod
def get_metadata_cls() -> type["VideoMobaAttentionMetadata"]:
return VideoMobaAttentionMetadata
@staticmethod
def get_builder_cls() -> type["VideoMobaAttentionMetadataBuilder"]:
return VideoMobaAttentionMetadataBuilder
@dataclass
class VideoMobaAttentionMetadata(AttentionMetadata):
current_timestep: int
temporal_chunk_size: int
temporal_topk: int
spatial_chunk_size: tuple[int, int]
spatial_topk: int
st_chunk_size: tuple[int, int, int]
st_topk: int
moba_select_mode: str
moba_threshold: float
moba_threshold_type: str
patch_resolution: list[int]
first_full_step: int = 12
first_full_layer: int = 0
# temporal_layer -> spatial_layer -> st_layer
temporal_layer: int = 1
spatial_layer: int = 1
st_layer: int = 1
def pad_input(hidden_states, indices, batch, seqlen):
"""
Arguments:
hidden_states: (total_nnz, ...), where total_nnz = number of tokens in selected in attention_mask.
indices: (total_nnz), the indices that represent the non-masked tokens of the original padded input sequence.
batch: int, batch size for the padded sequence.
seqlen: int, maximum sequence length for the padded sequence.
Return:
hidden_states: (batch, seqlen, ...)
"""
dim = hidden_states.shape[1:]
output = torch.zeros(
(batch * seqlen), *dim, device=hidden_states.device, dtype=hidden_states.dtype
)
output[indices] = hidden_states
return rearrange(output, "(b s) ... -> b s ...", b=batch)
class VideoMobaAttentionMetadataBuilder(AttentionMetadataBuilder):
def __init__(self):
pass
def prepare(self):
pass
def build( # type: ignore
self,
current_timestep: int,
raw_latent_shape: tuple[int, int, int],
patch_size: tuple[int, int, int],
temporal_chunk_size: int,
temporal_topk: int,
spatial_chunk_size: tuple[int, int],
spatial_topk: int,
st_chunk_size: tuple[int, int, int],
st_topk: int,
moba_select_mode: str = "threshold",
moba_threshold: float = 0.25,
moba_threshold_type: str = "query_head",
device: torch.device = None,
first_full_layer: int = 0,
first_full_step: int = 12,
temporal_layer: int = 1,
spatial_layer: int = 1,
st_layer: int = 1,
**kwargs,
) -> VideoMobaAttentionMetadata:
if device is None:
device = torch.device("cpu")
assert (
raw_latent_shape[0] % patch_size[0] == 0
and raw_latent_shape[1] % patch_size[1] == 0
and raw_latent_shape[2] % patch_size[2] == 0
), f"spatial patch_resolution {raw_latent_shape} should be divisible by patch_size {patch_size}"
patch_resolution = [
t // pt for t, pt in zip(raw_latent_shape, patch_size, strict=False)
]
return VideoMobaAttentionMetadata(
current_timestep=current_timestep,
temporal_chunk_size=temporal_chunk_size,
temporal_topk=temporal_topk,
spatial_chunk_size=spatial_chunk_size,
spatial_topk=spatial_topk,
st_chunk_size=st_chunk_size,
st_topk=st_topk,
moba_select_mode=moba_select_mode,
moba_threshold=moba_threshold,
moba_threshold_type=moba_threshold_type,
patch_resolution=patch_resolution,
first_full_layer=first_full_layer,
first_full_step=first_full_step,
temporal_layer=temporal_layer,
spatial_layer=spatial_layer,
st_layer=st_layer,
)
class VMOBAAttentionImpl(AttentionImpl):
def __init__(
self,
num_heads,
head_size,
softmax_scale,
causal=False,
num_kv_heads=None,
prefix="",
**extra_impl_args,
) -> None:
self.prefix = prefix
self.layer_idx = self._get_layer_idx(prefix)
self.pad_input = pad_input
def _get_layer_idx(self, prefix: str) -> int | None:
match = re.search(r"blocks\.(\d+)", prefix)
if not match:
raise ValueError(f"Invalid prefix: {prefix}")
return int(match.group(1))
def forward(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attn_metadata: AttentionMetadata,
) -> torch.Tensor:
"""
query: [B, L, H, D]
key: [B, L, H, D]
value: [B, L, H, D]
attn_metadata: AttentionMetadata
"""
batch_size, sequence_length, num_heads, head_dim = query.shape
# select chunk type according to layer idx:
loop_layer_num = (
attn_metadata.temporal_layer
+ attn_metadata.spatial_layer
+ attn_metadata.st_layer
)
moba_layer = self.layer_idx - attn_metadata.first_full_layer
if moba_layer % loop_layer_num < attn_metadata.temporal_layer:
moba_chunk_size = attn_metadata.temporal_chunk_size
moba_topk = attn_metadata.temporal_topk
elif (
moba_layer % loop_layer_num
< attn_metadata.temporal_layer + attn_metadata.spatial_layer
):
moba_chunk_size = attn_metadata.spatial_chunk_size
moba_topk = attn_metadata.spatial_topk
elif (
moba_layer % loop_layer_num
< attn_metadata.temporal_layer
+ attn_metadata.spatial_layer
+ attn_metadata.st_layer
):
moba_chunk_size = attn_metadata.st_chunk_size
moba_topk = attn_metadata.st_topk
query, chunk_size = process_moba_input(
query, attn_metadata.patch_resolution, moba_chunk_size
)
key, chunk_size = process_moba_input(
key, attn_metadata.patch_resolution, moba_chunk_size
)
value, chunk_size = process_moba_input(
value, attn_metadata.patch_resolution, moba_chunk_size
)
max_seqlen = query.shape[1]
indices_q = torch.arange(
0, query.shape[0] * query.shape[1], device=query.device
)
cu_seqlens = torch.arange(
0,
query.shape[0] * query.shape[1] + 1,
query.shape[1],
dtype=torch.int32,
device=query.device,
)
query = rearrange(query, "b s ... -> (b s) ...")
key = rearrange(key, "b s ... -> (b s) ...")
value = rearrange(value, "b s ... -> (b s) ...")
# current_timestep=attn_metadata.current_timestep
hidden_states = moba_attn_varlen(
query,
key,
value,
cu_seqlens=cu_seqlens,
max_seqlen=max_seqlen,
moba_chunk_size=chunk_size,
moba_topk=moba_topk,
select_mode=attn_metadata.moba_select_mode,
simsum_threshold=attn_metadata.moba_threshold,
threshold_type=attn_metadata.moba_threshold_type,
)
hidden_states = self.pad_input(
hidden_states, indices_q, batch_size, sequence_length
)
hidden_states = process_moba_output(
hidden_states, attn_metadata.patch_resolution, moba_chunk_size
)
return hidden_states
@@ -0,0 +1,122 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
# SPDX-License-Identifier: Apache-2.0
from functools import lru_cache
import torch
from sglang.multimodal_gen.runtime.platforms import AttentionBackendEnum
try:
from sgl_kernel.flash_attn import flash_attn_varlen_func
flash_attn_func = flash_attn_varlen_func
except ImportError as e:
raise e
from sglang.multimodal_gen.runtime.layers.attention.backends.attention_backend import (
AttentionBackend,
AttentionImpl,
AttentionMetadata,
AttentionMetadataBuilder,
)
from sglang.multimodal_gen.runtime.layers.attention.backends.flash_attn import (
FlashAttentionMetadataBuilder,
)
class XPUAttentionBackend(AttentionBackend):
accept_output_buffer: bool = True
@staticmethod
def get_supported_head_sizes() -> list[int]:
return [64, 96, 128, 192, 256]
@staticmethod
def get_enum() -> AttentionBackendEnum:
return AttentionBackendEnum.FA
@staticmethod
def get_impl_cls() -> type["XPUAttentionImpl"]:
return XPUAttentionImpl
@staticmethod
def get_metadata_cls() -> type["AttentionMetadata"]:
"""XPU backend does not require special metadata."""
return AttentionMetadata
@staticmethod
def get_builder_cls() -> type["AttentionMetadataBuilder"]:
return FlashAttentionMetadataBuilder
@lru_cache(maxsize=128)
def _get_cu_seqlens(device_index: int, bsz: int, seqlen: int) -> torch.Tensor:
return torch.arange(
0,
(bsz + 1) * seqlen,
step=seqlen,
device=torch.device("xpu", device_index),
dtype=torch.int32,
)
class XPUAttentionImpl(AttentionImpl):
def __init__(
self,
num_heads: int,
head_size: int,
causal: bool,
softmax_scale: float,
num_kv_heads: int | None = None,
prefix: str = "",
**extra_impl_args,
) -> None:
self.num_heads = num_heads
self.num_kv_heads = num_kv_heads
self.head_size = head_size
self.causal = causal
self.softmax_scale = softmax_scale
def forward(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attn_metadata: AttentionMetadata = None,
*,
return_softmax_lse: bool = False,
):
bsz, seqlen_q, nheads_q, d = tuple(query.shape)
_, seqlen_k, nheads_k, _ = tuple(key.shape)
max_seqlen_q = seqlen_q
max_seqlen_k = seqlen_k
q_ = query.contiguous().reshape(bsz * seqlen_q, nheads_q, d)
k_ = key.contiguous().reshape(bsz * seqlen_k, nheads_k, d)
v_ = value.contiguous().reshape(bsz * seqlen_k, nheads_k, d)
cu_q = _get_cu_seqlens(q_.device.index, bsz, seqlen_q)
cu_k = _get_cu_seqlens(q_.device.index, bsz, seqlen_k)
out = flash_attn_func(
q=q_,
k=k_,
v=v_,
cu_seqlens_q=cu_q,
cu_seqlens_k=cu_k,
max_seqlen_q=max_seqlen_q,
max_seqlen_k=max_seqlen_k,
softmax_scale=self.softmax_scale,
causal=self.causal,
return_softmax_lse=return_softmax_lse,
)
if return_softmax_lse:
out_tensor, softmax_lse = out[:2]
result = out_tensor.reshape(bsz, seqlen_q, nheads_q, d)
return result, softmax_lse
result = out.reshape(bsz, seqlen_q, nheads_q, d)
return result