Files
vLLM/tests/models/kimi_k3/test_kda_metadata.py
T
+12 7c6729b769 [Model] Add Kimi K3 support: model files and kernels [1/N] (#50089)
Signed-off-by: zjy0516 <riverclouds.zhu@qq.com>
Signed-off-by: Isotr0py <Isotr0py@outlook.com>
Signed-off-by: Bugen Zhao <i@bugenzhao.com>
Signed-off-by: Yongye Zhu <zyy1102000@gmail.com>
Co-authored-by: Isotr0py <Isotr0py@outlook.com>
Co-authored-by: Thien Tran <gau.nernst@yahoo.com.sg>
Co-authored-by: Bugen Zhao <i@bugenzhao.com>
Co-authored-by: Yongye Zhu <zyy1102000@gmail.com>
Co-authored-by: Ziming Huang <zelda.huanghuang@gmail.com>
Co-authored-by: Roger Wang <hey@rogerw.io>
Co-authored-by: Isotr0py <mozf@inferact.ai>
Co-authored-by: aoshen02 <aoshen02@users.noreply.github.com>
Co-authored-by: Woosuk Kwon <woosuk@inferact.ai>
Co-authored-by: Jee Jee Li <jeejeelee@inferact.ai>
Co-authored-by: aoshen02 <aoshen@inferact.ai>
Co-authored-by: Summer Yang <girasoleyang@gmail.com>
Co-authored-by: Kevin H. Luu <khluu000@gmail.com>
Co-authored-by: Bowen Wang <abmfy@icloud.com>
Co-authored-by: gnovack <novackgm@gmail.com>
Co-authored-by: Nick Hill <nickhill123@gmail.com>
Co-authored-by: Yifan Qiao <yifanqiao@inferact.ai>
Co-authored-by: xiaozhoupy <peiyuanzhou1994@gmail.com>
Co-authored-by: Roy Wang <yasong.wang@inferact.ai>
Co-authored-by: Jeff (Junze) Ma <93145857+majunze2001@users.noreply.github.com>
Co-authored-by: Codex <codex@openai.com>
2026-07-29 14:10:58 +08:00

412 lines
13 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from dataclasses import fields
import pytest
import torch
from tests.v1.attention.utils import (
BatchSpec,
create_common_attn_metadata,
create_vllm_config,
)
from vllm.config import SpeculativeConfig
from vllm.config.compilation import CUDAGraphMode
from vllm.models.kimi_k3.nvidia.kda_metadata import (
KimiK3KDAAttentionBackend,
KimiK3KDAMetadata,
KimiK3KDAMetadataBuilder,
_mamba_get_block_table_tensor,
stage_spec_decode_metadata,
)
from vllm.v1.attention.backend import AttentionMetadataBuilder
from vllm.v1.attention.backends.gdn_attn import (
GDNAttentionBackend,
GDNAttentionMetadata,
GDNAttentionMetadataBuilder,
)
from vllm.v1.attention.backends.utils import (
NULL_BLOCK_ID,
mamba_get_block_table_tensor,
)
from vllm.v1.kv_cache_interface import MambaSpec
BLOCK_SIZE = 16
DEVICE = torch.device("cpu")
PRUNED_METADATA_FIELDS = {
"chunk_indices",
"chunk_offsets",
"prefill_query_start_loc",
"prefill_state_indices",
"prefill_has_initial_state",
"spec_sequence_masks",
}
def _assert_matches_shared_gdn(reference, actual: KimiK3KDAMetadata):
for field in fields(KimiK3KDAMetadata):
actual_value = getattr(actual, field.name)
expected_value = getattr(reference, field.name)
if field.name in PRUNED_METADATA_FIELDS:
assert actual_value is None
continue
if (
field.name in {"spec_token_indx", "non_spec_token_indx"}
and actual.num_spec_decodes > 0
and actual.num_prefills == 0
and actual.num_decodes == 0
):
assert actual_value is None
continue
if isinstance(actual_value, torch.Tensor):
torch.testing.assert_close(actual_value, expected_value)
elif field.name == "nums_dict":
assert (actual_value is None) == (expected_value is None)
if actual_value is not None:
assert actual_value[8]["tot"] == expected_value[8]["tot"]
torch.testing.assert_close(
actual_value[8]["nums"], expected_value[8]["nums"]
)
else:
assert actual_value == expected_value
def _make_builder(
builder_cls: type[AttentionMetadataBuilder],
num_speculative_tokens: int,
full_cuda_graph: bool,
device: torch.device = DEVICE,
mamba_cache_mode: str = "none",
) -> AttentionMetadataBuilder:
vllm_config = create_vllm_config(
model_name="Qwen/Qwen3.5-0.8B",
block_size=BLOCK_SIZE,
)
if num_speculative_tokens:
vllm_config.speculative_config = SpeculativeConfig(
method="ngram",
num_speculative_tokens=num_speculative_tokens,
)
vllm_config.compilation_config.cudagraph_mode = (
CUDAGraphMode.FULL_AND_PIECEWISE if full_cuda_graph else CUDAGraphMode.NONE
)
vllm_config.cache_config.mamba_cache_mode = mamba_cache_mode
return builder_cls(
kv_cache_spec=MambaSpec(
block_size=BLOCK_SIZE,
shapes=((16, 64),),
dtypes=(torch.float16,),
num_speculative_blocks=num_speculative_tokens,
),
layer_names=["layer.0"],
vllm_config=vllm_config,
device=device,
)
@pytest.mark.parametrize(
(
"batch",
"num_decode_draft_tokens",
"num_speculative_tokens",
"full_cuda_graph",
"is_prefilling",
),
[
pytest.param(
BatchSpec(seq_lens=[50, 30], query_lens=[3, 3]),
[2, 2],
2,
False,
[False, False],
id="pure-spec-decode",
),
pytest.param(
BatchSpec(seq_lens=[100, 65, 20], query_lens=[50, 1, 3]),
[-1, -1, 2],
2,
False,
[True, False, False],
id="mixed-prefill-and-spec-decode",
),
pytest.param(
BatchSpec(seq_lens=[40, 30], query_lens=[1, 1]),
None,
0,
False,
[False, False],
id="regular-decode",
),
pytest.param(
BatchSpec(seq_lens=[40, 30], query_lens=[1, 1]),
[0, 0],
2,
False,
[False, False],
id="no-scheduled-draft-tokens",
),
],
)
def test_kimi_k3_kda_metadata_matches_shared_gdn(
batch: BatchSpec,
num_decode_draft_tokens: list[int] | None,
num_speculative_tokens: int,
full_cuda_graph: bool,
is_prefilling: list[bool],
):
kwargs: dict[str, torch.Tensor] = {}
if num_decode_draft_tokens is not None:
kwargs = {
"num_decode_draft_tokens_cpu": torch.tensor(
num_decode_draft_tokens, dtype=torch.int32
),
"num_accepted_tokens": torch.ones(
batch.batch_size, dtype=torch.int32, device=DEVICE
),
}
common_attn_metadata = create_common_attn_metadata(
batch, BLOCK_SIZE, DEVICE
).replace(is_prefilling=torch.tensor(is_prefilling, dtype=torch.bool))
reference = _make_builder(
GDNAttentionMetadataBuilder,
num_speculative_tokens,
full_cuda_graph,
).build(
0,
common_attn_metadata,
**kwargs,
)
actual = _make_builder(
KimiK3KDAMetadataBuilder,
num_speculative_tokens,
full_cuda_graph,
).build(0, common_attn_metadata, **kwargs)
assert isinstance(actual, KimiK3KDAMetadata)
_assert_matches_shared_gdn(reference, actual)
def test_mixed_regular_and_spec_decode_uses_packed_decode_metadata():
batch = BatchSpec(seq_lens=[100, 65, 20], query_lens=[1, 1, 3])
common_attn_metadata = create_common_attn_metadata(
batch, BLOCK_SIZE, DEVICE
).replace(is_prefilling=torch.tensor([False, False, False]))
actual = _make_builder(
KimiK3KDAMetadataBuilder,
num_speculative_tokens=2,
full_cuda_graph=False,
).build(
0,
common_attn_metadata,
num_decode_draft_tokens_cpu=torch.tensor([-1, -1, 2], dtype=torch.int32),
num_accepted_tokens=torch.ones(3, dtype=torch.int32, device=DEVICE),
)
# The K3 layer dispatches the non-spec subgroup to packed decode whenever
# it contains no prefill request.
assert actual.num_decodes == 2
assert actual.num_decode_tokens == 2
assert actual.num_prefills == 0
assert actual.num_prefill_tokens == 0
assert actual.has_initial_state is None
assert actual.nums_dict is None
assert actual.non_spec_query_start_loc is None
torch.testing.assert_close(actual.non_spec_token_indx, torch.tensor([0, 1]))
torch.testing.assert_close(actual.spec_token_indx, torch.tensor([2, 3, 4]))
torch.testing.assert_close(
actual.spec_query_start_loc,
torch.tensor([0, 3], dtype=torch.int32),
)
def test_mixed_regular_and_spec_decode_excludes_request_padding():
batch = BatchSpec(seq_lens=[16, 65, 20], query_lens=[0, 1, 3])
common_attn_metadata = create_common_attn_metadata(
batch, BLOCK_SIZE, DEVICE
).replace(is_prefilling=torch.tensor([False, False, False]))
actual = _make_builder(
KimiK3KDAMetadataBuilder,
num_speculative_tokens=2,
full_cuda_graph=False,
).build(
0,
common_attn_metadata,
num_decode_draft_tokens_cpu=torch.tensor([-1, -1, 2], dtype=torch.int32),
num_accepted_tokens=torch.ones(3, dtype=torch.int32, device=DEVICE),
)
assert actual.num_decodes == 1
assert actual.non_spec_state_indices_tensor is not None
assert actual.non_spec_state_indices_tensor.shape == (1,)
torch.testing.assert_close(actual.non_spec_token_indx, torch.tensor([0]))
torch.testing.assert_close(actual.spec_token_indx, torch.tensor([1, 2, 3]))
@pytest.mark.parametrize(
("seq_len", "expected_has_initial_state"),
[
pytest.param(1, False, id="first-token-prefill"),
pytest.param(65, True, id="final-one-token-prefill-chunk"),
],
)
def test_mixed_one_token_prefill_and_spec_decode_uses_prefill_metadata(
seq_len: int,
expected_has_initial_state: bool,
):
batch = BatchSpec(seq_lens=[seq_len, 20], query_lens=[1, 3])
common_attn_metadata = create_common_attn_metadata(
batch, BLOCK_SIZE, DEVICE
).replace(is_prefilling=torch.tensor([True, False]))
actual = _make_builder(
KimiK3KDAMetadataBuilder,
num_speculative_tokens=2,
full_cuda_graph=False,
).build(
0,
common_attn_metadata,
num_decode_draft_tokens_cpu=torch.tensor([-1, 2], dtype=torch.int32),
num_accepted_tokens=torch.ones(2, dtype=torch.int32, device=DEVICE),
)
assert actual.num_prefills == 1
assert actual.num_prefill_tokens == 1
assert actual.num_decodes == 0
assert actual.num_decode_tokens == 0
assert actual.has_initial_state is not None
assert actual.has_initial_state.tolist() == [expected_has_initial_state]
assert actual.non_spec_query_start_loc is not None
torch.testing.assert_close(
actual.non_spec_query_start_loc,
torch.tensor([0, 1], dtype=torch.int32),
)
@pytest.mark.skipif(not torch.cuda.is_available(), reason="requires CUDA")
def test_kimi_k3_kda_cudagraph_capture_matches_shared_gdn():
device = torch.device("cuda")
batch = BatchSpec(seq_lens=[50, 30], query_lens=[3, 3])
common_attn_metadata = create_common_attn_metadata(
batch, BLOCK_SIZE, device
).replace(is_prefilling=torch.tensor([False, False]))
reference = _make_builder(
GDNAttentionMetadataBuilder,
num_speculative_tokens=2,
full_cuda_graph=True,
device=device,
).build_for_cudagraph_capture(common_attn_metadata)
actual = _make_builder(
KimiK3KDAMetadataBuilder,
num_speculative_tokens=2,
full_cuda_graph=True,
device=device,
).build_for_cudagraph_capture(common_attn_metadata)
assert isinstance(actual, KimiK3KDAMetadata)
_assert_matches_shared_gdn(reference, actual)
def test_kimi_k3_kda_backend_uses_private_metadata_builder():
assert KimiK3KDAAttentionBackend.get_builder_cls() is KimiK3KDAMetadataBuilder
assert KimiK3KDAAttentionBackend.is_ssm()
assert issubclass(KimiK3KDAAttentionBackend, GDNAttentionBackend)
assert issubclass(KimiK3KDAMetadata, GDNAttentionMetadata)
assert issubclass(KimiK3KDAMetadataBuilder, GDNAttentionMetadataBuilder)
@pytest.mark.skipif(not torch.cuda.is_available(), reason="requires CUDA")
def test_stage_spec_decode_metadata_matches_pytorch():
device = torch.device("cuda")
num_spec_decodes = 33
batch_size = 65
num_state_slots = 3
state_indices = torch.arange(
num_spec_decodes * 32,
dtype=torch.int32,
device=device,
).reshape(num_spec_decodes, 32)[:, :num_state_slots]
query_start_loc = (
torch.arange(num_spec_decodes + 1, dtype=torch.int32, device=device)
* num_state_slots
)
num_accepted_tokens = (
torch.arange(num_spec_decodes, dtype=torch.int32, device=device)
% num_state_slots
+ 1
)
staged_state_indices = torch.empty(
(batch_size, num_state_slots), dtype=torch.int32, device=device
)
staged_query_start_loc = torch.empty(
batch_size + 1, dtype=torch.int32, device=device
)
staged_num_accepted_tokens = torch.empty(
batch_size, dtype=torch.int32, device=device
)
stage_spec_decode_metadata(
state_indices,
query_start_loc,
num_accepted_tokens,
staged_state_indices,
staged_query_start_loc,
staged_num_accepted_tokens,
num_spec_decodes=num_spec_decodes,
)
expected_state_indices = torch.full_like(staged_state_indices, NULL_BLOCK_ID)
expected_state_indices[:num_spec_decodes] = state_indices
expected_query_start_loc = torch.full(
(batch_size + 1,),
query_start_loc[-1],
dtype=torch.int32,
device=device,
)
expected_query_start_loc[: num_spec_decodes + 1] = query_start_loc
expected_num_accepted_tokens = torch.ones(
batch_size, dtype=torch.int32, device=device
)
expected_num_accepted_tokens[:num_spec_decodes] = num_accepted_tokens
torch.testing.assert_close(staged_state_indices, expected_state_indices)
torch.testing.assert_close(staged_query_start_loc, expected_query_start_loc)
torch.testing.assert_close(staged_num_accepted_tokens, expected_num_accepted_tokens)
@pytest.mark.skipif(not torch.cuda.is_available(), reason="requires CUDA")
def test_aligned_block_table_matches_shared_gdn():
device = torch.device("cuda")
seq_lens = torch.tensor(
[0, 1, 15, 16, 17, 31, 32, 33, 511, 512, 513],
dtype=torch.int32,
device=device,
).repeat(6)[:65]
block_table_storage = torch.arange(
seq_lens.numel() * 128,
dtype=torch.int32,
device=device,
).reshape(seq_lens.numel(), 128)
block_table = block_table_storage[:, ::2]
kv_cache_spec = MambaSpec(
block_size=BLOCK_SIZE,
shapes=((16, 64),),
dtypes=(torch.float16,),
num_speculative_blocks=2,
)
expected = mamba_get_block_table_tensor(
block_table,
seq_lens,
kv_cache_spec,
"align",
)
actual = _mamba_get_block_table_tensor(
block_table,
seq_lens,
kv_cache_spec,
"align",
)
torch.testing.assert_close(actual, expected)