| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | """Tests for the FlashInfer TRTLLM BF16 MoE backend |
| 4 | (`TrtLlmBf16ExpertsModular`). |
| 5 | |
| 6 | This mirrors the TRTLLM NvFP4 modular test shape: construct the modular |
| 7 | expert wrapper directly, pass production-format BlockMajorK weights, and |
| 8 | compare against a torch MoE reference using the original BF16 weights. |
| 9 | """ |
| 10 | |
| 11 | import pytest |
| 12 | import torch |
| 13 | |
| 14 | import vllm.model_executor.layers.fused_moe.modular_kernel as mk |
| 15 | from tests.kernels.moe.utils import make_dummy_moe_config |
| 16 | from tests.kernels.utils import torch_moe |
| 17 | from vllm.config import ParallelConfig, VllmConfig, set_current_vllm_config |
| 18 | from vllm.model_executor.layers.fused_moe.activation import MoEActivation |
| 19 | from vllm.model_executor.layers.fused_moe.all2all_utils import ( |
| 20 | maybe_make_prepare_finalize, |
| 21 | ) |
| 22 | from vllm.model_executor.layers.fused_moe.config import ( |
| 23 | FUSED_MOE_UNQUANTIZED_CONFIG, |
| 24 | FusedMoEConfig, |
| 25 | FusedMoEParallelConfig, |
| 26 | RoutingMethodType, |
| 27 | ) |
| 28 | from vllm.model_executor.layers.fused_moe.experts.trtllm_bf16_moe import ( |
| 29 | TrtLlmBf16ExpertsModular, |
| 30 | ) |
| 31 | from vllm.model_executor.layers.fused_moe.experts.trtllm_lora_moe import ( |
| 32 | TrtLlmBf16LoRAExperts, |
| 33 | ) |
| 34 | from vllm.model_executor.layers.fused_moe.oracle.unquantized import ( |
| 35 | UnquantizedMoeBackend, |
| 36 | convert_to_unquantized_kernel_format, |
| 37 | ) |
| 38 | from vllm.platforms import current_platform |
| 39 | from vllm.utils.flashinfer import has_flashinfer_trtllm_fused_moe |
| 40 | from vllm.utils.math_utils import next_power_of_2 |
| 41 | from vllm.utils.torch_utils import set_random_seed |
| 42 | |
| 43 | if pytest and ( |
| 44 | not has_flashinfer_trtllm_fused_moe() |
| 45 | or not current_platform.is_device_capability_family(100) |
| 46 | ): |
| 47 | pytest.skip( |
| 48 | "Requires flashinfer TRTLLM fused MoE BF16 backend (SM100)", |
| 49 | allow_module_level=True, |
| 50 | ) |
| 51 | |
| 52 | # (m, n, k) = (tokens, intermediate_size_per_partition, hidden_dim). |
| 53 | MNK_FACTORS = [ |
| 54 | (2, 160, 2560), |
| 55 | (2, 1024, 1024), |
| 56 | (64, 2048, 1536), |
| 57 | (64, 1024, 4096), |
| 58 | ] |
| 59 | |
| 60 | |
| 61 | @pytest.mark.parametrize("m,n,k", MNK_FACTORS) |
| 62 | @pytest.mark.parametrize("e", [128]) |
| 63 | @pytest.mark.parametrize("topk", [8]) |
| 64 | @pytest.mark.parametrize("dtype", [torch.bfloat16]) |
| 65 | @torch.inference_mode() |
| 66 | def test_trtllm_bf16_moe_modular_no_graph( |
| 67 | m: int, |
| 68 | n: int, |
| 69 | k: int, |
| 70 | e: int, |
| 71 | topk: int, |
| 72 | dtype: torch.dtype, |
| 73 | workspace_init, |
| 74 | ): |
| 75 | set_random_seed(7) |
| 76 | with set_current_vllm_config( |
| 77 | VllmConfig(parallel_config=ParallelConfig(pipeline_parallel_size=1)) |
| 78 | ): |
| 79 | a = torch.randn((m, k), device="cuda", dtype=dtype) / 10 |
| 80 | w1 = torch.randn((e, 2 * n, k), device="cuda", dtype=dtype) / 10 |
| 81 | w2 = torch.randn((e, k, n), device="cuda", dtype=dtype) / 10 |
| 82 | # The FlashInfer conversion may rewrite unpadded input storage in place. |
| 83 | # Preserve the original layout for the independent torch reference. |
| 84 | reference_w1 = w1.clone() |
| 85 | reference_w2 = w2.clone() |
| 86 | score = torch.randn((m, e), device="cuda", dtype=dtype) |
| 87 | scores = torch.softmax(score, dim=-1, dtype=torch.float32) |
| 88 | topk_weights, topk_ids = torch.topk(scores, topk) |
| 89 | topk_weights = topk_weights.contiguous() |
| 90 | topk_ids = topk_ids.to(torch.int32).contiguous() |
| 91 | |
| 92 | moe_config = FusedMoEConfig( |
| 93 | num_experts=e, |
| 94 | experts_per_token=topk, |
| 95 | hidden_dim=k, |
| 96 | intermediate_size=n, |
| 97 | num_local_experts=e, |
| 98 | num_logical_experts=e, |
| 99 | activation=MoEActivation.SILU, |
| 100 | device="cuda", |
| 101 | moe_parallel_config=FusedMoEParallelConfig.make_no_parallel(), |
| 102 | in_dtype=dtype, |
| 103 | routing_method=RoutingMethodType.TopK, |
| 104 | max_num_tokens=next_power_of_2(m), |
| 105 | ) |
| 106 | |
| 107 | trtllm_w1, trtllm_w2 = convert_to_unquantized_kernel_format( |
| 108 | UnquantizedMoeBackend.FLASHINFER_TRTLLM, |
| 109 | moe_config, |
| 110 | w1, |
| 111 | w2, |
| 112 | ) |
| 113 | expected_n = (n + 127) // 128 * 128 |
| 114 | assert moe_config.intermediate_size_per_partition == expected_n |
| 115 | assert trtllm_w2.numel() == e * k * expected_n |
| 116 | |
| 117 | trtllm_experts = mk.FusedMoEKernel( |
| 118 | maybe_make_prepare_finalize( |
| 119 | moe=moe_config, |
| 120 | quant_config=FUSED_MOE_UNQUANTIZED_CONFIG, |
| 121 | allow_new_interface=True, |
| 122 | use_monolithic=False, |
| 123 | ), |
| 124 | TrtLlmBf16ExpertsModular( |
| 125 | moe_config=moe_config, |
| 126 | quant_config=FUSED_MOE_UNQUANTIZED_CONFIG, |
| 127 | ), |
| 128 | ) |
| 129 | |
| 130 | trtllm_output = trtllm_experts.apply( |
| 131 | hidden_states=a, |
| 132 | w1=trtllm_w1, |
| 133 | w2=trtllm_w2, |
| 134 | topk_weights=topk_weights, |
| 135 | topk_ids=topk_ids, |
| 136 | activation=MoEActivation.SILU, |
| 137 | global_num_experts=e, |
| 138 | expert_map=None, |
| 139 | apply_router_weight_on_input=False, |
| 140 | ) |
| 141 | |
| 142 | torch_output = torch_moe( |
| 143 | a, |
| 144 | reference_w1, |
| 145 | reference_w2, |
| 146 | score, |
| 147 | topk, |
| 148 | activation=MoEActivation.SILU, |
| 149 | ) |
| 150 | |
| 151 | torch.testing.assert_close( |
| 152 | torch_output, |
| 153 | trtllm_output, |
| 154 | atol=1e-1, |
| 155 | rtol=2e-1, |
| 156 | ) |
| 157 | |
| 158 | |
| 159 | @pytest.mark.parametrize("has_lora_delta", [False, True]) |
| 160 | @torch.inference_mode() |
| 161 | def test_trtllm_bf16_lora_accepts_checkpoint_shaped_weights(has_lora_delta): |
| 162 | """LoRA dispatch accepts both 3D parameters and legacy 4D packed views.""" |
| 163 | config = make_dummy_moe_config( |
| 164 | num_experts=128, hidden_dim=256, intermediate_size=128 |
| 165 | ) |
| 166 | experts = TrtLlmBf16LoRAExperts(config, FUSED_MOE_UNQUANTIZED_CONFIG) |
| 167 | x = torch.randn(2, 256, device="cuda", dtype=torch.bfloat16) / 10 |
| 168 | w1, w2 = convert_to_unquantized_kernel_format( |
| 169 | UnquantizedMoeBackend.FLASHINFER_TRTLLM, |
| 170 | config, |
| 171 | torch.randn(128, 256, 256, device="cuda", dtype=torch.bfloat16) / 10, |
| 172 | torch.randn(128, 256, 128, device="cuda", dtype=torch.bfloat16) / 10, |
| 173 | ) |
| 174 | assert w1.ndim == w2.ndim == 3 |
| 175 | topk = ( |
| 176 | torch.tensor([[0], [1]], device="cuda", dtype=torch.int32), |
| 177 | torch.ones(2, 1, device="cuda", dtype=torch.float32), |
| 178 | ) |
| 179 | delta = torch.randn(2, 1, 256, device="cuda", dtype=torch.bfloat16) |
| 180 | |
| 181 | def invoke(w1, w2): |
| 182 | result = experts.invoke_routed_moe( |
| 183 | hidden_states=x, |
| 184 | w1=w1, |
| 185 | w2=w2, |
| 186 | topk_ids_and_weights=topk, |
| 187 | gemm1_lora_delta=delta if has_lora_delta else None, |
| 188 | global_num_experts=128, |
| 189 | a1q_scale=None, |
| 190 | output=torch.empty_like(x), |
| 191 | ) |
| 192 | if has_lora_delta: |
| 193 | # Only compare rows belonging to real tokens, excluding padding. |
| 194 | indices = result[2].flatten().long() |
| 195 | return result[0][indices], result[3][indices] |
| 196 | return result |
| 197 | |
| 198 | actual = invoke(w1, w2) |
| 199 | expected = invoke(w1.view(128, 4, 256, 64), w2.view(128, 2, 256, 64)) |
| 200 | for actual_tensor, expected_tensor in zip(actual, expected): |
| 201 | torch.testing.assert_close(actual_tensor, expected_tensor) |