Files
vllm-omni/tests/diffusion/attention/test_attention_capabilities.py
T

360 lines
13 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
from __future__ import annotations
import sys
from dataclasses import replace
from types import SimpleNamespace
from unittest.mock import Mock
import pytest
import torch
from vllm_omni.diffusion.attention.backends.abstract import (
AttentionBackend,
AttentionMetadata,
)
from vllm_omni.diffusion.attention.backends.flash_attn import (
FlashAttentionBackend,
FlashAttentionImpl,
)
from vllm_omni.diffusion.attention.backends.utils import fa
from vllm_omni.diffusion.attention.capabilities import (
CapabilityResult,
CompilationMode,
ExecutionContext,
OuterBoundary,
ParallelStrategy,
SupportStatus,
)
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cpu]
_validate_fa4_head_dims = fa.validate_fa4_head_dims
@pytest.fixture(autouse=True)
def kernel_validator(monkeypatch):
# Capability unit tests do not require FA4 or a CUDA device. Real-kernel
# validation is exercised by test_flash_attn_compile.py.
monkeypatch.setattr(fa, "validate_fa4_head_dims", lambda *_args: True)
def _impl(*, kernel_variant: str | None = "fa4", causal: bool = False):
impl = FlashAttentionImpl.__new__(FlashAttentionImpl)
impl._kernel_variant = kernel_variant
impl.causal = causal
return impl
def _resolve(
impl,
*,
context: ExecutionContext | None = None,
attn_metadata: AttentionMetadata | None = None,
dtype=torch.bfloat16,
):
tensor = torch.empty((1, 16, 8, 64), dtype=dtype)
return impl.resolve_execution_path(
context or ExecutionContext(platform="cuda"),
tensor,
tensor,
tensor,
attn_metadata,
)
class _UnmigratedBackend(AttentionBackend):
@staticmethod
def get_name() -> str:
return "UNMIGRATED_TEST"
@staticmethod
def get_impl_cls():
return None
@staticmethod
def get_metadata_cls():
return None
@staticmethod
def get_builder_cls():
return None
@staticmethod
def get_supported_head_sizes() -> list[int]:
return []
def test_capability_result_is_tri_state():
assert CapabilityResult.supported().status is SupportStatus.SUPPORTED
assert CapabilityResult.unsupported("not available").status is SupportStatus.UNSUPPORTED
assert CapabilityResult.unmigrated().status is SupportStatus.UNMIGRATED
with pytest.raises(ValueError, match="actionable reason"):
CapabilityResult(SupportStatus.UNSUPPORTED)
def test_unmigrated_backend_default_is_conservative():
context = ExecutionContext(platform="cuda", require_fullgraph=True)
result = _UnmigratedBackend.resolve_capabilities(context)
assert result.support.status is SupportStatus.UNMIGRATED
assert result.compilation_mode is CompilationMode.EAGER_ONLY
requested = result.requested_support(context)
assert requested.status is SupportStatus.UNSUPPORTED
assert "no verified fullgraph declaration" in requested.reason
assert result.support.status is SupportStatus.UNMIGRATED
assert result.requested_support(replace(context, require_fullgraph=False)).status is SupportStatus.UNMIGRATED
@pytest.mark.parametrize("mode", list(CompilationMode))
def test_fullgraph_request_checks_compilation_mode(mode):
result = replace(_resolve(_impl()), compilation_mode=mode)
context = ExecutionContext(platform="cuda", require_fullgraph=True)
expected = SupportStatus.UNSUPPORTED if mode is CompilationMode.EAGER_ONLY else SupportStatus.SUPPORTED
assert result.requested_support(context).status is expected
assert result.requested_support(replace(context, require_fullgraph=False)).status is SupportStatus.SUPPORTED
def test_request_preserves_verified_rejection_reason():
result = replace(_resolve(_impl()), support=CapabilityResult.unsupported("unsupported head size"))
assert result.requested_support(ExecutionContext(platform="cuda", require_fullgraph=True)) == result.support
def test_exact_cuda_fa4_dense_path_is_supported():
context = ExecutionContext(
platform="cuda",
require_fullgraph=True,
)
result = _resolve(_impl(), context=context)
assert result.path == "fa4_dense"
assert result.support.status is SupportStatus.SUPPORTED
assert result.compilation_mode is CompilationMode.CUSTOM_OP
assert result.requested_support(context).status is SupportStatus.SUPPORTED
def test_causal_fa4_path_remains_unmigrated():
result = _resolve(_impl(causal=True))
assert result.support.status is SupportStatus.UNMIGRATED
def test_fa4_delegates_dimensions_to_kernel(monkeypatch):
head_dims = (80, 48)
validator = Mock(return_value=True)
monkeypatch.setattr(fa, "validate_fa4_head_dims", validator)
query = torch.empty((1, 16, 8, head_dims[0]), dtype=torch.bfloat16)
value = torch.empty((1, 16, 8, head_dims[1]), dtype=torch.bfloat16)
result = _impl().resolve_execution_path(ExecutionContext(platform="cuda"), query, query, value, None)
assert result.support.status is SupportStatus.SUPPORTED
validator.assert_called_once_with(*head_dims, 8)
def test_fa4_reports_kernel_rejection(monkeypatch):
monkeypatch.setattr(fa, "validate_fa4_head_dims", Mock(side_effect=AssertionError("kernel dimension constraint")))
result = _resolve(_impl())
assert result.support.status is SupportStatus.UNSUPPORTED
assert "kernel dimension constraint" in result.support.reason
assert "another backend" in result.support.reason
def test_fa4_missing_validator_remains_unmigrated(monkeypatch):
monkeypatch.setattr(fa, "validate_fa4_head_dims", lambda *_args: False)
result = _resolve(_impl())
assert result.support.status is SupportStatus.UNMIGRATED
assert result.compilation_mode is CompilationMode.EAGER_ONLY
def test_fa4_rejects_mismatched_query_key_dimensions():
query = torch.empty((1, 16, 8, 64), dtype=torch.bfloat16)
key = torch.empty((1, 16, 8, 32), dtype=torch.bfloat16)
result = _impl().resolve_execution_path(ExecutionContext(platform="cuda"), query, key, key, None)
assert result.support.status is SupportStatus.UNSUPPORTED
assert "Q and K head dimensions must match" in result.support.reason
@pytest.mark.parametrize("arch", [90, 100, 120])
def test_fa4_validator_adapter_uses_selected_architecture(monkeypatch, arch):
validator = Mock()
monkeypatch.setitem(
sys.modules,
"flash_attn.cute.interface",
SimpleNamespace(_get_device_arch=lambda: arch, _validate_head_dims=validator),
)
# Call the real adapter, not the fixture's stand-in.
verified = _validate_fa4_head_dims(80, 48, 8)
if arch == 120:
assert not verified
validator.assert_not_called()
else:
assert verified
validator.assert_called_once_with(80, 48, arch // 10, 8)
def test_fa4_validator_adapter_handles_missing_private_api(monkeypatch):
monkeypatch.setitem(sys.modules, "flash_attn.cute.interface", SimpleNamespace())
assert not _validate_fa4_head_dims(64, 64, 8)
def test_initialized_kernel_identity_overrides_caller_claim():
result = _resolve(
_impl(kernel_variant=None),
context=ExecutionContext(
platform="cuda",
kernel_variant="fa4",
),
)
assert result.support.status is SupportStatus.UNMIGRATED
def test_preconstruction_without_resolved_cuda_kernel_is_unmigrated():
result = FlashAttentionBackend.resolve_capabilities(ExecutionContext(platform="cuda"))
assert result.support.status is SupportStatus.UNMIGRATED
assert result.path == "unverified"
@pytest.mark.parametrize(
"changes",
[
{"platform": "rocm"},
{"paged_kv": True},
{"parallel_strategy": ParallelStrategy.ULYSSES},
{"parallel_strategy": ParallelStrategy.RING},
{"parallel_strategy": ParallelStrategy.HYBRID_ULYSSES_RING},
{"outer_boundaries": frozenset({OuterBoundary.HSDP})},
],
)
def test_unverified_flash_attention_variants_remain_unmigrated(changes):
result = _resolve(
_impl(),
context=replace(ExecutionContext(platform="cuda"), **changes),
)
assert result.support.status is SupportStatus.UNMIGRATED
assert result.compilation_mode is CompilationMode.EAGER_ONLY
def test_unverified_dtype_remains_unmigrated():
result = _resolve(_impl(), dtype=torch.float32)
assert result.support.status is SupportStatus.UNMIGRATED
@pytest.mark.parametrize(
"attn_metadata",
[
AttentionMetadata(full_attn_spans=[[(0, 8)]]),
AttentionMetadata(
extra={
"cu_seqlens_q": torch.tensor([0, 16], dtype=torch.int32),
"cu_seqlens_k": torch.tensor([0, 16], dtype=torch.int32),
"max_seqlen_q": 16,
"max_seqlen_k": 16,
}
),
AttentionMetadata(extra={"kv_cache_dtype": "fp8"}),
],
)
def test_metadata_normalization_prevents_dense_support(attn_metadata):
result = _resolve(_impl(), attn_metadata=attn_metadata)
assert result.support.status is SupportStatus.UNMIGRATED
def test_unpublished_all_true_mask_remains_runtime_dependent():
metadata = AttentionMetadata(
attn_mask=torch.ones((1, 16), dtype=torch.bool),
)
result = _resolve(_impl(), attn_metadata=metadata)
assert result.path == "runtime_mask_dependent"
assert result.support.status is SupportStatus.UNMIGRATED
def test_producer_published_noop_mask_resolves_dense_fa4():
metadata = AttentionMetadata(
attn_mask=torch.ones((1, 16), dtype=torch.bool),
extra={"attention_mask_mode": "none"},
)
result = _resolve(_impl(), attn_metadata=metadata)
assert result.path == "fa4_dense"
assert result.support.status is SupportStatus.SUPPORTED
def test_fixed_shape_mask_semantic_change_invalidates_dense_resolution():
metadata = AttentionMetadata(
attn_mask=torch.ones((1, 16), dtype=torch.bool),
extra={"attention_mask_mode": "none"},
)
impl = _impl()
dense = _resolve(impl, attn_metadata=metadata)
assert dense.support.status is SupportStatus.SUPPORTED
metadata.attn_mask[:, 8:] = False
metadata.extra["attention_mask_mode"] = "padding"
masked = _resolve(impl, attn_metadata=metadata)
assert masked.support.status is SupportStatus.UNMIGRATED
request = ExecutionContext(platform="cuda", require_fullgraph=True)
assert masked.requested_support(request).status is SupportStatus.UNSUPPORTED
def test_piecewise_dispatch_ignores_unused_incomplete_packed_metadata(monkeypatch):
from vllm_omni.diffusion.attention.backends import flash_attn
from vllm_omni.diffusion.attention.backends.utils import fa
metadata = AttentionMetadata(full_attn_spans=[[(0, 16)]], extra={"max_seqlen_q": 16})
impl = _impl()
impl.fa_deterministic = False
impl.softmax_scale = 0.125
assert _resolve(impl, attn_metadata=metadata).support.status is SupportStatus.UNMIGRATED
query = torch.empty((1, 16, 8, 64), dtype=torch.bfloat16)
monkeypatch.setattr(fa, "HAS_FLASH_ATTN", True)
monkeypatch.setattr(fa, "flash_attn_func", lambda *args, **kwargs: query)
monkeypatch.setattr(flash_attn, "piecewise_attn", lambda *args, **kwargs: query)
assert impl.forward_cuda(query, query, query, metadata) is query
@pytest.mark.parametrize("input_index", [1, 2])
@pytest.mark.parametrize("mismatch", ["dtype", "device"])
def test_fa4_rejects_incompatible_key_value(input_index, mismatch):
tensors = [torch.empty((1, 16, 8, 64), dtype=torch.bfloat16) for _ in range(3)]
tensors[input_index] = (
tensors[input_index].to(dtype=torch.float16) if mismatch == "dtype" else tensors[input_index].to("meta")
)
result = _impl().resolve_execution_path(ExecutionContext(platform="cuda"), *tensors, None)
assert result.support.status is SupportStatus.UNSUPPORTED
assert f"{mismatch}s must match" in result.support.reason
@pytest.mark.parametrize("skip_quant", [False, True])
def test_layer_resolution_uses_effective_kv_quantization(skip_quant):
from vllm_omni.diffusion.attention.layer import Attention
from vllm_omni.diffusion.attention.parallel.base import NoParallelAttention
layer = Attention.__new__(Attention)
torch.nn.Module.__init__(layer)
layer.attention = _impl(kernel_variant=None)
layer._hsdp_compile_boundary_enabled = False
layer.skip_sequence_parallel = False
layer._no_parallel_strategy = NoParallelAttention()
layer.parallel_strategy = layer._no_parallel_strategy
layer.use_ring = False
layer.paged_kv_cache_role = None
layer._kv_cache_dtype = "fp8"
layer._disable_kv_quant = False
layer._kv_cache_skip_steps = None
layer._kv_cache_skip_layers = {0} if skip_quant else None
layer.layer_idx = 0
query = torch.empty((1, 16, 8, 64), dtype=torch.bfloat16)
metadata = AttentionMetadata(extra={"kv_cache_dtype": "fp8"})
result = layer.resolve_execution_path(ExecutionContext(platform="npu"), query, query, query, metadata)
assert result.path == ("npu_dense" if skip_quant else "npu_unverified")
assert result.support.status is SupportStatus.UNMIGRATED
assert metadata.extra == {"kv_cache_dtype": "fp8"}