190 lines
6.8 KiB
Python
190 lines
6.8 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
|
|
from types import SimpleNamespace
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
import torch
|
|
|
|
from vllm.config import CompilationConfig, VllmConfig
|
|
from vllm.forward_context import set_forward_context
|
|
from vllm.model_executor.layers.mamba.short_conv import ShortConv
|
|
from vllm.model_executor.layers.utils import dispatch_cpu_unquantized_gemm
|
|
from vllm.platforms import current_platform
|
|
from vllm.v1.attention.backends.short_conv_attn import ShortConvAttentionMetadata
|
|
|
|
if not current_platform.is_cpu():
|
|
pytest.skip("skipping CPU-only tests", allow_module_level=True)
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def mock_dist():
|
|
with (
|
|
patch(
|
|
"vllm.model_executor.layers.linear.get_tensor_model_parallel_rank",
|
|
return_value=0,
|
|
),
|
|
patch(
|
|
"vllm.model_executor.layers.linear.get_tensor_model_parallel_world_size",
|
|
return_value=1,
|
|
),
|
|
patch(
|
|
"vllm.distributed.parallel_state.model_parallel_is_initialized",
|
|
return_value=True,
|
|
),
|
|
patch(
|
|
"vllm.distributed.parallel_state.get_tp_group",
|
|
return_value=MagicMock(rank_in_group=0),
|
|
),
|
|
):
|
|
yield
|
|
|
|
|
|
@pytest.fixture
|
|
def vllm_config():
|
|
# ShortConv only needs compilation_config from the current vLLM config, so a
|
|
# minimal config (model_config=None) avoids mocking ModelConfig and the
|
|
# associated VllmConfig validation churn.
|
|
return VllmConfig(compilation_config=CompilationConfig())
|
|
|
|
|
|
def test_short_conv_forward_native_prefill(vllm_config):
|
|
prefix = "test_layer"
|
|
config = SimpleNamespace(conv_L_cache=4, conv_bias=True)
|
|
dim = 16
|
|
|
|
from vllm.config import set_current_vllm_config
|
|
|
|
with set_current_vllm_config(vllm_config):
|
|
layer = ShortConv(config=config, dim=dim, layer_idx=0, prefix=prefix)
|
|
|
|
layer.to("cpu")
|
|
# vLLM Linear layers allocate weights with torch.empty (uninitialized).
|
|
# On ARM these come back as zero-filled pages, so in_proj output is zero and
|
|
# the prefill state stays zero. Seed + init to make the test platform-safe.
|
|
torch.manual_seed(0)
|
|
for p in layer.parameters():
|
|
torch.nn.init.normal_(p)
|
|
dispatch_cpu_unquantized_gemm(layer.in_proj, remove_weight=False)
|
|
dispatch_cpu_unquantized_gemm(layer.out_proj, remove_weight=False)
|
|
|
|
# Mock AttentionMetadata
|
|
num_prefills = 1
|
|
num_prefill_tokens = 5
|
|
query_start_loc_p = torch.tensor([0, 5], dtype=torch.int32)
|
|
state_indices_tensor_p = torch.tensor([0], dtype=torch.int32)
|
|
|
|
# ShortConvAttentionMetadata
|
|
attn_metadata = ShortConvAttentionMetadata(
|
|
num_prefills=num_prefills,
|
|
num_prefill_tokens=num_prefill_tokens,
|
|
num_decodes=0,
|
|
num_decode_tokens=0,
|
|
num_reqs=1,
|
|
query_start_loc_p=query_start_loc_p,
|
|
has_initial_states_p=torch.tensor([False]),
|
|
state_indices_tensor_p=state_indices_tensor_p,
|
|
state_indices_tensor_d=torch.empty((0, 1), dtype=torch.int32),
|
|
num_accepted_tokens=None,
|
|
query_start_loc_d=None,
|
|
block_idx_last_scheduled_token=None,
|
|
block_idx_first_scheduled_token_p=None,
|
|
block_idx_last_computed_token=None,
|
|
block_idx_last_scheduled_token_prev_step=None,
|
|
num_computed_tokens_p=None,
|
|
seq_lens=torch.tensor([5]),
|
|
)
|
|
|
|
# Mock KV cache
|
|
# conv_state shape (num_blocks, L_cache - 1, dim)
|
|
conv_state = torch.zeros((1, config.conv_L_cache - 1, dim))
|
|
layer.kv_cache = (conv_state,)
|
|
|
|
hidden_states = torch.randn((num_prefill_tokens, dim))
|
|
output = torch.zeros_like(hidden_states)
|
|
|
|
attn_metadata_dict = {prefix: attn_metadata}
|
|
with set_forward_context(attn_metadata=attn_metadata_dict, vllm_config=vllm_config):
|
|
layer.forward_native(hidden_states, output)
|
|
|
|
# Check if KV cache was updated
|
|
assert not torch.allclose(conv_state, torch.zeros_like(conv_state))
|
|
|
|
|
|
def test_short_conv_forward_native_decode(vllm_config):
|
|
prefix = "test_layer_decode"
|
|
config = SimpleNamespace(conv_L_cache=4, conv_bias=True)
|
|
dim = 16
|
|
|
|
from vllm.config import set_current_vllm_config
|
|
|
|
with set_current_vllm_config(vllm_config):
|
|
layer = ShortConv(config=config, dim=dim, layer_idx=0, prefix=prefix)
|
|
|
|
layer.to("cpu")
|
|
torch.manual_seed(0)
|
|
for p in layer.parameters():
|
|
torch.nn.init.normal_(p)
|
|
dispatch_cpu_unquantized_gemm(layer.in_proj, remove_weight=False)
|
|
dispatch_cpu_unquantized_gemm(layer.out_proj, remove_weight=False)
|
|
|
|
# Mock AttentionMetadata for 2 decode requests
|
|
num_decodes = 2
|
|
state_indices_tensor_d = torch.tensor([0, 1], dtype=torch.int32)
|
|
|
|
attn_metadata = ShortConvAttentionMetadata(
|
|
num_prefills=0,
|
|
num_prefill_tokens=0,
|
|
num_decodes=num_decodes,
|
|
num_decode_tokens=num_decodes,
|
|
num_reqs=num_decodes,
|
|
query_start_loc_p=None,
|
|
has_initial_states_p=None,
|
|
state_indices_tensor_p=torch.empty((0,), dtype=torch.int32),
|
|
state_indices_tensor_d=state_indices_tensor_d,
|
|
num_accepted_tokens=None,
|
|
query_start_loc_d=torch.tensor([0, 1, 2], dtype=torch.int32),
|
|
block_idx_last_scheduled_token=None,
|
|
block_idx_first_scheduled_token_p=None,
|
|
block_idx_last_computed_token=None,
|
|
block_idx_last_scheduled_token_prev_step=None,
|
|
num_computed_tokens_p=None,
|
|
seq_lens=torch.tensor([1, 1]),
|
|
)
|
|
|
|
# Mock KV cache (2 blocks for 2 requests)
|
|
conv_state = torch.randn((2, config.conv_L_cache - 1, dim))
|
|
layer.kv_cache = (conv_state,)
|
|
|
|
hidden_states = torch.randn((num_decodes, dim))
|
|
output = torch.zeros_like(hidden_states)
|
|
|
|
old_conv_state = conv_state.clone()
|
|
|
|
attn_metadata_dict = {prefix: attn_metadata}
|
|
with set_forward_context(attn_metadata=attn_metadata_dict, vllm_config=vllm_config):
|
|
layer.forward_native(hidden_states, output)
|
|
|
|
# Check if KV cache was updated
|
|
assert not torch.allclose(conv_state, old_conv_state)
|
|
|
|
|
|
def test_dispatch_cpu_unquantized_gemm_conv_layer():
|
|
# Convolution layers have >2D weights; dispatch should skip them gracefully.
|
|
# Shape/dtype are AMX-pack safe (bf16, width==4, dim % block_size == 0) so
|
|
# the AMX prepack branch does not raise on AMX-capable CPUs.
|
|
class MockConvLayer(torch.nn.Module):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.weight = torch.nn.Parameter(
|
|
torch.randn(32, 1, 4, dtype=torch.bfloat16)
|
|
)
|
|
self.bias = torch.nn.Parameter(torch.randn(32, dtype=torch.bfloat16))
|
|
|
|
layer = MockConvLayer()
|
|
# The ndim != 2 guard returns early without raising.
|
|
dispatch_cpu_unquantized_gemm(layer, remove_weight=False)
|
|
# No cpu_linear set — conv layers are handled elsewhere.
|
|
assert not hasattr(layer, "cpu_linear")
|