Signed-off-by: lishunyang12 <lishunyang12@users.noreply.github.com> Co-authored-by: lishunyang12 <lishunyang12@users.noreply.github.com>
248 lines
8.0 KiB
Python
248 lines
8.0 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
|
|
|
import sys
|
|
import types
|
|
from typing import Any
|
|
|
|
import pytest
|
|
import torch
|
|
|
|
from vllm_omni.diffusion.attention.backends.abstract import (
|
|
AttentionMetadata,
|
|
PackedPaddingMetadata,
|
|
)
|
|
from vllm_omni.diffusion.attention.backends.fastvideo_vsa import (
|
|
FastVideoVSABackend,
|
|
FastVideoVSAImpl,
|
|
)
|
|
from vllm_omni.diffusion.attention.backends.registry import (
|
|
DiffusionAttentionBackendEnum,
|
|
)
|
|
|
|
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cpu]
|
|
|
|
|
|
def test_fastvideo_vsa_backend_is_registered():
|
|
assert DiffusionAttentionBackendEnum.FASTVIDEO_VSA.get_path().endswith("fastvideo_vsa.FastVideoVSABackend")
|
|
|
|
|
|
def test_fastvideo_vsa_reports_missing_optional_kernel(monkeypatch):
|
|
monkeypatch.setattr("importlib.util.find_spec", lambda name: None)
|
|
with pytest.raises(ImportError, match="fastvideo-kernel"):
|
|
FastVideoVSABackend.validate_available()
|
|
|
|
|
|
def test_fastvideo_vsa_tiles_3d_sequence_and_untiles(monkeypatch):
|
|
calls: dict[str, Any] = {}
|
|
fake_module = types.ModuleType("fastvideo_kernel")
|
|
|
|
def fake_video_sparse_attn_bshd(
|
|
q,
|
|
k,
|
|
v,
|
|
variable_block_sizes,
|
|
q_variable_block_sizes,
|
|
compress_attn_weight,
|
|
topk,
|
|
block_size,
|
|
):
|
|
calls["q_shape"] = tuple(q.shape)
|
|
calls["vbs"] = variable_block_sizes.detach().cpu().tolist()
|
|
calls["q_vbs"] = q_variable_block_sizes.detach().cpu().tolist()
|
|
calls["compress_sum"] = float(compress_attn_weight.detach().cpu().sum())
|
|
calls["compress_shape"] = tuple(compress_attn_weight.shape)
|
|
calls["topk"] = topk
|
|
calls["block_size"] = block_size
|
|
return q + k + v
|
|
|
|
setattr(fake_module, "video_sparse_attn_bshd", fake_video_sparse_attn_bshd)
|
|
monkeypatch.setitem(sys.modules, "fastvideo_kernel", fake_module)
|
|
|
|
impl = FastVideoVSAImpl(
|
|
num_heads=2,
|
|
head_size=8,
|
|
softmax_scale=8**-0.5,
|
|
causal=False,
|
|
backend_kwargs={
|
|
"topk": 1,
|
|
"block_size": (4, 8, 8),
|
|
"min_seq_len": 1,
|
|
"disable_when_sp_active": False,
|
|
},
|
|
)
|
|
query = torch.randn(1, 300, 2, 8)
|
|
key = torch.randn_like(query)
|
|
value = torch.randn_like(query)
|
|
attn_metadata = AttentionMetadata(extra={"vsa_dit_seq_shape": (3, 10, 10)})
|
|
monkeypatch.setattr(impl, "_fallback_reason", lambda *args, **kwargs: None)
|
|
|
|
output = impl.forward_cuda(query, key, value, attn_metadata)
|
|
|
|
assert output.shape == query.shape
|
|
assert calls["q_shape"] == (1, 1024, 2, 8)
|
|
assert calls["vbs"] == [192, 48, 48, 12]
|
|
assert calls["q_vbs"] == [192, 48, 48, 12]
|
|
assert calls["compress_shape"] == (1, 1024, 2, 8)
|
|
assert calls["compress_sum"] == 0.0
|
|
assert calls["topk"] == 1
|
|
assert calls["block_size"] == (4, 8, 8)
|
|
|
|
|
|
def test_fastvideo_vsa_uses_learned_gate_when_provided(monkeypatch):
|
|
calls: dict[str, Any] = {}
|
|
fake_module = types.ModuleType("fastvideo_kernel")
|
|
|
|
def fake_video_sparse_attn_bshd(
|
|
q,
|
|
k,
|
|
v,
|
|
variable_block_sizes,
|
|
q_variable_block_sizes,
|
|
compress_attn_weight,
|
|
topk,
|
|
block_size,
|
|
):
|
|
calls["compress_sum"] = float(compress_attn_weight.detach().cpu().sum())
|
|
calls["compress_shape"] = tuple(compress_attn_weight.shape)
|
|
return q + k + v
|
|
|
|
setattr(fake_module, "video_sparse_attn_bshd", fake_video_sparse_attn_bshd)
|
|
monkeypatch.setitem(sys.modules, "fastvideo_kernel", fake_module)
|
|
|
|
impl = FastVideoVSAImpl(
|
|
num_heads=2,
|
|
head_size=8,
|
|
softmax_scale=8**-0.5,
|
|
causal=False,
|
|
backend_kwargs={
|
|
"topk": 1,
|
|
"block_size": (4, 8, 8),
|
|
"min_seq_len": 1,
|
|
"disable_when_sp_active": False,
|
|
},
|
|
)
|
|
query = torch.randn(1, 300, 2, 8)
|
|
gate = torch.ones_like(query)
|
|
metadata = AttentionMetadata(extra={"vsa_dit_seq_shape": (3, 10, 10), "gate_compress": gate})
|
|
monkeypatch.setattr(impl, "_fallback_reason", lambda *args, **kwargs: None)
|
|
|
|
output = impl.forward_cuda(query, query, query, metadata)
|
|
|
|
assert output.shape == query.shape
|
|
assert calls["compress_shape"] == (1, 1024, 2, 8)
|
|
# Only the 300 real tokens carry ones; padded gate slots stay zero.
|
|
assert calls["compress_sum"] == gate.numel()
|
|
|
|
|
|
def test_fastvideo_vsa_falls_back_without_dit_shape(monkeypatch):
|
|
calls: dict[str, Any] = {}
|
|
|
|
impl = FastVideoVSAImpl(
|
|
num_heads=2,
|
|
head_size=8,
|
|
softmax_scale=8**-0.5,
|
|
causal=False,
|
|
backend_kwargs={
|
|
"topk": 1,
|
|
"block_size": (4, 8, 8),
|
|
"min_seq_len": 1,
|
|
"disable_when_sp_active": False,
|
|
},
|
|
)
|
|
|
|
def fake_fallback(query, key, value, attn_metadata, reason):
|
|
calls["reason"] = reason
|
|
return torch.zeros_like(query)
|
|
|
|
monkeypatch.setattr(impl, "_fallback", fake_fallback)
|
|
|
|
query = torch.randn(1, 512, 2, 8)
|
|
output = impl.forward_cuda(query, query, query, AttentionMetadata())
|
|
|
|
assert output.shape == query.shape
|
|
assert calls["reason"] == "vsa_dit_seq_shape metadata is required"
|
|
|
|
|
|
def test_fastvideo_vsa_falls_back_for_mask(monkeypatch):
|
|
calls: dict[str, Any] = {}
|
|
|
|
impl = FastVideoVSAImpl(
|
|
num_heads=2,
|
|
head_size=8,
|
|
softmax_scale=8**-0.5,
|
|
causal=False,
|
|
backend_kwargs={"min_seq_len": 1},
|
|
)
|
|
|
|
def fake_fallback(query, key, value, attn_metadata, reason):
|
|
calls["reason"] = reason
|
|
return torch.zeros_like(query)
|
|
|
|
monkeypatch.setattr(impl, "_fallback", fake_fallback)
|
|
|
|
query = torch.randn(1, 512, 2, 8)
|
|
mask = torch.ones(1, 512, dtype=torch.bool)
|
|
output = impl.forward_cuda(query, query, query, AttentionMetadata(attn_mask=mask))
|
|
|
|
assert output.shape == query.shape
|
|
assert calls["reason"]
|
|
|
|
|
|
def test_fastvideo_vsa_allows_topk_equal_to_num_blocks():
|
|
query = torch.randn(1, 512, 2, 8)
|
|
metadata = AttentionMetadata(extra={"vsa_dit_seq_shape": (4, 8, 16)})
|
|
|
|
all_blocks = FastVideoVSAImpl(
|
|
num_heads=2,
|
|
head_size=8,
|
|
softmax_scale=8**-0.5,
|
|
causal=False,
|
|
backend_kwargs={"topk": 2, "block_size": (4, 8, 8), "min_seq_len": 1},
|
|
)
|
|
too_many = FastVideoVSAImpl(
|
|
num_heads=2,
|
|
head_size=8,
|
|
softmax_scale=8**-0.5,
|
|
causal=False,
|
|
backend_kwargs={"topk": 3, "block_size": (4, 8, 8), "min_seq_len": 1},
|
|
)
|
|
|
|
# CPU/float32 is rejected later, but k=N itself must not trigger fallback.
|
|
assert all_blocks._fallback_reason(query, query, query, metadata) == "dtype torch.float32 is not supported"
|
|
assert too_many._fallback_reason(query, query, query, metadata) == "topk 3 > num_blocks 2"
|
|
|
|
|
|
def test_sdpa_fallback_never_attends_the_structural_padding(monkeypatch):
|
|
# The backend advertises supports_packed_mask_free, so a model may skip
|
|
# building the padding mask. Every fallback out of the VSA path must honour
|
|
# that contract itself; SDPA reads attn_mask and nothing else.
|
|
impl = FastVideoVSAImpl(
|
|
num_heads=2,
|
|
head_size=8,
|
|
softmax_scale=8**-0.5,
|
|
backend_kwargs={"topk": 1, "min_seq_len": 4096},
|
|
)
|
|
valid = 40
|
|
torch.manual_seed(0)
|
|
query = torch.randn(1, 64, 2, 8)
|
|
padding = PackedPaddingMetadata(
|
|
q_length=valid,
|
|
kv_length=valid,
|
|
cu_seqlens_q=torch.tensor([0, valid], dtype=torch.int32),
|
|
cu_seqlens_k=torch.tensor([0, valid], dtype=torch.int32),
|
|
)
|
|
|
|
monkeypatch.setattr(impl.sdpa_fallback, "forward", impl.sdpa_fallback.forward_cuda)
|
|
|
|
baseline = impl.forward_cuda(query, query, query, AttentionMetadata(packed_padding=padding, extra={}))
|
|
perturbed_input = query.clone()
|
|
perturbed_input[:, valid:] += 100.0
|
|
perturbed = impl.forward_cuda(
|
|
perturbed_input, perturbed_input, perturbed_input, AttentionMetadata(packed_padding=padding, extra={})
|
|
)
|
|
|
|
assert baseline.shape == query.shape
|
|
torch.testing.assert_close(baseline[:, :valid], perturbed[:, :valid])
|
|
assert torch.count_nonzero(baseline[:, valid:]) == 0
|