| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | """Tests for the fused_qk_norm_rope helion kernel. |
| 4 | |
| 5 | Run `pytest tests/kernels/helion/test_fused_qk_norm_rope.py`. |
| 6 | """ |
| 7 | |
| 8 | from typing import Any |
| 9 | |
| 10 | import pytest |
| 11 | import torch |
| 12 | from torch._subclasses.fake_tensor import FakeTensorMode |
| 13 | |
| 14 | from tests.kernels.helion.utils import skip_if_platform_unsupported |
| 15 | from vllm.benchmarks.lib.utils import default_vllm_config |
| 16 | from vllm.kernels.helion.case_key import CaseKey |
| 17 | from vllm.kernels.helion.config_manager import ConfigManager |
| 18 | from vllm.kernels.helion.ops.fused_qk_norm_rope import ( |
| 19 | _pick_cache, |
| 20 | baseline, |
| 21 | fused_qk_norm_rope, |
| 22 | pick_config, |
| 23 | ) |
| 24 | from vllm.model_executor.layers.rotary_embedding import RotaryEmbedding |
| 25 | from vllm.utils.import_utils import has_helion |
| 26 | |
| 27 | if not has_helion(): |
| 28 | pytest.skip( |
| 29 | "Helion is not installed. Install with: pip install vllm[helion]", |
| 30 | allow_module_level=True, |
| 31 | ) |
| 32 | |
| 33 | |
| 34 | @default_vllm_config() |
| 35 | def _generate_fake_input( |
| 36 | num_tokens: int, num_q_heads: int, num_kv_heads: int |
| 37 | ) -> tuple[Any, ...]: |
| 38 | with FakeTensorMode(): |
| 39 | head_dim = 128 |
| 40 | eps = 1e-6 |
| 41 | is_neox = True |
| 42 | rotary_ratio = 1.0 |
| 43 | device = "cuda" |
| 44 | dtype = torch.bfloat16 |
| 45 | total_dim = (num_q_heads + 2 * num_kv_heads) * head_dim |
| 46 | qkv = torch.randn(num_tokens, total_dim, dtype=dtype, device=device) |
| 47 | positions = torch.arange(num_tokens, dtype=torch.long, device=device) |
| 48 | q_weight = torch.normal( |
| 49 | mean=1.0, |
| 50 | std=1.0, |
| 51 | size=(head_dim,), |
| 52 | dtype=qkv.dtype, |
| 53 | device=device, |
| 54 | ) |
| 55 | k_weight = torch.normal( |
| 56 | mean=1.0, |
| 57 | std=1.0, |
| 58 | size=(head_dim,), |
| 59 | dtype=qkv.dtype, |
| 60 | device=device, |
| 61 | ) |
| 62 | rotary_dim = int(head_dim * rotary_ratio) |
| 63 | rope = RotaryEmbedding( |
| 64 | head_size=head_dim, |
| 65 | rotary_dim=rotary_dim, |
| 66 | max_position_embeddings=4096, |
| 67 | base=10000.0, |
| 68 | is_neox_style=is_neox, |
| 69 | dtype=dtype, |
| 70 | ).to(device) |
| 71 | args = ( |
| 72 | qkv, |
| 73 | num_q_heads, |
| 74 | num_kv_heads, |
| 75 | num_kv_heads, |
| 76 | head_dim, |
| 77 | eps, |
| 78 | q_weight, |
| 79 | k_weight, |
| 80 | rope.cos_sin_cache, |
| 81 | is_neox, |
| 82 | positions.view(-1), |
| 83 | ) |
| 84 | return args |
| 85 | |
| 86 | |
| 87 | @pytest.fixture(autouse=True) |
| 88 | def reset_config_manager_singleton(): |
| 89 | ConfigManager.reset_instance() |
| 90 | ConfigManager() |
| 91 | yield |
| 92 | ConfigManager.reset_instance() |
| 93 | |
| 94 | |
| 95 | class TestFusedQkNormRopeConfigPicker: |
| 96 | def setup_method(self): |
| 97 | _pick_cache.clear() |
| 98 | |
| 99 | def test_config_picker_exact_match(self): |
| 100 | config_keys = [ |
| 101 | CaseKey({"q_heads": 2048, "kv_heads": 64, "num_tokens": 16}), |
| 102 | CaseKey({"q_heads": 4096, "kv_heads": 128, "num_tokens": 16}), |
| 103 | ] |
| 104 | |
| 105 | args = _generate_fake_input(16, 4096, 128) |
| 106 | selected_key = pick_config(args, config_keys) |
| 107 | assert selected_key == CaseKey( |
| 108 | {"q_heads": 4096, "kv_heads": 128, "num_tokens": 16} |
| 109 | ) |
| 110 | |
| 111 | def test_config_picker_closest_match(self): |
| 112 | config_keys = [ |
| 113 | CaseKey({"q_heads": 2048, "kv_heads": 64, "num_tokens": 16}), |
| 114 | CaseKey({"q_heads": 2048, "kv_heads": 64, "num_tokens": 32}), |
| 115 | CaseKey({"q_heads": 2048, "kv_heads": 128, "num_tokens": 16}), |
| 116 | CaseKey({"q_heads": 2048, "kv_heads": 128, "num_tokens": 32}), |
| 117 | CaseKey({"q_heads": 4096, "kv_heads": 64, "num_tokens": 16}), |
| 118 | CaseKey({"q_heads": 4096, "kv_heads": 64, "num_tokens": 32}), |
| 119 | CaseKey({"q_heads": 4096, "kv_heads": 128, "num_tokens": 16}), |
| 120 | CaseKey({"q_heads": 4096, "kv_heads": 128, "num_tokens": 32}), |
| 121 | ] |
| 122 | |
| 123 | args = _generate_fake_input(20, 3000, 70) |
| 124 | selected_key = pick_config(args, config_keys) |
| 125 | assert selected_key == CaseKey( |
| 126 | {"q_heads": 2048, "kv_heads": 64, "num_tokens": 32} |
| 127 | ) |
| 128 | |
| 129 | def test_config_picker_no_configs(self): |
| 130 | config_keys: list[CaseKey] = [] |
| 131 | |
| 132 | args = _generate_fake_input(16, 4096, 128) |
| 133 | selected_key = pick_config(args, config_keys) |
| 134 | assert selected_key is None |
| 135 | |
| 136 | def test_config_picker_fallback_to_largest(self): |
| 137 | config_keys = [ |
| 138 | CaseKey({"q_heads": 2048, "kv_heads": 64, "num_tokens": 16}), |
| 139 | CaseKey({"q_heads": 2048, "kv_heads": 64, "num_tokens": 32}), |
| 140 | CaseKey({"q_heads": 2048, "kv_heads": 128, "num_tokens": 16}), |
| 141 | CaseKey({"q_heads": 2048, "kv_heads": 128, "num_tokens": 32}), |
| 142 | CaseKey({"q_heads": 4096, "kv_heads": 64, "num_tokens": 16}), |
| 143 | CaseKey({"q_heads": 4096, "kv_heads": 64, "num_tokens": 32}), |
| 144 | CaseKey({"q_heads": 4096, "kv_heads": 128, "num_tokens": 16}), |
| 145 | CaseKey({"q_heads": 4096, "kv_heads": 128, "num_tokens": 32}), |
| 146 | ] |
| 147 | |
| 148 | args = _generate_fake_input(64, 8192, 256) |
| 149 | selected_key = pick_config(args, config_keys) |
| 150 | assert selected_key == CaseKey( |
| 151 | {"q_heads": 4096, "kv_heads": 128, "num_tokens": 32} |
| 152 | ) |
| 153 | |
| 154 | |
| 155 | class TestFusedQkNormRopeCorrectness: |
| 156 | @pytest.mark.parametrize( |
| 157 | "num_heads, num_kv_heads, head_dim", [(16, 4, 128), (64, 8, 128)] |
| 158 | ) |
| 159 | @pytest.mark.parametrize("num_tokens", [1, 7, 1024, 1025]) |
| 160 | @pytest.mark.parametrize("is_neox", [False, True]) |
| 161 | @pytest.mark.parametrize("rotary_ratio", [1.0, 0.5, 0.25]) |
| 162 | @pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16]) |
| 163 | @default_vllm_config() |
| 164 | def test_fused_qk_norm_rope( |
| 165 | self, |
| 166 | num_heads: int, |
| 167 | num_kv_heads: int, |
| 168 | head_dim: int, |
| 169 | num_tokens: int, |
| 170 | is_neox: bool, |
| 171 | rotary_ratio: float, |
| 172 | dtype: torch.dtype, |
| 173 | ): |
| 174 | skip_if_platform_unsupported("fused_qk_norm_rope") |
| 175 | |
| 176 | torch.manual_seed(42) |
| 177 | eps = 1e-6 |
| 178 | device = "cuda" |
| 179 | total_dim = (num_heads + 2 * num_kv_heads) * head_dim |
| 180 | ref_qkv = torch.empty( |
| 181 | num_tokens, total_dim, dtype=dtype, device=device |
| 182 | ).uniform_(-0.1, 0.1) |
| 183 | ops_qkv = ref_qkv.clone() |
| 184 | positions = torch.arange(num_tokens, dtype=torch.long, device=device) |
| 185 | q_weight = torch.empty(head_dim, dtype=dtype, device=device).uniform_(0.8, 1.2) |
| 186 | k_weight = torch.empty(head_dim, dtype=dtype, device=device).uniform_(0.8, 1.2) |
| 187 | rotary_dim = int(head_dim * rotary_ratio) |
| 188 | rope = RotaryEmbedding( |
| 189 | head_size=head_dim, |
| 190 | rotary_dim=rotary_dim, |
| 191 | max_position_embeddings=40960, |
| 192 | base=10000.0, |
| 193 | is_neox_style=is_neox, |
| 194 | dtype=dtype, |
| 195 | ).to(device) |
| 196 | |
| 197 | baseline( |
| 198 | ref_qkv, |
| 199 | num_heads, |
| 200 | num_kv_heads, |
| 201 | num_kv_heads, |
| 202 | head_dim, |
| 203 | eps, |
| 204 | q_weight, |
| 205 | k_weight, |
| 206 | rope.cos_sin_cache, |
| 207 | is_neox, |
| 208 | positions.view(-1), |
| 209 | ) |
| 210 | |
| 211 | fused_qk_norm_rope( |
| 212 | ops_qkv, |
| 213 | num_heads, |
| 214 | num_kv_heads, |
| 215 | num_kv_heads, |
| 216 | head_dim, |
| 217 | eps, |
| 218 | q_weight, |
| 219 | k_weight, |
| 220 | rope.cos_sin_cache, |
| 221 | is_neox, |
| 222 | positions.view(-1), |
| 223 | ) |
| 224 | |
| 225 | if dtype == torch.bfloat16: |
| 226 | atol = 5e-2 |
| 227 | rtol = 5e-2 |
| 228 | else: |
| 229 | atol = 1e-2 |
| 230 | rtol = 1e-2 |
| 231 | |
| 232 | torch.testing.assert_close( |
| 233 | ref_qkv, |
| 234 | ops_qkv, |
| 235 | atol=atol, |
| 236 | rtol=rtol, |
| 237 | ) |
| 238 | |
| 239 | |
| 240 | class TestFusedQkNormRopeIntegration: |
| 241 | def test_kernel_registration_integration(self): |
| 242 | from vllm.kernels.helion.register import get_registered_kernels |
| 243 | |
| 244 | registered_kernels = get_registered_kernels() |
| 245 | assert "fused_qk_norm_rope" in registered_kernels |
| 246 | |
| 247 | kernel_wrapper = registered_kernels["fused_qk_norm_rope"] |
| 248 | assert kernel_wrapper.op_name == "fused_qk_norm_rope" |
| 249 | assert kernel_wrapper._config_picker is not None |
| 250 | assert kernel_wrapper._mutates_args == ["qkv"] |