| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | |
| 4 | from types import SimpleNamespace |
| 5 | from unittest.mock import Mock, patch |
| 6 | |
| 7 | import numpy as np |
| 8 | import pytest |
| 9 | import torch |
| 10 | |
| 11 | from vllm.config.mamba import MambaBackendEnum |
| 12 | from vllm.model_executor.layers.mamba.mamba_mixer2 import MambaMixer2 |
| 13 | from vllm.model_executor.warmup import replayssm_warmup as warmup |
| 14 | from vllm.platforms import current_platform |
| 15 | from vllm.utils.flashinfer import has_flashinfer |
| 16 | |
| 17 | pytestmark = pytest.mark.skipif( |
| 18 | not current_platform.is_cuda() or not has_flashinfer(), |
| 19 | reason="FlashInfer ReplaySSM warmup tests require CUDA and FlashInfer", |
| 20 | ) |
| 21 | |
| 22 | PREFILL_KWARGS = { |
| 23 | "num_tokens": 128, |
| 24 | "skip_eplb": True, |
| 25 | "is_profile": True, |
| 26 | "randomize_inputs": True, |
| 27 | } |
| 28 | |
| 29 | |
| 30 | def _autotune_runner( |
| 31 | *, |
| 32 | use_v2_model_runner: bool = False, |
| 33 | query_len: int = 6, |
| 34 | max_num_seqs: int = 32, |
| 35 | num_blocks: int = 17, |
| 36 | max_num_tokens: int = 100, |
| 37 | use_replayssm: bool = True, |
| 38 | backend: MambaBackendEnum = MambaBackendEnum.FLASHINFER, |
| 39 | ) -> SimpleNamespace: |
| 40 | return SimpleNamespace( |
| 41 | vllm_config=SimpleNamespace( |
| 42 | cache_config=SimpleNamespace(use_replayssm=use_replayssm), |
| 43 | mamba_config=SimpleNamespace(backend=backend), |
| 44 | use_v2_model_runner=use_v2_model_runner, |
| 45 | ), |
| 46 | uniform_decode_query_len=query_len, |
| 47 | decode_query_len=query_len, |
| 48 | max_num_tokens=max_num_tokens, |
| 49 | scheduler_config=SimpleNamespace(max_num_seqs=max_num_seqs), |
| 50 | kv_cache_config=SimpleNamespace(num_blocks=num_blocks), |
| 51 | ) |
| 52 | |
| 53 | |
| 54 | @pytest.mark.parametrize( |
| 55 | ("runner_kwargs", "expected_num_reqs"), |
| 56 | [ |
| 57 | # max_num_seqs (32) vs max_num_tokens // query_len (16) vs blocks-1 (16). |
| 58 | (dict(query_len=6, use_v2_model_runner=False), 16), |
| 59 | (dict(query_len=6, use_v2_model_runner=True), 16), |
| 60 | # num_blocks - 1 is the binding constraint. |
| 61 | (dict(query_len=1, max_num_tokens=128, max_num_seqs=64, num_blocks=5), 4), |
| 62 | ], |
| 63 | ids=["v1", "v2", "clamped_to_state_capacity"], |
| 64 | ) |
| 65 | def test_replayssm_autotune_decode_kwargs(runner_kwargs, expected_num_reqs): |
| 66 | query_len = runner_kwargs["query_len"] |
| 67 | with patch.object( |
| 68 | warmup, "flashinfer_replayssm_autotune_supported", return_value=True |
| 69 | ): |
| 70 | result = warmup._replayssm_autotune_kwargs( |
| 71 | _autotune_runner(**runner_kwargs), PREFILL_KWARGS |
| 72 | ) |
| 73 | |
| 74 | assert result is not None |
| 75 | max_num_reqs, decode_kwargs = result |
| 76 | assert max_num_reqs == expected_num_reqs |
| 77 | expected_kwargs = { |
| 78 | **PREFILL_KWARGS, |
| 79 | "num_tokens": expected_num_reqs * query_len, |
| 80 | "uniform_decode": True, |
| 81 | } |
| 82 | if runner_kwargs.get("use_v2_model_runner"): |
| 83 | expected_kwargs["valid_dummy_state_slots"] = True |
| 84 | else: |
| 85 | expected_kwargs.update( |
| 86 | allow_microbatching=False, |
| 87 | force_attention=True, |
| 88 | profile_seq_lens=query_len + 1, |
| 89 | ) |
| 90 | assert decode_kwargs == expected_kwargs |
| 91 | |
| 92 | |
| 93 | @pytest.mark.parametrize( |
| 94 | ("runner_kwargs", "flashinfer_supported"), |
| 95 | [ |
| 96 | (dict(use_replayssm=False), True), |
| 97 | (dict(backend=MambaBackendEnum.TRITON), True), |
| 98 | ({}, False), |
| 99 | (dict(num_blocks=1), True), |
| 100 | ], |
| 101 | ids=[ |
| 102 | "replayssm_disabled", |
| 103 | "non_flashinfer_backend", |
| 104 | "kernel_unavailable", |
| 105 | "zero_non_padding_slots", |
| 106 | ], |
| 107 | ) |
| 108 | def test_replayssm_autotune_kwargs_skipped(runner_kwargs, flashinfer_supported): |
| 109 | with patch.object( |
| 110 | warmup, |
| 111 | "flashinfer_replayssm_autotune_supported", |
| 112 | return_value=flashinfer_supported, |
| 113 | ): |
| 114 | result = warmup._replayssm_autotune_kwargs( |
| 115 | _autotune_runner(**runner_kwargs), PREFILL_KWARGS |
| 116 | ) |
| 117 | assert result is None |
| 118 | |
| 119 | |
| 120 | def test_replayssm_autotune_slots_restore_state_and_trackers(): |
| 121 | mixer = MambaMixer2.__new__(MambaMixer2) |
| 122 | torch.nn.Module.__init__(mixer) |
| 123 | mixer.use_replayssm = True |
| 124 | mixer.replayssm_buffer_len = 16 |
| 125 | mixer.kv_cache = ( |
| 126 | torch.full((4, 2), 3.0), |
| 127 | torch.full((4, 2), 3.0), |
| 128 | *(torch.full((4, 2, 17), 3.0) for _ in range(3)), |
| 129 | ) |
| 130 | mixer._replayssm_ring_start = torch.full((4,), 3, dtype=torch.int32) |
| 131 | mixer._replayssm_prev_num_accepted = torch.full((4,), 3, dtype=torch.int32) |
| 132 | mixer._replayssm_prev_query_len = torch.full((4,), 3, dtype=torch.int32) |
| 133 | tracked = ( |
| 134 | *mixer.kv_cache, |
| 135 | mixer._replayssm_ring_start, |
| 136 | mixer._replayssm_prev_num_accepted, |
| 137 | mixer._replayssm_prev_query_len, |
| 138 | ) |
| 139 | |
| 140 | block_ids = np.arange(10, 14, dtype=np.int32).reshape(4, 1) |
| 141 | original_block_ids = block_ids.copy() |
| 142 | block_table = SimpleNamespace(block_table=SimpleNamespace(np=block_ids)) |
| 143 | multi_group_block_table = SimpleNamespace( |
| 144 | block_tables=[block_table], commit_block_table=Mock() |
| 145 | ) |
| 146 | runner = SimpleNamespace( |
| 147 | vllm_config=SimpleNamespace(use_v2_model_runner=False), |
| 148 | input_batch=SimpleNamespace(block_table=multi_group_block_table), |
| 149 | get_model=lambda: SimpleNamespace(modules=lambda: (mixer,)), |
| 150 | ) |
| 151 | |
| 152 | with warmup._temporary_replayssm_autotune_state(runner, 2): |
| 153 | assert block_ids[:2, 0].tolist() == [1, 2] |
| 154 | for tensor in tracked: |
| 155 | tensor[1:3].fill_(9) |
| 156 | |
| 157 | assert np.array_equal(block_ids, original_block_ids) |
| 158 | assert multi_group_block_table.commit_block_table.call_args_list == [ |
| 159 | ((2,), {}), |
| 160 | ((2,), {}), |
| 161 | ] |
| 162 | for tensor in tracked: |
| 163 | assert torch.count_nonzero(tensor[1:3]) == 0 |
| 164 | assert torch.all(tensor[0] == 3) |
| 165 | assert torch.all(tensor[3] == 3) |