| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | # This is a test for the AITER ops. |
| 4 | # It tests if the AITER ops are |
| 5 | # 1. correctly registered as custom ops |
| 6 | # 2. correctly defined the relationship between |
| 7 | # implementation and fake function |
| 8 | # 3. can be used with torch.compile |
| 9 | # This file will be skipped if AITER is not installed |
| 10 | # and the platform is not ROCm. |
| 11 | |
| 12 | import importlib.util |
| 13 | |
| 14 | import pytest |
| 15 | import torch |
| 16 | |
| 17 | from vllm.platforms import current_platform |
| 18 | |
| 19 | if not current_platform.is_rocm(): |
| 20 | pytest.skip("This test can only run on ROCm.", allow_module_level=True) |
| 21 | |
| 22 | # this import statement is needed to ensure the ops are registered |
| 23 | import vllm.model_executor.layers.fused_moe.experts.rocm_aiter_moe # noqa: F401 |
| 24 | |
| 25 | # need to import once to ensure the ops are registered |
| 26 | # Check if aiter package is installed |
| 27 | aiter_available = importlib.util.find_spec("aiter") is not None |
| 28 | |
| 29 | if not aiter_available: |
| 30 | pytest.skip("These tests require AITER to run.", allow_module_level=True) |
| 31 | |
| 32 | |
| 33 | def test_rocm_aiter_biased_grouped_topk_custom_op_registration(): |
| 34 | """Test that the custom op is correctly registered.""" |
| 35 | # Check if the op exists in torch.ops.vllm |
| 36 | assert hasattr(torch.ops.vllm, "rocm_aiter_biased_grouped_topk") |
| 37 | |
| 38 | # Check if the op is callable |
| 39 | assert callable(torch.ops.vllm.rocm_aiter_biased_grouped_topk) |
| 40 | |
| 41 | |
| 42 | def test_rocm_aiter_grouped_topk_custom_op_registration(): |
| 43 | """Test that the custom op is correctly registered.""" |
| 44 | # Check if the op exists in torch.ops.vllm |
| 45 | assert hasattr(torch.ops.vllm, "rocm_aiter_grouped_topk") |
| 46 | |
| 47 | # Check if the op is callable |
| 48 | assert callable(torch.ops.vllm.rocm_aiter_grouped_topk) |
| 49 | |
| 50 | |
| 51 | def test_rocm_aiter_biased_grouped_topk_torch_compile_compatibility(): |
| 52 | """Test that the op can be used with torch.compile.""" |
| 53 | # Create test tensors |
| 54 | token = 64 |
| 55 | expert = 256 |
| 56 | num_expert_group = 8 |
| 57 | topk = 8 |
| 58 | topk_group = 4 |
| 59 | renormalize = True |
| 60 | scale_factor = 1.0 |
| 61 | |
| 62 | gating_output = torch.randn((token, expert), dtype=torch.bfloat16, device="cuda") |
| 63 | e_score_correction_bias = torch.randn( |
| 64 | (expert,), dtype=torch.bfloat16, device="cuda" |
| 65 | ) |
| 66 | |
| 67 | device = gating_output.device |
| 68 | topk_ids = torch.empty((token, topk), dtype=torch.int32, device=device) |
| 69 | topk_weights = torch.empty((token, topk), dtype=torch.float32, device=device) |
| 70 | |
| 71 | # Define a function that uses the op |
| 72 | def biased_grouped_topk_fn( |
| 73 | gating_output, e_score_correction_bias, topk_weights, topk_ids |
| 74 | ): |
| 75 | return torch.ops.vllm.rocm_aiter_biased_grouped_topk( |
| 76 | gating_output, |
| 77 | e_score_correction_bias, |
| 78 | topk_weights, |
| 79 | topk_ids, |
| 80 | num_expert_group, |
| 81 | topk_group, |
| 82 | renormalize, |
| 83 | scale_factor, |
| 84 | ) |
| 85 | |
| 86 | # Verify the op's fake implementation |
| 87 | torch.library.opcheck( |
| 88 | torch.ops.vllm.rocm_aiter_biased_grouped_topk, |
| 89 | (gating_output, e_score_correction_bias, topk_weights, topk_ids), |
| 90 | kwargs={ |
| 91 | "num_expert_group": num_expert_group, |
| 92 | "topk_group": topk_group, |
| 93 | "need_renorm": renormalize, |
| 94 | "routed_scaling_factor": scale_factor, |
| 95 | }, |
| 96 | test_utils=("test_faketensor"), |
| 97 | ) |
| 98 | |
| 99 | # Compile the function with appropriate settings |
| 100 | compiled_fn = torch.compile( |
| 101 | biased_grouped_topk_fn, |
| 102 | fullgraph=True, |
| 103 | backend="inductor", |
| 104 | mode="reduce-overhead", |
| 105 | dynamic=False, |
| 106 | ) |
| 107 | |
| 108 | topk_weights_original = torch.empty( |
| 109 | (token, topk), dtype=torch.float32, device=device |
| 110 | ) |
| 111 | topk_ids_original = torch.empty((token, topk), dtype=torch.int32, device=device) |
| 112 | |
| 113 | topk_weights_compiled = torch.empty( |
| 114 | (token, topk), dtype=torch.float32, device=device |
| 115 | ) |
| 116 | topk_ids_compiled = torch.empty((token, topk), dtype=torch.int32, device=device) |
| 117 | |
| 118 | # Run both compiled (V1 graph mode) and uncompiled versions (V1 eager mode) |
| 119 | biased_grouped_topk_fn( |
| 120 | gating_output, e_score_correction_bias, topk_weights_original, topk_ids_original |
| 121 | ) |
| 122 | compiled_fn( |
| 123 | gating_output, e_score_correction_bias, topk_weights_compiled, topk_ids_compiled |
| 124 | ) |
| 125 | |
| 126 | # Sort the results for comparison since the order might not be deterministic |
| 127 | topk_ids_original, indices_original = torch.sort(topk_ids_original) |
| 128 | topk_weights_original = torch.gather(topk_weights_original, 1, indices_original) |
| 129 | |
| 130 | topk_ids_compiled, indices_compiled = torch.sort(topk_ids_compiled) |
| 131 | topk_weights_compiled = torch.gather(topk_weights_compiled, 1, indices_compiled) |
| 132 | |
| 133 | # Verify results match |
| 134 | assert torch.allclose( |
| 135 | topk_weights_original, topk_weights_compiled, rtol=1e-2, atol=1e-2 |
| 136 | ) |
| 137 | assert torch.allclose(topk_ids_original, topk_ids_compiled) |
| 138 | |
| 139 | |
| 140 | def test_rocm_aiter_grouped_topk_torch_compile_compatibility(): |
| 141 | """Test that the op can be used with torch.compile.""" |
| 142 | # Create test tensors |
| 143 | token = 64 |
| 144 | expert = 256 |
| 145 | num_expert_group = 8 |
| 146 | topk = 8 |
| 147 | topk_group = 4 |
| 148 | renormalize = True |
| 149 | scoring_func = "softmax" |
| 150 | scale_factor = 1.0 |
| 151 | |
| 152 | gating_output = torch.randn((token, expert), dtype=torch.bfloat16, device="cuda") |
| 153 | |
| 154 | device = gating_output.device |
| 155 | topk_ids = torch.empty((token, topk), dtype=torch.int32, device=device) |
| 156 | topk_weights = torch.empty((token, topk), dtype=torch.float32, device=device) |
| 157 | |
| 158 | # Define a function that uses the op |
| 159 | def grouped_topk_fn(gating_output, topk_weights, topk_ids, scoring_func): |
| 160 | return torch.ops.vllm.rocm_aiter_grouped_topk( |
| 161 | gating_output, |
| 162 | topk_weights, |
| 163 | topk_ids, |
| 164 | num_expert_group, |
| 165 | topk_group, |
| 166 | renormalize, |
| 167 | scoring_func, |
| 168 | scale_factor, |
| 169 | ) |
| 170 | |
| 171 | # Verify the op's fake implementation |
| 172 | torch.library.opcheck( |
| 173 | torch.ops.vllm.rocm_aiter_grouped_topk, |
| 174 | (gating_output, topk_weights, topk_ids), |
| 175 | kwargs={ |
| 176 | "num_expert_group": num_expert_group, |
| 177 | "topk_group": topk_group, |
| 178 | "need_renorm": renormalize, |
| 179 | "scoring_func": scoring_func, |
| 180 | "routed_scaling_factor": scale_factor, |
| 181 | }, |
| 182 | test_utils=("test_faketensor"), |
| 183 | ) |
| 184 | |
| 185 | # Compile the function with appropriate settings |
| 186 | compiled_fn = torch.compile( |
| 187 | grouped_topk_fn, |
| 188 | fullgraph=True, |
| 189 | backend="inductor", |
| 190 | mode="reduce-overhead", |
| 191 | dynamic=False, |
| 192 | ) |
| 193 | |
| 194 | topk_weights_original = torch.empty( |
| 195 | (token, topk), dtype=torch.float32, device=device |
| 196 | ) |
| 197 | topk_ids_original = torch.empty((token, topk), dtype=torch.int32, device=device) |
| 198 | |
| 199 | topk_weights_compiled = torch.empty( |
| 200 | (token, topk), dtype=torch.float32, device=device |
| 201 | ) |
| 202 | topk_ids_compiled = torch.empty((token, topk), dtype=torch.int32, device=device) |
| 203 | |
| 204 | # Run both compiled (V1 graph mode) and uncompiled versions (V1 eager mode) |
| 205 | grouped_topk_fn( |
| 206 | gating_output, topk_weights_original, topk_ids_original, scoring_func |
| 207 | ) |
| 208 | compiled_fn(gating_output, topk_weights_compiled, topk_ids_compiled, scoring_func) |
| 209 | |
| 210 | # Sort the results for comparison since the order might not be deterministic |
| 211 | topk_ids_original, indices_original = torch.sort(topk_ids_original) |
| 212 | topk_weights_original = torch.gather(topk_weights_original, 1, indices_original) |
| 213 | |
| 214 | topk_ids_compiled, indices_compiled = torch.sort(topk_ids_compiled) |
| 215 | topk_weights_compiled = torch.gather(topk_weights_compiled, 1, indices_compiled) |
| 216 | |
| 217 | # Verify results match |
| 218 | assert torch.allclose( |
| 219 | topk_weights_original, topk_weights_compiled, rtol=1e-2, atol=1e-2 |
| 220 | ) |
| 221 | assert torch.allclose(topk_ids_original, topk_ids_compiled) |