[diffusion] Clean up VSA attention hot path (#25514)

This commit is contained in:
Xiaoyu Zhang
2026-05-24 16:46:03 +08:00
committed by GitHub
parent 4c2b32bfbf
commit 0b65588c18
5 changed files with 88 additions and 37 deletions
@@ -323,8 +323,8 @@ class SparseLinearAttentionImpl(AttentionImpl, nn.Module):
) )
# Apply feature maps # Apply feature maps
query = self.feature_map_q(query).contiguous().to(self.dtype) # c_q query = self.feature_map_q(query).to(self.dtype) # c_q
key = self.feature_map_k(key).contiguous().to(self.dtype) # c_k key = self.feature_map_k(key).to(self.dtype) # c_k
# Linear attention computation # Linear attention computation
o_l = self._calc_linear_attention_with_torch(query, key, value) o_l = self._calc_linear_attention_with_torch(query, key, value)
@@ -681,8 +681,8 @@ class SageSparseLinearAttentionImpl(AttentionImpl, nn.Module):
########## SPARGE END ########## ########## SPARGE END ##########
# Linear attention with feature maps # Linear attention with feature maps
q_linear = self.feature_map_q(q).contiguous().to(self.dtype) q_linear = self.feature_map_q(q).to(self.dtype)
k_linear = self.feature_map_k(k).contiguous().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) o_l = self._calc_linear_attention_with_torch(q_linear, k_linear, v)
# Project linear attention output and combine # Project linear attention output and combine
@@ -159,6 +159,8 @@ class VideoSparseAttentionMetadata(AttentionMetadata):
reverse_tile_partition_indices: torch.LongTensor reverse_tile_partition_indices: torch.LongTensor
variable_block_sizes: torch.LongTensor variable_block_sizes: torch.LongTensor
non_pad_index: 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 # adaption for FastWan2.1-T2V-1.3B-Diffusers
# Sequence lengths for the forward batch # Sequence lengths for the forward batch
@@ -211,6 +213,7 @@ class VideoSparseAttentionMetadataBuilder(AttentionMetadataBuilder):
non_pad_index = get_non_pad_index( non_pad_index = get_non_pad_index(
variable_block_sizes, math.prod(VSA_TILE_SIZE) variable_block_sizes, math.prod(VSA_TILE_SIZE)
) )
untile_combined_index = non_pad_index[reverse_tile_partition_indices]
return VideoSparseAttentionMetadata( return VideoSparseAttentionMetadata(
current_timestep=current_timestep, current_timestep=current_timestep,
@@ -222,6 +225,7 @@ class VideoSparseAttentionMetadataBuilder(AttentionMetadataBuilder):
reverse_tile_partition_indices=reverse_tile_partition_indices, reverse_tile_partition_indices=reverse_tile_partition_indices,
variable_block_sizes=variable_block_sizes, variable_block_sizes=variable_block_sizes,
non_pad_index=non_pad_index, non_pad_index=non_pad_index,
untile_combined_index=untile_combined_index,
) )
@@ -244,58 +248,52 @@ class VideoSparseAttentionImpl(AttentionImpl):
def tile( def tile(
self, self,
x: torch.Tensor, x: torch.Tensor,
num_tiles: list[int], attn_metadata: VideoSparseAttentionMetadata,
tile_partition_indices: torch.LongTensor,
non_pad_index: torch.LongTensor,
) -> torch.Tensor: ) -> torch.Tensor:
num_tiles = attn_metadata.num_tiles
t_padded_size = num_tiles[0] * VSA_TILE_SIZE[0] t_padded_size = num_tiles[0] * VSA_TILE_SIZE[0]
h_padded_size = num_tiles[1] * VSA_TILE_SIZE[1] h_padded_size = num_tiles[1] * VSA_TILE_SIZE[1]
w_padded_size = num_tiles[2] * VSA_TILE_SIZE[2] w_padded_size = num_tiles[2] * VSA_TILE_SIZE[2]
target_shape = (
x_padded = torch.zeros( x.shape[0],
( t_padded_size * h_padded_size * w_padded_size,
x.shape[0], x.shape[-2],
t_padded_size * h_padded_size * w_padded_size, x.shape[-1],
x.shape[-2],
x.shape[-1],
),
device=x.device,
dtype=x.dtype,
) )
x_padded[:, non_pad_index] = x[:, tile_partition_indices]
return x_padded 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( def untile(
self, self,
x: torch.Tensor, x: torch.Tensor,
reverse_tile_partition_indices: torch.LongTensor, untile_combined_index: torch.LongTensor,
non_pad_index: torch.LongTensor,
) -> torch.Tensor: ) -> torch.Tensor:
x = x[:, non_pad_index][:, reverse_tile_partition_indices] return x[:, untile_combined_index]
return x
def preprocess_qkv( def preprocess_qkv(
self, self,
qkv: torch.Tensor, qkv: torch.Tensor,
attn_metadata: VideoSparseAttentionMetadata, attn_metadata: VideoSparseAttentionMetadata,
) -> torch.Tensor: ) -> torch.Tensor:
return self.tile( return self.tile(qkv, attn_metadata)
qkv,
attn_metadata.num_tiles,
attn_metadata.tile_partition_indices,
attn_metadata.non_pad_index,
)
def postprocess_output( def postprocess_output(
self, self,
output: torch.Tensor, output: torch.Tensor,
attn_metadata: VideoSparseAttentionMetadata, attn_metadata: VideoSparseAttentionMetadata,
) -> torch.Tensor: ) -> torch.Tensor:
return self.untile( return self.untile(output, attn_metadata.untile_combined_index)
output,
attn_metadata.reverse_tile_partition_indices,
attn_metadata.non_pad_index,
)
def forward( # type: ignore[override] def forward( # type: ignore[override]
self, self,
@@ -624,6 +624,8 @@ class WanTransformerBlock_VSA(nn.Module):
added_kv_proj_dim: int | None = None, added_kv_proj_dim: int | None = None,
supported_attention_backends: set[AttentionBackendEnum] | None = None, supported_attention_backends: set[AttentionBackendEnum] | None = None,
prefix: str = "", prefix: str = "",
attention_type: str = "original",
sla_topk: float = 0.0,
quant_config: QuantizationConfig | None = None, quant_config: QuantizationConfig | None = None,
): ):
super().__init__() super().__init__()
@@ -221,12 +221,20 @@ class DenoisingStage(PipelineStage, RolloutDenoisingMixin):
if not backends: if not backends:
return None return None
if len(backends) > 1: if len(backends) > 1:
sparse_backends = {backend for backend in backends if backend.is_sparse}
selected_backend = (
sorted(sparse_backends, key=lambda backend: backend.name)[0]
if sparse_backends
else sorted(backends, key=lambda backend: backend.name)[0]
)
logger.warning( logger.warning(
"Multiple transformer attention backends detected: %s. " "Multiple transformer attention backends detected: %s. "
"Using one backend for denoising metadata.", "Using %s for denoising metadata.",
sorted(backend.name.lower() for backend in backends), sorted(backend.name.lower() for backend in backends),
selected_backend.name.lower(),
) )
return sorted(backends, key=lambda backend: backend.name)[0] return selected_backend
return next(iter(backends))
def component_uses( def component_uses(
self, server_args: ServerArgs, stage_name: str | None = None self, server_args: ServerArgs, stage_name: str | None = None
@@ -1474,12 +1482,16 @@ class DenoisingStage(PipelineStage, RolloutDenoisingMixin):
self.attn_backend.get_enum() == AttentionBackendEnum.SLIDING_TILE_ATTN self.attn_backend.get_enum() == AttentionBackendEnum.SLIDING_TILE_ATTN
or self.attn_backend.get_enum() == AttentionBackendEnum.VIDEO_SPARSE_ATTN or self.attn_backend.get_enum() == AttentionBackendEnum.VIDEO_SPARSE_ATTN
): ):
attention_backend_config = server_args.attention_backend_config or {}
vsa_sparsity = attention_backend_config.get(
"VSA_sparsity", attention_backend_config.get("sparsity", 0.0)
)
attn_metadata = self.attn_metadata_builder.build( attn_metadata = self.attn_metadata_builder.build(
current_timestep=i, current_timestep=i,
raw_latent_shape=batch.raw_latent_shape[2:5], raw_latent_shape=batch.raw_latent_shape[2:5],
patch_size=server_args.pipeline_config.dit_config.patch_size, patch_size=server_args.pipeline_config.dit_config.patch_size,
STA_param=batch.STA_param, STA_param=batch.STA_param,
VSA_sparsity=server_args.attention_backend_config.VSA_sparsity, VSA_sparsity=vsa_sparsity,
device=get_local_torch_device(), device=get_local_torch_device(),
) )
elif ( elif (
@@ -0,0 +1,39 @@
import torch
from sglang.multimodal_gen.runtime.layers.attention.backends.video_sparse_attn import (
VideoSparseAttentionImpl,
VideoSparseAttentionMetadataBuilder,
)
def test_video_sparse_attention_tile_buffer_reuse_and_untile():
metadata = VideoSparseAttentionMetadataBuilder().build(
current_timestep=0,
raw_latent_shape=(5, 7, 9),
patch_size=(1, 1, 1),
VSA_sparsity=0.5,
device=torch.device("cpu"),
)
impl = object.__new__(VideoSparseAttentionImpl)
total_seq_length = metadata.total_seq_length
x = torch.arange(2 * total_seq_length * 3 * 4, dtype=torch.float32).reshape(
2, total_seq_length, 3, 4
)
tiled = impl.preprocess_qkv(x, metadata)
assert metadata.tile_buf is tiled
assert torch.equal(
metadata.untile_combined_index,
metadata.non_pad_index[metadata.reverse_tile_partition_indices],
)
assert torch.equal(impl.postprocess_output(tiled, metadata), x)
next_x = x + 1
next_tiled = impl.preprocess_qkv(next_x, metadata)
assert next_tiled.data_ptr() == tiled.data_ptr()
assert torch.equal(impl.postprocess_output(next_tiled, metadata), next_x)
pad_mask = torch.ones(next_tiled.shape[1], dtype=torch.bool)
pad_mask[metadata.non_pad_index.cpu()] = False
assert torch.all(next_tiled[:, pad_mask] == 0)