Files
vllm-project--vllm/tests/v1/test_kv_cache_spec_registry.py
T
wehub-resource-sync 7ce4c8e27e
pre-commit / pre-run-check (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:55:37 +08:00

323 lines
10 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from dataclasses import dataclass, replace
from typing import Any
import pytest
import torch
from vllm.config import (
CacheConfig,
DeviceConfig,
VllmConfig,
)
from vllm.v1.core.single_type_kv_cache_manager import (
ChunkedLocalAttentionManager,
CrossAttentionManager,
FullAttentionManager,
MambaManager,
SingleTypeKVCacheManager,
SinkFullAttentionManager,
SlidingWindowManager,
register_all_kvcache_specs,
)
from vllm.v1.kv_cache_interface import (
ChunkedLocalAttentionSpec,
CrossAttentionSpec,
FullAttentionSpec,
HiddenStateCacheSpec,
KVCacheSpec,
MambaSpec,
MLAAttentionSpec,
SinkFullAttentionSpec,
SlidingWindowMLASpec,
SlidingWindowSpec,
TQFullAttentionSpec,
UniformTypeKVCacheSpecs,
)
from vllm.v1.kv_cache_spec_registry import (
_REGISTRY_KVCACHESPEC_LIST,
KVCacheSpecRegistry,
register_kv_cache_spec,
)
def make_vllm_config() -> VllmConfig:
return VllmConfig(
cache_config=CacheConfig(
block_size=64,
cache_dtype="bfloat16",
),
device_config=DeviceConfig(device="cpu"),
)
vllm_config = make_vllm_config()
register_all_kvcache_specs(vllm_config)
@pytest.fixture(autouse=True)
def restore_kv_cache_spec_registry():
registry = _REGISTRY_KVCACHESPEC_LIST.copy()
yield
_REGISTRY_KVCACHESPEC_LIST.clear()
_REGISTRY_KVCACHESPEC_LIST.update(registry)
@dataclass(frozen=True)
class _TrulyUnregisteredSpec(KVCacheSpec):
"""
A spec that inherits directly from KVCacheSpec with no registered
ancestor in the MRO. Used to test that the registry correctly raises
when no entry can be found.
"""
@property
def page_size_bytes(self) -> int:
return self.block_size * 128
def max_memory_usage_bytes(self, _) -> int:
return 0
spec_manager_map: dict[type[KVCacheSpec], type[SingleTypeKVCacheManager]] = {
FullAttentionSpec: FullAttentionManager,
TQFullAttentionSpec: FullAttentionManager,
MLAAttentionSpec: FullAttentionManager,
HiddenStateCacheSpec: FullAttentionManager,
SlidingWindowSpec: SlidingWindowManager,
SlidingWindowMLASpec: SlidingWindowManager,
ChunkedLocalAttentionSpec: ChunkedLocalAttentionManager,
MambaSpec: MambaManager,
CrossAttentionSpec: CrossAttentionManager,
SinkFullAttentionSpec: SinkFullAttentionManager,
}
spec_uniform_base_map: dict[type[KVCacheSpec], type[KVCacheSpec]] = {
FullAttentionSpec: FullAttentionSpec,
TQFullAttentionSpec: FullAttentionSpec,
MLAAttentionSpec: FullAttentionSpec,
HiddenStateCacheSpec: FullAttentionSpec,
SlidingWindowSpec: SlidingWindowSpec,
SlidingWindowMLASpec: SlidingWindowMLASpec,
ChunkedLocalAttentionSpec: ChunkedLocalAttentionSpec,
MambaSpec: MambaSpec,
CrossAttentionSpec: CrossAttentionSpec,
SinkFullAttentionSpec: FullAttentionSpec,
}
spec_args_map: dict[type[KVCacheSpec], dict[str, Any]] = {
FullAttentionSpec: dict(
block_size=64, num_kv_heads=8, head_size=128, dtype=torch.bfloat16
),
TQFullAttentionSpec: dict(
block_size=64,
num_kv_heads=8,
head_size=128,
dtype=torch.bfloat16,
tq_slot_size=256,
),
MLAAttentionSpec: dict(
block_size=64, num_kv_heads=1, head_size=128, dtype=torch.bfloat16
),
HiddenStateCacheSpec: dict(
block_size=64, num_kv_heads=1, head_size=128, dtype=torch.bfloat16
),
SlidingWindowSpec: dict(
block_size=64,
num_kv_heads=8,
head_size=128,
dtype=torch.bfloat16,
sliding_window=128,
),
SlidingWindowMLASpec: dict(
block_size=64,
num_kv_heads=1,
head_size=128,
dtype=torch.bfloat16,
sliding_window=128,
),
ChunkedLocalAttentionSpec: dict(
block_size=64,
num_kv_heads=8,
head_size=128,
dtype=torch.bfloat16,
attention_chunk_size=4,
),
MambaSpec: dict(
block_size=64,
shapes=((2, 512), (3, 32, 32)),
dtypes=(torch.float32, torch.float32),
mamba_cache_mode="align",
num_speculative_blocks=2,
),
CrossAttentionSpec: dict(
block_size=64, num_kv_heads=8, head_size=128, dtype=torch.bfloat16
),
SinkFullAttentionSpec: dict(
block_size=64, num_kv_heads=8, head_size=128, dtype=torch.bfloat16, sink_len=16
),
}
def make_spec(spec_cls: type[KVCacheSpec]) -> KVCacheSpec:
return spec_cls(**spec_args_map[spec_cls])
def are_uniform_specs(*specs: KVCacheSpec) -> bool:
return UniformTypeKVCacheSpecs.is_uniform_type(
{f"layer_{i}": spec for i, spec in enumerate(specs)}
)
class TestKVCacheSpecRegistry:
"""Test the core registry functionality."""
def test_builtin_kvcache_specs_registered(self):
assert set(spec_manager_map) <= set(_REGISTRY_KVCACHESPEC_LIST)
for spec_cls, manager in spec_manager_map.items():
spec = make_spec(spec_cls)
assert KVCacheSpecRegistry.get_manager_class(spec) is manager
assert (
KVCacheSpecRegistry.get_uniform_type_base_spec(spec)
is spec_uniform_base_map[spec_cls]
)
@pytest.mark.parametrize("spec_cls", list(spec_manager_map))
def test_custom_spec_register(self, spec_cls):
"""A decorated custom spec resolves to the declared manager."""
manager = spec_manager_map[spec_cls]
uniform_base_spec = spec_uniform_base_map[spec_cls]
@register_kv_cache_spec(
manager_class=manager,
uniform_type_base_spec=uniform_base_spec,
)
@dataclass(frozen=True, kw_only=True)
class _CustomSpec(spec_cls): # type: ignore[valid-type,misc]
custom_param: int = 16
spec = _CustomSpec(**spec_args_map[spec_cls], custom_param=100)
assert KVCacheSpecRegistry.get_manager_class(spec) is manager
assert KVCacheSpecRegistry.get_uniform_type_base_spec(spec) is uniform_base_spec
def test_custom_spec_register_requires_manager(self):
"""Invalid register decorator arguments fail early."""
with pytest.raises(AssertionError, match="manager_class is required"):
@register_kv_cache_spec(
uniform_type_base_spec=FullAttentionSpec,
)
@dataclass(frozen=True, kw_only=True)
class _CustomFullSpecWithoutManager(FullAttentionSpec):
custom_param: int = 16
def test_unregistered_spec_no_registered_parent_raises(self):
"""
A spec whose entire MRO contains no registered class resolves to None.
Runtime callers should use check_kv_cache_spec_registry to fail early.
Subclasses of registered specs intentionally do not fail — they inherit
their parent's manager via MRO walking.
"""
spec = _TrulyUnregisteredSpec(block_size=16)
assert KVCacheSpecRegistry.get_manager_class(spec) is None
assert KVCacheSpecRegistry.get_uniform_type_base_spec(spec) is None
with pytest.raises(
ValueError, match="Unsupported KV cache spec type for layer layer_0"
):
KVCacheSpecRegistry.check_kv_cache_spec_registry({"layer_0": spec})
with pytest.raises(AssertionError, match="Unsupported KV cache spec type"):
UniformTypeKVCacheSpecs.is_uniform_type({"layer_0": spec})
def test_unregistered_subclass_inherits_parent_manager(self):
"""
An unregistered subclass of a registered spec resolves via MRO
to its parent's manager — this is intentional registry behaviour.
"""
@dataclass(frozen=True, kw_only=True)
class _ImplicitlyInheritedSpec(FullAttentionSpec):
pass
spec = _ImplicitlyInheritedSpec(
block_size=16, num_kv_heads=8, head_size=128, dtype=torch.bfloat16
)
# MRO walk finds FullAttentionSpec → FullAttentionManager
assert KVCacheSpecRegistry.get_manager_class(spec) is FullAttentionManager
@pytest.mark.parametrize("spec_cls", list(spec_manager_map))
def test_builtin_specs_are_uniform_with_same_spec_type(self, spec_cls):
spec = make_spec(spec_cls)
assert are_uniform_specs(spec, replace(spec))
def test_full_attention_family_specs_are_uniform(self):
specs = [
make_spec(FullAttentionSpec),
make_spec(TQFullAttentionSpec),
make_spec(MLAAttentionSpec),
make_spec(HiddenStateCacheSpec),
make_spec(SinkFullAttentionSpec),
]
assert are_uniform_specs(*specs)
@pytest.mark.parametrize(
("spec_cls", "field", "value"),
[
(SlidingWindowSpec, "sliding_window", 256),
(SlidingWindowMLASpec, "sliding_window", 256),
(ChunkedLocalAttentionSpec, "attention_chunk_size", 8),
(MambaSpec, "num_speculative_blocks", 4),
],
)
def test_specs_with_type_specific_uniform_fields(self, spec_cls, field, value):
spec = make_spec(spec_cls)
changed_spec = replace(spec, **{field: value})
assert not are_uniform_specs(spec, changed_spec)
@pytest.mark.parametrize(
("left_cls", "right_cls"),
[
(FullAttentionSpec, CrossAttentionSpec),
(FullAttentionSpec, SlidingWindowSpec),
(FullAttentionSpec, ChunkedLocalAttentionSpec),
(FullAttentionSpec, MambaSpec),
(SlidingWindowMLASpec, SlidingWindowSpec),
(ChunkedLocalAttentionSpec, SlidingWindowSpec),
(MambaSpec, CrossAttentionSpec),
],
)
def test_different_uniform_groups_are_not_uniform(self, left_cls, right_cls):
assert not are_uniform_specs(make_spec(left_cls), make_spec(right_cls))
def test_different_block_sizes_are_not_uniform(self):
spec = make_spec(FullAttentionSpec)
assert not are_uniform_specs(spec, replace(spec, block_size=32))
def test_registered_custom_spec_uses_base_uniform_rule(self):
@register_kv_cache_spec(
manager_class=FullAttentionManager,
uniform_type_base_spec=FullAttentionSpec,
)
@dataclass(frozen=True, kw_only=True)
class _CustomFullSpec(FullAttentionSpec):
custom_param: int = 16
custom_spec = _CustomFullSpec(
block_size=64,
num_kv_heads=8,
head_size=128,
dtype=torch.bfloat16,
)
assert are_uniform_specs(custom_spec, make_spec(FullAttentionSpec))