| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | |
| 4 | from collections.abc import Callable |
| 5 | from itertools import product |
| 6 | |
| 7 | import pytest |
| 8 | import torch |
| 9 | |
| 10 | from tests.kernels.allclose_default import get_default_atol, get_default_rtol |
| 11 | from vllm.model_executor.layers.rotary_embedding import get_rope |
| 12 | from vllm.utils.torch_utils import set_random_seed |
| 13 | |
| 14 | IS_NEOX_STYLE = [True, False] |
| 15 | DTYPES = [torch.bfloat16, torch.float] |
| 16 | HEAD_SIZES = [64, 80, 120, 256] |
| 17 | ROTARY_DIMS = [None, 32] # None means rotary dim == head size |
| 18 | NUM_HEADS = [17] # Arbitrary values for testing |
| 19 | BATCH_SIZES = [5] # Arbitrary values for testing |
| 20 | SEQ_LENS = [11, 8192] # Arbitrary values for testing |
| 21 | SEEDS = [0] |
| 22 | CUDA_DEVICES = [ |
| 23 | f"cuda:{i}" for i in range(1 if torch.accelerator.device_count() == 1 else 2) |
| 24 | ] |
| 25 | USE_KEY = [True, False] |
| 26 | |
| 27 | |
| 28 | def _get_flat_tensor_shape( |
| 29 | batch_size: int, seq_len: int, num_heads: int, head_size: int |
| 30 | ) -> tuple[int, ...]: |
| 31 | return (batch_size, seq_len, num_heads * head_size) |
| 32 | |
| 33 | |
| 34 | # For testing sliced tensors |
| 35 | def _get_padded_tensor_shape( |
| 36 | batch_size: int, seq_len: int, num_heads: int, head_size: int |
| 37 | ) -> tuple[int, ...]: |
| 38 | return (batch_size, seq_len, num_heads, head_size + 64) |
| 39 | |
| 40 | |
| 41 | def _get_batch_tensor_shape( |
| 42 | batch_size: int, seq_len: int, num_heads: int, head_size: int |
| 43 | ) -> tuple[int, ...]: |
| 44 | return (batch_size, seq_len, num_heads, head_size) |
| 45 | |
| 46 | |
| 47 | TENSORS_SHAPES_FN = [ |
| 48 | _get_batch_tensor_shape, |
| 49 | _get_flat_tensor_shape, |
| 50 | _get_padded_tensor_shape, |
| 51 | ] |
| 52 | |
| 53 | |
| 54 | @pytest.mark.parametrize("is_neox_style", IS_NEOX_STYLE) |
| 55 | @pytest.mark.parametrize("tensor_shape_fn", TENSORS_SHAPES_FN) |
| 56 | @pytest.mark.parametrize("batch_size", BATCH_SIZES) |
| 57 | @pytest.mark.parametrize("seq_len", SEQ_LENS) |
| 58 | @pytest.mark.parametrize("num_heads", NUM_HEADS) |
| 59 | @pytest.mark.parametrize("head_size", HEAD_SIZES) |
| 60 | @pytest.mark.parametrize("rotary_dim", ROTARY_DIMS) |
| 61 | @pytest.mark.parametrize("dtype", DTYPES) |
| 62 | @pytest.mark.parametrize("seed", SEEDS) |
| 63 | @pytest.mark.parametrize("device", CUDA_DEVICES) |
| 64 | @pytest.mark.parametrize("use_key", USE_KEY) |
| 65 | @torch.inference_mode() |
| 66 | def test_rotary_embedding( |
| 67 | default_vllm_config, |
| 68 | is_neox_style: bool, |
| 69 | tensor_shape_fn: Callable[[int, int, int, int], tuple[int, ...]], |
| 70 | batch_size: int, |
| 71 | seq_len: int, |
| 72 | num_heads: int, |
| 73 | head_size: int, |
| 74 | rotary_dim: int | None, |
| 75 | dtype: torch.dtype, |
| 76 | seed: int, |
| 77 | device: str, |
| 78 | use_key: bool, |
| 79 | max_position: int = 8192, |
| 80 | rope_theta: float = 10000, |
| 81 | ) -> None: |
| 82 | if rotary_dim is None: |
| 83 | rotary_dim = head_size |
| 84 | |
| 85 | set_random_seed(seed) |
| 86 | torch.set_default_device(device) |
| 87 | if rotary_dim is None: |
| 88 | rotary_dim = head_size |
| 89 | rope_parameters = { |
| 90 | "rope_type": "default", |
| 91 | "rope_theta": rope_theta, |
| 92 | "partial_rotary_factor": rotary_dim / head_size, |
| 93 | } |
| 94 | rope = get_rope(head_size, max_position, is_neox_style, rope_parameters) |
| 95 | rope = rope.to(dtype=dtype, device=torch.get_default_device()) |
| 96 | |
| 97 | positions = torch.randint(0, max_position, (batch_size, seq_len)) |
| 98 | query_shape = tensor_shape_fn(batch_size, seq_len, num_heads, head_size) |
| 99 | # slice tensor if required, noop otherwise |
| 100 | query = torch.randn(query_shape, dtype=dtype)[..., :head_size] |
| 101 | key = torch.randn_like(query)[..., :head_size] if use_key else None |
| 102 | |
| 103 | # NOTE(woosuk): The reference implementation should be executed first |
| 104 | # because the custom kernel is in-place. |
| 105 | ref_query, ref_key = rope.forward_native(positions, query, key) |
| 106 | out_query, out_key = rope.forward(positions, query, key) |
| 107 | # Compare the results. |
| 108 | torch.testing.assert_close( |
| 109 | out_query, |
| 110 | ref_query, |
| 111 | atol=get_default_atol(out_query), |
| 112 | rtol=get_default_rtol(out_query), |
| 113 | ) |
| 114 | if use_key: |
| 115 | torch.testing.assert_close( |
| 116 | out_key, |
| 117 | ref_key, |
| 118 | atol=get_default_atol(out_key), |
| 119 | rtol=get_default_rtol(out_key), |
| 120 | ) |
| 121 | else: |
| 122 | assert ref_key is None and out_key is None, "expected returned key to be None" |
| 123 | |
| 124 | |
| 125 | @torch.inference_mode() |
| 126 | def test_rope_module_cache(default_vllm_config): |
| 127 | MAX_POSITIONS = [123, 1234] |
| 128 | ROPE_THETAS = [10000, 1000000] |
| 129 | ROPE_PARAMETERS = ( |
| 130 | {"rope_type": "default"}, |
| 131 | {"rope_type": "linear", "factor": (1,)}, |
| 132 | {"rope_type": "dynamic", "factor": 1}, |
| 133 | ) |
| 134 | settings = ( |
| 135 | HEAD_SIZES, |
| 136 | ROTARY_DIMS, |
| 137 | MAX_POSITIONS, |
| 138 | ROPE_THETAS, |
| 139 | IS_NEOX_STYLE, |
| 140 | ROPE_PARAMETERS, |
| 141 | DTYPES, |
| 142 | ) |
| 143 | rope_setting_id_map: dict[str, int] = {} |
| 144 | for setting in product(*settings): |
| 145 | ( |
| 146 | head_size, |
| 147 | rotary_dim, |
| 148 | max_position, |
| 149 | rope_theta, |
| 150 | is_neox_style, |
| 151 | rope_parameters, |
| 152 | dtype, |
| 153 | ) = setting |
| 154 | if rotary_dim is None: |
| 155 | rotary_dim = head_size |
| 156 | rope_parameters["rope_theta"] = rope_theta |
| 157 | rope_parameters["partial_rotary_factor"] = rotary_dim / head_size |
| 158 | rope = get_rope( |
| 159 | head_size, |
| 160 | max_position, |
| 161 | is_neox_style, |
| 162 | rope_parameters, |
| 163 | dtype, |
| 164 | ) |
| 165 | # different settings cannot share the same rope module |
| 166 | assert id(rope) not in rope_setting_id_map.values() |
| 167 | assert all(x.dtype == dtype for x in rope.buffers()) |
| 168 | assert all(x.dtype == dtype for x in rope.parameters()) |
| 169 | rope_setting_id_map[str(setting)] = id(rope) |
| 170 | |
| 171 | for setting in product(*settings): |
| 172 | ( |
| 173 | head_size, |
| 174 | rotary_dim, |
| 175 | max_position, |
| 176 | rope_theta, |
| 177 | is_neox_style, |
| 178 | rope_parameters, |
| 179 | dtype, |
| 180 | ) = setting |
| 181 | if rotary_dim is None: |
| 182 | rotary_dim = head_size |
| 183 | rope_parameters["rope_theta"] = rope_theta |
| 184 | rope_parameters["partial_rotary_factor"] = rotary_dim / head_size |
| 185 | rope = get_rope( |
| 186 | head_size, |
| 187 | max_position, |
| 188 | is_neox_style, |
| 189 | rope_parameters, |
| 190 | dtype, |
| 191 | ) |
| 192 | # check if cache take effect |
| 193 | assert id(rope) == rope_setting_id_map[str(setting)] |