Files
2026-07-13 12:24:33 +08:00

239 lines
7.9 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""Unit tests for the format-aware per-layer helpers in
:mod:`lmcache.v1.gpu_connector.utils`.
"""
# Third Party
import pytest
import torch
pytestmark = pytest.mark.skipif(
not torch.cuda.is_available(),
reason="PageBufferShapeDesc and GPUKVFormat require the CUDA build",
)
# First Party
from lmcache.v1.gpu_connector.kv_format.contiguity import ( # noqa: E402
attempt_permute_to_contiguous_view,
)
from lmcache.v1.gpu_connector.utils import ( # noqa: E402
get_device,
get_dtype,
get_group_data_ptrs,
get_head_size,
get_num_heads,
make_page_buffer_shape_desc,
)
import lmcache.c_ops as lmc_ops # noqa: E402
def test_make_shape_desc_vllm_flash_attn_nhd():
kv_caches = [torch.empty(2, 32, 16, 8, 64, dtype=torch.bfloat16) for _ in range(4)]
sd = make_page_buffer_shape_desc(
kv_caches,
lmc_ops.EngineKVFormat.NL_X_TWO_NB_BS_NH_HS,
layer_idx=0,
num_layers_in_group=4,
num_blocks=32,
block_size=16,
)
assert sd.kv_size == 2
assert sd.nl == 4
assert sd.nb == 32
assert sd.bs == 16
assert sd.nh == 8
assert sd.hs == 64
assert sd.element_size == 2
def test_make_shape_desc_vllm_flash_infer_nhd():
kv_caches = [torch.empty(32, 2, 16, 8, 64, dtype=torch.float16) for _ in range(2)]
sd = make_page_buffer_shape_desc(
kv_caches,
lmc_ops.EngineKVFormat.NL_X_NB_TWO_BS_NH_HS,
layer_idx=0,
num_layers_in_group=2,
num_blocks=32,
block_size=16,
)
assert sd.nh == 8
assert sd.hs == 64
assert sd.kv_size == 2
def test_make_shape_desc_vllm_mla():
kv_caches = [torch.empty(32, 16, 512, dtype=torch.bfloat16) for _ in range(3)]
sd = make_page_buffer_shape_desc(
kv_caches,
lmc_ops.EngineKVFormat.NL_X_NB_BS_HS,
layer_idx=0,
num_layers_in_group=3,
num_blocks=32,
block_size=16,
)
assert sd.kv_size == 1
assert sd.nh == 1
assert sd.hs == 512
def test_make_shape_desc_sglang_mla():
kv_caches = [torch.empty(512, 1, 128, dtype=torch.bfloat16) for _ in range(2)]
sd = make_page_buffer_shape_desc(
kv_caches,
lmc_ops.EngineKVFormat.NL_X_NBBS_ONE_HS,
layer_idx=0,
num_layers_in_group=2,
num_blocks=32,
block_size=16,
)
assert sd.kv_size == 1
assert sd.nh == 1
assert sd.hs == 128
def test_make_shape_desc_sglang_mha():
k = [torch.empty(512, 8, 64, dtype=torch.bfloat16) for _ in range(4)]
v = [torch.empty(512, 8, 64, dtype=torch.bfloat16) for _ in range(4)]
kv_caches = [k, v]
sd = make_page_buffer_shape_desc(
kv_caches,
lmc_ops.EngineKVFormat.TWO_X_NL_X_NBBS_NH_HS,
layer_idx=0,
num_layers_in_group=4,
num_blocks=32,
block_size=16,
)
assert sd.kv_size == 2
assert sd.nh == 8
assert sd.hs == 64
def test_per_layer_scalar_accessors_per_layer_list():
"""For per-layer list formats, each scalar accessor honours layer_idx."""
kv_caches = [
torch.randn(2, 32, 16, 8 + i, 64, dtype=torch.float16, device="cuda")
for i in range(3) # distinct num_heads per layer: 8, 9, 10
]
fmt = lmc_ops.EngineKVFormat.NL_X_TWO_NB_BS_NH_HS
assert get_num_heads(kv_caches, fmt, layer_idx=0) == 8
assert get_num_heads(kv_caches, fmt, layer_idx=2) == 10
assert get_head_size(kv_caches, fmt, layer_idx=1) == 64
assert get_dtype(kv_caches, fmt, layer_idx=0) == torch.float16
def test_per_layer_scalar_accessors_sglang_mha():
"""For SGLang MHA (nested list), accessors walk into [k_list|v_list]."""
k = [torch.randn(512, 8, 64, dtype=torch.bfloat16, device="cuda") for _ in range(2)]
v = [torch.randn(512, 8, 64, dtype=torch.bfloat16, device="cuda") for _ in range(2)]
kv_caches = [k, v]
fmt = lmc_ops.EngineKVFormat.TWO_X_NL_X_NBBS_NH_HS
assert get_num_heads(kv_caches, fmt, layer_idx=0) == 8
assert get_dtype(kv_caches, fmt, layer_idx=1) == torch.bfloat16
def test_get_group_data_ptrs_per_layer_list_flattens_in_order():
kv_caches = [
torch.randn(2, 32, 16, 8, 64, dtype=torch.float16, device="cuda")
for _ in range(4)
]
fmt = lmc_ops.EngineKVFormat.NL_X_TWO_NB_BS_NH_HS
ptrs = get_group_data_ptrs(kv_caches, fmt, [0, 2, 3])
assert ptrs == [
kv_caches[0].data_ptr(),
kv_caches[2].data_ptr(),
kv_caches[3].data_ptr(),
]
def test_get_group_data_ptrs_sglang_mha_groups_k_before_v():
"""SGLang MHA kernel contract: [K0, K1, ..., KN, V0, V1, ..., VN] —
not per-layer [K0, V0, K1, V1, ...]."""
k = [torch.randn(512, 8, 64, dtype=torch.bfloat16, device="cuda") for _ in range(3)]
v = [torch.randn(512, 8, 64, dtype=torch.bfloat16, device="cuda") for _ in range(3)]
kv_caches = [k, v]
fmt = lmc_ops.EngineKVFormat.TWO_X_NL_X_NBBS_NH_HS
ptrs = get_group_data_ptrs(kv_caches, fmt, [0, 1, 2])
expected = [
k[0].data_ptr(),
k[1].data_ptr(),
k[2].data_ptr(),
v[0].data_ptr(),
v[1].data_ptr(),
v[2].data_ptr(),
]
assert ptrs == expected
def test_get_group_data_ptrs_cross_layer_returns_single_base():
"""Cross-layer format packs every layer into one tensor; the kernel
(csrc/mp_mem_kernels.cu) reads paged_buffer_ptrs[0] and computes
per-layer offsets from shape_desc.nl internally. The group helper
must return a single base pointer, not num_layers entries."""
big = torch.empty(32, 80, 2, 16, 8, 64, dtype=torch.bfloat16, device="cuda")
fmt = lmc_ops.EngineKVFormat.NB_NL_TWO_BS_NH_HS
ptrs = get_group_data_ptrs(big, fmt, list(range(80)))
assert ptrs == [big.data_ptr()]
def test_attempt_permute_preserves_bare_tensor():
"""A bare torch.Tensor input (cross-layer shape) must pass through
attempt_permute_to_contiguous_view unchanged — no list wrapping — so the
DiscoverableKVCache recursive union is respected end-to-end."""
big = torch.empty(32, 80, 2, 16, 8, 64, dtype=torch.bfloat16, device="cuda")
out = attempt_permute_to_contiguous_view(big)
assert isinstance(out, torch.Tensor)
assert out is big
def test_attempt_permute_recurses_all_shapes():
"""attempt_permute_to_contiguous_view must descend into every
DiscoverableKVCache shape and permute non-contiguous tensor leaves."""
# Build a flash-attention HND-layout tensor (non-contiguous after
# logical→physical permute exposure — the vLLM HND case).
nhd_view = (
torch.empty(2, 32, 8, 16, 64, dtype=torch.bfloat16, device="cuda")
.permute(0, 1, 3, 2, 4)
.contiguous()
.permute(0, 1, 3, 2, 4) # NHD logical view over HND physical
)
assert not nhd_view.is_contiguous()
# bare tensor
out_tensor = attempt_permute_to_contiguous_view(nhd_view)
assert out_tensor.is_contiguous()
# flat list
lst = [nhd_view.clone() for _ in range(2)]
out_list = attempt_permute_to_contiguous_view(lst)
assert isinstance(out_list, list)
assert all(t.is_contiguous() for t in out_list)
# nested list (SGLang-shaped)
k = [nhd_view.clone() for _ in range(2)]
v = [nhd_view.clone() for _ in range(2)]
out_nested = attempt_permute_to_contiguous_view([k, v])
assert isinstance(out_nested, list)
assert all(t.is_contiguous() for sublist in out_nested for t in sublist)
def test_get_device_handles_every_kvcaches_shape():
"""get_device must work for every DiscoverableKVCache shape without format hints."""
t = torch.empty(8, dtype=torch.bfloat16, device="cuda")
assert get_device(t) == t.device
flat = [torch.empty(4, dtype=torch.bfloat16, device="cuda") for _ in range(3)]
assert get_device(flat) == flat[0].device
k = [torch.empty(4, dtype=torch.bfloat16, device="cuda") for _ in range(2)]
v = [torch.empty(4, dtype=torch.bfloat16, device="cuda") for _ in range(2)]
assert get_device([k, v]) == k[0].device
if __name__ == "__main__":
pytest.main([__file__, "-v"])